From 8ce29e1436b5ad8b78eb46e4e2b66ff2b72575fd Mon Sep 17 00:00:00 2001 From: scawful Date: Tue, 31 Dec 2024 21:00:27 -0500 Subject: [PATCH] backend-infra-engineer: Release 0.2.2 snapshot --- .gitignore | 17 +- .gitmodules | 6 - CMakeLists.txt | 13 +- Doxyfile | 4 +- cmake/absl.cmake | 6 +- cmake/imgui.cmake | 22 +- cmake/sdl2.cmake | 15 +- cmake/vcpkg.cmake | 6 + docs/asm-style-guide.md | 69 +- docs/build-instructions.md | 42 +- docs/changelog.md | 103 +- docs/contributing.md | 33 +- docs/infrastructure.md | 7 +- incl/dungeon.h | 8 +- incl/overworld.h | 2 +- incl/system/extension.h | 91 - incl/yaze.h | 126 +- src/CMakeLists.txt | 1 + src/app/app.cmake | 64 +- src/app/core/common.cc | 90 +- src/app/core/common.h | 130 +- src/app/core/controller.cc | 118 +- src/app/core/controller.h | 11 +- src/app/core/platform/app_delegate.mm | 6 +- src/app/core/platform/clipboard.cc | 2 - src/app/core/platform/clipboard.h | 2 - src/app/core/platform/clipboard.mm | 4 +- src/app/core/platform/file_dialog.cc | 6 +- src/app/core/platform/file_dialog.h | 2 - src/app/core/platform/file_dialog.mm | 20 +- src/app/core/platform/file_path.h | 2 - src/app/core/platform/file_path.mm | 4 +- src/app/core/platform/font_loader.cc | 19 +- src/app/core/platform/font_loader.h | 2 - src/app/core/platform/font_loader.mm | 4 +- src/app/core/platform/renderer.h | 2 - src/app/core/platform/view_controller.h | 2 +- src/app/core/project.cc | 3 +- src/app/core/project.h | 5 +- src/app/core/utils/file_util.cc | 49 +- src/app/core/utils/file_util.h | 11 +- src/app/core/utils/sdl_deleter.h | 10 +- src/app/editor/code/assembly_editor.cc | 16 +- src/app/editor/code/assembly_editor.h | 4 +- src/app/editor/code/memory_editor.h | 2 - src/app/editor/dungeon/dungeon_editor.cc | 81 +- src/app/editor/dungeon/dungeon_editor.h | 16 +- src/app/editor/editor.cc | 45 - src/app/editor/editor.h | 28 +- src/app/editor/editor_manager.cc | 114 +- src/app/editor/editor_manager.h | 24 +- src/app/editor/graphics/gfx_group_editor.cc | 2 - src/app/editor/graphics/gfx_group_editor.h | 2 - src/app/editor/graphics/graphics_editor.cc | 167 +- src/app/editor/graphics/graphics_editor.h | 28 +- src/app/editor/graphics/palette_editor.cc | 2 - src/app/editor/graphics/palette_editor.h | 2 - src/app/editor/graphics/screen_editor.cc | 39 +- src/app/editor/graphics/screen_editor.h | 23 +- src/app/editor/graphics/tile16_editor.cc | 24 +- src/app/editor/graphics/tile16_editor.h | 10 +- src/app/editor/message/message_data.cc | 4 +- src/app/editor/message/message_data.h | 4 +- src/app/editor/message/message_editor.cc | 28 +- src/app/editor/message/message_editor.h | 2 - src/app/editor/music/music_editor.cc | 2 - src/app/editor/music/music_editor.h | 2 - src/app/editor/overworld/entity.cc | 18 +- src/app/editor/overworld/entity.h | 8 +- src/app/editor/overworld/overworld_editor.cc | 268 +- src/app/editor/overworld/overworld_editor.h | 69 +- src/app/editor/sprite/sprite_editor.cc | 4 +- src/app/editor/sprite/sprite_editor.h | 2 - src/app/editor/sprite/zsprite.h | 2 - src/app/editor/system/command_manager.cc | 2 - src/app/editor/system/command_manager.h | 18 +- src/app/editor/system/constant_manager.h | 20 +- src/app/editor/system/extension_manager.cc | 4 +- src/app/editor/system/extension_manager.h | 4 +- src/app/editor/system/flags.h | 39 +- src/app/editor/system/history_manager.h | 2 - src/app/editor/system/popup_manager.cc | 3 +- src/app/editor/system/popup_manager.h | 3 +- src/app/editor/system/resource_manager.h | 2 - src/app/editor/system/settings_editor.cc | 2 - src/app/editor/system/settings_editor.h | 2 - src/app/emu/audio/apu.cc | 9 +- src/app/emu/audio/apu.h | 9 +- src/app/emu/audio/dsp.cc | 6 +- src/app/emu/audio/dsp.h | 6 +- src/app/emu/audio/internal/addressing.cc | 6 +- src/app/emu/audio/internal/instructions.cc | 6 +- src/app/emu/audio/spc700.cc | 5 - src/app/emu/audio/spc700.h | 10 +- src/app/emu/cpu/clock.h | 34 +- src/app/emu/cpu/cpu.cc | 6 +- src/app/emu/cpu/cpu.h | 12 +- src/app/emu/cpu/internal/addressing.cc | 3 +- src/app/emu/cpu/internal/instructions.cc | 3 +- src/app/emu/emu.cc | 31 +- src/app/emu/emu.cmake | 1 + src/app/emu/emulator.cc | 6 +- src/app/emu/emulator.h | 35 +- src/app/emu/memory/asm_parser.h | 9 +- src/app/emu/memory/dma.cc | 28 +- src/app/emu/memory/dma.h | 26 +- src/app/emu/memory/dma_channel.h | 37 - src/app/emu/memory/memory.cc | 18 +- src/app/emu/memory/memory.h | 78 +- src/app/emu/snes.cc | 75 +- src/app/emu/snes.h | 24 +- src/app/emu/video/ppu.cc | 6 - src/app/emu/video/ppu.h | 11 +- src/app/emu/video/ppu_registers.h | 10 +- src/app/gfx/bitmap.cc | 8 +- src/app/gfx/bitmap.h | 5 +- src/app/gfx/compression.cc | 597 ++- src/app/gfx/compression.h | 80 +- src/app/gfx/scad_format.cc | 3 - src/app/gfx/scad_format.h | 12 +- src/app/gfx/snes_color.cc | 2 - src/app/gfx/snes_color.h | 2 - src/app/gfx/snes_palette.cc | 7 +- src/app/gfx/snes_palette.h | 63 +- src/app/gfx/snes_tile.cc | 7 +- src/app/gfx/snes_tile.h | 3 +- src/app/gfx/tilesheet.cc | 4 - src/app/gfx/tilesheet.h | 13 +- src/app/gui/canvas.cc | 2 - src/app/gui/canvas.h | 8 +- src/app/gui/color.cc | 16 +- src/app/gui/color.h | 41 +- src/app/gui/gui.cmake | 1 + src/app/gui/input.cc | 41 +- src/app/gui/input.h | 53 +- src/app/gui/modules/asset_browser.cc | 3 +- src/app/gui/modules/asset_browser.h | 3 +- src/app/gui/modules/text_editor.cc | 3766 +++++++++++++++++ src/app/gui/modules/text_editor.h | 387 ++ src/app/gui/style.cc | 546 ++- src/app/gui/style.h | 58 +- src/app/gui/zeml.cc | 3 +- src/app/gui/zeml.h | 5 +- src/app/main.cc | 30 +- src/app/rom.cc | 41 +- src/app/rom.h | 73 +- src/app/zelda3/common.h | 157 +- src/app/zelda3/dungeon/object_names.h | 8 +- src/app/zelda3/dungeon/object_renderer.cc | 8 +- src/app/zelda3/dungeon/object_renderer.h | 10 +- src/app/zelda3/dungeon/room.cc | 8 +- src/app/zelda3/dungeon/room.h | 149 - src/app/zelda3/dungeon/room_entrance.h | 8 - src/app/zelda3/dungeon/room_object.cc | 8 +- src/app/zelda3/dungeon/room_object.h | 13 +- src/app/zelda3/dungeon/room_tag.h | 6 +- src/app/zelda3/music/tracker.cc | 23 +- src/app/zelda3/music/tracker.h | 5 +- src/app/zelda3/overworld/overworld.cc | 410 +- src/app/zelda3/overworld/overworld.h | 459 +- src/app/zelda3/overworld/overworld_entrance.h | 112 + src/app/zelda3/overworld/overworld_exit.h | 206 + src/app/zelda3/overworld/overworld_item.h | 101 + src/app/zelda3/overworld/overworld_map.cc | 90 +- src/app/zelda3/overworld/overworld_map.h | 41 +- src/app/zelda3/screen/dungeon_map.cc | 53 + src/app/zelda3/screen/dungeon_map.h | 19 +- src/app/zelda3/screen/inventory.cc | 5 +- src/app/zelda3/screen/inventory.h | 3 +- src/app/zelda3/screen/title_screen.cc | 3 +- src/app/zelda3/screen/title_screen.h | 3 +- src/app/zelda3/sprite/overlord.h | 2 - src/app/zelda3/sprite/sprite.cc | 6 +- src/app/zelda3/sprite/sprite.h | 2 - src/app/zelda3/sprite/sprite_builder.cc | 2 - src/app/zelda3/sprite/sprite_builder.h | 2 - src/cli/handlers/compress.cc | 2 +- src/cli/handlers/patch.cc | 8 +- src/cli/handlers/tile16_transfer.cc | 4 +- src/cli/tui.cc | 70 + src/cli/tui.h | 27 + src/cli/z3ed.cc | 23 +- src/cli/z3ed.cmake | 21 +- src/cli/{command.h => z3ed.h} | 11 +- src/ios/main.mm | 4 +- src/ios/yaze.xcodeproj/project.pbxproj | 38 +- .../UserInterfaceState.xcuserstate | Bin 407072 -> 462719 bytes .../xcschemes/xcschememanagement.plist | 4 +- src/lib/ImGuiColorTextEdit | 1 - src/lib/ImGuiFileDialog | 1 - src/lib/imgui | 2 +- src/lib/imgui_test_engine | 2 +- src/test/CMakeLists.txt | 1 + src/test/emu/cpu_test.cc | 14 +- src/test/emu/ppu_test.cc | 20 +- src/test/emu/spc700_test.cc | 8 +- src/test/gfx/compression_test.cc | 36 +- src/test/gfx/snes_palette_test.cc | 28 +- src/test/integration/test_editor.cc | 9 +- src/test/integration/test_editor.h | 2 +- src/test/mocks/mock_memory.h | 6 +- src/test/rom_test.cc | 20 +- src/test/yaze_test.cc | 8 - src/test/zelda3/dungeon_room_test.cc | 16 +- src/test/zelda3/message_test.cc | 8 +- src/test/zelda3/overworld_test.cc | 12 +- src/test/zelda3/sprite_builder_test.cc | 6 +- src/yaze.cc | 66 +- src/yaze_config.h.in | 4 + 209 files changed, 7446 insertions(+), 3633 deletions(-) create mode 100644 cmake/vcpkg.cmake delete mode 100644 incl/system/extension.h delete mode 100644 src/app/emu/memory/dma_channel.h create mode 100644 src/app/gui/modules/text_editor.cc create mode 100644 src/app/gui/modules/text_editor.h create mode 100644 src/app/zelda3/overworld/overworld_entrance.h create mode 100644 src/app/zelda3/overworld/overworld_exit.h create mode 100644 src/app/zelda3/overworld/overworld_item.h create mode 100644 src/app/zelda3/screen/dungeon_map.cc create mode 100644 src/cli/tui.cc create mode 100644 src/cli/tui.h rename src/cli/{command.h => z3ed.h} (96%) delete mode 160000 src/lib/ImGuiColorTextEdit delete mode 160000 src/lib/ImGuiFileDialog create mode 100644 src/yaze_config.h.in diff --git a/.gitignore b/.gitignore index b52d20e7..d060c50d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,19 +4,7 @@ build/ .vscode/ disasm/ src/etc -src/lib/SDL2 -src/lib/cmake -src/lib/GL -src/lib/abseil-cpp -src/lib/libGLEW.2.2.0.dylib -src/lib/libGLEW.2.2.dylib -src/lib/libGLEW.a -src/lib/libGLEW.dylib -src/lib/libSDL2_test.a -src/lib/libSDL2-2.0.0.dylib -src/lib/libSDL2.a -src/lib/libSDL2.dylib -src/lib/libSDL2main.a +src/lib/ checks.json assets/lib/libasar.dll cmake/yaze.plist.in @@ -29,4 +17,5 @@ assets/asm/EditorCore.asm src/app/emu/cpu/internal/old_cpu.cc build-windows src/lib/libpng -src/lib/zlib \ No newline at end of file +src/lib/zlib +assets/layouts/ow_toolset.zeml \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index bf88cd5f..31b9b626 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,6 @@ [submodule "src/lib/imgui"] path = src/lib/imgui url = https://github.com/ocornut/imgui.git -[submodule "src/lib/ImGuiFileDialog"] - path = src/lib/ImGuiFileDialog - url = https://github.com/aiekick/ImGuiFileDialog.git -[submodule "src/lib/ImGuiColorTextEdit"] - path = src/lib/ImGuiColorTextEdit - url = https://github.com/BalazsJako/ImGuiColorTextEdit.git [submodule "assets/asm/alttp-hacker-workspace"] path = assets/asm/alttp-hacker-workspace url = https://github.com/scawful/alttp-hacker-workspace.git diff --git a/CMakeLists.txt b/CMakeLists.txt index eea47774..f61889b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,10 @@ # Yet Another Zelda3 Editor # by scawful cmake_minimum_required(VERSION 3.10) -project(yaze VERSION 0.2.0 +project(yaze VERSION 0.2.2 DESCRIPTION "Yet Another Zelda3 Editor" LANGUAGES CXX) +configure_file(src/yaze_config.h.in yaze_config.h) # Build Flags set(YAZE_BUILD_APP ON) @@ -18,7 +19,7 @@ set(YAZE_INSTALL_LIB OFF) add_definitions("-DYAZE_LIB_PNG=1") # C++ Standard and CMake Specifications -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) @@ -28,11 +29,19 @@ set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) set(BUILD_SHARED_LIBS OFF) set(CMAKE_FIND_FRAMEWORK LAST) set(CMAKE_SHARED_MODULE_PREFIX "") + +if (UNIX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Dlinux -Dstricmp=strcasecmp") +endif() + if (MACOS) set(CMAKE_INSTALL_PREFIX /usr/local) endif() +if (WIN32) + include(cmake/vcpkg.cmake) +endif() + # Abseil Standard Specifications include(cmake/absl.cmake) diff --git a/Doxyfile b/Doxyfile index 0dfa6199..2abc6e02 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = "yaze" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = "0.2.0" +PROJECT_NUMBER = "0.2.2" # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a @@ -1284,7 +1284,7 @@ ALPHABETICAL_INDEX = YES # after removing the prefix. # This tag requires that the tag ALPHABETICAL_INDEX is set to YES. -IGNORE_PREFIX = +IGNORE_PREFIX = googletest #--------------------------------------------------------------------------- # Configuration options related to the HTML output diff --git a/cmake/absl.cmake b/cmake/absl.cmake index fed9eb81..bb3791c0 100644 --- a/cmake/absl.cmake +++ b/cmake/absl.cmake @@ -1,14 +1,14 @@ if (MINGW) add_subdirectory(src/lib/abseil-cpp) else() -find_package(absl) + find_package(absl) endif() set(ABSL_PROPAGATE_CXX_STD ON) set(ABSL_CXX_STANDARD 17) set(ABSL_USE_GOOGLETEST_HEAD ON) set(ABSL_ENABLE_INSTALL ON) set( - ABSL_TARGETS + ABSL_TARGETS absl::strings absl::flags absl::status @@ -21,4 +21,4 @@ set( absl::raw_logging_internal absl::failure_signal_handler absl::flat_hash_map -) \ No newline at end of file +) diff --git a/cmake/imgui.cmake b/cmake/imgui.cmake index bde97cee..14b366a0 100644 --- a/cmake/imgui.cmake +++ b/cmake/imgui.cmake @@ -4,19 +4,9 @@ file(GLOB IMGUI_SOURCES ${IMGUI_PATH}/*.cpp) add_library("ImGui" STATIC ${IMGUI_SOURCES}) target_include_directories("ImGui" PUBLIC ${IMGUI_PATH}) target_include_directories(ImGui PUBLIC ${SDL2_INCLUDE_DIR}) -target_compile_definitions(ImGui PUBLIC +target_compile_definitions(ImGui PUBLIC IMGUI_IMPL_OPENGL_LOADER_CUSTOM= GL_GLEXT_PROTOTYPES=1) -set(IMGUI_FILE_DLG_PATH ${CMAKE_SOURCE_DIR}/src/lib/ImGuiFileDialog) -file(GLOB IMGUI_FILE_DLG_SOURCES ${IMGUI_FILE_DLG_PATH}/*.cpp) -add_library("ImGuiFileDialog" STATIC ${IMGUI_FILE_DLG_SOURCES}) -target_include_directories(ImGuiFileDialog PUBLIC ${IMGUI_PATH} ${CMAKE_SOURCE_DIR}/src/lib) - -set(IMGUI_COLOR_TEXT_EDIT_PATH ${CMAKE_SOURCE_DIR}/src/lib/ImGuiColorTextEdit) -file(GLOB IMGUI_COLOR_TEXT_EDIT_SOURCES ${IMGUI_COLOR_TEXT_EDIT_PATH}/*.cpp) -add_library("ImGuiColorTextEdit" STATIC ${IMGUI_COLOR_TEXT_EDIT_SOURCES}) -target_include_directories(ImGuiColorTextEdit PUBLIC ${IMGUI_PATH} ${CMAKE_SOURCE_DIR}/src/lib) - set(IMGUI_TEST_ENGINE_PATH ${CMAKE_SOURCE_DIR}/src/lib/imgui_test_engine/imgui_test_engine) file(GLOB IMGUI_TEST_ENGINE_SOURCES ${IMGUI_TEST_ENGINE_PATH}/*.cpp) add_library("ImGuiTestEngine" STATIC ${IMGUI_TEST_ENGINE_SOURCES}) @@ -25,16 +15,14 @@ target_link_libraries(ImGuiTestEngine PUBLIC ImGui) set( IMGUI_SRC - ${IMGUI_PATH}/imgui.cpp + ${IMGUI_PATH}/imgui.cpp ${IMGUI_PATH}/imgui_demo.cpp - ${IMGUI_PATH}/imgui_draw.cpp + ${IMGUI_PATH}/imgui_draw.cpp ${IMGUI_PATH}/imgui_widgets.cpp ${IMGUI_PATH}/backends/imgui_impl_sdl2.cpp - ${IMGUI_PATH}/backends/imgui_impl_sdlrenderer2.cpp + ${IMGUI_PATH}/backends/imgui_impl_sdlrenderer2.cpp ${IMGUI_PATH}/misc/cpp/imgui_stdlib.cpp - ${IMGUI_FILE_DLG_PATH}/ImGuiFileDialog.cpp - ${IMGUI_COLOR_TEXT_EDIT_PATH}/TextEditor.cpp ) # For integration test -add_definitions("-DIMGUI_ENABLE_TEST_ENGINE -DIMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL=1") \ No newline at end of file +add_definitions("-DIMGUI_ENABLE_TEST_ENGINE -DIMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL=1") diff --git a/cmake/sdl2.cmake b/cmake/sdl2.cmake index 94cc6531..e15a52d9 100644 --- a/cmake/sdl2.cmake +++ b/cmake/sdl2.cmake @@ -9,19 +9,8 @@ set(SDL_TARGETS SDL2::SDL2) if(WIN32 OR MINGW) list(PREPEND SDL_TARGETS SDL2::SDL2main ws2_32) - add_definitions(-DSDL_MAIN_HANDLED) + add_definitions("-DSDL_MAIN_HANDLED") endif() # libpng -if (MINGW) - set(ZLIB_ROOT ${CMAKE_SOURCE_DIR}/build-windows/src/lib/zlib) - set(ZLIB_LIBRARY ${CMAKE_SOURCE_DIR}/build-windows/lib/libzlib.dll.a) - include_directories(${CMAKE_SOURCE_DIR}/src/lib/zlib ${CMAKE_SOURCE_DIR}/src/lib/libpng) - set(ZLIB_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/src/lib/zlib) - set(YAZE_BUILD_PYTHON OFF) - set(YAZE_BUILD_EXTENSIONS OFF) - add_subdirectory(src/lib/zlib) - add_subdirectory(src/lib/libpng) -else() - find_package(PNG REQUIRED) -endif() \ No newline at end of file +find_package(PNG REQUIRED) \ No newline at end of file diff --git a/cmake/vcpkg.cmake b/cmake/vcpkg.cmake new file mode 100644 index 00000000..23ec9035 --- /dev/null +++ b/cmake/vcpkg.cmake @@ -0,0 +1,6 @@ + +add_definitions("-DMICROSOFT_WINDOWS_WINBASE_H_DEFINE_INTERLOCKED_CPLUSPLUS_OVERLOADS=0") + +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE dynamic) diff --git a/docs/asm-style-guide.md b/docs/asm-style-guide.md index 1d2d94b2..f58e2313 100644 --- a/docs/asm-style-guide.md +++ b/docs/asm-style-guide.md @@ -23,13 +23,12 @@ Custom assembly code applied to the game should be included through the `yaze.as ## File Structure - **File Extension**: Use `.asm` as the file extension for 65816 assembly files. -- **Header Comments**: Include a header comment at the beginning of each file describing its purpose and the author. +- **Header Comments**: Include a header comment at the beginning of each file describing its purpose and the author. Example: ```asm ; ========================================================= -; File: my_file.asm ; Purpose: [Brief description of the file’s functionality] ; Author: [Your Name] ; ========================================================= @@ -68,11 +67,10 @@ Sprite_Minecart_Main: } ``` - ## Comments - **Purpose**: Comments should explain why the code exists and what it is intended to do, especially for complex logic. -- **Placement**: +- **Placement**: - Comments can be placed above the code block they describe for longer explanations. - Inline comments can be used for single lines of code where the purpose might not be immediately clear. - **Clarity**: Avoid stating the obvious. Focus on explaining the logic rather than restating the code. @@ -83,22 +81,6 @@ Example: LDA $22 : SEC : SBC $3F : STA $31 ; Adjust X position for camera movement ``` -## Directives - -- **Organization**: Use `%macro`, `include`, and other Asar directives in a structured manner, keeping related directives grouped together. -- **Usage**: Ensure all directives are used consistently throughout the codebase, following the naming conventions and formatting rules established. - -Example: - -```asm -%macro InitMovement - LDA.b $22 : STA.b $3F - LDA.b $23 : STA.b $41 - LDA.b $20 : STA.b $3E - LDA.b $21 : STA.b $40 -endmacro -``` - ## Instructions - **Single Line Instructions**: Combine multiple instructions on a single line using colons (`:`) where appropriate for related operations. @@ -121,11 +103,11 @@ Example: ```asm %macro HandlePlayerCamera - LDA $22 : SEC : SBC $3F : STA $31 - LDA $20 : SEC : SBC $3E : STA $30 - JSL Link_HandleMovingAnimation_FullLongEntry - JSL HandleIndoorCameraAndDoors - RTS + LDA $22 : SEC : SBC $3F : STA $31 + LDA $20 : SEC : SBC $3E : STA $30 + JSL Link_HandleMovingAnimation_FullLongEntry + JSL HandleIndoorCameraAndDoors + RTS endmacro ``` @@ -137,12 +119,12 @@ endmacro Example: ```asm -.loop_start - LDA $00 : CMP #$10 : BEQ .end_loop + .loop_start + LDA $00 : CMP #$10 : BEQ .end_loop INC $00 BRA .loop_start -.end_loop - RTS + .end_loop + RTS ``` ## Data Structures @@ -155,10 +137,10 @@ Example: ```asm .DirectionTileLookup { - db $02, $00, $04, $00 ; North - db $00, $00, $03, $01 ; East - db $00, $02, $00, $04 ; South - db $03, $01, $00, $00 ; West + db $02, $00, $04, $00 ; North + db $00, $00, $03, $01 ; East + db $00, $02, $00, $04 ; South + db $03, $01, $00, $00 ; West } ``` @@ -206,7 +188,6 @@ AncillaAdd_Hookshot: - **Logical Grouping**: Organize code into logical sections, with related routines and macros grouped together. - **Separation of Concerns**: Ensure that each section of code is responsible for a specific task or set of related tasks, avoiding tightly coupled code. - **Modularity**: Write code in a modular way, making it easier to reuse and maintain. -- **Status Registers and Stack Operations**: Indent code blocks when using status register operations (REP, SEP, PHX, PLX, etc.) to improve readability. Example: @@ -216,22 +197,20 @@ Example: ; ========================================================= Sprite_Minecart_Main: { - JSR HandleTileDirections - JSR HandleDynamicSwitchTileDirections - PHX - JSR HandleMinecartMovement - PLX + PHX + JSR HandleMinecartMovement + PLX - REP #$20 - LDA !SpriteDirection : STA $00 - SEP #$20 - RTS + REP #$20 + LDA !SpriteDirection : STA $00 + SEP #$20 + RTS } ``` ## Custom Code -- **Integration**: Include custom assembly code in the `yaze.asm` file to ensure it is applied correctly to the ROM. The module should include a define and conditional statement to allow users to disable the module if needed. +- **Integration**: Include custom assembly code in the `yaze.asm` file to ensure it is applied correctly to the ROM. The module should include a define and conditional statement to allow users to disable the module if needed. Example: @@ -241,4 +220,4 @@ Example: if !YAZE_CUSTOM_MOSAIC != 0 incsrc "mosaic_change.asm" endif -``` \ No newline at end of file +``` diff --git a/docs/build-instructions.md b/docs/build-instructions.md index b24d4526..75c839e9 100644 --- a/docs/build-instructions.md +++ b/docs/build-instructions.md @@ -9,11 +9,47 @@ Yaze uses CMake to build the project. If you are unexperienced with CMake, pleas The gui editor is built using SDL2 and ImGui. For reference on how to use ImGui, see the [Getting Started](https://github.com/ocornut/imgui/wiki/Getting-Started) guide. For SDL2, see the [SDL2 documentation](https://wiki.libsdl.org/). -For those who want to reduce compile times, consider installing the dependencies on your system. +For those who want to reduce compile times, consider installing the dependencies on your system. ## Windows -Recommended to use [msys2](https://www.msys2.org/) for a Unix-like environment on Windows. +### vcpkg + +For Visual Studio users, follow the [Install and use packages with CMake](https://learn.microsoft.com/en-us/vcpkg/get_started/get-started) tutorial from Microsoft. + +Define the following dependencies in `vcpkg.json` + +``` +{ + "dependencies": [ + "abseil", + "sdl2", + "libpng" + ] +} +``` + +Target the architecture in `CMakePresets.json` + +``` +{ + "name": "vcpkg", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "architecture": { + "value": "arm64/x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "CMAKE_SYSTEM_PROCESSOR": "arm64/x64" + } +} +``` + +### msys2 + +[msys2](https://www.msys2.org/) is an alternative you may use for a Unix-like environment on Windows. Beware that this is for more experienced developers who know how to manage their system PATH. Add to environment variables `C:\msys64\mingw64\bin` @@ -55,4 +91,4 @@ You will need to link `SDL2.framework` and `libpng.a` to the project. You can use your package manager to install the same dependencies as macOS. -I trust you know how to use your package manager. \ No newline at end of file +I trust you know how to use your package manager. diff --git a/docs/changelog.md b/docs/changelog.md index 63caeec4..4720b9f0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,26 +1,39 @@ # Changelog -## 0.0.1 (06-08-2022) +## 0.2.2 (12-31-2024) -- Started project -- Added ImGui -- Added SDL2 -- Added yaze_test target with gtest +- DungeonMap editing improvements +- ZSCustomOverworld support +- Cross platform file handling -## 0.0.2 - 0.0.4 +## 0.2.1 (08-20-2024) -- TODO: Track changes over this time +- Improved MessageEditor parsing +- Added integration test window +- Bitmap bug fixes -## 0.0.5 (11-21-2023) +## 0.2.0 (07-20-2024) -- DungeonEditor -- DungeonObjectRenderer +- iOS app support +- Graphics Sheet Browser +- Project Files -## 0.0.6 (11-22-2023) +## 0.1.0 (05-11-2024) -- ScreenEditor DungeonMap -- Tile16 Editor -- Canvas updates +- Bitmap bug fixes +- Error handling improvements + +## 0.0.9 (04-14-2024) + +- Documentation updates +- Entrance tile types +- Emulator subsystem overhaul + +## 0.0.8 (02-08-2024) + +- Hyrule Magic Compression +- Dungeon Room Entrances +- Png Export ## 0.0.7 (01-27-2024) @@ -30,18 +43,60 @@ - Items - Sprites -## 0.1.0 (05-11-2024) +## 0.0.6 (11-22-2023) -- TODO: Track changes over this time +- ScreenEditor DungeonMap +- Tile16 Editor +- Canvas updates -## 0.2.0 (07-20-2024) +## 0.0.5 (11-21-2023) -- iOS app support -- Graphics Sheet Browser -- Project Files +- DungeonEditor +- DungeonObjectRenderer -## 0.2.1 (08-20-2024) +## 0.0.4 (11-11-2023) -- Improved MessageEditor parsing -- Added integration test window -- Bitmap bug fixes +- Tile16Editor +- GfxGroupEditor +- Add GfxGroups fns to Rom +- Add Tile16Editor and GfxGroupEditor to OverworldEditor + +## 0.0.3 (10-26-2023) + +- Emulator subsystem + - Snes Ppu and PpuRegisters + - Direct Memory Access + - Cpu Tests +- Read/Write Tile16 functions +- CompressionV3 +- Rom::LoadLinkGraphics + + +## 0.0.2 (08-26-2023) + +- Emulator subsystem + - Spc700 + - Emulator loop + - Clock and MockClock + - Ppu and Apu cycling + - Setup Snes initialization + - 65816 Cpu opcodes +- JP Font support +- SCAD Format support for CGX, COL, OBJ files +- Overworld Save +- Overworld Map Tile Editing + +## 0.0.1 (07-22-2023) + +- GraphicsEditor +- Palette management +- lc_lz2 Compression +- SnesTo8bppSheet +- Bitmap Canvas + +## 0.0.0 (06-08-2022) + +- Started project +- Added ImGui +- Added SDL2 +- Added yaze_test target with gtest diff --git a/docs/contributing.md b/docs/contributing.md index 4531e184..93bc4898 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -20,7 +20,7 @@ Assembly code should follow the [65816 Style Guide](docs/asm-style-guide.md). ## Testing Facilities -The project includes the `yaze_test` target which defines unit tests and an integration test window. The unit tests make use of GoogleTest and GoogleMock. The integration test window is an ImGui window build out of the yaze::app::core::Controller and yaze::test::integration::TestEditor. The integration test window can be accessed by passing the argument `integration` to the target. +The project includes the `yaze_test` target which defines unit tests and an integration test window. The unit tests make use of GoogleTest and GoogleMock. The integration test window is an ImGui window build out of the yaze::core::Controller and yaze::test::integration::TestEditor. The integration test window can be accessed by passing the argument `integration` to the target. New modules should define unit tests in the `src/test` directory and integration tests in the `src/test/integration` directory. The `yaze_test` target will automatically include all tests in these directories. @@ -54,37 +54,6 @@ yaze includes an emulator subsystem that allows developers to test their modific - Implementing new debugging tools, such as memory viewers, breakpoints, or trace logs. - Extending the emulator to support additional features, such as save states, cheat codes, or multiplayer modes. -### 4. Editor Management - -The `EditorManager` class manages the core functionalities of YAZE, including rendering the UI, handling user input, and managing multiple editors. While this class is central to yaze's operations, it has many responsibilities. You can help by: - -- Refactoring `EditorManager` to delegate responsibilities to specialized managers (e.g., `MenuManager`, `TabManager`, `StatusManager`). -- Optimizing the rendering and update loop to improve performance, especially when handling large textures or complex editors. -- Implementing new features that streamline the editing process, such as better keyboard shortcuts, command palette integration, or project management tools. - -### 5. User Interface and UX - -yaze's UI is built with ImGui, offering a flexible and customizable interface. Contributions to the UI might include: - -- Designing and implementing new themes or layouts to improve the user experience. -- Adding new UI components, such as toolbars, context menus, or customizable panels. -- Improving the accessibility of the editor, ensuring it is usable by a wide range of users, including those with disabilities. - -### 6. ROM Manipulation - -The `Rom` class is at the heart of yaze's ability to modify and interact with ROM data. Contributions here might involve: - -- Optimizing the loading and saving processes to handle larger ROMs or more complex modifications efficiently. -- Extensions should be able to change the way the `Rom` class interacts with the ROM data with custom pointers to expanded data structures. - -### 7. Testing and Documentation - -Quality assurance and documentation are critical to yaze's success. Contributions in this area include: - -- Writing unit tests for new and existing features to ensure they work correctly and remain stable over time. -- Contributing to the documentation, both for end-users and developers, to make yaze easier to use and extend. -- Creating tutorials or guides that help new developers get started with building extensions or contributing to the project. - ## Building the Project For detailed instructions on building YAZE, including its dependencies and supported platforms, refer to [build-instructions.md](docs/build-instructions.md). diff --git a/docs/infrastructure.md b/docs/infrastructure.md index 61e70294..834ac7d1 100644 --- a/docs/infrastructure.md +++ b/docs/infrastructure.md @@ -18,14 +18,14 @@ The goal of yaze is to build a cross platform editor for the Legend of Zelda: A - **assets**: Hosts assets like fonts, icons, assembly source, etc. - **cmake**: Contains CMake configurations. - **docs**: Contains documentation for users and developers. -- **src**: Contains source files. +- **incl**: Contains the public headers for `yaze_c` +- **src**: Contains source files. - **app**: Contains the GUI editor `yaze` - **app/emu**: Contains a standalone Snes emulator application `yaze_emu` - **cli**: Contains the command line interface `z3ed` - - **incl**: Contains the data headers for `yaze_c` + - **cli/python**: Contains the Python module `yaze_py` - **ios**: Contains the iOS application `yaze_ios` - **lib**: Contains the dependencies as git submodules - - **py**: Contains the Python module `yaze_py` - **test**: Contains testing interface `yaze_test` - **win32**: Contains Windows resource file and icon @@ -83,4 +83,3 @@ Currently implemented as a singleton with SharedRom which is not great but has h - app/gfx/bitmap.h This class is responsible for creating, managing, and manipulating bitmap data, which can be displayed on the screen using SDL2 Textures and the ImGui draw list. It also provides functions for exporting these bitmaps to the clipboard in PNG format using libpng. - diff --git a/incl/dungeon.h b/incl/dungeon.h index d476e1f7..92852a67 100644 --- a/incl/dungeon.h +++ b/incl/dungeon.h @@ -54,8 +54,14 @@ typedef enum z3_dungeon_background2 { DarkRoom } z3_dungeon_background2; +typedef struct z3_dungeon_room { + z3_dungeon_background2 bg2; + z3_dungeon_destination pits; + z3_dungeon_destination stairs[4]; +} z3_dungeon_room; + #ifdef __cplusplus } #endif -#endif // YAZE_BASE_DUNGEON_H_ +#endif // YAZE_BASE_DUNGEON_H_ diff --git a/incl/overworld.h b/incl/overworld.h index 086857db..6934e7bd 100644 --- a/incl/overworld.h +++ b/incl/overworld.h @@ -31,7 +31,7 @@ typedef struct z3_overworld_map { * @brief Primitive of the overworld. */ typedef struct z3_overworld { - void *impl; // yaze::app::Overworld* + void *impl; // yaze::Overworld* uint8_t *tile32_data; /**< Pointer to the 32x32 tile data. */ uint8_t *tile16_data; /**< Pointer to the 16x16 tile data. */ diff --git a/incl/system/extension.h b/incl/system/extension.h deleted file mode 100644 index 7c9e2f60..00000000 --- a/incl/system/extension.h +++ /dev/null @@ -1,91 +0,0 @@ -#ifndef EXTENSION_INTERFACE_H -#define EXTENSION_INTERFACE_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include "yaze.h" - -typedef void (*yaze_initialize_func)(yaze_editor_context* context); -typedef void (*yaze_cleanup_func)(void); -typedef void (*yaze_extend_ui_func)(yaze_editor_context* context); -typedef void (*yaze_manipulate_rom_func)(z3_rom* rom); -typedef void (*yaze_command_func)(void); -typedef void (*yaze_event_hook_func)(void); - -typedef enum { - YAZE_EVENT_ROM_LOADED, - YAZE_EVENT_ROM_SAVED, - YAZE_EVENT_SPRITE_MODIFIED, - YAZE_EVENT_PALETTE_CHANGED, -} yaze_event_type; - -/** - * @brief Extension interface for Yaze. - * - * @details Yaze extensions can be written in C or Python. - */ -typedef struct yaze_extension { - const char* name; - const char* version; - - /** - * @brief Function to initialize the extension. - * - * @details This function is called when the extension is loaded. It can be - * used to set up any resources or state needed by the extension. - */ - yaze_initialize_func initialize; - - /** - * @brief Function to clean up the extension. - * - * @details This function is called when the extension is unloaded. It can be - * used to clean up any resources or state used by the extension. - */ - yaze_cleanup_func cleanup; - - /** - * @brief Function to manipulate the ROM. - * - * @param rom The ROM to manipulate. - * - */ - yaze_manipulate_rom_func manipulate_rom; - - /** - * @brief Function to extend the UI. - * - * @param context The editor context. - * - * @details This function is called when the extension is loaded. It can be - * used to add custom UI elements to the editor. The context parameter - * provides access to the project, command registry, event dispatcher, and - * ImGui context. - */ - yaze_extend_ui_func extend_ui; - - /** - * @brief Register commands in the yaze_command_registry. - */ - yaze_command_func register_commands; - - /** - * @brief Register custom tools in the yaze_command_registry. - */ - yaze_command_func register_custom_tools; - - /** - * @brief Register event hooks in the yaze_event_dispatcher. - */ - void (*register_event_hooks)(yaze_event_type event, - yaze_event_hook_func hook); - -} yaze_extension; - -#ifdef __cplusplus -} -#endif - -#endif // EXTENSION_INTERFACE_H diff --git a/incl/yaze.h b/incl/yaze.h index a0a9a0a9..a736b55b 100644 --- a/incl/yaze.h +++ b/incl/yaze.h @@ -19,9 +19,6 @@ typedef struct yaze_project yaze_project; typedef struct yaze_command_registry yaze_command_registry; typedef struct yaze_event_dispatcher yaze_event_dispatcher; -/** - * @brief Extension editor context. - */ typedef struct yaze_editor_context { z3_rom* rom; yaze_project* project; @@ -30,19 +27,10 @@ typedef struct yaze_editor_context { yaze_event_dispatcher* event_dispatcher; } yaze_editor_context; -/** - * @brief Initialize the Yaze library. - */ +void yaze_check_version(const char* version); int yaze_init(yaze_editor_context*); - -/** - * @brief Clean up the Yaze library. - */ void yaze_cleanup(yaze_editor_context*); -/** - * @brief Primitive of a Yaze project. - */ struct yaze_project { const char* name; const char* filepath; @@ -53,29 +41,16 @@ struct yaze_project { yaze_project yaze_load_project(const char* filename); -/** - * @brief Primitive of a Zelda3 ROM. - */ struct z3_rom { const char* filename; const uint8_t* data; size_t size; - void* impl; // yaze::app::Rom* + void* impl; // yaze::Rom* }; -/** - * @brief Load a Zelda3 ROM from a file. - */ z3_rom* yaze_load_rom(const char* filename); - -/** - * @brief Unload a Zelda3 ROM. - */ void yaze_unload_rom(z3_rom* rom); -/** - * @brief Primitive of a Bitmap - */ typedef struct yaze_bitmap { int width; int height; @@ -83,41 +58,100 @@ typedef struct yaze_bitmap { uint8_t* data; } yaze_bitmap; -/** - * @brief Load a bitmap from a file. - */ yaze_bitmap yaze_load_bitmap(const char* filename); -/** - * @brief Get a color from a palette set. - */ snes_color yaze_get_color_from_paletteset(const z3_rom* rom, int palette_set, int palette, int color); -/** - * @brief Load the overworld from a Zelda3 ROM. - */ z3_overworld* yaze_load_overworld(const z3_rom* rom); -/** - * @brief Check the version of the Yaze library. - */ -void yaze_check_version(const char* version); +z3_dungeon_room* yaze_load_all_rooms(const z3_rom* rom); -/** - * @brief Command registry. - */ struct yaze_command_registry { void (*register_command)(const char* name, void (*command)(void)); }; -/** - * @brief Event dispatcher. - */ struct yaze_event_dispatcher { void (*register_event_hook)(void (*event_hook)(void)); }; +typedef void (*yaze_initialize_func)(yaze_editor_context* context); +typedef void (*yaze_cleanup_func)(void); +typedef void (*yaze_extend_ui_func)(yaze_editor_context* context); +typedef void (*yaze_manipulate_rom_func)(z3_rom* rom); +typedef void (*yaze_command_func)(void); +typedef void (*yaze_event_hook_func)(void); + +typedef enum { + YAZE_EVENT_ROM_LOADED, + YAZE_EVENT_ROM_SAVED, + YAZE_EVENT_SPRITE_MODIFIED, + YAZE_EVENT_PALETTE_CHANGED, +} yaze_event_type; + +/** + * @brief Extension interface for Yaze. + * + * @details Yaze extensions can be written in C or Python. + */ +typedef struct yaze_extension { + const char* name; + const char* version; + + /** + * @brief Function to initialize the extension. + * + * @details This function is called when the extension is loaded. It can be + * used to set up any resources or state needed by the extension. + */ + yaze_initialize_func initialize; + + /** + * @brief Function to clean up the extension. + * + * @details This function is called when the extension is unloaded. It can be + * used to clean up any resources or state used by the extension. + */ + yaze_cleanup_func cleanup; + + /** + * @brief Function to manipulate the ROM. + * + * @param rom The ROM to manipulate. + * + */ + yaze_manipulate_rom_func manipulate_rom; + + /** + * @brief Function to extend the UI. + * + * @param context The editor context. + * + * @details This function is called when the extension is loaded. It can be + * used to add custom UI elements to the editor. The context parameter + * provides access to the project, command registry, event dispatcher, and + * ImGui context. + */ + yaze_extend_ui_func extend_ui; + + /** + * @brief Register commands in the yaze_command_registry. + */ + yaze_command_func register_commands; + + /** + * @brief Register custom tools in the yaze_command_registry. + */ + yaze_command_func register_custom_tools; + + /** + * @brief Register event hooks in the yaze_event_dispatcher. + */ + void (*register_event_hooks)(yaze_event_type event, + yaze_event_hook_func hook); + +} yaze_extension; + #ifdef __cplusplus } #endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 58c2a023..8bf25992 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -105,6 +105,7 @@ if (YAZE_BUILD_LIB) ${CMAKE_SOURCE_DIR}/src/ ${PNG_INCLUDE_DIRS} ${SDL2_INCLUDE_DIR} + ${PROJECT_BINARY_DIR} ) target_link_libraries( diff --git a/src/app/app.cmake b/src/app/app.cmake index a00f9c96..b93f3aa7 100644 --- a/src/app/app.cmake +++ b/src/app/app.cmake @@ -5,35 +5,34 @@ include(app/gui/gui.cmake) include(app/zelda3/zelda3.cmake) if (APPLE) -add_executable( - yaze - MACOSX_BUNDLE - app/main.cc - app/rom.cc - ${YAZE_APP_EMU_SRC} - ${YAZE_APP_CORE_SRC} - ${YAZE_APP_EDITOR_SRC} - ${YAZE_APP_GFX_SRC} - ${YAZE_APP_ZELDA3_SRC} - ${YAZE_GUI_SRC} - ${IMGUI_SRC} - - # Bundled Resources - ${YAZE_RESOURCE_FILES} -) + add_executable( + yaze + MACOSX_BUNDLE + app/main.cc + app/rom.cc + ${YAZE_APP_EMU_SRC} + ${YAZE_APP_CORE_SRC} + ${YAZE_APP_EDITOR_SRC} + ${YAZE_APP_GFX_SRC} + ${YAZE_APP_ZELDA3_SRC} + ${YAZE_GUI_SRC} + ${IMGUI_SRC} + # Bundled Resources + ${YAZE_RESOURCE_FILES} + ) else() -add_executable( - yaze - app/main.cc - app/rom.cc - ${YAZE_APP_EMU_SRC} - ${YAZE_APP_CORE_SRC} - ${YAZE_APP_EDITOR_SRC} - ${YAZE_APP_GFX_SRC} - ${YAZE_APP_ZELDA3_SRC} - ${YAZE_GUI_SRC} - ${IMGUI_SRC} -) + add_executable( + yaze + app/main.cc + app/rom.cc + ${YAZE_APP_EMU_SRC} + ${YAZE_APP_CORE_SRC} + ${YAZE_APP_EDITOR_SRC} + ${YAZE_APP_GFX_SRC} + ${YAZE_APP_ZELDA3_SRC} + ${YAZE_GUI_SRC} + ${IMGUI_SRC} + ) endif() target_include_directories( @@ -46,6 +45,7 @@ target_include_directories( ${CMAKE_SOURCE_DIR}/src/lib/imgui_test_engine ${PNG_INCLUDE_DIRS} ${SDL2_INCLUDE_DIR} + ${PROJECT_BINARY_DIR} ) target_link_libraries( @@ -63,11 +63,3 @@ if (APPLE) target_link_libraries(yaze PUBLIC ${COCOA_LIBRARY}) endif() -if (WIN32 OR MINGW) - target_link_libraries( - yaze PUBLIC - ${CMAKE_SOURCE_DIR}/build/build-windows/bin/libpng16.dll - zlib - mingw32 - ws2_32) -endif() diff --git a/src/app/core/common.cc b/src/app/core/common.cc index 4cfe1919..0dfc1045 100644 --- a/src/app/core/common.cc +++ b/src/app/core/common.cc @@ -9,11 +9,11 @@ #include #include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" namespace yaze { -namespace app { namespace core { namespace { @@ -59,7 +59,6 @@ unsigned ldle(uint8_t const *const p_arr, unsigned const p_index) { void stle(uint8_t *const p_arr, size_t const p_index, unsigned const p_val) { uint8_t v = (p_val >> (8 * p_index)) & 0xff; - p_arr[p_index] = v; } @@ -91,30 +90,75 @@ uint32_t ldle2(uint8_t const *const p_arr) { return ldle(p_arr, 2); } // Helper function to get the third byte in a little endian number uint32_t ldle3(uint8_t const *const p_arr) { return ldle(p_arr, 3); } +void HandleHexStringParams(const std::string &hex, + const HexStringParams ¶ms) { + std::string result = hex; + switch (params.prefix) { + case HexStringParams::Prefix::kDollar: + result = absl::StrCat("$", result); + break; + case HexStringParams::Prefix::kHash: + result = absl::StrCat("#", result); + break; + case HexStringParams::Prefix::k0x: + result = absl::StrCat("0x", result); + case HexStringParams::Prefix::kNone: + default: + break; + } +} + } // namespace -std::string UppercaseHexByte(uint8_t byte, bool leading) { - if (leading) { - std::string result = absl::StrFormat("0x%02X", byte); - return result; +std::string HexByte(uint8_t byte, HexStringParams params) { + std::string result; + const static std::string kLowerFormat = "%02x"; + const static std::string kUpperFormat = "%02X"; + if (params.uppercase) { + result = absl::StrFormat(kUpperFormat.c_str(), byte); + } else { + result = absl::StrFormat(kLowerFormat.c_str(), byte); } - std::string result = absl::StrFormat("%02X", byte); + HandleHexStringParams(result, params); return result; } -std::string UppercaseHexWord(uint16_t word, bool leading) { - if (leading) { - std::string result = absl::StrFormat("0x%04X", word); - return result; + +std::string HexWord(uint16_t word, HexStringParams params) { + std::string result; + const static std::string kLowerFormat = "%04x"; + const static std::string kUpperFormat = "%04X"; + if (params.uppercase) { + result = absl::StrFormat(kUpperFormat.c_str(), word); + } else { + result = absl::StrFormat(kLowerFormat.c_str(), word); } - std::string result = absl::StrFormat("%04X", word); + HandleHexStringParams(result, params); return result; } -std::string UppercaseHexLong(uint32_t dword) { - std::string result = absl::StrFormat("0x%06X", dword); + +std::string HexLong(uint32_t dword, HexStringParams params) { + std::string result; + const static std::string kLowerFormat = "%06x"; + const static std::string kUpperFormat = "%06X"; + if (params.uppercase) { + result = absl::StrFormat(kUpperFormat.c_str(), dword); + } else { + result = absl::StrFormat(kLowerFormat.c_str(), dword); + } + HandleHexStringParams(result, params); return result; } -std::string UppercaseHexLongLong(uint64_t qword) { - std::string result = absl::StrFormat("0x%08X", qword); + +std::string HexLongLong(uint64_t qword, HexStringParams params) { + std::string result; + const static std::string kLowerFormat = "%08x"; + const static std::string kUpperFormat = "%08X"; + if (params.uppercase) { + result = absl::StrFormat(kUpperFormat.c_str(), qword); + } else { + result = absl::StrFormat(kLowerFormat.c_str(), qword); + } + HandleHexStringParams(result, params); return result; } @@ -127,8 +171,6 @@ bool StringReplace(std::string &str, const std::string &from, return true; } -std::shared_ptr ExperimentFlags::flags_; - uint32_t Get24LocalFromPC(uint8_t *data, int addr, bool pc) { uint32_t ret = (PcToSnes(addr) & 0xFF0000) | (data[addr + 1] << 8) | data[addr]; @@ -317,17 +359,5 @@ void ApplyBpsPatch(const std::vector &source, } } -absl::StatusOr CheckVersion(const char *version) { - std::string version_string = version; - if (version_string != kYazeVersion) { - std::string message = - absl::StrFormat("Yaze version mismatch: expected %s, got %s", - kYazeVersion.data(), version_string.c_str()); - return absl::InvalidArgumentError(message); - } - return version_string; -} - } // namespace core -} // namespace app } // namespace yaze diff --git a/src/app/core/common.h b/src/app/core/common.h index f4f0791d..a09723c9 100644 --- a/src/app/core/common.h +++ b/src/app/core/common.h @@ -7,23 +7,27 @@ #include #include +#include "absl/container/flat_hash_map.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" -#include "absl/container/flat_hash_map.h" namespace yaze { -namespace app { /** - * @namespace yaze::app::core + * @namespace yaze::core * @brief Core application logic and utilities. */ namespace core { -std::string UppercaseHexByte(uint8_t byte, bool leading = false); -std::string UppercaseHexWord(uint16_t word, bool leading = false); -std::string UppercaseHexLong(uint32_t dword); -std::string UppercaseHexLongLong(uint64_t qword); +struct HexStringParams { + enum class Prefix { kNone, kDollar, kHash, k0x } prefix = Prefix::kDollar; + bool uppercase = true; +}; + +std::string HexByte(uint8_t byte, HexStringParams params = {}); +std::string HexWord(uint16_t word, HexStringParams params = {}); +std::string HexLong(uint32_t dword, HexStringParams params = {}); +std::string HexLongLong(uint64_t qword, HexStringParams params = {}); bool StringReplace(std::string &str, const std::string &from, const std::string &to); @@ -33,16 +37,11 @@ bool StringReplace(std::string &str, const std::string &from, * @brief A class to manage experimental feature flags. */ class ExperimentFlags { -public: + public: struct Flags { // Log instructions to the GUI debugger. bool kLogInstructions = true; - // Flag to enable ImGui input config flags. Currently is - // handled manually by controller class but should be - // ported away from that eventually. - bool kUseNewImGuiInput = false; - // Flag to enable the saving of all palettes to the Rom. bool kSaveAllPalettes = false; @@ -97,60 +96,44 @@ public: } overworld; }; - ExperimentFlags() = default; - virtual ~ExperimentFlags() = default; - auto flags() const { - if (!flags_) { - flags_ = std::make_shared(); - } - Flags *flags = flags_.get(); - return flags; - } - Flags *mutable_flags() { - if (!flags_) { - flags_ = std::make_shared(); - } - return flags_.get(); + static Flags &get() { + static Flags instance; + return instance; } + std::string Serialize() const { std::string result; result += - "kLogInstructions: " + std::to_string(flags_->kLogInstructions) + "\n"; + "kLogInstructions: " + std::to_string(get().kLogInstructions) + "\n"; result += - "kUseNewImGuiInput: " + std::to_string(flags_->kUseNewImGuiInput) + + "kSaveAllPalettes: " + std::to_string(get().kSaveAllPalettes) + "\n"; + result += "kSaveGfxGroups: " + std::to_string(get().kSaveGfxGroups) + "\n"; + result += + "kSaveWithChangeQueue: " + std::to_string(get().kSaveWithChangeQueue) + "\n"; - result += - "kSaveAllPalettes: " + std::to_string(flags_->kSaveAllPalettes) + "\n"; - result += - "kSaveGfxGroups: " + std::to_string(flags_->kSaveGfxGroups) + "\n"; - result += "kSaveWithChangeQueue: " + - std::to_string(flags_->kSaveWithChangeQueue) + "\n"; result += "kDrawDungeonRoomGraphics: " + - std::to_string(flags_->kDrawDungeonRoomGraphics) + "\n"; + std::to_string(get().kDrawDungeonRoomGraphics) + "\n"; result += "kNewFileDialogWrapper: " + - std::to_string(flags_->kNewFileDialogWrapper) + "\n"; + std::to_string(get().kNewFileDialogWrapper) + "\n"; result += "kLoadTexturesAsStreaming: " + - std::to_string(flags_->kLoadTexturesAsStreaming) + "\n"; + std::to_string(get().kLoadTexturesAsStreaming) + "\n"; result += - "kSaveDungeonMaps: " + std::to_string(flags_->kSaveDungeonMaps) + "\n"; - result += "kLogToConsole: " + std::to_string(flags_->kLogToConsole) + "\n"; + "kSaveDungeonMaps: " + std::to_string(get().kSaveDungeonMaps) + "\n"; + result += "kLogToConsole: " + std::to_string(get().kLogToConsole) + "\n"; result += "kDrawOverworldSprites: " + - std::to_string(flags_->overworld.kDrawOverworldSprites) + "\n"; + std::to_string(get().overworld.kDrawOverworldSprites) + "\n"; result += "kSaveOverworldMaps: " + - std::to_string(flags_->overworld.kSaveOverworldMaps) + "\n"; + std::to_string(get().overworld.kSaveOverworldMaps) + "\n"; result += "kSaveOverworldEntrances: " + - std::to_string(flags_->overworld.kSaveOverworldEntrances) + "\n"; + std::to_string(get().overworld.kSaveOverworldEntrances) + "\n"; result += "kSaveOverworldExits: " + - std::to_string(flags_->overworld.kSaveOverworldExits) + "\n"; + std::to_string(get().overworld.kSaveOverworldExits) + "\n"; result += "kSaveOverworldItems: " + - std::to_string(flags_->overworld.kSaveOverworldItems) + "\n"; + std::to_string(get().overworld.kSaveOverworldItems) + "\n"; result += "kSaveOverworldProperties: " + - std::to_string(flags_->overworld.kSaveOverworldProperties) + "\n"; + std::to_string(get().overworld.kSaveOverworldProperties) + "\n"; return result; } - -private: - static std::shared_ptr flags_; }; /** @@ -158,8 +141,9 @@ private: * @brief A class to manage a value that can be modified and notify when it * changes. */ -template class NotifyValue { -public: +template +class NotifyValue { + public: NotifyValue() : value_(), modified_(false), temp_value_() {} NotifyValue(const T &value) : value_(value), modified_(false), temp_value_() {} @@ -192,14 +176,14 @@ public: bool modified() const { return modified_; } -private: + private: T value_; bool modified_; T temp_value_; }; static bool log_to_console = false; -static std::string log_file_out = "log.txt"; +static const std::string kLogFileOut = "yaze_log.txt"; template static void logf(const absl::FormatSpec &format, const Args &...args) { @@ -207,39 +191,10 @@ static void logf(const absl::FormatSpec &format, const Args &...args) { if (log_to_console) { std::cout << message << std::endl; } - static std::ofstream fout(log_file_out, std::ios::out | std::ios::app); + static std::ofstream fout(kLogFileOut, std::ios::out | std::ios::app); fout << message << std::endl; } -struct StructuredLog { - std::string raw_message; - std::string category; -}; - -static absl::flat_hash_map> log_categories; - -template -static void logm(const std::string &category, - const absl::FormatSpec &format, const Args &...args) { - std::string message = absl::StrFormat(format, args...); - if (log_to_console) { - std::cout << category << ": " << message << std::endl; - } - if (log_categories.contains(category)) { - log_categories[category].push_back(message); - } else { - log_categories[category] = {message}; - } -} - -class Logger { -public: - static void log(std::string message) { - static std::ofstream fout(log_file_out, std::ios::out | std::ios::app); - fout << message << std::endl; - } -}; - constexpr uint32_t kFastRomRegion = 0x808000; inline uint32_t SnesToPc(uint32_t addr) noexcept { @@ -310,12 +265,7 @@ void ApplyBpsPatch(const std::vector &source, const std::vector &patch, std::vector &target); -constexpr std::string_view kYazeVersion = "0.2.1"; - -absl::StatusOr CheckVersion(const char *version); - -} // namespace core -} // namespace app -} // namespace yaze +} // namespace core +} // namespace yaze #endif diff --git a/src/app/core/controller.cc b/src/app/core/controller.cc index 7da81fa3..23862b92 100644 --- a/src/app/core/controller.cc +++ b/src/app/core/controller.cc @@ -16,40 +16,8 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace core { -namespace { - -constexpr ImGuiWindowFlags kMainEditorFlags = - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_MenuBar | - ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoTitleBar; - -using ImGui::Begin; -using ImGui::End; -using ImGui::GetIO; -using ImGui::NewFrame; -using ImGui::SetNextWindowPos; -using ImGui::SetNextWindowSize; - -void NewMasterFrame() { - const ImGuiIO &io = GetIO(); - ImGui_ImplSDLRenderer2_NewFrame(); - ImGui_ImplSDL2_NewFrame(); - NewFrame(); - SetNextWindowPos(gui::kZeroPos); - ImVec2 dimensions(io.DisplaySize.x, io.DisplaySize.y); - SetNextWindowSize(dimensions, ImGuiCond_Always); - - if (!Begin("##YazeMain", nullptr, kMainEditorFlags)) { - End(); - return; - } -} - -} // namespace - absl::Status Controller::OnEntry(std::string filename) { #if defined(__APPLE__) && defined(__MACH__) #if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1 @@ -68,7 +36,7 @@ absl::Status Controller::OnEntry(std::string filename) { RETURN_IF_ERROR(CreateRenderer()) RETURN_IF_ERROR(CreateGuiContext()) RETURN_IF_ERROR(LoadAudioDevice()) - editor_manager_.SetupScreen(filename); + editor_manager_.Initialize(filename); active_ = true; return absl::OkStatus(); } @@ -81,30 +49,30 @@ void Controller::OnInput() { while (SDL_PollEvent(&event)) { ImGui_ImplSDL2_ProcessEvent(&event); switch (event.type) { - case SDL_KEYDOWN: - case SDL_KEYUP: { - ImGuiIO &io = ImGui::GetIO(); - io.KeyShift = ((SDL_GetModState() & KMOD_SHIFT) != 0); - io.KeyCtrl = ((SDL_GetModState() & KMOD_CTRL) != 0); - io.KeyAlt = ((SDL_GetModState() & KMOD_ALT) != 0); - io.KeySuper = ((SDL_GetModState() & KMOD_GUI) != 0); + case SDL_KEYDOWN: + case SDL_KEYUP: { + ImGuiIO &io = ImGui::GetIO(); + io.KeyShift = ((SDL_GetModState() & KMOD_SHIFT) != 0); + io.KeyCtrl = ((SDL_GetModState() & KMOD_CTRL) != 0); + io.KeyAlt = ((SDL_GetModState() & KMOD_ALT) != 0); + io.KeySuper = ((SDL_GetModState() & KMOD_GUI) != 0); + break; + } + case SDL_WINDOWEVENT: + switch (event.window.event) { + case SDL_WINDOWEVENT_CLOSE: + active_ = false; break; - } - case SDL_WINDOWEVENT: - switch (event.window.event) { - case SDL_WINDOWEVENT_CLOSE: - active_ = false; - break; - case SDL_WINDOWEVENT_SIZE_CHANGED: - io.DisplaySize.x = static_cast(event.window.data1); - io.DisplaySize.y = static_cast(event.window.data2); - break; - default: - break; - } + case SDL_WINDOWEVENT_SIZE_CHANGED: + io.DisplaySize.x = static_cast(event.window.data1); + io.DisplaySize.y = static_cast(event.window.data2); break; default: break; + } + break; + default: + break; } } @@ -125,15 +93,26 @@ absl::Status Controller::OnLoad() { active_ = false; } #if TARGET_OS_IPHONE != 1 - if (platform_ != Platform::kiOS) { - NewMasterFrame(); + constexpr ImGuiWindowFlags kMainEditorFlags = + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_MenuBar | + ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoTitleBar; + + const ImGuiIO &io = ImGui::GetIO(); + ImGui_ImplSDLRenderer2_NewFrame(); + ImGui_ImplSDL2_NewFrame(); + ImGui::NewFrame(); + ImGui::SetNextWindowPos(gui::kZeroPos); + ImVec2 dimensions(io.DisplaySize.x, io.DisplaySize.y); + ImGui::SetNextWindowSize(dimensions, ImGuiCond_Always); + + if (!ImGui::Begin("##YazeMain", nullptr, kMainEditorFlags)) { + ImGui::End(); } #endif RETURN_IF_ERROR(editor_manager_.Update()); #if TARGET_OS_IPHONE != 1 - if (platform_ != Platform::kiOS) { - End(); - } + ImGui::End(); #endif return absl::OkStatus(); } @@ -162,9 +141,6 @@ void Controller::OnExit() { absl::Status Controller::CreateWindow() { auto sdl_flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER; - if (flags()->kUseNewImGuiInput) { - sdl_flags |= SDL_INIT_GAMECONTROLLER; - } if (SDL_Init(sdl_flags) != 0) { return absl::InternalError( @@ -177,11 +153,11 @@ absl::Status Controller::CreateWindow() { int screen_height = display_mode.h * 0.8; window_ = std::unique_ptr( - SDL_CreateWindow("Yet Another Zelda3 Editor", // window title - SDL_WINDOWPOS_UNDEFINED, // initial x position - SDL_WINDOWPOS_UNDEFINED, // initial y position - screen_width, // width, in pixels - screen_height, // height, in pixels + SDL_CreateWindow("Yet Another Zelda3 Editor", // window title + SDL_WINDOWPOS_UNDEFINED, // initial x position + SDL_WINDOWPOS_UNDEFINED, // initial y position + screen_width, // width, in pixels + screen_height, // height, in pixels SDL_WINDOW_RESIZABLE), core::SDL_Deleter()); if (window_ == nullptr) { @@ -202,9 +178,6 @@ absl::Status Controller::CreateGuiContext() { ImGuiIO &io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; - if (flags()->kUseNewImGuiInput) { - io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; - } // Initialize ImGui based on the backend ImGui_ImplSDL2_InitForSDLRenderer(window_.get(), @@ -235,7 +208,7 @@ absl::Status Controller::LoadAudioDevice() { want.format = AUDIO_S16; want.channels = 2; want.samples = 2048; - want.callback = NULL; // Uses the queue + want.callback = NULL; // Uses the queue audio_device_ = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); if (audio_device_ == 0) { return absl::InternalError( @@ -277,6 +250,5 @@ absl::Status Controller::LoadConfigFiles() { return absl::OkStatus(); } -} // namespace core -} // namespace app -} // namespace yaze +} // namespace core +} // namespace yaze diff --git a/src/app/core/controller.h b/src/app/core/controller.h index 726189d2..9d7f3f0b 100644 --- a/src/app/core/controller.h +++ b/src/app/core/controller.h @@ -8,8 +8,8 @@ #include "absl/status/status.h" #include "app/core/platform/renderer.h" #include "app/core/utils/file_util.h" -#include "app/editor/editor_manager.h" #include "app/editor/editor.h" +#include "app/editor/editor_manager.h" #include "imgui/backends/imgui_impl_sdl2.h" #include "imgui/backends/imgui_impl_sdlrenderer2.h" #include "imgui/imconfig.h" @@ -18,7 +18,6 @@ int main(int argc, char **argv); namespace yaze { -namespace app { namespace core { /** @@ -45,7 +44,7 @@ class Controller : public ExperimentFlags { absl::Status LoadConfigFiles(); void SetupScreen(std::string filename = "") { - editor_manager_.SetupScreen(filename); + editor_manager_.Initialize(filename); } auto editor_manager() -> editor::EditorManager & { return editor_manager_; } auto renderer() -> SDL_Renderer * { @@ -54,13 +53,14 @@ class Controller : public ExperimentFlags { auto window() -> SDL_Window * { return window_.get(); } void init_test_editor(editor::Editor *editor) { test_editor_ = editor; } void set_active(bool active) { active_ = active; } + auto active() { return active_; } private: friend int ::main(int argc, char **argv); - bool active_; + bool active_ = false; Platform platform_; - editor::Editor *test_editor_; + editor::Editor *test_editor_ = nullptr; editor::EditorManager editor_manager_; int audio_frequency_ = 48000; @@ -70,7 +70,6 @@ class Controller : public ExperimentFlags { }; } // namespace core -} // namespace app } // namespace yaze #endif // YAZE_APP_CORE_CONTROLLER_H diff --git a/src/app/core/platform/app_delegate.mm b/src/app/core/platform/app_delegate.mm index e03dec3c..7c0237ac 100644 --- a/src/app/core/platform/app_delegate.mm +++ b/src/app/core/platform/app_delegate.mm @@ -206,8 +206,8 @@ } - (void)openFileAction:(id)sender { - if (!yaze::app::SharedRom::shared_rom_ - ->LoadFromFile(yaze::app::core::FileDialogWrapper::ShowOpenFileDialog()) + if (!yaze::SharedRom::shared_rom_ + ->LoadFromFile(yaze::core::FileDialogWrapper::ShowOpenFileDialog()) .ok()) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Error"]; @@ -238,7 +238,7 @@ extern "C" void yaze_initialize_cococa() { extern "C" void yaze_run_cocoa_app_delegate(const char *filename) { yaze_initialize_cococa(); - yaze::app::core::Controller controller; + yaze::core::Controller controller; RETURN_VOID_IF_ERROR(controller.OnEntry(filename)); while (controller.IsActive()) { @autoreleasepool { diff --git a/src/app/core/platform/clipboard.cc b/src/app/core/platform/clipboard.cc index 5607bd0c..99264cb8 100644 --- a/src/app/core/platform/clipboard.cc +++ b/src/app/core/platform/clipboard.cc @@ -4,7 +4,6 @@ #include namespace yaze { -namespace app { namespace core { void CopyImageToClipboard(const std::vector& data) {} @@ -12,5 +11,4 @@ void GetImageFromClipboard(std::vector& data, int& width, int& height) {} } // namespace core -} // namespace app } // namespace yaze \ No newline at end of file diff --git a/src/app/core/platform/clipboard.h b/src/app/core/platform/clipboard.h index 325808ba..ab693926 100644 --- a/src/app/core/platform/clipboard.h +++ b/src/app/core/platform/clipboard.h @@ -5,14 +5,12 @@ #include namespace yaze { -namespace app { namespace core { void CopyImageToClipboard(const std::vector &data); void GetImageFromClipboard(std::vector &data, int &width, int &height); } // namespace core -} // namespace app } // namespace yaze #endif // YAZE_APP_CORE_PLATFORM_CLIPBOARD_H \ No newline at end of file diff --git a/src/app/core/platform/clipboard.mm b/src/app/core/platform/clipboard.mm index e756b29e..cb0967d8 100644 --- a/src/app/core/platform/clipboard.mm +++ b/src/app/core/platform/clipboard.mm @@ -6,7 +6,7 @@ #ifdef TARGET_OS_MAC #import -void yaze::app::core::CopyImageToClipboard(const std::vector& pngData) { +void yaze::core::CopyImageToClipboard(const std::vector& pngData) { NSData* data = [NSData dataWithBytes:pngData.data() length:pngData.size()]; NSImage* image = [[NSImage alloc] initWithData:data]; @@ -15,7 +15,7 @@ void yaze::app::core::CopyImageToClipboard(const std::vector& pngData) [pasteboard writeObjects:@[ image ]]; } -void yaze::app::core::GetImageFromClipboard(std::vector& pixel_data, int& width, int& height) { +void yaze::core::GetImageFromClipboard(std::vector& pixel_data, int& width, int& height) { NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; NSArray* classArray = [NSArray arrayWithObject:[NSImage class]]; NSDictionary* options = [NSDictionary dictionary]; diff --git a/src/app/core/platform/file_dialog.cc b/src/app/core/platform/file_dialog.cc index 1643d1dd..6b552e71 100644 --- a/src/app/core/platform/file_dialog.cc +++ b/src/app/core/platform/file_dialog.cc @@ -7,12 +7,11 @@ #endif // _WIN32 namespace yaze { -namespace app { namespace core { #ifdef _WIN32 - std::string FileDialogWrapper::ShowOpenFileDialog() { +std::string FileDialogWrapper::ShowOpenFileDialog() { CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); IFileDialog *pfd = NULL; @@ -44,7 +43,7 @@ namespace core { return file_path_windows; } - std::string FileDialogWrapper::ShowOpenFolderDialog() { +std::string FileDialogWrapper::ShowOpenFolderDialog() { CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); IFileDialog *pfd = NULL; @@ -141,5 +140,4 @@ std::vector FileDialogWrapper::GetFilesInFolder( #endif } // namespace core -} // namespace app } // namespace yaze \ No newline at end of file diff --git a/src/app/core/platform/file_dialog.h b/src/app/core/platform/file_dialog.h index 6276f86e..3e405aa6 100644 --- a/src/app/core/platform/file_dialog.h +++ b/src/app/core/platform/file_dialog.h @@ -5,7 +5,6 @@ #include namespace yaze { -namespace app { namespace core { class FileDialogWrapper { @@ -28,7 +27,6 @@ class FileDialogWrapper { }; } // namespace core -} // namespace app } // namespace yaze #endif // YAZE_APP_CORE_PLATFORM_FILE_DIALOG_H diff --git a/src/app/core/platform/file_dialog.mm b/src/app/core/platform/file_dialog.mm index 8b11a3d6..3709b44f 100644 --- a/src/app/core/platform/file_dialog.mm +++ b/src/app/core/platform/file_dialog.mm @@ -37,20 +37,18 @@ std::string ShowOpenFileDialogSync() { return result; } -} +} // namespace -std::string yaze::app::core::FileDialogWrapper::ShowOpenFileDialog() { - return ShowOpenFileDialogSync(); -} +std::string yaze::core::FileDialogWrapper::ShowOpenFileDialog() { return ShowOpenFileDialogSync(); } -std::string yaze::app::core::FileDialogWrapper::ShowOpenFolderDialog() { return ""; } +std::string yaze::core::FileDialogWrapper::ShowOpenFolderDialog() { return ""; } -std::vector yaze::app::core::FileDialogWrapper::GetFilesInFolder( +std::vector yaze::core::FileDialogWrapper::GetFilesInFolder( const std::string &folder) { return {}; } -std::vector yaze::app::core::FileDialogWrapper::GetSubdirectoriesInFolder( +std::vector yaze::core::FileDialogWrapper::GetSubdirectoriesInFolder( const std::string &folder) { return {}; } @@ -60,7 +58,7 @@ std::vector yaze::app::core::FileDialogWrapper::GetSubdirectoriesIn #import -std::string yaze::app::core::FileDialogWrapper::ShowOpenFileDialog() { +std::string yaze::core::FileDialogWrapper::ShowOpenFileDialog() { NSOpenPanel* openPanel = [NSOpenPanel openPanel]; [openPanel setCanChooseFiles:YES]; [openPanel setCanChooseDirectories:NO]; @@ -75,7 +73,7 @@ std::string yaze::app::core::FileDialogWrapper::ShowOpenFileDialog() { return ""; } -std::string yaze::app::core::FileDialogWrapper::ShowOpenFolderDialog() { +std::string yaze::core::FileDialogWrapper::ShowOpenFolderDialog() { NSOpenPanel* openPanel = [NSOpenPanel openPanel]; [openPanel setCanChooseFiles:NO]; [openPanel setCanChooseDirectories:YES]; @@ -90,7 +88,7 @@ std::string yaze::app::core::FileDialogWrapper::ShowOpenFolderDialog() { return ""; } -std::vector yaze::app::core::FileDialogWrapper::GetFilesInFolder( +std::vector yaze::core::FileDialogWrapper::GetFilesInFolder( const std::string& folder) { std::vector filenames; NSFileManager* fileManager = [NSFileManager defaultManager]; @@ -106,7 +104,7 @@ std::vector yaze::app::core::FileDialogWrapper::GetFilesInFolder( return filenames; } -std::vector yaze::app::core::FileDialogWrapper::GetSubdirectoriesInFolder( +std::vector yaze::core::FileDialogWrapper::GetSubdirectoriesInFolder( const std::string& folder) { std::vector subdirectories; NSFileManager* fileManager = [NSFileManager defaultManager]; diff --git a/src/app/core/platform/file_path.h b/src/app/core/platform/file_path.h index c06ef3e3..b5bbf87d 100644 --- a/src/app/core/platform/file_path.h +++ b/src/app/core/platform/file_path.h @@ -4,7 +4,6 @@ #include namespace yaze { -namespace app { namespace core { /** @@ -14,7 +13,6 @@ namespace core { std::string GetBundleResourcePath(); } // namespace core -} // namespace app } // namespace yaze #endif // YAZE_APP_CORE_PLATFORM_FILE_PATH_H diff --git a/src/app/core/platform/file_path.mm b/src/app/core/platform/file_path.mm index 617e80cf..0f3dc248 100644 --- a/src/app/core/platform/file_path.mm +++ b/src/app/core/platform/file_path.mm @@ -8,14 +8,14 @@ #include #if TARGET_IPHONE_SIMULATOR == 1 || TARGET_OS_IPHONE == 1 -std::string yaze::app::core::GetBundleResourcePath() { +std::string yaze::core::GetBundleResourcePath() { NSBundle* bundle = [NSBundle mainBundle]; NSString* resourceDirectoryPath = [bundle bundlePath]; NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"]; return [path UTF8String]; } #elif TARGET_OS_MAC == 1 -std::string yaze::app::core::GetBundleResourcePath() { +std::string yaze::core::GetBundleResourcePath() { NSBundle* bundle = [NSBundle mainBundle]; NSString* resourceDirectoryPath = [bundle bundlePath]; NSString* path = [resourceDirectoryPath stringByAppendingString:@"/"]; diff --git a/src/app/core/platform/font_loader.cc b/src/app/core/platform/font_loader.cc index 18d8c3a3..fef22367 100644 --- a/src/app/core/platform/font_loader.cc +++ b/src/app/core/platform/font_loader.cc @@ -1,9 +1,9 @@ #include "app/core/platform/font_loader.h" +#include #include #include #include -#include #include "absl/status/status.h" #include "absl/strings/str_cat.h" @@ -13,7 +13,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace core { absl::Status LoadPackageFonts() { @@ -63,7 +62,8 @@ absl::Status LoadPackageFonts() { "Contents/Resources/font/", font_path); #endif #else - actual_font_path = std::filesystem::absolute(font_path).string(); + actual_font_path = absl::StrCat("assets/font/", font_path); + actual_font_path = std::filesystem::absolute(actual_font_path).string(); #endif if (!io.Fonts->AddFontFromFileTTF(actual_font_path.data(), font_size)) { @@ -86,8 +86,11 @@ absl::Status LoadPackageFonts() { #else actual_icon_font_path = std::filesystem::absolute(icon_font_path).string(); #endif - io.Fonts->AddFontFromFileTTF(actual_icon_font_path.data(), ICON_FONT_SIZE, - &icons_config, icons_ranges); + if (!io.Fonts->AddFontFromFileTTF(actual_icon_font_path.data(), + ICON_FONT_SIZE, &icons_config, + icons_ranges)) { + return absl::InternalError("Failed to load icon fonts"); + } // Merge Japanese font std::string actual_japanese_font_path = ""; @@ -102,8 +105,9 @@ absl::Status LoadPackageFonts() { japanese_font_path); #endif #else + actual_japanese_font_path = absl::StrCat("assets/font/", japanese_font_path); actual_japanese_font_path = - std::filesystem::absolute(japanese_font_path).string(); + std::filesystem::absolute(actual_japanese_font_path).string(); #endif io.Fonts->AddFontFromFileTTF(actual_japanese_font_path.data(), 18.0f, &japanese_font_config, @@ -237,5 +241,4 @@ void LoadSystemFonts() { #endif } // namespace core -} // namespace app -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/core/platform/font_loader.h b/src/app/core/platform/font_loader.h index 685dcc2b..bac2cfe5 100644 --- a/src/app/core/platform/font_loader.h +++ b/src/app/core/platform/font_loader.h @@ -4,14 +4,12 @@ #include "absl/status/status.h" namespace yaze { -namespace app { namespace core { void LoadSystemFonts(); absl::Status LoadPackageFonts(); } // namespace core -} // namespace app } // namespace yaze #endif // YAZE_APP_CORE_PLATFORM_FONTLOADER_H diff --git a/src/app/core/platform/font_loader.mm b/src/app/core/platform/font_loader.mm index 48e0a258..04e63efc 100644 --- a/src/app/core/platform/font_loader.mm +++ b/src/app/core/platform/font_loader.mm @@ -8,13 +8,13 @@ #if TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1 /* iOS */ -void yaze::app::core::LoadSystemFonts() {} +void yaze::core::LoadSystemFonts() {} #elif TARGET_OS_MAC == 1 /* macOS */ #import -void yaze::app::core::LoadSystemFonts() { +void yaze::core::LoadSystemFonts() { NSArray *fontNames = @[ @"Helvetica", @"Times New Roman", @"Courier", @"Arial", @"Verdana" ]; for (NSString *fontName in fontNames) { diff --git a/src/app/core/platform/renderer.h b/src/app/core/platform/renderer.h index 1f173fa9..c39f3c39 100644 --- a/src/app/core/platform/renderer.h +++ b/src/app/core/platform/renderer.h @@ -11,7 +11,6 @@ #include "app/gfx/bitmap.h" namespace yaze { -namespace app { namespace core { /** @@ -77,7 +76,6 @@ class Renderer { }; } // namespace core -} // namespace app } // namespace yaze #endif diff --git a/src/app/core/platform/view_controller.h b/src/app/core/platform/view_controller.h index 7c4318a3..c83c652c 100644 --- a/src/app/core/platform/view_controller.h +++ b/src/app/core/platform/view_controller.h @@ -10,7 +10,7 @@ @end #else @interface AppViewController : UIViewController -@property(nonatomic) yaze::app::core::Controller *controller; +@property(nonatomic) yaze::core::Controller *controller; @property(nonatomic) UIHoverGestureRecognizer *hoverGestureRecognizer; @property(nonatomic) UIPinchGestureRecognizer *pinchRecognizer; @property(nonatomic) UISwipeGestureRecognizer *swipeRecognizer; diff --git a/src/app/core/project.cc b/src/app/core/project.cc index 98051bf2..9eed5206 100644 --- a/src/app/core/project.cc +++ b/src/app/core/project.cc @@ -9,7 +9,6 @@ #include "imgui/misc/cpp/imgui_stdlib.h" namespace yaze { -namespace app { absl::Status Project::Open(const std::string& project_path) { filepath = project_path; @@ -185,5 +184,5 @@ std::string ResourceLabelManager::CreateOrGetLabel( return labels_[type][key]; } -} // namespace app + } // namespace yaze diff --git a/src/app/core/project.h b/src/app/core/project.h index cfe21019..b12cc3a7 100644 --- a/src/app/core/project.h +++ b/src/app/core/project.h @@ -11,7 +11,6 @@ #include "app/core/utils/file_util.h" namespace yaze { -namespace app { const std::string kRecentFilesFilename = "recent_files.txt"; constexpr char kEndOfProjectFile[] = "EndOfProjectFile"; @@ -25,7 +24,7 @@ constexpr char kEndOfProjectFile[] = "EndOfProjectFile"; * user can have different rom file names for a single project and keep track of * backups. */ -struct Project : public core::ExperimentFlags { +struct Project { absl::Status Create(const std::string& project_name) { name = project_name; project_opened_ = true; @@ -130,7 +129,7 @@ class RecentFilesManager { std::vector recent_files_; }; -} // namespace app + } // namespace yaze #endif // YAZE_APP_CORE_PROJECT_H diff --git a/src/app/core/utils/file_util.cc b/src/app/core/utils/file_util.cc index 1b332a9a..6f2be10e 100644 --- a/src/app/core/utils/file_util.cc +++ b/src/app/core/utils/file_util.cc @@ -11,7 +11,6 @@ #include namespace yaze { -namespace app { namespace core { std::string GetFileExtension(const std::string &filename) { @@ -30,8 +29,31 @@ std::string GetFileName(const std::string &filename) { return filename.substr(slash + 1); } -std::string LoadFile(const std::string &filename, Platform platform) { +std::string LoadFile(const std::string &filename) { std::string contents; + std::ifstream file(filename); + if (file.is_open()) { + std::stringstream buffer; + buffer << file.rdbuf(); + contents = buffer.str(); + file.close(); + } else { + // Throw an exception + throw std::runtime_error("Could not open file: " + filename); + } + return contents; +} + +std::string LoadConfigFile(const std::string &filename) { + std::string contents; + Platform platform; +#if defined(_WIN32) + platform = Platform::kWindows; +#elif defined(__APPLE__) + platform = Platform::kMacOS; +#else + platform = Platform::kLinux; +#endif std::string filepath = GetConfigDirectory(platform) + "/" + filename; std::ifstream file(filepath); if (file.is_open()) { @@ -56,19 +78,18 @@ void SaveFile(const std::string &filename, const std::string &contents, std::string GetConfigDirectory(Platform platform) { std::string config_directory = ".yaze"; switch (platform) { - case Platform::kWindows: - config_directory = "~/AppData/Roaming/yaze"; - break; - case Platform::kMacOS: - case Platform::kLinux: - config_directory = "~/.config/yaze"; - break; - default: - break; + case Platform::kWindows: + config_directory = "~/AppData/Roaming/yaze"; + break; + case Platform::kMacOS: + case Platform::kLinux: + config_directory = "~/.config/yaze"; + break; + default: + break; } return config_directory; } -} // namespace core -} // namespace app -} // namespace yaze +} // namespace core +} // namespace yaze diff --git a/src/app/core/utils/file_util.h b/src/app/core/utils/file_util.h index 67dd7ff1..be446313 100644 --- a/src/app/core/utils/file_util.h +++ b/src/app/core/utils/file_util.h @@ -4,21 +4,20 @@ #include namespace yaze { -namespace app { namespace core { enum class Platform { kUnknown, kMacOS, kiOS, kWindows, kLinux }; std::string GetFileExtension(const std::string &filename); std::string GetFileName(const std::string &filename); -std::string LoadFile(const std::string &filename, Platform platform); +std::string LoadFile(const std::string &filename); +std::string LoadConfigFile(const std::string &filename); std::string GetConfigDirectory(Platform platform); void SaveFile(const std::string &filename, const std::string &data, Platform platform); -} // namespace core -} // namespace app -} // namespace yaze +} // namespace core +} // namespace yaze -#endif // YAZE_APP_CORE_UTILS_FILE_UTIL_H +#endif // YAZE_APP_CORE_UTILS_FILE_UTIL_H diff --git a/src/app/core/utils/sdl_deleter.h b/src/app/core/utils/sdl_deleter.h index 8fd4100d..fb0f7383 100644 --- a/src/app/core/utils/sdl_deleter.h +++ b/src/app/core/utils/sdl_deleter.h @@ -4,7 +4,6 @@ #include namespace yaze { -namespace app { namespace core { /** @@ -19,22 +18,17 @@ struct SDL_Deleter { * @brief Deleter for SDL_Texture. */ struct SDL_Texture_Deleter { - void operator()(SDL_Texture *p) const { - SDL_DestroyTexture(p); - } + void operator()(SDL_Texture *p) const { SDL_DestroyTexture(p); } }; /** * @brief Deleter for SDL_Surface. */ struct SDL_Surface_Deleter { - void operator()(SDL_Surface *p) const { - SDL_FreeSurface(p); - } + void operator()(SDL_Surface *p) const { SDL_FreeSurface(p); } }; } // namespace core -} // namespace app } // namespace yaze #endif // YAZE_APP_CORE_UTILS_SDL_DELETER_H_ diff --git a/src/app/editor/code/assembly_editor.cc b/src/app/editor/code/assembly_editor.cc index d833ce05..03f56fe0 100644 --- a/src/app/editor/code/assembly_editor.cc +++ b/src/app/editor/code/assembly_editor.cc @@ -1,13 +1,11 @@ #include "assembly_editor.h" -#include "ImGuiColorTextEdit/TextEditor.h" -#include "ImGuiFileDialog/ImGuiFileDialog.h" #include "absl/strings/str_cat.h" #include "app/core/platform/file_dialog.h" #include "app/gui/icons.h" +#include "app/gui/modules/text_editor.h" namespace yaze { -namespace app { namespace editor { using core::FileDialogWrapper; @@ -278,21 +276,14 @@ void AssemblyEditor::DrawFileTabView() { void AssemblyEditor::DrawFileMenu() { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Open", "Ctrl+O")) { - ImGuiFileDialog::Instance()->OpenDialog( - "ChooseASMFileDlg", "Open ASM file", ".asm,.txt", "."); + auto filename = core::FileDialogWrapper::ShowOpenFileDialog(); + ChangeActiveFile(filename); } if (ImGui::MenuItem("Save", "Ctrl+S")) { // TODO: Implement this } ImGui::EndMenu(); } - - if (ImGuiFileDialog::Instance()->Display("ChooseASMFileDlg")) { - if (ImGuiFileDialog::Instance()->IsOk()) { - ChangeActiveFile(ImGuiFileDialog::Instance()->GetFilePathName()); - } - ImGuiFileDialog::Instance()->Close(); - } } void AssemblyEditor::DrawEditMenu() { @@ -364,5 +355,4 @@ absl::Status AssemblyEditor::Redo() { absl::Status AssemblyEditor::Update() { return absl::OkStatus(); } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/code/assembly_editor.h b/src/app/editor/code/assembly_editor.h index 192f9327..0a1cc5af 100644 --- a/src/app/editor/code/assembly_editor.h +++ b/src/app/editor/code/assembly_editor.h @@ -3,13 +3,12 @@ #include -#include "ImGuiColorTextEdit/TextEditor.h" #include "app/core/common.h" #include "app/editor/editor.h" +#include "app/gui/modules/text_editor.h" #include "app/gui/style.h" namespace yaze { -namespace app { namespace editor { /** @@ -69,7 +68,6 @@ class AssemblyEditor : public Editor { }; } // namespace editor -} // namespace app } // namespace yaze #endif diff --git a/src/app/editor/code/memory_editor.h b/src/app/editor/code/memory_editor.h index 01ecc981..ea5290bc 100644 --- a/src/app/editor/code/memory_editor.h +++ b/src/app/editor/code/memory_editor.h @@ -28,7 +28,6 @@ #include "imgui_memory_editor.h" namespace yaze { -namespace app { namespace editor { using ImGui::SameLine; @@ -78,7 +77,6 @@ struct MemoryEditorWithDiffChecker : public SharedRom { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_CODE_MEMORY_EDITOR_H diff --git a/src/app/editor/dungeon/dungeon_editor.cc b/src/app/editor/dungeon/dungeon_editor.cc index b6b42d3d..0887fffb 100644 --- a/src/app/editor/dungeon/dungeon_editor.cc +++ b/src/app/editor/dungeon/dungeon_editor.cc @@ -10,10 +10,10 @@ #include "app/rom.h" #include "app/zelda3/dungeon/object_names.h" #include "imgui/imgui.h" +#include "imgui_memory_editor.h" #include "zelda3/dungeon/room.h" namespace yaze { -namespace app { namespace editor { using core::Renderer; @@ -72,11 +72,12 @@ absl::Status DungeonEditor::Update() { absl::Status DungeonEditor::Initialize() { auto dungeon_man_pal_group = rom()->palette_group().dungeon_main; + for (int i = 0; i < 0x100 + 40; i++) { - rooms_.emplace_back(zelda3::dungeon::Room(/*room_id=*/i)); + rooms_.emplace_back(zelda3::Room(/*room_id=*/i)); rooms_[i].LoadHeader(); rooms_[i].LoadRoomFromROM(); - if (flags()->kDrawDungeonRoomGraphics) { + if (core::ExperimentFlags::get().kDrawDungeonRoomGraphics) { rooms_[i].LoadRoomGraphics(); } @@ -96,11 +97,11 @@ absl::Status DungeonEditor::Initialize() { LoadDungeonRoomSize(); // LoadRoomEntrances for (int i = 0; i < 0x07; ++i) { - entrances_.emplace_back(zelda3::dungeon::RoomEntrance(*rom(), i, true)); + entrances_.emplace_back(zelda3::RoomEntrance(*rom(), i, true)); } for (int i = 0; i < 0x85; ++i) { - entrances_.emplace_back(zelda3::dungeon::RoomEntrance(*rom(), i, false)); + entrances_.emplace_back(zelda3::RoomEntrance(*rom(), i, false)); } // Load the palette group and palette for the dungeon @@ -117,31 +118,36 @@ absl::Status DungeonEditor::Initialize() { } absl::Status DungeonEditor::RefreshGraphics() { - for (int i = 0; i < 8; i++) { - int block = rooms_[current_room_id_].blocks()[i]; - RETURN_IF_ERROR(graphics_bin_[block].ApplyPaletteWithTransparent( - current_palette_group_[current_palette_id_], 0)); - Renderer::GetInstance().UpdateBitmap(&graphics_bin_[block]); - } + std::for_each_n( + rooms_[current_room_id_].blocks().begin(), 8, + [this](int block) -> absl::Status { + RETURN_IF_ERROR(graphics_bin_[block].ApplyPaletteWithTransparent( + current_palette_group_[current_palette_id_], 0)); + Renderer::GetInstance().UpdateBitmap(&graphics_bin_[block]); + return absl::OkStatus(); + }); + auto sprites_aux1_pal_group = rom()->palette_group().sprites_aux1; - for (int i = 9; i < 16; i++) { - int block = rooms_[current_room_id_].blocks()[i]; - RETURN_IF_ERROR(graphics_bin_[block].ApplyPaletteWithTransparent( - sprites_aux1_pal_group[current_palette_id_], 0)); - Renderer::GetInstance().UpdateBitmap(&graphics_bin_[block]); - } + std::for_each_n( + rooms_[current_room_id_].blocks().begin() + 8, 8, + [this, &sprites_aux1_pal_group](int block) -> absl::Status { + RETURN_IF_ERROR(graphics_bin_[block].ApplyPaletteWithTransparent( + sprites_aux1_pal_group[current_palette_id_], 0)); + Renderer::GetInstance().UpdateBitmap(&graphics_bin_[block]); + return absl::OkStatus(); + }); return absl::OkStatus(); } void DungeonEditor::LoadDungeonRoomSize() { std::map> rooms_by_bank; - for (const auto& room : room_size_addresses_) { + for (const auto &room : room_size_addresses_) { int bank = room.second >> 16; rooms_by_bank[bank].push_back(room.second); } // Process and calculate room sizes within each bank - for (auto& bank_rooms : rooms_by_bank) { + for (auto &bank_rooms : rooms_by_bank) { // Sort the rooms within this bank std::sort(bank_rooms.second.begin(), bank_rooms.second.end()); @@ -151,7 +157,7 @@ void DungeonEditor::LoadDungeonRoomSize() { // Identify the room ID for the current room pointer int room_id = std::find_if(room_size_addresses_.begin(), room_size_addresses_.end(), - [room_ptr](const auto& entry) { + [room_ptr](const auto &entry) { return entry.second == room_ptr; }) ->first; @@ -315,14 +321,14 @@ void DungeonEditor::DrawRoomSelector() { gui::InputHexWord("Room ID", ¤t_room_id_); gui::InputHex("Palette ID", ¤t_palette_id_); - if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)9); + if (ImGuiID child_id = ImGui::GetID((void *)(intptr_t)9); BeginChild(child_id, ImGui::GetContentRegionAvail(), true, ImGuiWindowFlags_AlwaysVerticalScrollbar)) { int i = 0; - for (const auto each_room_name : zelda3::dungeon::kRoomNames) { + for (const auto each_room_name : zelda3::kRoomNames) { rom()->resource_label()->SelectableLabelWithNameEdit( current_room_id_ == i, "Dungeon Room Names", - core::UppercaseHexByte(i), each_room_name.data()); + core::HexByte(i), each_room_name.data()); if (ImGui::IsItemClicked()) { // TODO: Jump to tab if room is already open current_room_id_ = i; @@ -396,8 +402,8 @@ void DungeonEditor::DrawEntranceSelector() { for (int i = 0; i < 0x85 + 7; i++) { rom()->resource_label()->SelectableLabelWithNameEdit( current_entrance_id_ == i, "Dungeon Entrance Names", - core::UppercaseHexByte(i), - zelda3::dungeon::kEntranceNames[i].data()); + core::HexByte(i), + zelda3::kEntranceNames[i].data()); if (ImGui::IsItemClicked()) { current_entrance_id_ = i; @@ -428,12 +434,12 @@ void DungeonEditor::DrawDungeonTabView() { for (int n = 0; n < active_rooms_.Size;) { bool open = true; - if (active_rooms_[n] > sizeof(zelda3::dungeon::kRoomNames) / 4) { + if (active_rooms_[n] > sizeof(zelda3::kRoomNames) / 4) { active_rooms_.erase(active_rooms_.Data + n); continue; } - if (BeginTabItem(zelda3::dungeon::kRoomNames[active_rooms_[n]].data(), + if (BeginTabItem(zelda3::kRoomNames[active_rooms_[n]].data(), &open, ImGuiTabItemFlags_None)) { DrawDungeonCanvas(active_rooms_[n]); EndTabItem(); @@ -514,7 +520,7 @@ void DungeonEditor::DrawRoomGraphics() { void DungeonEditor::DrawTileSelector() { if (BeginTabBar("##TabBar", ImGuiTabBarFlags_FittingPolicyScroll)) { if (BeginTabItem("Room Graphics")) { - if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)3); + if (ImGuiID child_id = ImGui::GetID((void *)(intptr_t)3); BeginChild(child_id, ImGui::GetContentRegionAvail(), true, ImGuiWindowFlags_AlwaysVerticalScrollbar)) { DrawRoomGraphics(); @@ -543,7 +549,7 @@ void DungeonEditor::DrawObjectRenderer() { int selected_object = 0; int i = 0; - for (const auto object_name : zelda3::dungeon::Type1RoomObjectNames) { + for (const auto object_name : zelda3::Type1RoomObjectNames) { if (ImGui::Selectable(object_name.data(), selected_object == i)) { selected_object = i; current_object_ = i; @@ -577,7 +583,7 @@ void DungeonEditor::DrawObjectRenderer() { if (object_loaded_) { ImGui::Begin("Memory Viewer", &object_loaded_, 0); static MemoryEditor mem_edit; - mem_edit.DrawContents((void*)object_renderer_.mutable_memory(), + mem_edit.DrawContents((void *)object_renderer_.mutable_memory(), object_renderer_.mutable_memory()->size()); ImGui::End(); } @@ -586,7 +592,7 @@ void DungeonEditor::DrawObjectRenderer() { // ============================================================================ void DungeonEditor::CalculateUsageStats() { - for (const auto& room : rooms_) { + for (const auto &room : rooms_) { if (blockset_usage_.find(room.blockset) == blockset_usage_.end()) { blockset_usage_[room.blockset] = 1; } else { @@ -608,15 +614,15 @@ void DungeonEditor::CalculateUsageStats() { } void DungeonEditor::RenderSetUsage( - const absl::flat_hash_map& usage_map, uint16_t& selected_set, + const absl::flat_hash_map &usage_map, uint16_t &selected_set, int spriteset_offset) { // Sort the usage map by set number std::vector> sorted_usage(usage_map.begin(), usage_map.end()); std::sort(sorted_usage.begin(), sorted_usage.end(), - [](const auto& a, const auto& b) { return a.first < b.first; }); + [](const auto &a, const auto &b) { return a.first < b.first; }); - for (const auto& [set, count] : sorted_usage) { + for (const auto &[set, count] : sorted_usage) { std::string display_str; if (spriteset_offset != 0x00) { display_str = absl::StrFormat("%#02x, %#02x: %d", set, @@ -637,7 +643,7 @@ namespace { // Range for spritesets 0-0x8F // Range for palettes 0-0x47 template -void RenderUnusedSets(const absl::flat_hash_map& usage_map, int max_set, +void RenderUnusedSets(const absl::flat_hash_map &usage_map, int max_set, int spriteset_offset = 0x00) { std::vector unused_sets; for (int i = 0; i < max_set; i++) { @@ -645,7 +651,7 @@ void RenderUnusedSets(const absl::flat_hash_map& usage_map, int max_set, unused_sets.push_back(i); } } - for (const auto& set : unused_sets) { + for (const auto &set : unused_sets) { if (spriteset_offset != 0x00) { Text("%#02x, %#02x", set, (set + spriteset_offset)); } else { @@ -755,7 +761,7 @@ void DungeonEditor::DrawUsageGrid() { break; } // Determine if this square should be highlighted - const auto& room = rooms_[row * squaresWide + col]; + const auto &room = rooms_[row * squaresWide + col]; // Create a button or selectable for each square ImGui::BeginGroup(); @@ -828,5 +834,4 @@ void DungeonEditor::DrawUsageGrid() { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/dungeon/dungeon_editor.h b/src/app/editor/dungeon/dungeon_editor.h index 4a8aea3d..076150bd 100644 --- a/src/app/editor/dungeon/dungeon_editor.h +++ b/src/app/editor/dungeon/dungeon_editor.h @@ -1,11 +1,11 @@ #ifndef YAZE_APP_EDITOR_DUNGEONEDITOR_H #define YAZE_APP_EDITOR_DUNGEONEDITOR_H -#include "app/core/common.h" #include "absl/container/flat_hash_map.h" +#include "app/core/common.h" +#include "app/editor/editor.h" #include "app/editor/graphics/gfx_group_editor.h" #include "app/editor/graphics/palette_editor.h" -#include "app/editor/editor.h" #include "app/gui/canvas.h" #include "app/rom.h" #include "imgui/imgui.h" @@ -14,7 +14,6 @@ #include "zelda3/dungeon/room_object.h" namespace yaze { -namespace app { namespace editor { constexpr ImGuiTabItemFlags kDungeonTabFlags = @@ -40,9 +39,7 @@ constexpr ImGuiTableFlags kDungeonTableFlags = * tile selector, and object renderer. Additionally, it handles loading room * entrances, calculating usage statistics, and rendering set usage. */ -class DungeonEditor : public Editor, - public SharedRom, - public core::ExperimentFlags { +class DungeonEditor : public Editor, public SharedRom { public: DungeonEditor() { type_ = EditorType::kDungeon; } @@ -120,9 +117,9 @@ class DungeonEditor : public Editor, std::array graphics_bin_; std::vector room_gfx_sheets_; - std::vector rooms_; - std::vector entrances_; - zelda3::dungeon::DungeonObjectRenderer object_renderer_; + std::vector rooms_; + std::vector entrances_; + zelda3::DungeonObjectRenderer object_renderer_; absl::flat_hash_map spriteset_usage_; absl::flat_hash_map blockset_usage_; @@ -143,7 +140,6 @@ class DungeonEditor : public Editor, }; } // namespace editor -} // namespace app } // namespace yaze #endif diff --git a/src/app/editor/editor.cc b/src/app/editor/editor.cc index 44055a1b..2bb00a42 100644 --- a/src/app/editor/editor.cc +++ b/src/app/editor/editor.cc @@ -1,52 +1,7 @@ #include "editor.h" -#include "app/core/constants.h" -#include "imgui/imgui.h" - namespace yaze { -namespace app { namespace editor { -absl::Status DrawEditor(EditorLayoutParams *params) { - if (params->editor == nullptr) { - return absl::InternalError("Editor is not initialized"); - } - - // Draw the editors based on h_split and v_split in a recursive manner - if (params->v_split) { - ImGui::BeginTable("##VerticalSplitTable", 2); - - ImGui::TableNextColumn(); - if (params->left) - RETURN_IF_ERROR(DrawEditor(params->left)); - - ImGui::TableNextColumn(); - if (params->right) - RETURN_IF_ERROR(DrawEditor(params->right)); - - ImGui::EndTable(); - } else if (params->h_split) { - ImGui::BeginTable("##HorizontalSplitTable", 1); - - ImGui::TableNextColumn(); - if (params->top) - RETURN_IF_ERROR(DrawEditor(params->top)); - - ImGui::TableNextColumn(); - if (params->bottom) - RETURN_IF_ERROR(DrawEditor(params->bottom)); - - ImGui::EndTable(); - } else { - // No split, just draw the single editor - ImGui::Text("%s Editor", - kEditorNames[static_cast(params->editor->type())]); - RETURN_IF_ERROR(params->editor->Update()); - } - - return absl::OkStatus(); -} - } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/editor.h b/src/app/editor/editor.h index 685b487b..442681e8 100644 --- a/src/app/editor/editor.h +++ b/src/app/editor/editor.h @@ -11,10 +11,9 @@ #include "app/editor/system/resource_manager.h" namespace yaze { -namespace app { /** - * @namespace yaze::app::editor + * @namespace yaze::editor * @brief Editors are the view controllers for the application. */ namespace editor { @@ -74,32 +73,7 @@ class Editor { EditorContext context_; }; -/** - * @brief Dynamic Editor Layout Parameters - */ -typedef struct EditorLayoutParams { - bool v_split; - bool h_split; - int v_split_pos; - int h_split_pos; - Editor *editor = nullptr; - EditorLayoutParams *left = nullptr; - EditorLayoutParams *right = nullptr; - EditorLayoutParams *top = nullptr; - EditorLayoutParams *bottom = nullptr; - - EditorLayoutParams() { - v_split = false; - h_split = false; - v_split_pos = 0; - h_split_pos = 0; - } -} EditorLayoutParams; - -absl::Status DrawEditor(EditorLayoutParams *params); - } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_CORE_EDITOR_H diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index c1e0eb80..48db5bf2 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -24,7 +24,6 @@ #include "imgui/misc/cpp/imgui_stdlib.h" namespace yaze { -namespace app { namespace editor { using namespace ImGui; @@ -32,8 +31,8 @@ using core::FileDialogWrapper; namespace { -bool BeginCentered(const char* name) { - ImGuiIO const& io = GetIO(); +bool BeginCentered(const char *name) { + ImGuiIO const &io = GetIO(); ImVec2 pos(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGuiWindowFlags flags = @@ -42,19 +41,18 @@ bool BeginCentered(const char* name) { return Begin(name, nullptr, flags); } -bool IsEditorActive(Editor* editor, std::vector& active_editors) { +bool IsEditorActive(Editor *editor, std::vector &active_editors) { return std::find(active_editors.begin(), active_editors.end(), editor) != active_editors.end(); } } // namespace -void EditorManager::SetupScreen(std::string filename) { +void EditorManager::Initialize(std::string filename) { if (!filename.empty()) { PRINT_IF_ERROR(rom()->LoadFromFile(filename)); } - overworld_editor_.InitializeZeml(); - InitializeCommands(); + overworld_editor_.Initialize(); } absl::Status EditorManager::Update() { @@ -66,17 +64,12 @@ absl::Status EditorManager::Update() { DrawInfoPopup(); if (rom()->is_loaded() && !rom_assets_loaded_) { - // Load all of the graphics data from the game. RETURN_IF_ERROR(rom()->LoadAllGraphicsData()) - // Initialize overworld graphics, maps, and palettes RETURN_IF_ERROR(overworld_editor_.LoadGraphics()); rom_assets_loaded_ = true; } - if (dynamic_layout_) - RETURN_IF_ERROR(DrawDynamicLayout()) - else - ManageActiveEditors(); + ManageActiveEditors(); return absl::OkStatus(); } @@ -244,12 +237,6 @@ void EditorManager::ManageActiveEditors() { } } -absl::Status EditorManager::DrawDynamicLayout() { - // Dynamic layout for multiple editors to be open at once - // Allows for tiling and resizing of editors using ImGui - return DrawEditor(&root_layout_); -} - void EditorManager::ManageKeyboardShortcuts() { bool ctrl_or_super = (GetIO().KeyCtrl || GetIO().KeySuper); @@ -309,81 +296,6 @@ void EditorManager::ManageKeyboardShortcuts() { } } -void EditorManager::InitializeCommands() { - if (root_layout_.editor == nullptr) { - root_layout_.editor = &overworld_editor_; - } - - // New editor popup for window management commands - static EditorLayoutParams new_layout; - if (ImGui::BeginPopup("NewEditor")) { - ImGui::Text("New Editor"); - ImGui::Separator(); - if (ImGui::Button("Overworld")) { - new_layout.editor = &overworld_editor_; - ImGui::CloseCurrentPopup(); - } - if (ImGui::Button("Dungeon")) { - new_layout.editor = &dungeon_editor_; - ImGui::CloseCurrentPopup(); - } - if (ImGui::Button("Graphics")) { - new_layout.editor = &graphics_editor_; - ImGui::CloseCurrentPopup(); - } - if (ImGui::Button("Music")) { - new_layout.editor = &music_editor_; - ImGui::CloseCurrentPopup(); - } - if (ImGui::Button("Palette")) { - new_layout.editor = &palette_editor_; - ImGui::CloseCurrentPopup(); - } - if (ImGui::Button("Screen")) { - new_layout.editor = &screen_editor_; - ImGui::CloseCurrentPopup(); - } - if (ImGui::Button("Sprite")) { - new_layout.editor = &sprite_editor_; - ImGui::CloseCurrentPopup(); - } - if (ImGui::Button("Code")) { - new_layout.editor = &assembly_editor_; - ImGui::CloseCurrentPopup(); - } - if (ImGui::Button("Settings")) { - new_layout.editor = &settings_editor_; - ImGui::CloseCurrentPopup(); - } - if (ImGui::Button("Message")) { - new_layout.editor = &message_editor_; - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - - editor_context_.command_manager.RegisterPrefix("window", 'w', - "window management", ""); - editor_context_.command_manager.RegisterSubcommand( - "window", "vsplit", '/', "vertical split", - "split windows vertically and place editor in new window", [this]() { - ImGui::OpenPopup("NewEditor"); - root_layout_.v_split = true; - }); - editor_context_.command_manager.RegisterSubcommand( - "window", "hsplit", '-', "horizontal split", - "split windows horizontally and place editor in new window", [this]() { - ImGui::OpenPopup("NewEditor"); - root_layout_.h_split = true; - }); - editor_context_.command_manager.RegisterSubcommand( - "window", "close", 'd', "close", "close the current editor", [this]() { - if (root_layout_.editor != nullptr) { - root_layout_.editor = nullptr; - } - }); -} - void EditorManager::DrawStatusPopup() { static absl::Status prev_status; if (!status_.ok()) { @@ -415,7 +327,7 @@ void EditorManager::DrawStatusPopup() { void EditorManager::DrawAboutPopup() { if (about_) OpenPopup("About"); if (BeginPopupModal("About", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - Text("Yet Another Zelda3 Editor - v%s", core::kYazeVersion.data()); + Text("Yet Another Zelda3 Editor - v%s", version_.c_str()); Text("Written by: scawful"); Spacing(); Text("Special Thanks: Zarby89, JaredBrian"); @@ -434,7 +346,7 @@ void EditorManager::DrawInfoPopup() { if (BeginPopupModal("ROM Information", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { Text("Title: %s", rom()->title().c_str()); - Text("ROM Size: %s", core::UppercaseHexLongLong(rom()->size()).c_str()); + Text("ROM Size: %s", core::HexLongLong(rom()->size()).c_str()); if (Button("Close", gui::kDefaultModalSize) || IsKeyPressed(ImGuiKey_Escape)) { @@ -457,7 +369,7 @@ void EditorManager::DrawYazeMenu() { show_display_settings = !show_display_settings; } PopStyleColor(); - Text("yaze v%s", core::kYazeVersion.data()); + Text("yaze v%s", version_.c_str()); EndMenuBar(); } @@ -483,7 +395,7 @@ void EditorManager::DrawYazeMenuBar() { if (manager.GetRecentFiles().empty()) { MenuItem("No Recent Files", nullptr, false, false); } else { - for (const auto& filePath : manager.GetRecentFiles()) { + for (const auto &filePath : manager.GetRecentFiles()) { if (MenuItem(filePath.c_str())) { OpenRomOrProject(filePath); } @@ -643,7 +555,6 @@ void EditorManager::DrawYazeMenuBar() { } if (BeginMenu("View")) { - MenuItem("Dynamic Layout", nullptr, &dynamic_layout_); MenuItem("Emulator", nullptr, &show_emulator); Separator(); MenuItem("Memory Editor", nullptr, &show_memory_editor); @@ -772,7 +683,7 @@ void EditorManager::LoadRom() { } void EditorManager::SaveRom() { - if (flags()->kSaveDungeonMaps) { + if (core::ExperimentFlags::get().kSaveDungeonMaps) { status_ = screen_editor_.SaveDungeonMaps(); RETURN_VOID_IF_ERROR(status_); } @@ -783,7 +694,7 @@ void EditorManager::SaveRom() { status_ = rom()->SaveToFile(backup_rom_, save_new_auto_); } -void EditorManager::OpenRomOrProject(const std::string& filename) { +void EditorManager::OpenRomOrProject(const std::string &filename) { if (absl::StrContains(filename, ".yaze")) { status_ = current_project_.Open(filename); if (status_.ok()) { @@ -816,5 +727,4 @@ absl::Status EditorManager::OpenProject() { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/editor_manager.h b/src/app/editor/editor_manager.h index cdb23019..04846fe6 100644 --- a/src/app/editor/editor_manager.h +++ b/src/app/editor/editor_manager.h @@ -19,9 +19,9 @@ #include "app/emu/emulator.h" #include "app/gui/input.h" #include "app/rom.h" +#include "yaze_config.h" namespace yaze { -namespace app { namespace editor { /** @@ -35,7 +35,7 @@ namespace editor { * variable points to the currently active editor in the tab view. * */ -class EditorManager : public SharedRom, public core::ExperimentFlags { +class EditorManager : public SharedRom { public: EditorManager() { current_editor_ = &overworld_editor_; @@ -46,9 +46,13 @@ class EditorManager : public SharedRom, public core::ExperimentFlags { active_editors_.push_back(&sprite_editor_); active_editors_.push_back(&message_editor_); active_editors_.push_back(&screen_editor_); + std::stringstream ss; + ss << YAZE_VERSION_MAJOR << "." << YAZE_VERSION_MINOR << "." + << YAZE_VERSION_PATCH; + ss >> version_; } - void SetupScreen(std::string filename = ""); + void Initialize(std::string filename = ""); absl::Status Update(); auto emulator() -> emu::Emulator & { return emulator_; } @@ -56,10 +60,7 @@ class EditorManager : public SharedRom, public core::ExperimentFlags { private: void ManageActiveEditors(); - absl::Status DrawDynamicLayout(); - void ManageKeyboardShortcuts(); - void InitializeCommands(); void DrawStatusPopup(); void DrawAboutPopup(); @@ -81,16 +82,12 @@ class EditorManager : public SharedRom, public core::ExperimentFlags { bool save_new_auto_ = true; bool show_status_ = false; bool rom_assets_loaded_ = false; - bool dynamic_layout_ = false; + + std::string version_ = ""; absl::Status status_; - emu::Emulator emulator_; - std::vector active_editors_; - std::vector active_layouts_; - - EditorLayoutParams root_layout_; Project current_project_; EditorContext editor_context_; @@ -100,7 +97,7 @@ class EditorManager : public SharedRom, public core::ExperimentFlags { DungeonEditor dungeon_editor_; GraphicsEditor graphics_editor_; MusicEditor music_editor_; - OverworldEditor overworld_editor_; + OverworldEditor overworld_editor_{*rom()}; PaletteEditor palette_editor_; ScreenEditor screen_editor_; SpriteEditor sprite_editor_; @@ -110,7 +107,6 @@ class EditorManager : public SharedRom, public core::ExperimentFlags { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_EDITOR_MANAGER_H diff --git a/src/app/editor/graphics/gfx_group_editor.cc b/src/app/editor/graphics/gfx_group_editor.cc index b3bd3c75..7d984dac 100644 --- a/src/app/editor/graphics/gfx_group_editor.cc +++ b/src/app/editor/graphics/gfx_group_editor.cc @@ -11,7 +11,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { using ImGui::BeginChild; @@ -297,5 +296,4 @@ void GfxGroupEditor::DrawPaletteViewer() { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/graphics/gfx_group_editor.h b/src/app/editor/graphics/gfx_group_editor.h index 14cd4c0c..56a5c4d7 100644 --- a/src/app/editor/graphics/gfx_group_editor.h +++ b/src/app/editor/graphics/gfx_group_editor.h @@ -7,7 +7,6 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace editor { /** @@ -43,6 +42,5 @@ class GfxGroupEditor : public SharedRom { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_GFX_GROUP_EDITOR_H diff --git a/src/app/editor/graphics/graphics_editor.cc b/src/app/editor/graphics/graphics_editor.cc index d43a751b..70bf024a 100644 --- a/src/app/editor/graphics/graphics_editor.cc +++ b/src/app/editor/graphics/graphics_editor.cc @@ -1,9 +1,11 @@ #include "graphics_editor.h" -#include "ImGuiFileDialog/ImGuiFileDialog.h" +#include + #include "absl/status/status.h" #include "absl/status/statusor.h" #include "app/core/platform/clipboard.h" +#include "app/core/platform/file_dialog.h" #include "app/core/platform/renderer.h" #include "app/editor/graphics/palette_editor.h" #include "app/gfx/bitmap.h" @@ -23,7 +25,6 @@ #include "imgui_memory_editor.h" namespace yaze { -namespace app { namespace editor { using core::Renderer; @@ -515,22 +516,19 @@ absl::Status GraphicsEditor::DrawCgxImport() { gui::TextWithSeparators("Cgx Import"); InputInt("BPP", ¤t_bpp_); - InputText("##CGXFile", cgx_file_name_, sizeof(cgx_file_name_)); + InputText("##CGXFile", &cgx_file_name_); SameLine(); - gui::FileDialogPipeline("ImportCgxKey", ".CGX,.cgx\0", "Open CGX", [this]() { - strncpy(cgx_file_path_, - ImGuiFileDialog::Instance()->GetFilePathName().c_str(), - sizeof(cgx_file_path_)); - strncpy(cgx_file_name_, - ImGuiFileDialog::Instance()->GetCurrentFileName().c_str(), - sizeof(cgx_file_name_)); + if (ImGui::Button("Open CGX")) { + auto filename = core::FileDialogWrapper::ShowOpenFileDialog(); + cgx_file_name_ = filename; + cgx_file_path_ = std::filesystem::absolute(filename).string(); is_open_ = true; cgx_loaded_ = true; - }); + } if (ImGui::Button("Copy CGX Path")) { - ImGui::SetClipboardText(cgx_file_path_); + ImGui::SetClipboardText(cgx_file_path_.c_str()); } if (ImGui::Button("Load CGX Data")) { @@ -548,19 +546,15 @@ absl::Status GraphicsEditor::DrawCgxImport() { } absl::Status GraphicsEditor::DrawScrImport() { - InputText("##ScrFile", scr_file_name_, sizeof(scr_file_name_)); + InputText("##ScrFile", &scr_file_name_); - gui::FileDialogPipeline( - "ImportScrKey", ".SCR,.scr,.BAK\0", "Open SCR", [this]() { - strncpy(scr_file_path_, - ImGuiFileDialog::Instance()->GetFilePathName().c_str(), - sizeof(scr_file_path_)); - strncpy(scr_file_name_, - ImGuiFileDialog::Instance()->GetCurrentFileName().c_str(), - sizeof(scr_file_name_)); - is_open_ = true; - scr_loaded_ = true; - }); + if (ImGui::Button("Open SCR")) { + auto filename = core::FileDialogWrapper::ShowOpenFileDialog(); + scr_file_name_ = filename; + scr_file_path_ = std::filesystem::absolute(filename).string(); + is_open_ = true; + scr_loaded_ = true; + } InputInt("SCR Mod", &scr_mod_value_); @@ -584,38 +578,35 @@ absl::Status GraphicsEditor::DrawScrImport() { absl::Status GraphicsEditor::DrawPaletteControls() { gui::TextWithSeparators("COL Import"); - InputText("##ColFile", col_file_name_, sizeof(col_file_name_)); + InputText("##ColFile", &col_file_name_); SameLine(); - gui::FileDialogPipeline( - "ImportColKey", ".COL,.col,.BAK,.bak\0", "Open COL", [this]() { - strncpy(col_file_path_, - ImGuiFileDialog::Instance()->GetFilePathName().c_str(), - sizeof(col_file_path_)); - strncpy(col_file_name_, - ImGuiFileDialog::Instance()->GetCurrentFileName().c_str(), - sizeof(col_file_name_)); - status_ = temp_rom_.LoadFromFile(col_file_path_, - /*z3_load=*/false); - auto col_data_ = gfx::GetColFileData(temp_rom_.data()); - if (col_file_palette_group_.size() != 0) { - col_file_palette_group_.clear(); - } - auto col_file_palette_group_status = - gfx::CreatePaletteGroupFromColFile(col_data_); - if (col_file_palette_group_status.ok()) { - col_file_palette_group_ = col_file_palette_group_status.value(); - } - col_file_palette_ = gfx::SnesPalette(col_data_); + if (ImGui::Button("Open COL")) { + auto filename = core::FileDialogWrapper::ShowOpenFileDialog(); + col_file_name_ = filename; + col_file_path_ = std::filesystem::absolute(filename).string(); + status_ = temp_rom_.LoadFromFile(col_file_path_, + /*z3_load=*/false); + auto col_data_ = gfx::GetColFileData(temp_rom_.mutable_data()); + if (col_file_palette_group_.size() != 0) { + col_file_palette_group_.clear(); + } + auto col_file_palette_group_status = + gfx::CreatePaletteGroupFromColFile(col_data_); + if (col_file_palette_group_status.ok()) { + col_file_palette_group_ = col_file_palette_group_status.value(); + } + col_file_palette_ = gfx::SnesPalette(col_data_); - // gigaleak dev format based code - decoded_col_ = gfx::scad_format::DecodeColFile(col_file_path_); - col_file_ = true; - is_open_ = true; - }); + // gigaleak dev format based code + decoded_col_ = gfx::scad_format::DecodeColFile(col_file_path_); + col_file_ = true; + is_open_ = true; + } + HOVER_HINT(".COL, .BAK"); if (ImGui::Button("Copy Col Path")) { - ImGui::SetClipboardText(col_file_path_); + ImGui::SetClipboardText(col_file_path_.c_str()); } if (rom()->is_loaded()) { @@ -636,17 +627,17 @@ absl::Status GraphicsEditor::DrawPaletteControls() { absl::Status GraphicsEditor::DrawObjImport() { gui::TextWithSeparators("OBJ Import"); - InputText("##ObjFile", obj_file_path_, sizeof(obj_file_path_)); + InputText("##ObjFile", &obj_file_path_); SameLine(); - gui::FileDialogPipeline( - "ImportObjKey", ".obj,.OBJ,.bak,.BAK\0", "Open OBJ", [this]() { - strncpy(file_path_, - ImGuiFileDialog::Instance()->GetFilePathName().c_str(), - sizeof(file_path_)); - status_ = temp_rom_.LoadFromFile(file_path_); - is_open_ = true; - }); + if (ImGui::Button("Open OBJ")) { + auto filename = core::FileDialogWrapper::ShowOpenFileDialog(); + obj_file_path_ = std::filesystem::absolute(filename).string(); + status_ = temp_rom_.LoadFromFile(obj_file_path_); + is_open_ = true; + obj_loaded_ = true; + } + HOVER_HINT(".OBJ, .BAK"); return absl::OkStatus(); } @@ -654,23 +645,22 @@ absl::Status GraphicsEditor::DrawObjImport() { absl::Status GraphicsEditor::DrawTilemapImport() { gui::TextWithSeparators("Tilemap Import"); - InputText("##TMapFile", tilemap_file_path_, sizeof(tilemap_file_path_)); + InputText("##TMapFile", &tilemap_file_path_); SameLine(); - gui::FileDialogPipeline( - "ImportTilemapKey", ".DAT,.dat,.BIN,.bin,.hex,.HEX\0", "Open Tilemap", - [this]() { - strncpy(tilemap_file_path_, - ImGuiFileDialog::Instance()->GetFilePathName().c_str(), - sizeof(tilemap_file_path_)); - status_ = tilemap_rom_.LoadFromFile(tilemap_file_path_); + if (ImGui::Button("Open Tilemap")) { + auto filename = core::FileDialogWrapper::ShowOpenFileDialog(); + tilemap_file_path_ = std::filesystem::absolute(filename).string(); + status_ = tilemap_rom_.LoadFromFile(tilemap_file_path_); + status_ = tilemap_rom_.LoadFromFile(tilemap_file_path_); - // Extract the high and low bytes from the file. - auto decomp_sheet = gfx::lc_lz2::DecompressV2( - tilemap_rom_.data(), gfx::lc_lz2::kNintendoMode1); - tilemap_loaded_ = true; - is_open_ = true; - }); + // Extract the high and low bytes from the file. + auto decomp_sheet = gfx::lc_lz2::DecompressV2(tilemap_rom_.data(), + gfx::lc_lz2::kNintendoMode1); + tilemap_loaded_ = true; + is_open_ = true; + } + HOVER_HINT(".DAT, .BIN, .HEX"); return absl::OkStatus(); } @@ -678,30 +668,30 @@ absl::Status GraphicsEditor::DrawTilemapImport() { absl::Status GraphicsEditor::DrawFileImport() { gui::TextWithSeparators("BIN Import"); - InputText("##ROMFile", file_path_, sizeof(file_path_)); + InputText("##ROMFile", &file_path_); SameLine(); - gui::FileDialogPipeline("ImportDlgKey", ".bin,.hex\0", "Open BIN", [this]() { - strncpy(file_path_, ImGuiFileDialog::Instance()->GetFilePathName().c_str(), - sizeof(file_path_)); + if (ImGui::Button("Open BIN")) { + auto filename = core::FileDialogWrapper::ShowOpenFileDialog(); + file_path_ = filename; status_ = temp_rom_.LoadFromFile(file_path_); is_open_ = true; - }); + } + HOVER_HINT(".BIN, .HEX"); if (Button("Copy File Path")) { - ImGui::SetClipboardText(file_path_); + ImGui::SetClipboardText(file_path_.c_str()); } gui::InputHex("BIN Offset", ¤t_offset_); gui::InputHex("BIN Size", &bin_size_); if (Button("Decompress BIN")) { - if (strlen(file_path_) > 0) { - RETURN_IF_ERROR(DecompressImportData(bin_size_)) - } else { + if (file_path_.empty()) { return absl::InvalidArgumentError( - "Please select a file before importing."); + "Please select a file before decompressing."); } + RETURN_IF_ERROR(DecompressImportData(bin_size_)) } return absl::OkStatus(); @@ -740,13 +730,12 @@ absl::Status GraphicsEditor::DrawClipboardImport() { absl::Status GraphicsEditor::DrawExperimentalFeatures() { gui::TextWithSeparators("Experimental"); if (Button("Decompress Super Donkey Full")) { - if (strlen(file_path_) > 0) { - RETURN_IF_ERROR(DecompressSuperDonkey()) - } else { + if (file_path_.empty()) { return absl::InvalidArgumentError( "Please select `super_donkey_1.bin` before " "importing."); } + RETURN_IF_ERROR(DecompressSuperDonkey()) } ImGui::SetItemTooltip( "Requires `super_donkey_1.bin` to be imported under the " @@ -758,7 +747,8 @@ absl::Status GraphicsEditor::DrawMemoryEditor() { std::string title = "Memory Editor"; if (is_open_) { static MemoryEditor mem_edit; - mem_edit.DrawWindow(title.c_str(), temp_rom_.data(), temp_rom_.size()); + mem_edit.DrawWindow(title.c_str(), temp_rom_.mutable_data(), + temp_rom_.size()); } return absl::OkStatus(); } @@ -844,5 +834,4 @@ absl::Status GraphicsEditor::DecompressSuperDonkey() { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/graphics/graphics_editor.h b/src/app/editor/graphics/graphics_editor.h index 2d6d7a52..34aabd63 100644 --- a/src/app/editor/graphics/graphics_editor.h +++ b/src/app/editor/graphics/graphics_editor.h @@ -4,19 +4,18 @@ #include #include "absl/status/status.h" -#include "app/editor/graphics/palette_editor.h" #include "app/editor/editor.h" +#include "app/editor/graphics/palette_editor.h" #include "app/gfx/bitmap.h" #include "app/gfx/snes_tile.h" -#include "app/gui/modules/asset_browser.h" #include "app/gui/canvas.h" +#include "app/gui/modules/asset_browser.h" #include "app/rom.h" #include "app/zelda3/overworld/overworld.h" #include "imgui/imgui.h" #include "imgui_memory_editor.h" namespace yaze { -namespace app { namespace editor { // "99973","A3D80", @@ -143,16 +142,16 @@ class GraphicsEditor : public SharedRom, public Editor { bool obj_loaded_ = false; bool tilemap_loaded_ = false; - char file_path_[256] = ""; - char col_file_path_[256] = ""; - char col_file_name_[256] = ""; - char cgx_file_path_[256] = ""; - char cgx_file_name_[256] = ""; - char scr_file_path_[256] = ""; - char scr_file_name_[256] = ""; - char obj_file_path_[256] = ""; - char tilemap_file_path_[256] = ""; - char tilemap_file_name_[256] = ""; + std::string file_path_ = ""; + std::string col_file_path_ = ""; + std::string col_file_name_ = ""; + std::string cgx_file_path_ = ""; + std::string cgx_file_name_ = ""; + std::string scr_file_path_ = ""; + std::string scr_file_name_ = ""; + std::string obj_file_path_ = ""; + std::string tilemap_file_path_ = ""; + std::string tilemap_file_name_ = ""; gui::GfxSheetAssetBrowser asset_browser_; @@ -160,7 +159,7 @@ class GraphicsEditor : public SharedRom, public Editor { Rom temp_rom_; Rom tilemap_rom_; - zelda3::overworld::Overworld overworld_; + zelda3::Overworld overworld_; MemoryEditor cgx_memory_editor_; MemoryEditor col_memory_editor_; PaletteEditor palette_editor_; @@ -196,7 +195,6 @@ class GraphicsEditor : public SharedRom, public Editor { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_GRAPHICS_EDITOR_H diff --git a/src/app/editor/graphics/palette_editor.cc b/src/app/editor/graphics/palette_editor.cc index b62b17e7..5669825c 100644 --- a/src/app/editor/graphics/palette_editor.cc +++ b/src/app/editor/graphics/palette_editor.cc @@ -8,7 +8,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { using ImGui::AcceptDragDropPayload; @@ -472,5 +471,4 @@ absl::Status PaletteEditor::ResetColorToOriginal( } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/graphics/palette_editor.h b/src/app/editor/graphics/palette_editor.h index 6f32df6c..775b033a 100644 --- a/src/app/editor/graphics/palette_editor.h +++ b/src/app/editor/graphics/palette_editor.h @@ -14,7 +14,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { namespace palette_internal { @@ -120,7 +119,6 @@ class PaletteEditor : public SharedRom, public Editor { }; } // namespace editor -} // namespace app } // namespace yaze #endif diff --git a/src/app/editor/graphics/screen_editor.cc b/src/app/editor/graphics/screen_editor.cc index a8c55329..52e430b4 100644 --- a/src/app/editor/graphics/screen_editor.cc +++ b/src/app/editor/graphics/screen_editor.cc @@ -13,12 +13,12 @@ #include "app/gfx/snes_tile.h" #include "app/gfx/tilesheet.h" #include "app/gui/canvas.h" +#include "app/gui/color.h" #include "app/gui/icons.h" #include "app/gui/input.h" #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { using core::Renderer; @@ -167,7 +167,7 @@ absl::Status ScreenEditor::LoadDungeonMaps() { gdata[j] = rom()->data()[pc_ptr_gfx++]; } - std::string label = core::UppercaseHexByte(rdata[j]); + std::string label = core::HexByte(rdata[j]); dungeon_map_labels_[d][i][j] = label; } @@ -318,7 +318,7 @@ void ScreenEditor::DrawDungeonMapsTabs() { std::string label = dungeon_map_labels_[selected_dungeon][floor_number][j]; screen_canvas_.DrawText(label, (posX * 2), (posY * 2)); - std::string gfx_id = core::UppercaseHexByte(tile16_id); + std::string gfx_id = core::HexByte(tile16_id); screen_canvas_.DrawText(gfx_id, (posX * 2), (posY * 2) + 16); } } @@ -392,13 +392,30 @@ void ScreenEditor::DrawDungeonMapsEditor() { sheets_.emplace(1, rom()->gfx_sheets()[213]); sheets_.emplace(2, rom()->gfx_sheets()[214]); sheets_.emplace(3, rom()->gfx_sheets()[215]); + int current_tile8 = 0; + int tile_data_offset = 0; + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 32; j++) { + std::vector tile_data(64, 0); // 8x8 tile (64 bytes + int tile_index = current_tile8 + j; + int x = (j % 8) * 8; + int y = (j / 8) * 8; + sheets_[i].Get8x8Tile(tile_index, 0, 0, tile_data, tile_data_offset); + tile8_individual_.emplace_back(gfx::Bitmap(8, 8, 4, tile_data)); + RETURN_VOID_IF_ERROR(tile8_individual_.back().ApplyPalette( + *rom()->mutable_dungeon_palette(3))); + Renderer::GetInstance().RenderBitmap(&tile8_individual_.back()); + } + tile_data_offset = 0; + } dungeon_maps_loaded_ = true; } else { ImGui::Text("Failed to load dungeon map tile16"); } } - if (ImGui::BeginTable("##DungeonMapToolset", 2, ImGuiTableFlags_SizingFixedFit)) { + if (ImGui::BeginTable("##DungeonMapToolset", 2, + ImGuiTableFlags_SizingFixedFit)) { ImGui::TableSetupColumn("Draw Mode"); ImGui::TableSetupColumn("Edit Mode"); @@ -470,8 +487,13 @@ void ScreenEditor::DrawDungeonMapsEditor() { } ImGui::Separator(); - current_tile_canvas_.DrawBackground(ImVec2(64 * 2 + 2, 64 * 2 + 4)); + current_tile_canvas_ + .DrawBackground(); // ImVec2(64 * 2 + 2, 64 * 2 + 4)); current_tile_canvas_.DrawContextMenu(); + if (current_tile_canvas_.DrawTilePainter( + tile8_individual_[selected_tile8_], 16)) { + // Modify the tile16 based on the selected tile and current_tile16_info + } current_tile_canvas_.DrawBitmap(tile16_individual_[selected_tile16_], 2, 4.0f); current_tile_canvas_.DrawGrid(16.f); @@ -538,8 +560,10 @@ void ScreenEditor::LoadBinaryGfx() { gfx_sheets.emplace_back(converted_bin.begin() + (i * 0x1000), converted_bin.begin() + ((i + 1) * 0x1000)); sheets_.emplace(i, gfx::Bitmap(128, 32, 8, gfx_sheets[i])); - sheets_[i].ApplyPalette(*rom()->mutable_dungeon_palette(3)); - Renderer::GetInstance().RenderBitmap(&sheets_[i]); + status_ = sheets_[i].ApplyPalette(*rom()->mutable_dungeon_palette(3)); + if (status_.ok()) { + Renderer::GetInstance().RenderBitmap(&sheets_[i]); + } } binary_gfx_loaded_ = true; } else { @@ -589,5 +613,4 @@ void ScreenEditor::DrawToolset() { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/graphics/screen_editor.h b/src/app/editor/graphics/screen_editor.h index 324c9a6b..563ae40b 100644 --- a/src/app/editor/graphics/screen_editor.h +++ b/src/app/editor/graphics/screen_editor.h @@ -15,7 +15,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { /** @@ -33,7 +32,7 @@ namespace editor { * The class inherits from the SharedRom class. */ class ScreenEditor : public SharedRom, public Editor { -public: + public: ScreenEditor() { screen_canvas_.SetCanvasSize(ImVec2(512, 512)); type_ = EditorType::kScreen; @@ -50,7 +49,7 @@ public: absl::Status SaveDungeonMaps(); -private: + private: void DrawTitleScreenEditor(); void DrawNamingScreenEditor(); void DrawOverworldMapEditor(); @@ -86,12 +85,13 @@ private: bool copy_button_pressed = false; bool paste_button_pressed = false; - std::vector all_gfx_; + std::array current_tile16_data_; std::unordered_map tile16_individual_; + std::vector tile8_individual_; + std::vector all_gfx_; + std::vector gfx_bin_data_; std::vector dungeon_maps_; std::vector>> dungeon_map_labels_; - std::array current_tile16_data_; - std::vector gfx_bin_data_; absl::Status status_; @@ -100,18 +100,17 @@ private: gfx::Tilesheet tile16_sheet_; gfx::InternalTile16 current_tile16_info; - gui::Canvas current_tile_canvas_{"##CurrentTileCanvas"}; + gui::Canvas current_tile_canvas_{"##CurrentTileCanvas", ImVec2(32, 32), + gui::CanvasGridSize::k16x16, 2.0f}; gui::Canvas screen_canvas_; gui::Canvas tilesheet_canvas_; - gui::Canvas tilemap_canvas_{"##TilemapCanvas", - ImVec2(128 + 2, (192) + 4), + gui::Canvas tilemap_canvas_{"##TilemapCanvas", ImVec2(128 + 2, (192) + 4), gui::CanvasGridSize::k8x8, 2.f}; zelda3::screen::Inventory inventory_; }; -} // namespace editor -} // namespace app -} // namespace yaze +} // namespace editor +} // namespace yaze #endif diff --git a/src/app/editor/graphics/tile16_editor.cc b/src/app/editor/graphics/tile16_editor.cc index 4b25faf0..465b1b67 100644 --- a/src/app/editor/graphics/tile16_editor.cc +++ b/src/app/editor/graphics/tile16_editor.cc @@ -2,12 +2,12 @@ #include -#include "ImGuiFileDialog/ImGuiFileDialog.h" #include "absl/status/status.h" #include "absl/status/statusor.h" +#include "app/core/platform/file_dialog.h" #include "app/core/platform/renderer.h" -#include "app/editor/graphics/palette_editor.h" #include "app/editor/editor.h" +#include "app/editor/graphics/palette_editor.h" #include "app/gfx/bitmap.h" #include "app/gfx/snes_palette.h" #include "app/gfx/snes_tile.h" @@ -21,7 +21,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { using core::Renderer; @@ -52,7 +51,7 @@ using ImGui::Text; absl::Status Tile16Editor::InitBlockset( const gfx::Bitmap& tile16_blockset_bmp, const gfx::Bitmap& current_gfx_bmp, const std::vector& tile16_individual, - uint8_t all_tiles_types[0x200]) { + std::array& all_tiles_types) { all_tiles_types_ = all_tiles_types; tile16_blockset_bmp_ = tile16_blockset_bmp; tile16_individual_ = tile16_individual; @@ -60,7 +59,7 @@ absl::Status Tile16Editor::InitBlockset( RETURN_IF_ERROR(LoadTile8()); ImVector tile16_names; for (int i = 0; i < 0x200; ++i) { - std::string str = core::UppercaseHexByte(all_tiles_types_[i]); + std::string str = core::HexByte(all_tiles_types_[i]); tile16_names.push_back(str); } @@ -251,7 +250,7 @@ absl::Status Tile16Editor::DrawTileEditControls() { gui::InputHexByte("Palette", ¬ify_palette.mutable_get()); notify_palette.apply_changes(); if (notify_palette.modified()) { - auto palette = palettesets_[current_palette_].main; + auto palette = palettesets_[current_palette_].main_; auto value = notify_palette.get(); if (notify_palette.get() > 0x04 && notify_palette.get() < 0x06) { palette = palettesets_[current_palette_].aux1; @@ -364,16 +363,10 @@ absl::Status Tile16Editor::UpdateTile16Transfer() { absl::Status Tile16Editor::UpdateTransferTileCanvas() { // Create a button for loading another ROM if (Button("Load ROM")) { - ImGuiFileDialog::Instance()->OpenDialog( - "ChooseTransferFileDlgKey", "Open Transfer ROM", ".sfc,.smc", "."); + auto file_name = core::FileDialogWrapper::ShowOpenFileDialog(); + transfer_status_ = transfer_rom_.LoadFromFile(file_name); + transfer_started_ = true; } - gui::FileDialogPipeline( - "ChooseTransferFileDlgKey", ".sfc,.smc", std::nullopt, [&]() { - std::string filePathName = - ImGuiFileDialog::Instance()->GetFilePathName(); - transfer_status_ = transfer_rom_.LoadFromFile(filePathName); - transfer_started_ = true; - }); // TODO: Implement tile16 transfer if (transfer_started_ && !transfer_blockset_loaded_) { @@ -400,5 +393,4 @@ absl::Status Tile16Editor::UpdateTransferTileCanvas() { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/graphics/tile16_editor.h b/src/app/editor/graphics/tile16_editor.h index 9889ec61..278dac0c 100644 --- a/src/app/editor/graphics/tile16_editor.h +++ b/src/app/editor/graphics/tile16_editor.h @@ -13,18 +13,17 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { /** * @brief Popup window to edit Tile16 data */ -class Tile16Editor : public GfxContext, public SharedRom { +class Tile16Editor : public gfx::GfxContext, public SharedRom { public: absl::Status InitBlockset(const gfx::Bitmap& tile16_blockset_bmp, const gfx::Bitmap& current_gfx_bmp, const std::vector& tile16_individual, - uint8_t all_tiles_types[0x200]); + std::array& all_tiles_types); absl::Status Update(); absl::Status DrawMenu(); @@ -63,7 +62,7 @@ class Tile16Editor : public GfxContext, public SharedRom { bool priority_tile; int tile_size; - uint8_t* all_tiles_types_; + std::array all_tiles_types_; // Tile16 blockset for selecting the tile to edit gui::Canvas blockset_canvas_{"blocksetCanvas", ImVec2(0x100, 0x4000), @@ -91,7 +90,7 @@ class Tile16Editor : public GfxContext, public SharedRom { PaletteEditor palette_editor_; gfx::SnesPalette palette_; - zelda3::overworld::Overworld transfer_overworld_; + zelda3::Overworld transfer_overworld_; absl::Status status_; @@ -100,6 +99,5 @@ class Tile16Editor : public GfxContext, public SharedRom { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_TILE16EDITOR_H diff --git a/src/app/editor/message/message_data.cc b/src/app/editor/message/message_data.cc index 4e00286d..a3931912 100644 --- a/src/app/editor/message/message_data.cc +++ b/src/app/editor/message/message_data.cc @@ -3,7 +3,6 @@ #include "app/core/common.h" namespace yaze { -namespace app { namespace editor { uint8_t FindMatchingCharacter(char value) { @@ -145,7 +144,7 @@ std::vector ParseMessageToData(std::string str) { return bytes; } -std::vector BuildDictionaryEntries(app::Rom* rom) { +std::vector BuildDictionaryEntries(Rom* rom) { std::vector AllDictionaries; for (int i = 0; i < kNumDictionaryEntries; i++) { std::vector bytes; @@ -178,5 +177,4 @@ std::vector BuildDictionaryEntries(app::Rom* rom) { } } // namespace editor -} // namespace app } // namespace yaze \ No newline at end of file diff --git a/src/app/editor/message/message_data.h b/src/app/editor/message/message_data.h index 4934d057..d8bf05f0 100644 --- a/src/app/editor/message/message_data.h +++ b/src/app/editor/message/message_data.h @@ -11,7 +11,6 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace editor { const uint8_t kMessageTerminator = 0x7F; @@ -78,7 +77,7 @@ constexpr int kTextDataEnd = 0xE7FFF; constexpr int kNumDictionaryEntries = 97; constexpr int kPointersDictionaries = 0x74703; -std::vector BuildDictionaryEntries(app::Rom* rom); +std::vector BuildDictionaryEntries(Rom* rom); std::string ReplaceAllDictionaryWords(std::string str, std::vector dictionary); @@ -278,7 +277,6 @@ ParsedElement FindMatchingElement(const std::string& str); std::string ParseTextDataByte(uint8_t value); } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_MESSAGE_MESSAGE_DATA_H diff --git a/src/app/editor/message/message_editor.cc b/src/app/editor/message/message_editor.cc index 31cee107..492c961a 100644 --- a/src/app/editor/message/message_editor.cc +++ b/src/app/editor/message/message_editor.cc @@ -15,9 +15,9 @@ #include "app/gui/style.h" #include "app/rom.h" #include "imgui.h" +#include "imgui/misc/cpp/imgui_stdlib.h" namespace yaze { -namespace app { namespace editor { using core::Renderer; @@ -88,7 +88,7 @@ absl::Status MessageEditor::Initialize() { for (const auto& each_message : list_of_texts_) { std::cout << "Message #" << each_message.ID << " at address " - << core::UppercaseHexLong(each_message.Address) << std::endl; + << core::HexLong(each_message.Address) << std::endl; std::cout << " " << each_message.RawString << std::endl; // Each string has a [:XX] char encoded @@ -162,10 +162,6 @@ absl::Status MessageEditor::Update() { } void MessageEditor::DrawMessageList() { - if (InputText("Search", &search_text_)) { - // TODO: ImGui style text filtering - } - if (BeginChild("##MessagesList", ImVec2(0, 0), true, ImGuiWindowFlags_AlwaysVerticalScrollbar)) { if (BeginTable("##MessagesTable", 3, kMessageTableFlags)) { @@ -177,7 +173,7 @@ void MessageEditor::DrawMessageList() { for (const auto& message : list_of_texts_) { TableNextColumn(); - if (Button(core::UppercaseHexWord(message.ID).c_str())) { + if (Button(core::HexWord(message.ID).c_str())) { current_message_ = message; DrawMessagePreview(); } @@ -186,7 +182,7 @@ void MessageEditor::DrawMessageList() { TableNextColumn(); TextWrapped( "%s", - core::UppercaseHexLong(list_of_texts_[message.ID].Address).c_str()); + core::HexLong(list_of_texts_[message.ID].Address).c_str()); } EndTable(); @@ -256,7 +252,7 @@ void MessageEditor::DrawDictionary() { for (const auto& dictionary : all_dictionaries_) { TableNextColumn(); - Text("%s", core::UppercaseHexWord(dictionary.ID).c_str()); + Text("%s", core::HexWord(dictionary.ID).c_str()); TableNextColumn(); Text("%s", dictionary.Contents.c_str()); } @@ -336,13 +332,14 @@ void MessageEditor::ReadAllTextDataV2() { current_raw_message.append("["); current_raw_message.append(DICTIONARYTOKEN); current_raw_message.append(":"); - current_raw_message.append(core::UppercaseHexWord(dictionary)); + current_raw_message.append(core::HexWord(dictionary)); current_raw_message.append("]"); uint32_t address = core::Get24LocalFromPC( - rom()->data(), kPointersDictionaries + (dictionary * 2)); + rom()->mutable_data(), kPointersDictionaries + (dictionary * 2)); uint32_t address_end = core::Get24LocalFromPC( - rom()->data(), kPointersDictionaries + ((dictionary + 1) * 2)); + rom()->mutable_data(), + kPointersDictionaries + ((dictionary + 1) * 2)); for (uint32_t i = address; i < address_end; i++) { parsed_message.push_back(rom()->data()[i]); @@ -435,13 +432,13 @@ void MessageEditor::ReadAllTextData() { current_message_raw.append("["); current_message_raw.append(DICTIONARYTOKEN); current_message_raw.append(":"); - current_message_raw.append(core::UppercaseHexWord(dictionary)); + current_message_raw.append(core::HexWord(dictionary)); current_message_raw.append("]"); uint32_t address = core::Get24LocalFromPC( - rom()->data(), kPointersDictionaries + (dictionary * 2)); + rom()->mutable_data(), kPointersDictionaries + (dictionary * 2)); uint32_t address_end = core::Get24LocalFromPC( - rom()->data(), kPointersDictionaries + ((dictionary + 1) * 2)); + rom()->mutable_data(), kPointersDictionaries + ((dictionary + 1) * 2)); for (uint32_t i = address; i < address_end; i++) { temp_bytes_parsed.push_back(rom()->data()[i]); @@ -703,5 +700,4 @@ void MessageEditor::SelectAll() { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/message/message_editor.h b/src/app/editor/message/message_editor.h index b75e471b..93c8c294 100644 --- a/src/app/editor/message/message_editor.h +++ b/src/app/editor/message/message_editor.h @@ -12,7 +12,6 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace editor { constexpr int kGfxFont = 0x70000; // 2bpp format @@ -163,7 +162,6 @@ class MessageEditor : public Editor, public SharedRom { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_MESSAGE_EDITOR_H diff --git a/src/app/editor/music/music_editor.cc b/src/app/editor/music/music_editor.cc index 47001c94..f78aeefd 100644 --- a/src/app/editor/music/music_editor.cc +++ b/src/app/editor/music/music_editor.cc @@ -9,7 +9,6 @@ #include "app/gui/input.h" namespace yaze { -namespace app { namespace editor { absl::Status MusicEditor::Update() { @@ -218,5 +217,4 @@ void MusicEditor::DrawToolset() { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/music/music_editor.h b/src/app/editor/music/music_editor.h index b82af24a..d1ee9260 100644 --- a/src/app/editor/music/music_editor.h +++ b/src/app/editor/music/music_editor.h @@ -12,7 +12,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { static const char* kGameSongs[] = {"Title", @@ -85,7 +84,6 @@ class MusicEditor : public SharedRom, public Editor { }; } // namespace editor -} // namespace app } // namespace yaze #endif diff --git a/src/app/editor/overworld/entity.cc b/src/app/editor/overworld/entity.cc index 785a9012..4f0cd998 100644 --- a/src/app/editor/overworld/entity.cc +++ b/src/app/editor/overworld/entity.cc @@ -5,7 +5,6 @@ #include "app/gui/style.h" namespace yaze { -namespace app { namespace editor { using ImGui::BeginChild; @@ -92,7 +91,7 @@ void HandleEntityDragging(zelda3::GameEntity *entity, ImVec2 canvas_p0, ImGui::SetDragDropPayload("ENTITY_PAYLOAD", &entity, sizeof(zelda3::GameEntity)); Text("Moving %s ID: %s", entity_type.c_str(), - core::UppercaseHexByte(entity->entity_id_).c_str()); + core::HexByte(entity->entity_id_).c_str()); ImGui::EndDragDropSource(); } MoveEntityOnGrid(dragged_entity, canvas_p0, scrolling, free_movement); @@ -128,7 +127,7 @@ bool DrawEntranceInserterPopup() { // TODO: Implement deleting OverworldEntrance objects, currently only hides them bool DrawOverworldEntrancePopup( - zelda3::overworld::OverworldEntrance &entrance) { + zelda3::OverworldEntrance &entrance) { static bool set_done = false; if (set_done) { set_done = false; @@ -178,7 +177,7 @@ void DrawExitInserterPopup() { } } -bool DrawExitEditorPopup(zelda3::overworld::OverworldExit &exit) { +bool DrawExitEditorPopup(zelda3::OverworldExit &exit) { static bool set_done = false; if (set_done) { set_done = false; @@ -317,8 +316,8 @@ void DrawItemInsertPopup() { Text("Add Item"); BeginChild("ScrollRegion", ImVec2(150, 150), true, ImGuiWindowFlags_AlwaysVerticalScrollbar); - for (size_t i = 0; i < zelda3::overworld::kSecretItemNames.size(); i++) { - if (Selectable(zelda3::overworld::kSecretItemNames[i].c_str(), + for (size_t i = 0; i < zelda3::kSecretItemNames.size(); i++) { + if (Selectable(zelda3::kSecretItemNames[i].c_str(), i == new_item_id)) { new_item_id = i; } @@ -341,7 +340,7 @@ void DrawItemInsertPopup() { } // TODO: Implement deleting OverworldItem objects, currently only hides them -bool DrawItemEditorPopup(zelda3::overworld::OverworldItem &item) { +bool DrawItemEditorPopup(zelda3::OverworldItem &item) { static bool set_done = false; if (set_done) { set_done = false; @@ -351,8 +350,8 @@ bool DrawItemEditorPopup(zelda3::overworld::OverworldItem &item) { BeginChild("ScrollRegion", ImVec2(150, 150), true, ImGuiWindowFlags_AlwaysVerticalScrollbar); ImGui::BeginGroup(); - for (size_t i = 0; i < zelda3::overworld::kSecretItemNames.size(); i++) { - if (Selectable(zelda3::overworld::kSecretItemNames[i].c_str(), + for (size_t i = 0; i < zelda3::kSecretItemNames.size(); i++) { + if (Selectable(zelda3::kSecretItemNames[i].c_str(), item.id_ == i)) { item.id_ = i; } @@ -489,5 +488,4 @@ bool DrawSpriteEditorPopup(zelda3::Sprite &sprite) { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/overworld/entity.h b/src/app/editor/overworld/entity.h index 7a2904a3..f38e3d78 100644 --- a/src/app/editor/overworld/entity.h +++ b/src/app/editor/overworld/entity.h @@ -7,7 +7,6 @@ #include "app/zelda3/overworld/overworld.h" namespace yaze { -namespace app { namespace editor { bool IsMouseHoveringOverEntity(const zelda3::GameEntity &entity, @@ -23,14 +22,14 @@ void HandleEntityDragging(zelda3::GameEntity *entity, ImVec2 canvas_p0, bool free_movement = false); bool DrawEntranceInserterPopup(); -bool DrawOverworldEntrancePopup(zelda3::overworld::OverworldEntrance &entrance); +bool DrawOverworldEntrancePopup(zelda3::OverworldEntrance &entrance); void DrawExitInserterPopup(); -bool DrawExitEditorPopup(zelda3::overworld::OverworldExit &exit); +bool DrawExitEditorPopup(zelda3::OverworldExit &exit); void DrawItemInsertPopup(); -bool DrawItemEditorPopup(zelda3::overworld::OverworldItem &item); +bool DrawItemEditorPopup(zelda3::OverworldItem &item); enum MyItemColumnID { MyItemColumnID_ID, @@ -81,7 +80,6 @@ void DrawSpriteInserterPopup(); bool DrawSpriteEditorPopup(zelda3::Sprite &sprite); } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_OVERWORLD_ENTITY_H diff --git a/src/app/editor/overworld/overworld_editor.cc b/src/app/editor/overworld/overworld_editor.cc index fdc7a64e..285027a3 100644 --- a/src/app/editor/overworld/overworld_editor.cc +++ b/src/app/editor/overworld/overworld_editor.cc @@ -15,20 +15,21 @@ #include "app/gfx/bitmap.h" #include "app/gfx/snes_palette.h" #include "app/gui/canvas.h" +#include "app/gui/color.h" #include "app/gui/icons.h" #include "app/gui/input.h" #include "app/gui/style.h" #include "app/gui/zeml.h" #include "app/rom.h" +#include "app/zelda3/common.h" #include "app/zelda3/overworld/overworld.h" #include "imgui/imgui.h" +#include "imgui_memory_editor.h" namespace yaze { -namespace app { namespace editor { using core::Renderer; - using ImGui::BeginChild; using ImGui::BeginTabBar; using ImGui::BeginTabItem; @@ -58,11 +59,11 @@ constexpr int kTile16Size = 0x10; absl::Status OverworldEditor::Update() { status_ = absl::OkStatus(); - if (rom()->is_loaded() && !all_gfx_loaded_) { + if (rom_.is_loaded() && !all_gfx_loaded_) { RETURN_IF_ERROR(tile16_editor_.InitBlockset( tile16_blockset_bmp_, current_gfx_bmp_, tile16_individual_, *overworld_.mutable_all_tiles_types())); - RETURN_IF_ERROR(LoadEntranceTileTypes(*rom())); + ASSIGN_OR_RETURN(entrance_tiletypes_, zelda3::LoadEntranceTileTypes(rom_)); all_gfx_loaded_ = true; } @@ -96,125 +97,106 @@ void OverworldEditor::DrawToolset() { static bool show_gfx_group = false; static bool show_properties = false; - if (BeginTable("OWToolset", 22, kToolsetTableFlags, ImVec2(0, 0))) { - for (const auto &name : kToolsetColumnNames) - ImGui::TableSetupColumn(name.data()); - - NEXT_COLUMN() - if (Button(ICON_MD_UNDO)) { - status_ = Undo(); - } - - NEXT_COLUMN() - if (Button(ICON_MD_REDO)) { - status_ = Redo(); - } - - TEXT_COLUMN(ICON_MD_MORE_VERT) // Separator - - NEXT_COLUMN() - if (Button(ICON_MD_ZOOM_OUT)) { - ow_map_canvas_.ZoomOut(); - } - - NEXT_COLUMN() - if (Button(ICON_MD_ZOOM_IN)) { - ow_map_canvas_.ZoomIn(); - } - - NEXT_COLUMN() - if (Button(ICON_MD_OPEN_IN_FULL)) { - overworld_canvas_fullscreen_ = !overworld_canvas_fullscreen_; - } - HOVER_HINT("Fullscreen Canvas") - - TEXT_COLUMN(ICON_MD_MORE_VERT) // Separator - - NEXT_COLUMN() - if (Selectable(ICON_MD_PAN_TOOL_ALT, current_mode == EditingMode::PAN)) { - current_mode = EditingMode::PAN; - ow_map_canvas_.set_draggable(true); - } - HOVER_HINT("Pan (Right click and drag)") - - NEXT_COLUMN() - if (Selectable(ICON_MD_DRAW, current_mode == EditingMode::DRAW_TILE)) { - current_mode = EditingMode::DRAW_TILE; - } - HOVER_HINT("Draw Tile") - - NEXT_COLUMN() - if (Selectable(ICON_MD_DOOR_FRONT, current_mode == EditingMode::ENTRANCES)) - current_mode = EditingMode::ENTRANCES; - HOVER_HINT("Entrances") - - NEXT_COLUMN() - if (Selectable(ICON_MD_DOOR_BACK, current_mode == EditingMode::EXITS)) - current_mode = EditingMode::EXITS; - HOVER_HINT("Exits") - - NEXT_COLUMN() - if (Selectable(ICON_MD_GRASS, current_mode == EditingMode::ITEMS)) - current_mode = EditingMode::ITEMS; - HOVER_HINT("Items") - - NEXT_COLUMN() - if (Selectable(ICON_MD_PEST_CONTROL_RODENT, - current_mode == EditingMode::SPRITES)) - current_mode = EditingMode::SPRITES; - HOVER_HINT("Sprites") - - NEXT_COLUMN() - if (Selectable(ICON_MD_ADD_LOCATION, - current_mode == EditingMode::TRANSPORTS)) - current_mode = EditingMode::TRANSPORTS; - HOVER_HINT("Transports") - - NEXT_COLUMN() - if (Selectable(ICON_MD_MUSIC_NOTE, current_mode == EditingMode::MUSIC)) - current_mode = EditingMode::MUSIC; - HOVER_HINT("Music") - - TableNextColumn(); - if (Button(ICON_MD_GRID_VIEW)) { - show_tile16_editor_ = !show_tile16_editor_; - } - HOVER_HINT("Tile16 Editor") - - TableNextColumn(); - if (Button(ICON_MD_TABLE_CHART)) { - show_gfx_group = !show_gfx_group; - } - HOVER_HINT("Gfx Group Editor") - - TEXT_COLUMN(ICON_MD_MORE_VERT) // Separator - - TableNextColumn(); - if (Button(ICON_MD_CONTENT_COPY)) { - std::vector png_data; - if (gfx::ConvertSurfaceToPNG(maps_bmp_[current_map_].surface(), - png_data)) { - core::CopyImageToClipboard(png_data); - } else { - status_ = absl::InternalError( - "Failed to convert overworld map surface to PNG"); + if (toolset_table_.column_contents.empty()) { + gui::AddTableColumn(toolset_table_, "##Undo", [&]() { + if (Button(ICON_MD_UNDO)) status_ = Undo(); + }); + gui::AddTableColumn(toolset_table_, "##Redo", [&]() { + if (Button(ICON_MD_REDO)) status_ = Redo(); + }); + gui::AddTableColumn(toolset_table_, "##Sep1", ICON_MD_MORE_VERT); + gui::AddTableColumn(toolset_table_, "##ZoomOut", [&]() { + if (Button(ICON_MD_ZOOM_OUT)) ow_map_canvas_.ZoomOut(); + }); + gui::AddTableColumn(toolset_table_, "##ZoomIn", [&]() { + if (Button(ICON_MD_ZOOM_IN)) ow_map_canvas_.ZoomIn(); + }); + gui::AddTableColumn(toolset_table_, "##Fullscreen", [&]() { + if (Button(ICON_MD_OPEN_IN_FULL)) + overworld_canvas_fullscreen_ = !overworld_canvas_fullscreen_; + HOVER_HINT("Fullscreen Canvas") + }); + gui::AddTableColumn(toolset_table_, "##Sep2", ICON_MD_MORE_VERT); + gui::AddTableColumn(toolset_table_, "##Pan", [&]() { + if (Selectable(ICON_MD_PAN_TOOL_ALT, current_mode == EditingMode::PAN)) { + current_mode = EditingMode::PAN; + ow_map_canvas_.set_draggable(true); } - } - HOVER_HINT("Copy Map to Clipboard"); + HOVER_HINT("Pan (Right click and drag)"); + }); + gui::AddTableColumn(toolset_table_, "##DrawTile", [&]() { + if (Selectable(ICON_MD_DRAW, current_mode == EditingMode::DRAW_TILE)) { + current_mode = EditingMode::DRAW_TILE; + } + HOVER_HINT("Draw Tile"); + }); + gui::AddTableColumn(toolset_table_, "##Entrances", [&]() { + if (Selectable(ICON_MD_DOOR_FRONT, + current_mode == EditingMode::ENTRANCES)) + current_mode = EditingMode::ENTRANCES; + HOVER_HINT("Entrances"); + }); + gui::AddTableColumn(toolset_table_, "##Exits", [&]() { + if (Selectable(ICON_MD_DOOR_BACK, current_mode == EditingMode::EXITS)) + current_mode = EditingMode::EXITS; + HOVER_HINT("Exits"); + }); + gui::AddTableColumn(toolset_table_, "##Items", [&]() { + if (Selectable(ICON_MD_GRASS, current_mode == EditingMode::ITEMS)) + current_mode = EditingMode::ITEMS; + HOVER_HINT("Items"); + }); + gui::AddTableColumn(toolset_table_, "##Sprites", [&]() { + if (Selectable(ICON_MD_PEST_CONTROL_RODENT, + current_mode == EditingMode::SPRITES)) + current_mode = EditingMode::SPRITES; + HOVER_HINT("Sprites"); + }); + gui::AddTableColumn(toolset_table_, "##Transports", [&]() { + if (Selectable(ICON_MD_ADD_LOCATION, + current_mode == EditingMode::TRANSPORTS)) + current_mode = EditingMode::TRANSPORTS; + HOVER_HINT("Transports"); + }); + gui::AddTableColumn(toolset_table_, "##Music", [&]() { + if (Selectable(ICON_MD_MUSIC_NOTE, current_mode == EditingMode::MUSIC)) + current_mode = EditingMode::MUSIC; + HOVER_HINT("Music"); + }); + gui::AddTableColumn(toolset_table_, "##Tile16Editor", [&]() { + if (Button(ICON_MD_GRID_VIEW)) show_tile16_editor_ = !show_tile16_editor_; + HOVER_HINT("Tile16 Editor"); + }); + gui::AddTableColumn(toolset_table_, "##GfxGroupEditor", [&]() { + if (Button(ICON_MD_TABLE_CHART)) show_gfx_group = !show_gfx_group; + HOVER_HINT("Gfx Group Editor"); + }); + gui::AddTableColumn(toolset_table_, "##sep3", ICON_MD_MORE_VERT); + gui::AddTableColumn(toolset_table_, "##Properties", [&]() { + if (Button(ICON_MD_CONTENT_COPY)) { + std::vector png_data; + if (gfx::ConvertSurfaceToPNG(maps_bmp_[current_map_].surface(), + png_data)) { + core::CopyImageToClipboard(png_data); + } else { + status_ = absl::InternalError( + "Failed to convert overworld map surface to PNG"); + } + } + HOVER_HINT("Copy Map to Clipboard"); + }); + gui::AddTableColumn(toolset_table_, "##Palette", [&]() { + status_ = DisplayPalette(palette_, overworld_.is_loaded()); + }); + gui::AddTableColumn(toolset_table_, "##Sep4", ICON_MD_MORE_VERT); + gui::AddTableColumn(toolset_table_, "##Properties", + [&]() { Checkbox("Properties", &show_properties); }); - TableNextColumn(); // Palette - status_ = DisplayPalette(palette_, overworld_.is_loaded()); - - TEXT_COLUMN(ICON_MD_MORE_VERT) // Separator - - TableNextColumn(); - Checkbox("Properties", &show_properties); - - ImGui::EndTable(); + } else { + gui::DrawTable(toolset_table_); } if (show_tile16_editor_) { - // Create a table in ImGui for the Tile16 Editor ImGui::Begin("Tile16 Editor", &show_tile16_editor_, ImGuiWindowFlags_MenuBar); status_ = tile16_editor_.Update(); @@ -639,7 +621,7 @@ void OverworldEditor::CheckForMousePan() { void OverworldEditor::DrawOverworldCanvas() { if (all_gfx_loaded_) { - if (flags()->overworld.kLoadCustomOverworld) { + if (core::ExperimentFlags::get().overworld.kLoadCustomOverworld) { DrawCustomOverworldMapSettings(); } else { DrawOverworldMapSettings(); @@ -724,7 +706,7 @@ void OverworldEditor::DrawTile8Selector() { graphics_bin_canvas_.DrawContextMenu(); if (all_gfx_loaded_) { int key = 0; - for (auto &value : rom()->gfx_sheets()) { + for (auto &value : rom_.gfx_sheets()) { int offset = 0x40 * (key + 1); int top_left_y = graphics_bin_canvas_.zero_point().y + 2; if (key >= 1) { @@ -808,7 +790,7 @@ void OverworldEditor::DrawOverworldEntrances(ImVec2 canvas_p0, ImVec2 scrolling, color = ImVec4(255, 255, 255, 200); } ow_map_canvas_.DrawRect(each.x_, each.y_, 16, 16, color); - std::string str = core::UppercaseHexByte(each.entrance_id_); + std::string str = core::HexByte(each.entrance_id_); if (current_mode == EditingMode::ENTRANCES) { HandleEntityDragging(&each, canvas_p0, scrolling, is_dragging_entity_, @@ -891,7 +873,7 @@ void OverworldEditor::DrawOverworldExits(ImVec2 canvas_p0, ImVec2 scrolling) { } } - std::string str = core::UppercaseHexByte(i); + std::string str = core::HexByte(i); ow_map_canvas_.DrawText(str, each.x_, each.y_); } i++; @@ -937,8 +919,8 @@ void OverworldEditor::DrawOverworldItems() { } } std::string item_name = ""; - if (item.id_ < zelda3::overworld::kSecretItemNames.size()) { - item_name = zelda3::overworld::kSecretItemNames[item.id_]; + if (item.id_ < zelda3::kSecretItemNames.size()) { + item_name = zelda3::kSecretItemNames[item.id_]; } else { item_name = absl::StrFormat("0x%02X", item.id_); } @@ -1027,22 +1009,22 @@ void OverworldEditor::DrawOverworldSprites() { } absl::Status OverworldEditor::Save() { - if (flags()->overworld.kSaveOverworldMaps) { + if (core::ExperimentFlags::get().overworld.kSaveOverworldMaps) { RETURN_IF_ERROR(overworld_.CreateTile32Tilemap()); RETURN_IF_ERROR(overworld_.SaveMap32Tiles()); RETURN_IF_ERROR(overworld_.SaveMap16Tiles()); RETURN_IF_ERROR(overworld_.SaveOverworldMaps()); } - if (flags()->overworld.kSaveOverworldEntrances) { + if (core::ExperimentFlags::get().overworld.kSaveOverworldEntrances) { RETURN_IF_ERROR(overworld_.SaveEntrances()); } - if (flags()->overworld.kSaveOverworldExits) { + if (core::ExperimentFlags::get().overworld.kSaveOverworldExits) { RETURN_IF_ERROR(overworld_.SaveExits()); } - if (flags()->overworld.kSaveOverworldItems) { + if (core::ExperimentFlags::get().overworld.kSaveOverworldItems) { RETURN_IF_ERROR(overworld_.SaveItems()); } - if (flags()->overworld.kSaveOverworldProperties) { + if (core::ExperimentFlags::get().overworld.kSaveOverworldProperties) { RETURN_IF_ERROR(overworld_.SaveMapProperties()); } return absl::OkStatus(); @@ -1050,7 +1032,7 @@ absl::Status OverworldEditor::Save() { absl::Status OverworldEditor::LoadGraphics() { // Load the Link to the Past overworld. - RETURN_IF_ERROR(overworld_.Load(*rom())) + RETURN_IF_ERROR(overworld_.Load(rom_)) palette_ = overworld_.current_area_palette(); // Create the area graphics image @@ -1066,10 +1048,10 @@ absl::Status OverworldEditor::LoadGraphics() { // Copy the tile16 data into individual tiles. auto tile16_data = overworld_.tile16_blockset_data(); - tile16_individual_.reserve(zelda3::overworld::kNumTile16Individual); + tile16_individual_.reserve(zelda3::kNumTile16Individual); // Loop through the tiles and copy their pixel data into separate vectors - for (uint i = 0; i < zelda3::overworld::kNumTile16Individual; i++) { + for (uint i = 0; i < zelda3::kNumTile16Individual; i++) { std::vector tile_data(kTile16Size * kTile16Size, 0x00); // Copy the pixel data for the current tile into the vector @@ -1092,7 +1074,7 @@ absl::Status OverworldEditor::LoadGraphics() { } // Render the overworld maps loaded from the ROM. - for (int i = 0; i < zelda3::overworld::kNumOverworldMaps; ++i) { + for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) { overworld_.set_current_map(i); auto palette = overworld_.current_area_palette(); RETURN_IF_ERROR(Renderer::GetInstance().CreateAndRenderBitmap( @@ -1100,7 +1082,7 @@ absl::Status OverworldEditor::LoadGraphics() { overworld_.current_map_bitmap_data(), maps_bmp_[i], palette)); } - if (flags()->overworld.kDrawOverworldSprites) { + if (core::ExperimentFlags::get().overworld.kDrawOverworldSprites) { RETURN_IF_ERROR(LoadSpriteGraphics()); } @@ -1235,7 +1217,7 @@ absl::Status OverworldEditor::RefreshTile16Blockset() { std::vector> futures; // Loop through the tiles and copy their pixel data into separate vectors - for (uint i = 0; i < zelda3::overworld::kNumTile16Individual; i++) { + for (uint i = 0; i < zelda3::kNumTile16Individual; i++) { futures.push_back(std::async( std::launch::async, [&](int index) { @@ -1259,7 +1241,7 @@ absl::Status OverworldEditor::RefreshTile16Blockset() { } // Render the bitmaps of each tile. - for (uint id = 0; id < zelda3::overworld::kNumTile16Individual; id++) { + for (uint id = 0; id < zelda3::kNumTile16Individual; id++) { RETURN_IF_ERROR(tile16_individual_[id].ApplyPalette(palette_)); Renderer::GetInstance().UpdateBitmap(&tile16_individual_[id]); } @@ -1385,8 +1367,9 @@ absl::Status OverworldEditor::UpdateUsageStats() { if (BeginChild("UnusedSpritesetScroll", ImVec2(0, 0), true, ImGuiWindowFlags_HorizontalScrollbar)) { for (int i = 0; i < 0x81; i++) { - auto entrance_name = rom()->resource_label()->GetLabel( - "Dungeon Entrance Names", core::UppercaseHexByte(i)); + auto entrance_name = rom_.resource_label()->CreateOrGetLabel( + "Dungeon Entrance Names", core::HexByte(i), + zelda3::kEntranceNames[i].data()); std::string str = absl::StrFormat("%#x - %s", i, entrance_name); if (Selectable(str.c_str(), selected_entrance_ == i, overworld_.entrances().at(i).deleted @@ -1500,7 +1483,7 @@ void OverworldEditor::DrawDebugWindow() { } } -void OverworldEditor::InitializeZeml() { +void OverworldEditor::Initialize() { // Load zeml string from layouts/overworld.zeml std::string layout = gui::zeml::LoadFile("overworld.zeml"); // Parse the zeml string into a Node object @@ -1511,24 +1494,23 @@ void OverworldEditor::InitializeZeml() { gui::zeml::Bind(&*layout_node_.GetNode("OverworldTileSelector"), [this]() { status_ = DrawTileSelector(); }); gui::zeml::Bind(&*layout_node_.GetNode("OwUsageStats"), [this]() { - if (rom()->is_loaded()) { + if (rom_.is_loaded()) { status_ = UpdateUsageStats(); } }); gui::zeml::Bind(&*layout_node_.GetNode("owToolset"), [this]() { DrawToolset(); }); gui::zeml::Bind(&*layout_node_.GetNode("OwTile16Editor"), [this]() { - if (rom()->is_loaded()) { + if (rom_.is_loaded()) { status_ = tile16_editor_.Update(); } }); gui::zeml::Bind(&*layout_node_.GetNode("OwGfxGroupEditor"), [this]() { - if (rom()->is_loaded()) { + if (rom_.is_loaded()) { status_ = gfx_group_editor_.Update(); } }); } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/overworld/overworld_editor.h b/src/app/editor/overworld/overworld_editor.h index dbbacd76..c5dd58df 100644 --- a/src/app/editor/overworld/overworld_editor.h +++ b/src/app/editor/overworld/overworld_editor.h @@ -1,30 +1,21 @@ #ifndef YAZE_APP_EDITOR_OVERWORLDEDITOR_H #define YAZE_APP_EDITOR_OVERWORLDEDITOR_H -#include -#include - -#include "absl/container/flat_hash_map.h" #include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/str_format.h" #include "app/editor/editor.h" #include "app/editor/graphics/gfx_group_editor.h" #include "app/editor/graphics/palette_editor.h" #include "app/editor/graphics/tile16_editor.h" -#include "app/editor/overworld/entity.h" #include "app/gfx/bitmap.h" #include "app/gfx/snes_palette.h" -#include "app/gfx/snes_tile.h" #include "app/gui/canvas.h" -#include "app/gui/icons.h" +#include "app/gui/input.h" #include "app/gui/zeml.h" #include "app/rom.h" #include "app/zelda3/overworld/overworld.h" #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { constexpr uint k4BPP = 4; @@ -65,32 +56,6 @@ constexpr absl::string_view kTileSelectorTab = "##TileSelectorTabBar"; constexpr absl::string_view kOWEditTable = "##OWEditTable"; constexpr absl::string_view kOWMapTable = "#MapSettingsTable"; -constexpr int kEntranceTileTypePtrLow = 0xDB8BF; -constexpr int kEntranceTileTypePtrHigh = 0xDB917; -constexpr int kNumEntranceTileTypes = 0x2C; - -class EntranceContext { - public: - absl::Status LoadEntranceTileTypes(Rom& rom) { - int offset_low = kEntranceTileTypePtrLow; - int offset_high = kEntranceTileTypePtrHigh; - - for (int i = 0; i < kNumEntranceTileTypes; i++) { - // Load entrance tile types - ASSIGN_OR_RETURN(auto value_low, rom.ReadWord(offset_low + i)); - entrance_tile_types_low_.push_back(value_low); - ASSIGN_OR_RETURN(auto value_high, rom.ReadWord(offset_high + i)); - entrance_tile_types_low_.push_back(value_high); - } - - return absl::OkStatus(); - } - - private: - std::vector entrance_tile_types_low_; - std::vector entrance_tile_types_high_; -}; - /** * @class OverworldEditor * @brief Manipulates the Overworld and OverworldMap data in a Rom. @@ -107,15 +72,11 @@ class EntranceContext { * Provides access to the GfxGroupEditor and Tile16Editor through popup windows. * */ -class OverworldEditor : public Editor, - public SharedRom, - public EntranceContext, - public GfxContext, - public core::ExperimentFlags { +class OverworldEditor : public Editor, public gfx::GfxContext { public: - OverworldEditor() { type_ = EditorType::kOverworld; } + OverworldEditor(Rom& rom) : rom_(rom) { type_ = EditorType::kOverworld; } - void InitializeZeml(); + void Initialize(); absl::Status Update() final; absl::Status Undo() override { return absl::UnimplementedError("Undo"); } @@ -130,9 +91,6 @@ class OverworldEditor : public Editor, auto overworld() { return &overworld_; } - /** - * @brief - */ int jump_to_tab() { return jump_to_tab_; } int jump_to_tab_ = -1; @@ -281,6 +239,8 @@ class OverworldEditor : public Editor, std::vector> tile8_individual_data_; std::vector tile8_individual_; + Rom& rom_; + Tile16Editor tile16_editor_; GfxGroupEditor gfx_group_editor_; PaletteEditor palette_editor_; @@ -296,14 +256,15 @@ class OverworldEditor : public Editor, gfx::BitmapTable current_graphics_set_; gfx::BitmapTable sprite_previews_; - zelda3::overworld::Overworld overworld_; - zelda3::OWBlockset refresh_blockset_; + zelda3::Overworld overworld_; + zelda3::OverworldBlockset refresh_blockset_; zelda3::Sprite current_sprite_; - zelda3::overworld::OverworldEntrance current_entrance_; - zelda3::overworld::OverworldExit current_exit_; - zelda3::overworld::OverworldItem current_item_; + zelda3::OverworldEntrance current_entrance_; + zelda3::OverworldExit current_exit_; + zelda3::OverworldItem current_item_; + zelda3::OverworldEntranceTileTypes entrance_tiletypes_; zelda3::GameEntity* current_entity_; zelda3::GameEntity* dragged_entity_; @@ -317,11 +278,15 @@ class OverworldEditor : public Editor, gui::Canvas graphics_bin_canvas_{"GraphicsBin", kGraphicsBinCanvasSize, gui::CanvasGridSize::k16x16}; gui::Canvas properties_canvas_; + + gui::Table toolset_table_{"##ToolsetTable0", 22, kToolsetTableFlags}; + gui::Table map_settings_table_{kOWMapTable.data(), 8, kOWMapFlags, + ImVec2(0, 0)}; + gui::zeml::Node layout_node_; absl::Status status_; }; } // namespace editor -} // namespace app } // namespace yaze #endif diff --git a/src/app/editor/sprite/sprite_editor.cc b/src/app/editor/sprite/sprite_editor.cc index 5c0cd211..3e84aa66 100644 --- a/src/app/editor/sprite/sprite_editor.cc +++ b/src/app/editor/sprite/sprite_editor.cc @@ -7,7 +7,6 @@ #include "app/zelda3/sprite/sprite.h" namespace yaze { -namespace app { namespace editor { using ImGui::BeginTable; @@ -191,7 +190,7 @@ void SpriteEditor::DrawSpritesList() { int i = 0; for (const auto each_sprite_name : zelda3::kSpriteDefaultNames) { rom()->resource_label()->SelectableLabelWithNameEdit( - current_sprite_id_ == i, "Sprite Names", core::UppercaseHexByte(i), + current_sprite_id_ == i, "Sprite Names", core::HexByte(i), zelda3::kSpriteDefaultNames[i].data()); if (ImGui::IsItemClicked()) { current_sprite_id_ = i; @@ -275,5 +274,4 @@ void SpriteEditor::DrawCustomSpritesMetadata() { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/sprite/sprite_editor.h b/src/app/editor/sprite/sprite_editor.h index 2504a62f..6a01bf71 100644 --- a/src/app/editor/sprite/sprite_editor.h +++ b/src/app/editor/sprite/sprite_editor.h @@ -8,7 +8,6 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace editor { constexpr ImGuiTabItemFlags kSpriteTabFlags = @@ -111,7 +110,6 @@ class SpriteEditor : public SharedRom, public Editor { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_SPRITE_EDITOR_H \ No newline at end of file diff --git a/src/app/editor/sprite/zsprite.h b/src/app/editor/sprite/zsprite.h index 5cedb73a..ac8494b5 100644 --- a/src/app/editor/sprite/zsprite.h +++ b/src/app/editor/sprite/zsprite.h @@ -13,7 +13,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { /** * @brief Namespace for the ZSprite format from Zarby's ZSpriteMaker. @@ -390,7 +389,6 @@ struct ZSprite { } // namespace zsprite } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_SPRITE_ZSPRITE_H diff --git a/src/app/editor/system/command_manager.cc b/src/app/editor/system/command_manager.cc index bcb3995c..99047c15 100644 --- a/src/app/editor/system/command_manager.cc +++ b/src/app/editor/system/command_manager.cc @@ -5,7 +5,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { ImGuiKey MapKeyToImGuiKey(char key) { @@ -139,5 +138,4 @@ void CommandManager::LoadKeybindings(const std::string &filepath) { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/system/command_manager.h b/src/app/editor/system/command_manager.h index c6dd7036..58669be7 100644 --- a/src/app/editor/system/command_manager.h +++ b/src/app/editor/system/command_manager.h @@ -8,16 +8,15 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { ImGuiKey MapKeyToImGuiKey(char key); class CommandManager { -public: + public: CommandManager() = default; ~CommandManager() = default; - + using Command = std::function; struct CommandInfo { @@ -27,7 +26,9 @@ public: std::string desc; CommandInfo(Command command, char mnemonic, const std::string &name, const std::string &desc) - : command(std::move(command)), mnemonic(mnemonic), name(name), + : command(std::move(command)), + mnemonic(mnemonic), + name(name), desc(desc) {} CommandInfo() = default; }; @@ -72,12 +73,11 @@ public: void SaveKeybindings(const std::string &filepath); void LoadKeybindings(const std::string &filepath); -private: + private: std::unordered_map commands_; }; -} // namespace editor -} // namespace app -} // namespace yaze +} // namespace editor +} // namespace yaze -#endif // YAZE_APP_EDITOR_SYSTEM_COMMAND_MANAGER_H +#endif // YAZE_APP_EDITOR_SYSTEM_COMMAND_MANAGER_H diff --git a/src/app/editor/system/constant_manager.h b/src/app/editor/system/constant_manager.h index 4909e4a7..c966d713 100644 --- a/src/app/editor/system/constant_manager.h +++ b/src/app/editor/system/constant_manager.h @@ -7,7 +7,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { class ConstantManager { @@ -27,23 +26,23 @@ class ConstantManager { ImGui::Text("Overworld constants"); ImGui::Separator(); ImGui::Text("OverworldCustomASMHasBeenApplied: %d", - zelda3::overworld::OverworldCustomASMHasBeenApplied); + zelda3::OverworldCustomASMHasBeenApplied); ImGui::Text("OverworldCustomAreaSpecificBGPalette: %d", - zelda3::overworld::OverworldCustomAreaSpecificBGPalette); + zelda3::OverworldCustomAreaSpecificBGPalette); ImGui::Text("OverworldCustomAreaSpecificBGEnabled: %d", - zelda3::overworld::OverworldCustomAreaSpecificBGEnabled); + zelda3::OverworldCustomAreaSpecificBGEnabled); ImGui::Text("OverworldCustomMainPaletteArray: %d", - zelda3::overworld::OverworldCustomMainPaletteArray); + zelda3::OverworldCustomMainPaletteArray); ImGui::Text("OverworldCustomMainPaletteEnabled: %d", - zelda3::overworld::OverworldCustomMainPaletteEnabled); + zelda3::OverworldCustomMainPaletteEnabled); ImGui::Text("OverworldCustomMosaicArray: %d", - zelda3::overworld::OverworldCustomMosaicArray); + zelda3::OverworldCustomMosaicArray); ImGui::Text("OverworldCustomMosaicEnabled: %d", - zelda3::overworld::OverworldCustomMosaicEnabled); + zelda3::OverworldCustomMosaicEnabled); ImGui::Text("OverworldCustomAnimatedGFXArray: %d", - zelda3::overworld::OverworldCustomAnimatedGFXArray); + zelda3::OverworldCustomAnimatedGFXArray); ImGui::Text("OverworldCustomAnimatedGFXEnabled: %d", - zelda3::overworld::OverworldCustomAnimatedGFXEnabled); + zelda3::OverworldCustomAnimatedGFXEnabled); ImGui::EndTabItem(); } @@ -71,7 +70,6 @@ class ConstantManager { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_SYSTEM_CONSTANT_MANAGER_H diff --git a/src/app/editor/system/extension_manager.cc b/src/app/editor/system/extension_manager.cc index 12e8bc67..e0e42407 100644 --- a/src/app/editor/system/extension_manager.cc +++ b/src/app/editor/system/extension_manager.cc @@ -5,13 +5,12 @@ #include #endif -#include +#include #include #include namespace yaze { -namespace app { namespace editor { void ExtensionManager::LoadExtension(const std::string& filename, @@ -81,5 +80,4 @@ void ExtensionManager::ExecuteExtensionUI(yaze_editor_context* context) { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/system/extension_manager.h b/src/app/editor/system/extension_manager.h index eb471e92..a111da7a 100644 --- a/src/app/editor/system/extension_manager.h +++ b/src/app/editor/system/extension_manager.h @@ -1,13 +1,12 @@ #ifndef YAZE_APP_EDITOR_SYSTEM_EXTENSION_MANAGER_H #define YAZE_APP_EDITOR_SYSTEM_EXTENSION_MANAGER_H -#include +#include #include #include namespace yaze { -namespace app { namespace editor { class ExtensionManager { @@ -23,7 +22,6 @@ class ExtensionManager { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_SYSTEM_EXTENSION_MANAGER_H diff --git a/src/app/editor/system/flags.h b/src/app/editor/system/flags.h index 1b0035d2..fbada123 100644 --- a/src/app/editor/system/flags.h +++ b/src/app/editor/system/flags.h @@ -5,60 +5,59 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { +using core::ExperimentFlags; using ImGui::BeginMenu; using ImGui::Checkbox; using ImGui::EndMenu; using ImGui::MenuItem; using ImGui::Separator; -struct FlagsMenu : public core::ExperimentFlags { +struct FlagsMenu { void Draw() { if (BeginMenu("Overworld Flags")) { Checkbox("Enable Overworld Sprites", - &mutable_flags()->overworld.kDrawOverworldSprites); + &ExperimentFlags::get().overworld.kDrawOverworldSprites); Separator(); Checkbox("Save Overworld Maps", - &mutable_flags()->overworld.kSaveOverworldMaps); + &ExperimentFlags::get().overworld.kSaveOverworldMaps); Checkbox("Save Overworld Entrances", - &mutable_flags()->overworld.kSaveOverworldEntrances); + &ExperimentFlags::get().overworld.kSaveOverworldEntrances); Checkbox("Save Overworld Exits", - &mutable_flags()->overworld.kSaveOverworldExits); + &ExperimentFlags::get().overworld.kSaveOverworldExits); Checkbox("Save Overworld Items", - &mutable_flags()->overworld.kSaveOverworldItems); + &ExperimentFlags::get().overworld.kSaveOverworldItems); Checkbox("Save Overworld Properties", - &mutable_flags()->overworld.kSaveOverworldProperties); + &ExperimentFlags::get().overworld.kSaveOverworldProperties); Checkbox("Load Custom Overworld", - &mutable_flags()->overworld.kLoadCustomOverworld); + &ExperimentFlags::get().overworld.kLoadCustomOverworld); ImGui::EndMenu(); } if (BeginMenu("Dungeon Flags")) { Checkbox("Draw Dungeon Room Graphics", - &mutable_flags()->kDrawDungeonRoomGraphics); + &ExperimentFlags::get().kDrawDungeonRoomGraphics); Separator(); - Checkbox("Save Dungeon Maps", &mutable_flags()->kSaveDungeonMaps); + Checkbox("Save Dungeon Maps", &ExperimentFlags::get().kSaveDungeonMaps); ImGui::EndMenu(); } Checkbox("Use built-in file dialog", - &mutable_flags()->kNewFileDialogWrapper); - Checkbox("Enable Console Logging", &mutable_flags()->kLogToConsole); + &ExperimentFlags::get().kNewFileDialogWrapper); + Checkbox("Enable Console Logging", &ExperimentFlags::get().kLogToConsole); Checkbox("Enable Texture Streaming", - &mutable_flags()->kLoadTexturesAsStreaming); + &ExperimentFlags::get().kLoadTexturesAsStreaming); Checkbox("Log Instructions to Debugger", - &mutable_flags()->kLogInstructions); - Checkbox("Save All Palettes", &mutable_flags()->kSaveAllPalettes); - Checkbox("Save Gfx Groups", &mutable_flags()->kSaveGfxGroups); - Checkbox("Save Graphics Sheets", &mutable_flags()->kSaveGraphicsSheet); - Checkbox("Use New ImGui Input", &mutable_flags()->kUseNewImGuiInput); + &ExperimentFlags::get().kLogInstructions); + Checkbox("Save All Palettes", &ExperimentFlags::get().kSaveAllPalettes); + Checkbox("Save Gfx Groups", &ExperimentFlags::get().kSaveGfxGroups); + Checkbox("Save Graphics Sheets", + &ExperimentFlags::get().kSaveGraphicsSheet); } }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_UTILS_FLAGS_H_ diff --git a/src/app/editor/system/history_manager.h b/src/app/editor/system/history_manager.h index f059e6e8..fa7a87a4 100644 --- a/src/app/editor/system/history_manager.h +++ b/src/app/editor/system/history_manager.h @@ -6,7 +6,6 @@ #include namespace yaze { -namespace app { namespace editor { // System history manager, undo and redo. @@ -26,7 +25,6 @@ class HistoryManager { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_SYSTEM_HISTORY_MANAGER_H diff --git a/src/app/editor/system/popup_manager.cc b/src/app/editor/system/popup_manager.cc index fb5b3538..1667e86b 100644 --- a/src/app/editor/system/popup_manager.cc +++ b/src/app/editor/system/popup_manager.cc @@ -3,7 +3,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { PopupManager::PopupManager() { @@ -15,5 +14,5 @@ PopupManager::~PopupManager() { } // namespace editor -} // namespace app + } // namespace yaze diff --git a/src/app/editor/system/popup_manager.h b/src/app/editor/system/popup_manager.h index 056cc8c7..2e296ea9 100644 --- a/src/app/editor/system/popup_manager.h +++ b/src/app/editor/system/popup_manager.h @@ -2,7 +2,6 @@ #define YAZE_APP_EDITOR_POPUP_MANAGER_H namespace yaze { -namespace app { namespace editor { // ImGui popup manager. @@ -15,7 +14,7 @@ class PopupManager { }; } // namespace editor -} // namespace app + } // namespace yaze #endif // YAZE_APP_EDITOR_POPUP_MANAGER_H diff --git a/src/app/editor/system/resource_manager.h b/src/app/editor/system/resource_manager.h index 116ef9e4..69ea03ba 100644 --- a/src/app/editor/system/resource_manager.h +++ b/src/app/editor/system/resource_manager.h @@ -4,7 +4,6 @@ #include namespace yaze { -namespace app { namespace editor { // System resource manager. @@ -24,7 +23,6 @@ class ResourceManager { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_SYSTEM_RESOURCE_MANAGER_H diff --git a/src/app/editor/system/settings_editor.cc b/src/app/editor/system/settings_editor.cc index 2be90b39..89157d72 100644 --- a/src/app/editor/system/settings_editor.cc +++ b/src/app/editor/system/settings_editor.cc @@ -6,7 +6,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace editor { using ImGui::BeginChild; @@ -79,5 +78,4 @@ absl::Status SettingsEditor::DrawKeyboardShortcuts() { } } // namespace editor -} // namespace app } // namespace yaze diff --git a/src/app/editor/system/settings_editor.h b/src/app/editor/system/settings_editor.h index 3c2c0a07..ca13e09f 100644 --- a/src/app/editor/system/settings_editor.h +++ b/src/app/editor/system/settings_editor.h @@ -7,7 +7,6 @@ #include "app/editor/editor.h" namespace yaze { -namespace app { namespace editor { // Simple representation for a tree @@ -226,7 +225,6 @@ class SettingsEditor : public Editor { }; } // namespace editor -} // namespace app } // namespace yaze #endif // YAZE_APP_EDITOR_SETTINGS_EDITOR_H_ diff --git a/src/app/emu/audio/apu.cc b/src/app/emu/audio/apu.cc index 080fa422..3d51a955 100644 --- a/src/app/emu/audio/apu.cc +++ b/src/app/emu/audio/apu.cc @@ -3,19 +3,14 @@ #include #include -#include -#include #include #include "app/emu/audio/dsp.h" #include "app/emu/audio/spc700.h" -#include "app/emu/cpu/clock.h" #include "app/emu/memory/memory.h" namespace yaze { -namespace app { namespace emu { -namespace audio { static const double apuCyclesPerMaster = (32040 * 32) / (1364 * 262 * 60.0); static const double apuCyclesPerMasterPal = (32040 * 32) / (1364 * 312 * 50.0); @@ -201,7 +196,5 @@ void Apu::SpcWrite(uint16_t adr, uint8_t val) { void Apu::SpcIdle(bool waiting) { Cycle(); } -} // namespace audio } // namespace emu -} // namespace app -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/emu/audio/apu.h b/src/app/emu/audio/apu.h index b52c6ada..b971a0e2 100644 --- a/src/app/emu/audio/apu.h +++ b/src/app/emu/audio/apu.h @@ -2,7 +2,6 @@ #define YAZE_APP_EMU_APU_H_ #include -#include #include #include @@ -11,11 +10,7 @@ #include "app/emu/memory/memory.h" namespace yaze { -namespace app { namespace emu { -namespace audio { - -using namespace memory; typedef struct Timer { uint8_t cycles; @@ -93,9 +88,7 @@ class Apu { Spc700 spc700_{callbacks_}; }; -} // namespace audio } // namespace emu -} // namespace app } // namespace yaze -#endif \ No newline at end of file +#endif diff --git a/src/app/emu/audio/dsp.cc b/src/app/emu/audio/dsp.cc index 2bef50d5..7281d025 100644 --- a/src/app/emu/audio/dsp.cc +++ b/src/app/emu/audio/dsp.cc @@ -5,9 +5,7 @@ #include "app/emu/memory/memory.h" namespace yaze { -namespace app { namespace emu { -namespace audio { static const int rateValues[32] = {0, 2048, 1536, 1280, 1024, 768, 640, 512, 384, 320, 256, 192, 160, 128, 96, 80, @@ -631,7 +629,5 @@ void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame, } } -} // namespace audio } // namespace emu -} // namespace app -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/emu/audio/dsp.h b/src/app/emu/audio/dsp.h index 762692c9..647397ee 100644 --- a/src/app/emu/audio/dsp.h +++ b/src/app/emu/audio/dsp.h @@ -9,9 +9,7 @@ #include "app/emu/memory/memory.h" namespace yaze { -namespace app { namespace emu { -namespace audio { typedef struct DspChannel { // pitch @@ -155,9 +153,7 @@ class Dsp { uint32_t lastFrameBoundary; }; -} // namespace audio } // namespace emu -} // namespace app } // namespace yaze -#endif // YAZE_APP_EMU_AUDIO_S_DSP_H \ No newline at end of file +#endif // YAZE_APP_EMU_AUDIO_S_DSP_H diff --git a/src/app/emu/audio/internal/addressing.cc b/src/app/emu/audio/internal/addressing.cc index 447d3fd6..71ff2f49 100644 --- a/src/app/emu/audio/internal/addressing.cc +++ b/src/app/emu/audio/internal/addressing.cc @@ -1,9 +1,7 @@ #include "app/emu/audio/spc700.h" namespace yaze { -namespace app { namespace emu { -namespace audio { // adressing modes @@ -146,7 +144,5 @@ uint16_t Spc700::addr_plus_i_indexed() { return read(addr) | (read(addr + 1) << 8); } -} // namespace audio } // namespace emu -} // namespace app -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/emu/audio/internal/instructions.cc b/src/app/emu/audio/internal/instructions.cc index ee207e9e..b8d456a4 100644 --- a/src/app/emu/audio/internal/instructions.cc +++ b/src/app/emu/audio/internal/instructions.cc @@ -1,9 +1,7 @@ #include "app/emu/audio/spc700.h" namespace yaze { -namespace app { namespace emu { -namespace audio { // opcode functions @@ -481,7 +479,5 @@ void Spc700::SLEEP() {} void Spc700::STOP() {} -} // namespace audio } // namespace emu -} // namespace app -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/emu/audio/spc700.cc b/src/app/emu/audio/spc700.cc index c2626305..88fb20d6 100644 --- a/src/app/emu/audio/spc700.cc +++ b/src/app/emu/audio/spc700.cc @@ -4,14 +4,11 @@ #include #include #include -#include #include "app/emu/audio/internal/opcodes.h" namespace yaze { -namespace app { namespace emu { -namespace audio { void Spc700::Reset(bool hard) { if (hard) { @@ -1324,7 +1321,5 @@ void Spc700::LogInstruction(uint16_t initial_pc, uint8_t opcode) { // log_.push_back(log_entry); } -} // namespace audio } // namespace emu -} // namespace app } // namespace yaze diff --git a/src/app/emu/audio/spc700.h b/src/app/emu/audio/spc700.h index 07128599..dc854bb7 100644 --- a/src/app/emu/audio/spc700.h +++ b/src/app/emu/audio/spc700.h @@ -3,14 +3,11 @@ #include #include -#include -#include #include +#include namespace yaze { -namespace app { namespace emu { -namespace audio { /** * @brief AudioRam is an interface for the Audio RAM used by the SPC700. @@ -337,8 +334,7 @@ class Spc700 { // CBNE DBNZ }; -} // namespace audio } // namespace emu -} // namespace app } // namespace yaze -#endif // YAZE_APP_EMU_SPC700_H \ No newline at end of file + +#endif // YAZE_APP_EMU_SPC700_H diff --git a/src/app/emu/cpu/clock.h b/src/app/emu/cpu/clock.h index 17a7a42c..4d2c13e2 100644 --- a/src/app/emu/cpu/clock.h +++ b/src/app/emu/cpu/clock.h @@ -4,11 +4,10 @@ #include namespace yaze { -namespace app { namespace emu { class Clock { - public: +public: virtual ~Clock() = default; virtual void UpdateClock(double delta) = 0; virtual unsigned long long GetCycleCount() const = 0; @@ -18,23 +17,23 @@ class Clock { }; class ClockImpl : public Clock { - public: +public: ClockImpl() = default; virtual ~ClockImpl() = default; - void UpdateCycleCount(double deltaTime) { - accumulatedTime += deltaTime; - double cycleTime = 1.0 / frequency; + void UpdateCycleCount(double delta_time) { + accumulated_time += delta_time; + double cycle_time = 1.0 / frequency; - while (accumulatedTime >= cycleTime) { + while (accumulated_time >= cycle_time) { Cycle(); - accumulatedTime -= cycleTime; + accumulated_time -= cycle_time; } } void Cycle() { cycle++; - cycleCount++; + cycle_count++; } void UpdateClock(double delta) override { @@ -42,22 +41,21 @@ class ClockImpl : public Clock { ResetAccumulatedTime(); } - void ResetAccumulatedTime() override { accumulatedTime = 0.0; } - unsigned long long GetCycleCount() const override { return cycleCount; } + void ResetAccumulatedTime() override { accumulated_time = 0.0; } + unsigned long long GetCycleCount() const override { return cycle_count; } float GetFrequency() const override { return frequency; } void SetFrequency(float new_frequency) override { this->frequency = new_frequency; } - private: +private: uint64_t cycle = 0; // Current cycle float frequency = 0.0; // Frequency of the clock in Hz - unsigned long long cycleCount = 0; // Total number of cycles executed - double accumulatedTime = 0.0; // Accumulated time since the last cycle update + unsigned long long cycle_count = 0; // Total number of cycles executed + double accumulated_time = 0.0; // Accumulated time since the last cycle update }; -} // namespace emu -} // namespace app -} // namespace yaze +} // namespace emu +} // namespace yaze -#endif // YAZE_APP_EMU_CLOCK_H_ \ No newline at end of file +#endif // YAZE_APP_EMU_CLOCK_H_ diff --git a/src/app/emu/cpu/cpu.cc b/src/app/emu/cpu/cpu.cc index 163e1a6c..b2aa9361 100644 --- a/src/app/emu/cpu/cpu.cc +++ b/src/app/emu/cpu/cpu.cc @@ -9,7 +9,6 @@ #include "app/emu/cpu/internal/opcodes.h" namespace yaze { -namespace app { namespace emu { void Cpu::Reset(bool hard) { @@ -1804,7 +1803,7 @@ void Cpu::ExecuteInstruction(uint8_t opcode) { void Cpu::LogInstructions(uint16_t PC, uint8_t opcode, uint16_t operand, bool immediate, bool accumulator_mode) { - if (flags()->kLogInstructions) { + if (core::ExperimentFlags::get().kLogInstructions) { std::ostringstream oss; oss << "$" << std::uppercase << std::setw(2) << std::setfill('0') << static_cast(PB) << ":" << std::hex << PC << ": 0x" @@ -2260,5 +2259,4 @@ uint8_t Cpu::GetInstructionLength(uint8_t opcode) { */ } // namespace emu -} // namespace app -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/emu/cpu/cpu.h b/src/app/emu/cpu/cpu.h index 7623283b..dded241e 100644 --- a/src/app/emu/cpu/cpu.h +++ b/src/app/emu/cpu/cpu.h @@ -13,7 +13,6 @@ #include "app/emu/memory/memory.h" namespace yaze { -namespace app { namespace emu { class InstructionEntry { @@ -35,10 +34,9 @@ class InstructionEntry { std::string instruction; // Human-readable instruction text }; -class Cpu : public core::ExperimentFlags { +class Cpu { public: - explicit Cpu(memory::Memory& mem, Clock& vclock, - memory::CpuCallbacks& callbacks) + explicit Cpu(Memory& mem, Clock& vclock, CpuCallbacks& callbacks) : memory(mem), clock(vclock), callbacks_(callbacks) {} void Reset(bool hard = false); @@ -793,13 +791,13 @@ class Cpu : public core::ExperimentFlags { bool int_wanted_ = false; bool int_delay_ = false; - memory::CpuCallbacks callbacks_; - memory::Memory& memory; + CpuCallbacks callbacks_; + Memory& memory; Clock& clock; }; } // namespace emu -} // namespace app + } // namespace yaze #endif // YAZE_APP_EMU_CPU_H_ \ No newline at end of file diff --git a/src/app/emu/cpu/internal/addressing.cc b/src/app/emu/cpu/internal/addressing.cc index 7086cfd4..16a763a6 100644 --- a/src/app/emu/cpu/internal/addressing.cc +++ b/src/app/emu/cpu/internal/addressing.cc @@ -1,7 +1,6 @@ #include "app/emu/cpu/cpu.h" namespace yaze { -namespace app { namespace emu { void Cpu::AdrImp() { @@ -186,5 +185,5 @@ uint16_t Cpu::StackRelative() { } } // namespace emu -} // namespace app + } // namespace yaze \ No newline at end of file diff --git a/src/app/emu/cpu/internal/instructions.cc b/src/app/emu/cpu/internal/instructions.cc index c00d1f25..77121173 100644 --- a/src/app/emu/cpu/internal/instructions.cc +++ b/src/app/emu/cpu/internal/instructions.cc @@ -5,7 +5,6 @@ #include "app/emu/cpu/cpu.h" namespace yaze { -namespace app { namespace emu { /** @@ -398,5 +397,5 @@ void Cpu::ORA(uint32_t low, uint32_t high) { } } // namespace emu -} // namespace app + } // namespace yaze \ No newline at end of file diff --git a/src/app/emu/emu.cc b/src/app/emu/emu.cc index 394dbf7a..7ef80a27 100644 --- a/src/app/emu/emu.cc +++ b/src/app/emu/emu.cc @@ -1,6 +1,4 @@ -#if defined(_WIN32) -#define main SDL_main -#elif __APPLE__ +#if __APPLE__ #include "app/core/platform/app_delegate.h" #endif @@ -10,25 +8,14 @@ #include #include -#include "absl/base/internal/raw_logging.h" -#include "absl/base/macros.h" -#include "absl/container/flat_hash_map.h" #include "absl/debugging/failure_signal_handler.h" -#include "absl/debugging/leak_check.h" -#include "absl/debugging/stacktrace.h" #include "absl/debugging/symbolize.h" -#include "absl/flags/flag.h" #include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/str_format.h" #include "app/core/utils/sdl_deleter.h" #include "app/emu/snes.h" #include "app/rom.h" -#include "imgui/imgui.h" -#include "imgui_memory_editor.h" -using namespace yaze::app; -using yaze::app::core::SDL_Deleter; +using yaze::core::SDL_Deleter; int main(int argc, char **argv) { absl::InitializeSymbolizer(argv[0]); @@ -38,6 +25,8 @@ int main(int argc, char **argv) { options.alarm_on_failure_secs = true; absl::InstallFailureSignalHandler(options); + SDL_SetMainReady(); + std::unique_ptr window_; std::unique_ptr renderer_; if (SDL_Init(SDL_INIT_EVERYTHING) != 0) { @@ -96,8 +85,8 @@ int main(int argc, char **argv) { return EXIT_FAILURE; } - Rom rom_; - emu::SNES snes_; + yaze::Rom rom_; + yaze::emu::Snes snes_; std::vector rom_data_; bool running = true; @@ -191,10 +180,10 @@ int main(int argc, char **argv) { SDL_PauseAudioDevice(audio_device_, 1); SDL_CloseAudioDevice(audio_device_); - delete audio_buffer_; - // ImGui_ImplSDLRenderer2_Shutdown(); - // ImGui_ImplSDL2_Shutdown(); - // ImGui::DestroyContext(); + delete[] audio_buffer_; + //ImGui_ImplSDLRenderer2_Shutdown(); + //ImGui_ImplSDL2_Shutdown(); + //ImGui::DestroyContext(); SDL_Quit(); return EXIT_SUCCESS; diff --git a/src/app/emu/emu.cmake b/src/app/emu/emu.cmake index c326ff87..7938f172 100644 --- a/src/app/emu/emu.cmake +++ b/src/app/emu/emu.cmake @@ -24,6 +24,7 @@ target_include_directories( ${CMAKE_SOURCE_DIR}/src/ ${PNG_INCLUDE_DIRS} ${SDL2_INCLUDE_DIR} + ${PROJECT_BINARY_DIR} ) target_link_libraries( diff --git a/src/app/emu/emulator.cc b/src/app/emu/emulator.cc index ac912ac3..411016b8 100644 --- a/src/app/emu/emulator.cc +++ b/src/app/emu/emulator.cc @@ -3,19 +3,15 @@ #include #include -#include "app/core/constants.h" #include "app/core/platform/file_dialog.h" #include "app/core/platform/renderer.h" -#include "app/emu/snes.h" #include "app/gui/icons.h" #include "app/gui/input.h" #include "app/gui/zeml.h" -#include "app/rom.h" #include "imgui/imgui.h" #include "imgui_memory_editor.h" namespace yaze { -namespace app { namespace emu { namespace { @@ -540,5 +536,5 @@ void Emulator::RenderCpuInstructionLog( } } // namespace emu -} // namespace app + } // namespace yaze diff --git a/src/app/emu/emulator.h b/src/app/emu/emulator.h index 2b7d844d..642883fc 100644 --- a/src/app/emu/emulator.h +++ b/src/app/emu/emulator.h @@ -10,39 +10,13 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { /** - * @namespace yaze::app::emu + * @namespace yaze::emu * @brief SNES Emulation and debugging tools. */ namespace emu { -// case SDLK_z: -// editor.emulator().snes().SetButtonState(1, 0, false); -// case SDLK_a: -// editor.emulator().snes().SetButtonState(1, 1, false); -// case SDLK_RSHIFT: -// editor.emulator().snes().SetButtonState(1, 2, false); -// case SDLK_RETURN: -// editor.emulator().snes().SetButtonState(1, 3, false); -// case SDLK_UP: -// editor.emulator().snes().SetButtonState(1, 4, false); -// case SDLK_DOWN: -// editor.emulator().snes().SetButtonState(1, 5, false); -// case SDLK_LEFT: -// editor.emulator().snes().SetButtonState(1, 6, false); -// case SDLK_RIGHT: -// editor.emulator().snes().SetButtonState(1, 7, false); -// case SDLK_x: -// editor.emulator().snes().SetButtonState(1, 8, false); -// case SDLK_s: -// editor.emulator().snes().SetButtonState(1, 9, false); -// case SDLK_d: -// editor.emulator().snes().SetButtonState(1, 10, false); -// case SDLK_c: -// editor.emulator().snes().SetButtonState(1, 11, false); - struct EmulatorKeybindings { ImGuiKey a_button = ImGuiKey_Z; ImGuiKey b_button = ImGuiKey_A; @@ -134,7 +108,7 @@ class Emulator : public SharedRom { } void Run(); - auto snes() -> SNES& { return snes_; } + auto snes() -> Snes& { return snes_; } auto running() const -> bool { return running_; } void set_audio_buffer(int16_t* audio_buffer) { audio_buffer_ = audio_buffer; } auto set_audio_device_id(SDL_AudioDeviceID audio_device) { @@ -179,7 +153,7 @@ class Emulator : public SharedRom { int16_t* audio_buffer_; SDL_AudioDeviceID audio_device_; - SNES snes_; + Snes snes_; SDL_Texture* ppu_texture_; std::vector rom_data_; @@ -190,7 +164,6 @@ class Emulator : public SharedRom { }; } // namespace emu -} // namespace app } // namespace yaze -#endif // YAZE_APP_CORE_EMULATOR_H \ No newline at end of file +#endif // YAZE_APP_CORE_EMULATOR_H diff --git a/src/app/emu/memory/asm_parser.h b/src/app/emu/memory/asm_parser.h index f8b107aa..d19a6dca 100644 --- a/src/app/emu/memory/asm_parser.h +++ b/src/app/emu/memory/asm_parser.h @@ -2,18 +2,14 @@ #include #include -#include #include #include #include #include -#include "absl/strings/str_cat.h" -#include "absl/strings/str_split.h" -#include "app/emu/cpu/internal/opcodes.h" +#include "absl/strings/match.h" namespace yaze { -namespace app { namespace emu { enum class AddressingMode { @@ -444,5 +440,4 @@ class AsmParser { }; } // namespace emu -} // namespace app -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/emu/memory/dma.cc b/src/app/emu/memory/dma.cc index 935b04c8..a73be058 100644 --- a/src/app/emu/memory/dma.cc +++ b/src/app/emu/memory/dma.cc @@ -1,12 +1,7 @@ #include "app/emu/memory/dma.h" -#include - namespace yaze { -namespace app { namespace emu { -namespace memory { -namespace dma { static const int bAdrOffsets[8][4] = {{0, 0, 0, 0}, {0, 1, 0, 1}, {0, 0, 0, 0}, {0, 0, 1, 1}, {0, 1, 2, 3}, {0, 1, 0, 1}, @@ -14,7 +9,7 @@ static const int bAdrOffsets[8][4] = {{0, 0, 0, 0}, {0, 1, 0, 1}, {0, 0, 0, 0}, static const int transferLength[8] = {1, 2, 2, 4, 4, 4, 2, 4}; -void Reset(MemoryImpl* memory) { +void ResetDma(MemoryImpl* memory) { auto channel = memory->dma_channels(); for (int i = 0; i < 8; i++) { channel[i].b_addr = 0xff; @@ -41,7 +36,7 @@ void Reset(MemoryImpl* memory) { memory->set_hdma_run_requested(false); } -uint8_t Read(MemoryImpl* memory, uint16_t adr) { +uint8_t ReadDma(MemoryImpl* memory, uint16_t adr) { auto channel = memory->dma_channels(); uint8_t c = (adr & 0x70) >> 4; switch (adr & 0xf) { @@ -94,7 +89,7 @@ uint8_t Read(MemoryImpl* memory, uint16_t adr) { } } -void Write(MemoryImpl* memory, uint16_t adr, uint8_t val) { +void WriteDma(MemoryImpl* memory, uint16_t adr, uint8_t val) { auto channel = memory->dma_channels(); uint8_t c = (adr & 0x70) >> 4; switch (adr & 0xf) { @@ -158,7 +153,7 @@ void Write(MemoryImpl* memory, uint16_t adr, uint8_t val) { } } -void DoDma(SNES* snes, MemoryImpl* memory, int cpuCycles) { +void DoDma(Snes* snes, MemoryImpl* memory, int cpuCycles) { auto channel = memory->dma_channels(); snes->cpu().set_int_delay(true); @@ -192,7 +187,7 @@ void DoDma(SNES* snes, MemoryImpl* memory, int cpuCycles) { snes->SyncCycles(false, cpuCycles); } -void HandleDma(SNES* snes, MemoryImpl* memory, int cpu_cycles) { +void HandleDma(Snes* snes, MemoryImpl* memory, int cpu_cycles) { // if hdma triggered, do it, except if dmastate indicates dma will be done now // (it will be done as part of the dma in that case) if (memory->hdma_init_requested() && memory->dma_state() != 2) @@ -210,7 +205,7 @@ void HandleDma(SNES* snes, MemoryImpl* memory, int cpu_cycles) { } } -void WaitCycle(SNES* snes, MemoryImpl* memory) { +void WaitCycle(Snes* snes, MemoryImpl* memory) { // run hdma if requested, no sync (already sycned due to dma) if (memory->hdma_init_requested()) InitHdma(snes, memory, false, 0); if (memory->hdma_run_requested()) DoHdma(snes, memory, false, 0); @@ -218,7 +213,7 @@ void WaitCycle(SNES* snes, MemoryImpl* memory) { snes->RunCycles(8); } -void InitHdma(SNES* snes, MemoryImpl* memory, bool do_sync, int cpu_cycles) { +void InitHdma(Snes* snes, MemoryImpl* memory, bool do_sync, int cpu_cycles) { auto channel = memory->dma_channels(); memory->set_hdma_init_requested(false); bool hdmaEnabled = false; @@ -258,7 +253,7 @@ void InitHdma(SNES* snes, MemoryImpl* memory, bool do_sync, int cpu_cycles) { if (do_sync) snes->SyncCycles(false, cpu_cycles); } -void DoHdma(SNES* snes, MemoryImpl* memory, bool do_sync, int cycles) { +void DoHdma(Snes* snes, MemoryImpl* memory, bool do_sync, int cycles) { auto channel = memory->dma_channels(); memory->set_hdma_run_requested(false); bool hdmaActive = false; @@ -333,7 +328,7 @@ void DoHdma(SNES* snes, MemoryImpl* memory, bool do_sync, int cycles) { if (do_sync) snes->SyncCycles(false, cycles); } -void TransferByte(SNES* snes, MemoryImpl* memory, uint16_t aAdr, uint8_t aBank, +void TransferByte(Snes* snes, MemoryImpl* memory, uint16_t aAdr, uint8_t aBank, uint8_t bAdr, bool fromB) { // accessing 0x2180 via b-bus while a-bus accesses ram gives open bus bool validB = @@ -369,8 +364,5 @@ void StartDma(MemoryImpl* memory, uint8_t val, bool hdma) { } } -} // namespace dma -} // namespace memory } // namespace emu -} // namespace app -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/emu/memory/dma.h b/src/app/emu/memory/dma.h index 4cbd148e..7784778a 100644 --- a/src/app/emu/memory/dma.h +++ b/src/app/emu/memory/dma.h @@ -7,31 +7,25 @@ #include "app/emu/snes.h" namespace yaze { -namespace app { namespace emu { -namespace memory { -namespace dma { -void Reset(MemoryImpl* memory); -void HandleDma(SNES* snes, MemoryImpl* memory, int cpu_cycles); +void ResetDma(MemoryImpl* memory); +void HandleDma(Snes* snes, MemoryImpl* memory, int cpu_cycles); -void WaitCycle(SNES* snes, MemoryImpl* memory); +void WaitCycle(Snes* snes, MemoryImpl* memory); -void InitHdma(SNES* snes, MemoryImpl* memory, bool do_sync, int cycles); -void DoHdma(SNES* snes, MemoryImpl* memory, bool do_sync, int cycles); +void InitHdma(Snes* snes, MemoryImpl* memory, bool do_sync, int cycles); +void DoHdma(Snes* snes, MemoryImpl* memory, bool do_sync, int cycles); -void TransferByte(SNES* snes, MemoryImpl* memory, uint16_t aAdr, uint8_t aBank, +void TransferByte(Snes* snes, MemoryImpl* memory, uint16_t aAdr, uint8_t aBank, uint8_t bAdr, bool fromB); -uint8_t Read(MemoryImpl* memory, uint16_t address); -void Write(MemoryImpl* memory, uint16_t address, uint8_t data); +uint8_t ReadDma(MemoryImpl* memory, uint16_t address); +void WriteDma(MemoryImpl* memory, uint16_t address, uint8_t data); void StartDma(MemoryImpl* memory, uint8_t val, bool hdma); -void DoDma(SNES* snes, MemoryImpl* memory, int cycles); +void DoDma(Snes* snes, MemoryImpl* memory, int cycles); -} // namespace dma -} // namespace memory } // namespace emu -} // namespace app } // namespace yaze -#endif // YAZE_APP_EMU_MEMORY_DMA_H \ No newline at end of file +#endif // YAZE_APP_EMU_MEMORY_DMA_H diff --git a/src/app/emu/memory/dma_channel.h b/src/app/emu/memory/dma_channel.h deleted file mode 100644 index 385ab472..00000000 --- a/src/app/emu/memory/dma_channel.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef YAZE_APP_EMU_MEMORY_DMA_CHANNEL_H -#define YAZE_APP_EMU_MEMORY_DMA_CHANNEL_H - -#include - -namespace yaze { -namespace app { -namespace emu { -namespace memory { - -typedef struct DmaChannel { - uint8_t b_addr; - uint16_t a_addr; - uint8_t a_bank; - uint16_t size; // also indirect hdma adr - uint8_t ind_bank; // hdma - uint16_t table_addr; // hdma - uint8_t rep_count; // hdma - uint8_t unusedByte; - bool dma_active; - bool hdma_active; - uint8_t mode; - bool fixed; - bool decrement; - bool indirect; // hdma - bool from_b; - bool unusedBit; - bool do_transfer; // hdma - bool terminated; // hdma -} DmaChannel; - -} // namespace memory -} // namespace emu -} // namespace app -} // namespace yaze - -#endif // YAZE_APP_EMU_MEMORY_DMA_CHANNEL_H \ No newline at end of file diff --git a/src/app/emu/memory/memory.cc b/src/app/emu/memory/memory.cc index f890c789..acf60bda 100644 --- a/src/app/emu/memory/memory.cc +++ b/src/app/emu/memory/memory.cc @@ -1,16 +1,10 @@ #include "app/emu/memory/memory.h" #include -#include -#include #include -#include "imgui/imgui.h" - namespace yaze { -namespace app { namespace emu { -namespace memory { void MemoryImpl::Initialize(const std::vector& rom_data, bool verbose) { @@ -23,13 +17,9 @@ void MemoryImpl::Initialize(const std::vector& rom_data, rom_.resize(rom_size_); // Copy memory into rom_ - for (size_t i = 0; i < rom_size_; i++) { - rom_[i] = rom_data[i]; - } + std::copy(rom_data.begin(), rom_data.begin() + rom_size_, rom_.begin()); ram_.resize(sram_size_); - for (size_t i = 0; i < sram_size_; i++) { - ram_[i] = 0; - } + std::fill(ram_.begin(), ram_.end(), 0); // Clear memory memory_.resize(0x1000000); // 16 MB @@ -165,7 +155,5 @@ uint32_t MemoryImpl::GetMappedAddress(uint32_t address) const { return address; // Return the original address if no mapping is defined } -} // namespace memory } // namespace emu -} // namespace app -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/emu/memory/memory.h b/src/app/emu/memory/memory.h index 6485a147..097e0049 100644 --- a/src/app/emu/memory/memory.h +++ b/src/app/emu/memory/memory.h @@ -1,14 +1,11 @@ -#ifndef MEM_H -#define MEM_H +#ifndef YAZE_APP_EMU_MEMORY_H +#define YAZE_APP_EMU_MEMORY_H #include #include #include -#include #include -#include "app/emu/memory/dma_channel.h" - // LoROM (Mode 20): // Banks Offset Purpose @@ -27,9 +24,28 @@ // 7F 0000-FFFF System RAM namespace yaze { -namespace app { namespace emu { -namespace memory { + +typedef struct DmaChannel { + uint8_t b_addr; + uint16_t a_addr; + uint8_t a_bank; + uint16_t size; // also indirect hdma adr + uint8_t ind_bank; // hdma + uint16_t table_addr; // hdma + uint8_t rep_count; // hdma + uint8_t unusedByte; + bool dma_active; + bool hdma_active; + uint8_t mode; + bool fixed; + bool decrement; + bool indirect; // hdma + bool from_b; + bool unusedBit; + bool do_transfer; // hdma + bool terminated; // hdma +} DmaChannel; typedef struct CpuCallbacks { std::function read_byte; @@ -46,7 +62,7 @@ constexpr uint32_t kRAMSize = 0x20000; * @brief Memory interface */ class Memory { - public: +public: virtual ~Memory() = default; virtual uint8_t ReadByte(uint32_t address) const = 0; virtual uint16_t ReadWord(uint32_t address) const = 0; @@ -100,25 +116,25 @@ class Memory { * */ class MemoryImpl : public Memory { - public: - void Initialize(const std::vector& romData, bool verbose = false); +public: + void Initialize(const std::vector &romData, bool verbose = false); uint16_t GetHeaderOffset() { uint16_t offset; switch (memory_[(0x00 << 16) + 0xFFD5] & 0x07) { - case 0: // LoROM - offset = 0x7FC0; - break; - case 1: // HiROM - offset = 0xFFC0; - break; - case 5: // ExHiROM - offset = 0x40; - break; - default: - throw std::invalid_argument( - "Unable to locate supported ROM mapping mode in the provided ROM " - "file. Please try another ROM file."); + case 0: // LoROM + offset = 0x7FC0; + break; + case 1: // HiROM + offset = 0xFFC0; + break; + case 5: // ExHiROM + offset = 0x40; + break; + default: + throw std::invalid_argument( + "Unable to locate supported ROM mapping mode in the provided ROM " + "file. Please try another ROM file."); } return offset; @@ -221,7 +237,7 @@ class MemoryImpl : public Memory { // Stack Pointer access. uint16_t SP() const override { return SP_; } - auto mutable_sp() -> uint16_t& { return SP_; } + auto mutable_sp() -> uint16_t & { return SP_; } void SetSP(uint16_t value) override { SP_ = value; } void ClearMemory() override { std::fill(memory_.begin(), memory_.end(), 0); } @@ -261,15 +277,15 @@ class MemoryImpl : public Memory { auto v_pos() const -> uint16_t override { return v_pos_; } auto pal_timing() const -> bool override { return pal_timing_; } - auto dma_state() -> uint8_t& { return dma_state_; } + auto dma_state() -> uint8_t & { return dma_state_; } void set_dma_state(uint8_t value) { dma_state_ = value; } - auto dma_channels() -> DmaChannel* { return channel; } + auto dma_channels() -> DmaChannel * { return channel; } // Define memory regions std::vector rom_; std::vector ram_; - private: +private: uint32_t GetMappedAddress(uint32_t address) const; bool verbose_ = false; @@ -307,9 +323,7 @@ class MemoryImpl : public Memory { std::vector memory_; }; -} // namespace memory -} // namespace emu -} // namespace app -} // namespace yaze +} // namespace emu +} // namespace yaze -#endif // MEM_H \ No newline at end of file +#endif // YAZE_APP_EMU_MEMORY_H diff --git a/src/app/emu/snes.cc b/src/app/emu/snes.cc index e152e03b..dd43f2eb 100644 --- a/src/app/emu/snes.cc +++ b/src/app/emu/snes.cc @@ -1,21 +1,13 @@ #include "app/emu/snes.h" #include -#include -#include -#include #include "app/emu/audio/apu.h" -#include "app/emu/audio/spc700.h" -#include "app/emu/cpu/clock.h" -#include "app/emu/cpu/cpu.h" #include "app/emu/memory/dma.h" #include "app/emu/memory/memory.h" #include "app/emu/video/ppu.h" -#include "app/rom.h" namespace yaze { -namespace app { namespace emu { namespace { @@ -33,7 +25,7 @@ uint8_t input_read(Input* input) { } } // namespace -void SNES::Init(std::vector& rom_data) { +void Snes::Init(std::vector& rom_data) { // Initialize the CPU, PPU, and APU ppu_.Init(); apu_.Init(); @@ -45,11 +37,11 @@ void SNES::Init(std::vector& rom_data) { running_ = true; } -void SNES::Reset(bool hard) { +void Snes::Reset(bool hard) { cpu_.Reset(hard); apu_.Reset(); ppu_.Reset(); - memory::dma::Reset(&memory_); + ResetDma(&memory_); input1.latch_line_ = false; input2.latch_line_ = false; input1.latched_state_ = 0; @@ -85,7 +77,7 @@ void SNES::Reset(bool hard) { InitAccessTime(false); } -void SNES::RunFrame() { +void Snes::RunFrame() { while (in_vblank_) { cpu_.RunOpcode(); } @@ -95,9 +87,9 @@ void SNES::RunFrame() { } } -void SNES::CatchUpApu() { apu_.RunCycles(cycles_); } +void Snes::CatchUpApu() { apu_.RunCycles(cycles_); } -void SNES::HandleInput() { +void Snes::HandleInput() { memset(port_auto_read_, 0, sizeof(port_auto_read_)); // latch controllers input_latch(&input1, true); @@ -114,7 +106,7 @@ void SNES::HandleInput() { } } -void SNES::RunCycle() { +void Snes::RunCycle() { cycles_ += 2; // check for h/v timer irq's @@ -229,7 +221,7 @@ void SNES::RunCycle() { if (auto_joy_timer_ > 0) auto_joy_timer_ -= 2; } -void SNES::RunCycles(int cycles) { +void Snes::RunCycles(int cycles) { if (memory_.h_pos() + cycles >= 536 && memory_.h_pos() < 536) { // if we go past 536, add 40 cycles for dram refersh cycles += 40; @@ -239,7 +231,7 @@ void SNES::RunCycles(int cycles) { } } -void SNES::SyncCycles(bool start, int sync_cycles) { +void Snes::SyncCycles(bool start, int sync_cycles) { int count = 0; if (start) { sync_cycle_ = cycles_; @@ -250,7 +242,7 @@ void SNES::SyncCycles(bool start, int sync_cycles) { RunCycles(count); } -uint8_t SNES::ReadBBus(uint8_t adr) { +uint8_t Snes::ReadBBus(uint8_t adr) { if (adr < 0x40) { return ppu_.Read(adr, ppu_latch_); } @@ -266,7 +258,7 @@ uint8_t SNES::ReadBBus(uint8_t adr) { return memory_.open_bus(); } -uint8_t SNES::ReadReg(uint16_t adr) { +uint8_t Snes::ReadReg(uint16_t adr) { switch (adr) { case 0x4210: { uint8_t val = 0x2; // CPU version (4 bit) @@ -319,7 +311,7 @@ uint8_t SNES::ReadReg(uint16_t adr) { } } -uint8_t SNES::Rread(uint32_t adr) { +uint8_t Snes::Rread(uint32_t adr) { uint8_t bank = adr >> 16; adr &= 0xffff; if (bank == 0x7e || bank == 0x7f) { @@ -342,20 +334,20 @@ uint8_t SNES::Rread(uint32_t adr) { return ReadReg(adr); // internal registers } if (adr >= 0x4300 && adr < 0x4380) { - return memory::dma::Read(&memory_, adr); // dma registers + return ReadDma(&memory_, adr); // dma registers } } // read from cart return memory_.cart_read(bank, adr); } -uint8_t SNES::Read(uint32_t adr) { +uint8_t Snes::Read(uint32_t adr) { uint8_t val = Rread(adr); memory_.set_open_bus(val); return val; } -void SNES::WriteBBus(uint8_t adr, uint8_t val) { +void Snes::WriteBBus(uint8_t adr, uint8_t val) { if (adr < 0x40) { ppu_.Write(adr, val); return; @@ -386,7 +378,7 @@ void SNES::WriteBBus(uint8_t adr, uint8_t val) { } } -void SNES::WriteReg(uint16_t adr, uint8_t val) { +void Snes::WriteReg(uint16_t adr, uint8_t val) { switch (adr) { case 0x4200: { auto_joy_read_ = val & 0x1; @@ -456,11 +448,11 @@ void SNES::WriteReg(uint16_t adr, uint8_t val) { break; } case 0x420b: { - memory::dma::StartDma(&memory_, val, false); + StartDma(&memory_, val, false); break; } case 0x420c: { - memory::dma::StartDma(&memory_, val, true); + StartDma(&memory_, val, true); break; } case 0x420d: { @@ -473,7 +465,7 @@ void SNES::WriteReg(uint16_t adr, uint8_t val) { } } -void SNES::Write(uint32_t adr, uint8_t val) { +void Snes::Write(uint32_t adr, uint8_t val) { memory_.set_open_bus(val); uint8_t bank = adr >> 16; adr &= 0xffff; @@ -495,7 +487,7 @@ void SNES::Write(uint32_t adr, uint8_t val) { WriteReg(adr, val); // internal registers } if (adr >= 0x4300 && adr < 0x4380) { - memory::dma::Write(&memory_, adr, val); // dma registers + WriteDma(&memory_, adr, val); // dma registers } } @@ -503,7 +495,7 @@ void SNES::Write(uint32_t adr, uint8_t val) { memory_.cart_write(bank, adr, val); } -int SNES::GetAccessTime(uint32_t adr) { +int Snes::GetAccessTime(uint32_t adr) { uint8_t bank = adr >> 16; adr &= 0xffff; if ((bank < 0x40 || (bank >= 0x80 && bank < 0xc0)) && adr < 0x8000) { @@ -517,38 +509,38 @@ int SNES::GetAccessTime(uint32_t adr) { : 8; // depends on setting in banks 80+ } -uint8_t SNES::CpuRead(uint32_t adr) { +uint8_t Snes::CpuRead(uint32_t adr) { cpu_.set_int_delay(false); const int cycles = access_time[adr] - 4; - memory::dma::HandleDma(this, &memory_, cycles); + HandleDma(this, &memory_, cycles); RunCycles(cycles); uint8_t rv = Read(adr); - memory::dma::HandleDma(this, &memory_, 4); + HandleDma(this, &memory_, 4); RunCycles(4); return rv; } -void SNES::CpuWrite(uint32_t adr, uint8_t val) { +void Snes::CpuWrite(uint32_t adr, uint8_t val) { cpu_.set_int_delay(false); const int cycles = access_time[adr]; - memory::dma::HandleDma(this, &memory_, cycles); + HandleDma(this, &memory_, cycles); RunCycles(cycles); Write(adr, val); } -void SNES::CpuIdle(bool waiting) { +void Snes::CpuIdle(bool waiting) { cpu_.set_int_delay(false); - memory::dma::HandleDma(this, &memory_, 6); + HandleDma(this, &memory_, 6); RunCycles(6); } -void SNES::SetSamples(int16_t* sample_data, int wanted_samples) { +void Snes::SetSamples(int16_t* sample_data, int wanted_samples) { apu_.dsp().GetSamples(sample_data, wanted_samples, memory_.pal_timing()); } -void SNES::SetPixels(uint8_t* pixel_data) { ppu_.PutPixels(pixel_data); } +void Snes::SetPixels(uint8_t* pixel_data) { ppu_.PutPixels(pixel_data); } -void SNES::SetButtonState(int player, int button, bool pressed) { +void Snes::SetButtonState(int player, int button, bool pressed) { // set key in controller if (player == 1) { if (pressed) { @@ -565,7 +557,7 @@ void SNES::SetButtonState(int player, int button, bool pressed) { } } -void SNES::InitAccessTime(bool recalc) { +void Snes::InitAccessTime(bool recalc) { int start = (recalc) ? 0x800000 : 0; // recalc only updates fast rom access_time.resize(0x1000000); for (int i = start; i < 0x1000000; i++) { @@ -574,5 +566,4 @@ void SNES::InitAccessTime(bool recalc) { } } // namespace emu -} // namespace app -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/emu/snes.h b/src/app/emu/snes.h index 6b01c671..993c5338 100644 --- a/src/app/emu/snes.h +++ b/src/app/emu/snes.h @@ -10,22 +10,20 @@ #include "app/emu/video/ppu.h" namespace yaze { -namespace app { namespace emu { struct Input { uint8_t type; - // latchline bool latch_line_; // for controller uint16_t current_state_; // actual state uint16_t latched_state_; }; -class SNES { +class Snes { public: - SNES() = default; - ~SNES() = default; + Snes() = default; + ~Snes() = default; // Initialization void Init(std::vector& rom_data); @@ -62,9 +60,9 @@ class SNES { void SetButtonState(int player, int button, bool pressed); bool running() const { return running_; } auto cpu() -> Cpu& { return cpu_; } - auto ppu() -> video::Ppu& { return ppu_; } - auto apu() -> audio::Apu& { return apu_; } - auto Memory() -> memory::MemoryImpl& { return memory_; } + auto ppu() -> Ppu& { return ppu_; } + auto apu() -> Apu& { return apu_; } + auto Memory() -> MemoryImpl& { return memory_; } auto get_ram() -> uint8_t* { return ram; } auto mutable_cycles() -> uint64_t& { return cycles_; } void InitAccessTime(bool recalc); @@ -74,16 +72,16 @@ class SNES { private: // Components of the SNES ClockImpl clock_; - memory::MemoryImpl memory_; + MemoryImpl memory_; - memory::CpuCallbacks cpu_callbacks_ = { + CpuCallbacks cpu_callbacks_ = { [&](uint32_t adr) { return CpuRead(adr); }, [&](uint32_t adr, uint8_t val) { CpuWrite(adr, val); }, [&](bool waiting) { CpuIdle(waiting); }, }; Cpu cpu_{memory_, clock_, cpu_callbacks_}; - video::Ppu ppu_{memory_, clock_}; - audio::Apu apu_{memory_}; + Ppu ppu_{memory_, clock_}; + Apu apu_{memory_}; // Currently loaded ROM std::vector rom_data; @@ -131,7 +129,7 @@ class SNES { }; } // namespace emu -} // namespace app + } // namespace yaze #endif // YAZE_APP_EMU_SNES_H diff --git a/src/app/emu/video/ppu.cc b/src/app/emu/video/ppu.cc index f7fa8c32..a065aa0c 100644 --- a/src/app/emu/video/ppu.cc +++ b/src/app/emu/video/ppu.cc @@ -7,11 +7,7 @@ #include "app/emu/memory/memory.h" namespace yaze { -namespace app { namespace emu { -namespace video { - -using namespace PpuRegisters; // array for layer definitions per mode: // 0-7: mode 0-7; 8: mode 1 + l3prio; 9: mode 7 + extbg @@ -1054,7 +1050,5 @@ void Ppu::PutPixels(uint8_t* pixels) { } } -} // namespace video } // namespace emu -} // namespace app } // namespace yaze diff --git a/src/app/emu/video/ppu.h b/src/app/emu/video/ppu.h index 65418001..fe92e020 100644 --- a/src/app/emu/video/ppu.h +++ b/src/app/emu/video/ppu.h @@ -11,12 +11,7 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace emu { -namespace video { - -using namespace PpuRegisters; -using namespace memory; class PpuInterface { public: @@ -259,7 +254,7 @@ struct BackgroundLayer { class Ppu : public SharedRom { public: // Initializes the PPU with the necessary resources and dependencies - Ppu(memory::Memory& memory, Clock& clock) : memory_(memory), clock_(clock) {} + Ppu(Memory& memory, Clock& clock) : memory_(memory), clock_(clock) {} // Initialize the frame buffer void Init() { @@ -456,9 +451,7 @@ class Ppu : public SharedRom { std::array bgvofs_; }; -} // namespace video } // namespace emu -} // namespace app } // namespace yaze -#endif // YAZE_APP_EMU_PPU_H \ No newline at end of file +#endif // YAZE_APP_EMU_PPU_H diff --git a/src/app/emu/video/ppu_registers.h b/src/app/emu/video/ppu_registers.h index 5a9d8bc0..492ba5ca 100644 --- a/src/app/emu/video/ppu_registers.h +++ b/src/app/emu/video/ppu_registers.h @@ -1,15 +1,10 @@ #ifndef YAZE_APP_EMU_VIDEO_PPU_REGISTERS_H #define YAZE_APP_EMU_VIDEO_PPU_REGISTERS_H -#include #include -#include namespace yaze { -namespace app { namespace emu { -namespace video { -namespace PpuRegisters { constexpr uint16_t INIDISP = 0x2100; @@ -413,10 +408,7 @@ struct STAT78 { uint8_t unused : 1; }; -} // namespace PpuRegisters -} // namespace video } // namespace emu -} // namespace app } // namespace yaze -#endif // YAZE_APP_EMU_VIDEO_PPU_REGISTERS_H \ No newline at end of file +#endif // YAZE_APP_EMU_VIDEO_PPU_REGISTERS_H diff --git a/src/app/gfx/bitmap.cc b/src/app/gfx/bitmap.cc index 890574a0..78be9d45 100644 --- a/src/app/gfx/bitmap.cc +++ b/src/app/gfx/bitmap.cc @@ -18,7 +18,6 @@ } namespace yaze { -namespace app { namespace gfx { using core::SDL_Surface_Deleter; @@ -437,9 +436,8 @@ void Bitmap::Get8x8Tile(int tile_index, int x, int y, int tile_x = (x * 8) % width_; int tile_y = (y * 8) % height_; for (int i = 0; i < 8; i++) { - int row_offset = tile_offset + ((tile_y + i) * width_); for (int j = 0; j < 8; j++) { - int pixel_offset = row_offset + (tile_x + j); + int pixel_offset = tile_offset + (tile_y + i) * width_ + tile_x + j; int pixel_value = data_[pixel_offset]; tile_data[tile_data_offset] = pixel_value; tile_data_offset++; @@ -478,9 +476,11 @@ void Bitmap::WriteColor(int position, const ImVec4 &color) { // Write the color index to the pixel data pixel_data_[position] = index; + data_[position] = ConvertRgbToSnes(color); + modified_ = true; } } // namespace gfx -} // namespace app + } // namespace yaze diff --git a/src/app/gfx/bitmap.h b/src/app/gfx/bitmap.h index 48a97312..7ea9a144 100644 --- a/src/app/gfx/bitmap.h +++ b/src/app/gfx/bitmap.h @@ -12,10 +12,9 @@ #include "app/gfx/snes_palette.h" namespace yaze { -namespace app { /** - * @namespace yaze::app::gfx + * @namespace yaze::gfx * @brief Contains classes for handling graphical data. */ namespace gfx { @@ -214,7 +213,7 @@ class Bitmap { using BitmapTable = std::unordered_map; } // namespace gfx -} // namespace app + } // namespace yaze #endif // YAZE_APP_GFX_BITMAP_H diff --git a/src/app/gfx/compression.cc b/src/app/gfx/compression.cc index fcb5cfab..20d5a05e 100644 --- a/src/app/gfx/compression.cc +++ b/src/app/gfx/compression.cc @@ -6,19 +6,299 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" #include "app/core/constants.h" #include "app/rom.h" #define DEBUG_LOG(msg) std::cout << msg << std::endl namespace yaze { -namespace app { namespace gfx { -namespace lc_lz2 { +std::vector HyruleMagicCompress(uint8_t const* const src, + int const oldsize, int* const size, + int const flag) { + unsigned char* b2 = + (unsigned char*)malloc(0x1000); // allocate a 2^12 sized buffer -// Compression commands + int i, j, k, l, m = 0, n, o = 0, bd = 0, p, q = 0, r; + + for (i = 0; i < oldsize;) { + l = src[i]; // grab a char from the buffer. + + k = 0; + + r = !!q; // r = the same logical value (0 or 1) as q, but not the same + // value necesarily. + + for (j = 0; j < i - 1; j++) { + if (src[j] == l) { + m = oldsize - j; + + for (n = 0; n < m; n++) + if (src[n + j] != src[n + i]) break; + + if (n > k) k = n, o = j; + } + } + + for (n = i + 1; n < oldsize; n++) { + if (src[n] != l) { + // look for chars identical to the first one. + // stop if we can't find one. + // n will reach i+k+1 for some k >= 0. + + break; + } + } + + n -= i; // offset back by i. i.e. n = k+1 as above. + + if (n > 1 + r) + p = 1; + else { + m = src[i + 1]; + + for (n = i + 2; n < oldsize; n++) { + if (src[n] != l) break; + + n++; + + if (src[n] != m) break; + } + + n -= i; + + if (n > 2 + r) + p = 2; + else { + m = oldsize - i; + + for (n = 1; n < m; n++) + if (src[i + n] != l + n) break; + + if (n > 1 + r) + p = 3; + else + p = 0; + } + } + + if (k > 3 + r && k > n + (p & 1)) p = 4, n = k; + + if (!p) + q++, i++; + else { + if (q) { + q--; + + if (q > 31) { + b2[bd++] = (unsigned char)(224 + (q >> 8)); + } + + b2[bd++] = (unsigned char)q; + q++; + + memcpy(b2 + bd, src + i - q, q); + + bd += q; + q = 0; + } + + i += n; + n--; + + if (n > 31) { + b2[bd++] = (unsigned char)(224 + (n >> 8) + (p << 2)); + b2[bd++] = (unsigned char)n; + } else + b2[bd++] = (unsigned char)((p << 5) + n); + + switch (p) { + case 1: + case 3: + b2[bd++] = (unsigned char)l; + break; + + case 2: + b2[bd++] = (unsigned char)l; + b2[bd++] = (unsigned char)m; + + break; + + case 4: + if (flag) { + b2[bd++] = (unsigned char)(o >> 8); + b2[bd++] = (unsigned char)o; + } else { + b2[bd++] = (unsigned char)o; + b2[bd++] = (unsigned char)(o >> 8); + } + } + + continue; + } + } + + if (q) { + q--; + + if (q > 31) { + b2[bd++] = (unsigned char)(224 + (q >> 8)); + } + + b2[bd++] = (unsigned char)q; + q++; + + memcpy(b2 + bd, src + i - q, q); + + bd += q; + } + + b2[bd++] = 255; + b2 = (unsigned char*)realloc(b2, bd); + *size = bd; + + std::vector compressed_data(b2, b2 + bd); + free(b2); + return compressed_data; +} + +std::vector HyruleMagicDecompress(uint8_t const* src, int* const size, + int const p_big_endian) { + unsigned char* b2 = (unsigned char*)malloc(1024); + + int bd = 0, bs = 1024; + + unsigned char a; + unsigned char b; + unsigned short c, d; + + for (;;) { + // retrieve a uchar from the buffer. + a = *(src++); + + // end the decompression routine if we encounter 0xff. + if (a == 0xff) break; + + // examine the top 3 bits of a. + b = (a >> 5); + + if (b == 7) // i.e. 0b 111 + { + // get bits 0b 0001 1100 + b = ((a >> 2) & 7); + + // get bits 0b 0000 0011, multiply by 256, OR with the next byte. + c = ((a & 0x0003) << 8); + c |= *(src++); + } else + // or get bits 0b 0001 1111 + c = (uint16_t)(a & 31); + + c++; + + if ((bd + c) > (bs - 512)) { + // need to increase the buffer size. + bs += 1024; + b2 = (uint8_t*)realloc(b2, bs); + } + + // 7 was handled, here we handle other decompression codes. + + switch (b) { + case 0: // 0b 000 + + // raw copy + + // copy info from the src buffer to our new buffer, + // at offset bd (which we'll be increasing; + memcpy(b2 + bd, src, c); + + // increment the src pointer accordingly. + src += c; + + bd += c; + + break; + + case 1: // 0b 001 + + // rle copy + + // make c duplicates of one byte, inc the src pointer. + memset(b2 + bd, *(src++), c); + + // increase the b2 offset. + bd += c; + + break; + + case 2: // 0b 010 + + // rle 16-bit alternating copy + + d = core::ldle16b(src); + + src += 2; + + while (c > 1) { + // copy that 16-bit number c/2 times into the b2 buffer. + core::stle16b(b2 + bd, d); + + bd += 2; + c -= 2; // hence c/2 + } + + if (c) // if there's a remainder of c/2, this handles it. + b2[bd++] = (char)d; + + break; + + case 3: // 0b 011 + + // incrementing copy + + // get the current src byte. + a = *(src++); + + while (c--) { + // increment that byte and copy to b2 in c iterations. + // e.g. a = 4, b2 will have 4,5,6,7,8... written to it. + b2[bd++] = a++; + } + + break; + + default: // 0b 100, 101, 110 + + // lz copy + + if (p_big_endian) { + d = (*src << 8) + src[1]; + } else { + d = core::ldle16b(src); + } + + while (c--) { + // copy from a different location in the buffer. + b2[bd++] = b2[d++]; + } + + src += 2; + } + } + + b2 = (unsigned char*)realloc(b2, bd); + + if (size) (*size) = bd; + + // return the unsigned char* buffer b2, which contains the uncompressed data. + std::vector decompressed_data(b2, b2 + bd); + free(b2); + return decompressed_data; +} + +namespace lc_lz2 { void PrintCompressionPiece(const CompressionPiecePointer& piece) { std::cout << "Command: " << std::to_string(piece->command) << "\n"; @@ -179,9 +459,6 @@ void CompressionCommandAlternative(const uchar* rom_data, comp_accumulator = 0; } -// ============================================================================ -// Compression V2 - void CheckByteRepeatV2(const uchar* data, uint& src_pos, const uint last_pos, CompressionCommand& cmd) { uint i = 0; @@ -380,15 +657,17 @@ absl::StatusOr SplitCompressionPiece( new_piece->argument[0] = (char)(piece->argument[0] + kMaxLengthCompression); break; - case kCommandDirectCopy: + case kCommandDirectCopy: { + std::string empty; piece->argument_length = kMaxLengthCompression; new_piece = std::make_shared( - piece->command, length_left, nullptr, length_left); + piece->command, length_left, empty, length_left); // MEMCPY for (int i = 0; i < length_left; ++i) { new_piece->argument[i] = piece->argument[i + kMaxLengthCompression]; } break; + } case kCommandRepeatingBytes: { piece->argument_length = kMaxLengthCompression; uint offset = piece->argument[0] + (piece->argument[1] << 8); @@ -526,7 +805,9 @@ absl::StatusOr> CompressV2(const uchar* data, } // Worst case should be a copy of the string with extended header - auto compressed_chain = std::make_shared(1, 1, "aaa", 2); + std::string worst_case = "aaa"; + auto compressed_chain = + std::make_shared(1, 1, worst_case, 2); auto compressed_chain_start = compressed_chain; CompressionCommand current_cmd = {/*argument*/ {{}}, @@ -590,286 +871,6 @@ absl::StatusOr> CompressV2(const uchar* data, return CreateCompressionString(compressed_chain_start->next, mode); } -// Hyrule Magic -uint8_t* Compress(uint8_t const* const src, int const oldsize, int* const size, - int const flag) { - unsigned char* b2 = - (unsigned char*)malloc(0x1000); // allocate a 2^12 sized buffer - - int i, j, k, l, m = 0, n, o = 0, bd = 0, p, q = 0, r; - - for (i = 0; i < oldsize;) { - l = src[i]; // grab a char from the buffer. - - k = 0; - - r = !!q; // r = the same logical value (0 or 1) as q, but not the same - // value necesarily. - - for (j = 0; j < i - 1; j++) { - if (src[j] == l) { - m = oldsize - j; - - for (n = 0; n < m; n++) - if (src[n + j] != src[n + i]) break; - - if (n > k) k = n, o = j; - } - } - - for (n = i + 1; n < oldsize; n++) { - if (src[n] != l) { - // look for chars identical to the first one. - // stop if we can't find one. - // n will reach i+k+1 for some k >= 0. - - break; - } - } - - n -= i; // offset back by i. i.e. n = k+1 as above. - - if (n > 1 + r) - p = 1; - else { - m = src[i + 1]; - - for (n = i + 2; n < oldsize; n++) { - if (src[n] != l) break; - - n++; - - if (src[n] != m) break; - } - - n -= i; - - if (n > 2 + r) - p = 2; - else { - m = oldsize - i; - - for (n = 1; n < m; n++) - if (src[i + n] != l + n) break; - - if (n > 1 + r) - p = 3; - else - p = 0; - } - } - - if (k > 3 + r && k > n + (p & 1)) p = 4, n = k; - - if (!p) - q++, i++; - else { - if (q) { - q--; - - if (q > 31) { - b2[bd++] = (unsigned char)(224 + (q >> 8)); - } - - b2[bd++] = (unsigned char)q; - q++; - - memcpy(b2 + bd, src + i - q, q); - - bd += q; - q = 0; - } - - i += n; - n--; - - if (n > 31) { - b2[bd++] = (unsigned char)(224 + (n >> 8) + (p << 2)); - b2[bd++] = (unsigned char)n; - } else - b2[bd++] = (unsigned char)((p << 5) + n); - - switch (p) { - case 1: - case 3: - b2[bd++] = (unsigned char)l; - break; - - case 2: - b2[bd++] = (unsigned char)l; - b2[bd++] = (unsigned char)m; - - break; - - case 4: - if (flag) { - b2[bd++] = (unsigned char)(o >> 8); - b2[bd++] = (unsigned char)o; - } else { - b2[bd++] = (unsigned char)o; - b2[bd++] = (unsigned char)(o >> 8); - } - } - - continue; - } - } - - if (q) { - q--; - - if (q > 31) { - b2[bd++] = (unsigned char)(224 + (q >> 8)); - } - - b2[bd++] = (unsigned char)q; - q++; - - memcpy(b2 + bd, src + i - q, q); - - bd += q; - } - - b2[bd++] = 255; - b2 = (unsigned char*)realloc(b2, bd); - *size = bd; - - return b2; -} - -uint8_t* Uncompress(uint8_t const* src, int* const size, - int const p_big_endian) { - unsigned char* b2 = (unsigned char*)malloc(1024); - - int bd = 0, bs = 1024; - - unsigned char a; - unsigned char b; - unsigned short c, d; - - for (;;) { - // retrieve a uchar from the buffer. - a = *(src++); - - // end the decompression routine if we encounter 0xff. - if (a == 0xff) break; - - // examine the top 3 bits of a. - b = (a >> 5); - - if (b == 7) // i.e. 0b 111 - { - // get bits 0b 0001 1100 - b = ((a >> 2) & 7); - - // get bits 0b 0000 0011, multiply by 256, OR with the next byte. - c = ((a & 0x0003) << 8); - c |= *(src++); - } else - // or get bits 0b 0001 1111 - c = (uint16_t)(a & 31); - - c++; - - if ((bd + c) > (bs - 512)) { - // need to increase the buffer size. - bs += 1024; - b2 = (uint8_t*)realloc(b2, bs); - } - - // 7 was handled, here we handle other decompression codes. - - switch (b) { - case 0: // 0b 000 - - // raw copy - - // copy info from the src buffer to our new buffer, - // at offset bd (which we'll be increasing; - memcpy(b2 + bd, src, c); - - // increment the src pointer accordingly. - src += c; - - bd += c; - - break; - - case 1: // 0b 001 - - // rle copy - - // make c duplicates of one byte, inc the src pointer. - memset(b2 + bd, *(src++), c); - - // increase the b2 offset. - bd += c; - - break; - - case 2: // 0b 010 - - // rle 16-bit alternating copy - - d = core::ldle16b(src); - - src += 2; - - while (c > 1) { - // copy that 16-bit number c/2 times into the b2 buffer. - core::stle16b(b2 + bd, d); - - bd += 2; - c -= 2; // hence c/2 - } - - if (c) // if there's a remainder of c/2, this handles it. - b2[bd++] = (char)d; - - break; - - case 3: // 0b 011 - - // incrementing copy - - // get the current src byte. - a = *(src++); - - while (c--) { - // increment that byte and copy to b2 in c iterations. - // e.g. a = 4, b2 will have 4,5,6,7,8... written to it. - b2[bd++] = a++; - } - - break; - - default: // 0b 100, 101, 110 - - // lz copy - - if (p_big_endian) { - d = (*src << 8) + src[1]; - } else { - d = core::ldle16b(src); - } - - while (c--) { - // copy from a different location in the buffer. - b2[bd++] = b2[d++]; - } - - src += 2; - } - } - - b2 = (unsigned char*)realloc(b2, bd); - - if (size) (*size) = bd; - - // return the unsigned char* buffer b2, which contains the uncompressed data. - return b2; -} - absl::StatusOr> CompressGraphics(const uchar* data, const int pos, const int length) { @@ -887,9 +888,6 @@ absl::StatusOr> CompressOverworld( return CompressV3(data, pos, length, kNintendoMode1); } -// ============================================================================ -// Compression V3 - void CheckByteRepeatV3(CompressionContext& context) { uint pos = context.src_pos; @@ -1209,15 +1207,17 @@ absl::StatusOr SplitCompressionPieceV3( piece.argument_length); new_piece.argument[0] = (char)(piece.argument[0] + kMaxLengthCompression); break; - case kCommandDirectCopy: + case kCommandDirectCopy: { piece.argument_length = kMaxLengthCompression; - new_piece = - CompressionPiece(piece.command, length_left, nullptr, length_left); + std::string empty_string = ""; + new_piece = CompressionPiece(piece.command, length_left, empty_string, + length_left); // MEMCPY for (int i = 0; i < length_left; ++i) { new_piece.argument[i] = piece.argument[i + kMaxLengthCompression]; } break; + } case kCommandRepeatingBytes: { piece.argument_length = kMaxLengthCompression; uint offset = piece.argument[0] + (piece.argument[1] << 8); @@ -1340,8 +1340,6 @@ absl::StatusOr> CompressV3( context.compressed_data.end()); } -// Decompression - std::string SetBuffer(const uchar* data, int src_pos, int comp_accumulator) { std::string buffer; for (int i = 0; i < comp_accumulator; ++i) { @@ -1473,5 +1471,4 @@ absl::StatusOr> DecompressOverworld( } // namespace lc_lz2 } // namespace gfx -} // namespace app -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/gfx/compression.h b/src/app/gfx/compression.h index d2a0aa92..f074a788 100644 --- a/src/app/gfx/compression.h +++ b/src/app/gfx/compression.h @@ -3,6 +3,7 @@ #include #include +#include #include "absl/status/status.h" #include "absl/status/statusor.h" @@ -11,11 +12,17 @@ #define BUILD_HEADER(command, length) (command << 5) + (length - 1) namespace yaze { -namespace app { namespace gfx { +std::vector HyruleMagicCompress(uint8_t const* const src, + int const oldsize, int* const size, + int const flag); + +std::vector HyruleMagicDecompress(uint8_t const* src, int* const size, + int const p_big_endian); + /** - * @namespace yaze::app::gfx::lc_lz2 + * @namespace yaze::gfx::lc_lz2 * @brief Contains the LC_LZ2 compression algorithm. */ namespace lc_lz2 { @@ -75,7 +82,7 @@ struct CompressionPiece { std::string argument; std::shared_ptr next = nullptr; CompressionPiece() = default; - CompressionPiece(int cmd, int len, std::string args, int arg_len) + CompressionPiece(int cmd, int len, std::string& args, int arg_len) : command(cmd), length(len), argument_length(arg_len), argument(args) {} }; using CompressionPiece = struct CompressionPiece; @@ -139,31 +146,33 @@ void CompressionCommandAlternativeV2(const uchar* data, /** * @brief Compresses a buffer of data using the LC_LZ2 algorithm. - * \deprecated Use Compress and Uncompress instead. + * \deprecated Use HyruleMagicDecompress instead. */ -absl::StatusOr> CompressV2(const uchar* data, const int start, - const int length, int mode = 1, - bool check = false); +absl::StatusOr> CompressV2(const uchar* data, + const int start, + const int length, int mode = 1, + bool check = false); -absl::StatusOr> CompressGraphics(const uchar* data, const int pos, - const int length); -absl::StatusOr> CompressOverworld(const uchar* data, const int pos, - const int length); -absl::StatusOr> CompressOverworld(const std::vector data, - const int pos, const int length); +absl::StatusOr> CompressGraphics(const uchar* data, + const int pos, + const int length); +absl::StatusOr> CompressOverworld(const uchar* data, + const int pos, + const int length); +absl::StatusOr> CompressOverworld( + const std::vector data, const int pos, const int length); absl::StatusOr SplitCompressionPiece( CompressionPiecePointer& piece, int mode); -std::vector CreateCompressionString(CompressionPiecePointer& start, int mode); +std::vector CreateCompressionString(CompressionPiecePointer& start, + int mode); absl::Status ValidateCompressionResult(CompressionPiecePointer& chain_head, int mode, int start, int src_data_pos); CompressionPiecePointer MergeCopy(CompressionPiecePointer& start); -// Compression V3 - struct CompressionContext { std::vector data; std::vector compressed_data; @@ -210,42 +219,35 @@ void FinalizeCompression(CompressionContext& context); /** * @brief Compresses a buffer of data using the LC_LZ2 algorithm. - * \deprecated Use Compress and Uncompress instead. + * \deprecated Use HyruleMagicCompress */ -absl::StatusOr> CompressV3(const std::vector& data, - const int start, const int length, - int mode = 1, bool check = false); - -// Hyrule Magic -uint8_t* Compress(uint8_t const* const src, int const oldsize, int* const size, - int const flag); - -uint8_t* Uncompress(uint8_t const* src, int* const size, - int const p_big_endian); - -// Decompression +absl::StatusOr> CompressV3( + const std::vector& data, const int start, const int length, + int mode = 1, bool check = false); std::string SetBuffer(const std::vector& data, int src_pos, int comp_accumulator); std::string SetBuffer(const uchar* data, int src_pos, int comp_accumulator); -void memfill(const uchar* data, std::vector& buffer, int buffer_pos, int offset, - int length); +void memfill(const uchar* data, std::vector& buffer, int buffer_pos, + int offset, int length); /** * @brief Decompresses a buffer of data using the LC_LZ2 algorithm. - * \deprecated Use Compress and Uncompress instead. + * @note Works well for graphics but not overworld data. Prefer Hyrule Magic + * routines for overworld data. */ absl::StatusOr> DecompressV2(const uchar* data, int offset, - int size = 0x800, int mode = 1); -absl::StatusOr> DecompressGraphics(const uchar* data, int pos, int size); -absl::StatusOr> DecompressOverworld(const uchar* data, int pos, int size); -absl::StatusOr> DecompressOverworld(const std::vector data, - int pos, int size); + int size = 0x800, + int mode = 1); +absl::StatusOr> DecompressGraphics(const uchar* data, + int pos, int size); +absl::StatusOr> DecompressOverworld(const uchar* data, + int pos, int size); +absl::StatusOr> DecompressOverworld( + const std::vector data, int pos, int size); } // namespace lc_lz2 - } // namespace gfx -} // namespace app } // namespace yaze #endif // YAZE_APP_GFX_COMPRESSION_H diff --git a/src/app/gfx/scad_format.cc b/src/app/gfx/scad_format.cc index 591c5663..06b6b2a2 100644 --- a/src/app/gfx/scad_format.cc +++ b/src/app/gfx/scad_format.cc @@ -5,7 +5,6 @@ #include #include #include -#include #include #include "absl/status/status.h" @@ -13,7 +12,6 @@ #include "app/gfx/snes_tile.h" namespace yaze { -namespace app { namespace gfx { namespace scad_format { @@ -279,5 +277,4 @@ absl::Status DecodeObjFile( } // namespace scad_format } // namespace gfx -} // namespace app } // namespace yaze diff --git a/src/app/gfx/scad_format.h b/src/app/gfx/scad_format.h index 434b0468..98926fdf 100644 --- a/src/app/gfx/scad_format.h +++ b/src/app/gfx/scad_format.h @@ -3,27 +3,20 @@ #include -#include #include #include #include -#include -#include -#include #include #include #include #include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "app/core/constants.h" namespace yaze { -namespace app { namespace gfx { /** - * @namespace yaze::app::gfx::scad_format + * @namespace yaze::gfx::scad_format * @brief Loading from prototype SCAD format */ namespace scad_format { @@ -100,7 +93,6 @@ absl::Status DecodeObjFile( } // namespace scad_format } // namespace gfx -} // namespace app } // namespace yaze -#endif // YAZE_APP_GFX_scad_format_H \ No newline at end of file +#endif // YAZE_APP_GFX_scad_format_H diff --git a/src/app/gfx/snes_color.cc b/src/app/gfx/snes_color.cc index 1c74d444..34fa10b0 100644 --- a/src/app/gfx/snes_color.cc +++ b/src/app/gfx/snes_color.cc @@ -7,7 +7,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace gfx { constexpr uint16_t SNES_RED_MASK = 32; @@ -100,5 +99,4 @@ std::vector GetColFileData(uint8_t* data) { } } // namespace gfx -} // namespace app } // namespace yaze diff --git a/src/app/gfx/snes_color.h b/src/app/gfx/snes_color.h index 8045c1e8..82a37536 100644 --- a/src/app/gfx/snes_color.h +++ b/src/app/gfx/snes_color.h @@ -9,7 +9,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace gfx { constexpr int NumberOfColors = 3143; @@ -108,7 +107,6 @@ SnesColor GetCgxColor(uint16_t color); std::vector GetColFileData(uint8_t* data); } // namespace gfx -} // namespace app } // namespace yaze #endif // YAZE_APP_GFX_SNES_COLOR_H_ diff --git a/src/app/gfx/snes_palette.cc b/src/app/gfx/snes_palette.cc index a8005ae7..33471721 100644 --- a/src/app/gfx/snes_palette.cc +++ b/src/app/gfx/snes_palette.cc @@ -17,11 +17,10 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace gfx { /** - * @namespace yaze::app::gfx::palette_group_internal + * @namespace yaze::gfx::palette_group_internal * @brief Internal functions for loading palettes by group. */ namespace palette_group_internal { @@ -350,7 +349,7 @@ absl::Status LoadAllPalettes(const std::vector &rom_data, return absl::OkStatus(); } -} // namespace gfx std::unordered_map GfxContext::palettesets_; -} // namespace app + +} // namespace gfx } // namespace yaze diff --git a/src/app/gfx/snes_palette.h b/src/app/gfx/snes_palette.h index bbe7bf57..47128644 100644 --- a/src/app/gfx/snes_palette.h +++ b/src/app/gfx/snes_palette.h @@ -15,7 +15,6 @@ #include "snes_color.h" namespace yaze { -namespace app { namespace gfx { constexpr int kNumPalettes = 14; @@ -61,8 +60,9 @@ constexpr int kOverworldPaletteAux = 0xDE86C; constexpr int kOverworldPaletteAnimated = 0xDE604; constexpr int kGlobalSpritesLW = 0xDD218; constexpr int kGlobalSpritePalettesDW = 0xDD290; -constexpr int kArmorPalettes = - 0xDD308; /** < Green, Blue, Red, Bunny, Electrocuted (15 colors each) */ + +/** < Green, Blue, Red, Bunny, Electrocuted (15 colors each) */ +constexpr int kArmorPalettes = 0xDD308; constexpr int kSpritesPalettesAux1 = 0xDD39E; // 7 colors each constexpr int kSpritesPalettesAux2 = 0xDD446; // 7 colors each constexpr int kSpritesPalettesAux3 = 0xDD4E0; // 7 colors each @@ -70,8 +70,9 @@ constexpr int kSwordPalettes = 0xDD630; // 3 colors each - 4 entries constexpr int kShieldPalettes = 0xDD648; // 4 colors each - 3 entries constexpr int kHudPalettes = 0xDD660; constexpr int kDungeonMapPalettes = 0xDD70A; // 21 colors -constexpr int kDungeonMainPalettes = - 0xDD734; // (15*6) colors each - 20 entries + +// (15*6) colors each - 20 entries +constexpr int kDungeonMainPalettes = 0xDD734; constexpr int kDungeonMapBgPalettes = 0xDE544; // 16*6 // Mirrored Value at 0x75645 : 0x75625 @@ -81,9 +82,9 @@ constexpr int kHardcodedGrassSpecial = 0x75640; constexpr int kOverworldMiniMapPalettes = 0x55B27; constexpr int kTriforcePalette = 0x64425; constexpr int kCrystalPalette = 0xF4CD3; -constexpr int CustomAreaSpecificBGPalette = - 0x140000; /** < 2 bytes for each overworld area (320) */ +/** < 2 bytes for each overworld area (320) */ +constexpr int CustomAreaSpecificBGPalette = 0x140000; constexpr int CustomAreaSpecificBGASM = 0x140150; // 1 byte, not 0 if enabled @@ -126,6 +127,10 @@ class SnesPalette { } } + void Create(std::ranges::range auto &&cols) { + std::copy(cols.begin(), cols.end(), std::back_inserter(colors)); + } + void AddColor(const SnesColor &color) { colors.emplace_back(color); } void AddColor(const snes_color &color) { colors.emplace_back(color); } void AddColor(uint16_t color) { colors.emplace_back(color); } @@ -191,6 +196,7 @@ std::array ToFloatArray(const SnesColor &color); */ struct PaletteGroup { PaletteGroup() = default; + PaletteGroup(const std::string &name) : name_(name) {} void AddPalette(SnesPalette pal) { palettes.emplace_back(pal); } @@ -202,11 +208,10 @@ struct PaletteGroup { } void clear() { palettes.clear(); } - void SetName(const std::string &name) { name_ = name; } auto name() const { return name_; } auto size() const { return palettes.size(); } - auto mutable_palette(int i) { return &palettes[i]; } auto palette(int i) const { return palettes[i]; } + auto mutable_palette(int i) { return &palettes[i]; } SnesPalette operator[](int i) { if (i > palettes.size()) { @@ -237,21 +242,21 @@ struct PaletteGroup { * get the group by name. */ struct PaletteGroupMap { - PaletteGroup overworld_main; - PaletteGroup overworld_aux; - PaletteGroup overworld_animated; - PaletteGroup hud; - PaletteGroup global_sprites; - PaletteGroup armors; - PaletteGroup swords; - PaletteGroup shields; - PaletteGroup sprites_aux1; - PaletteGroup sprites_aux2; - PaletteGroup sprites_aux3; - PaletteGroup dungeon_main; - PaletteGroup grass; - PaletteGroup object_3d; - PaletteGroup overworld_mini_map; + PaletteGroup overworld_main = {kPaletteGroupAddressesKeys[0]}; + PaletteGroup overworld_aux = {kPaletteGroupAddressesKeys[1]}; + PaletteGroup overworld_animated = {kPaletteGroupAddressesKeys[2]}; + PaletteGroup hud = {kPaletteGroupAddressesKeys[3]}; + PaletteGroup global_sprites = {kPaletteGroupAddressesKeys[4]}; + PaletteGroup armors = {kPaletteGroupAddressesKeys[5]}; + PaletteGroup swords = {kPaletteGroupAddressesKeys[6]}; + PaletteGroup shields = {kPaletteGroupAddressesKeys[7]}; + PaletteGroup sprites_aux1 = {kPaletteGroupAddressesKeys[8]}; + PaletteGroup sprites_aux2 = {kPaletteGroupAddressesKeys[9]}; + PaletteGroup sprites_aux3 = {kPaletteGroupAddressesKeys[10]}; + PaletteGroup dungeon_main = {kPaletteGroupAddressesKeys[11]}; + PaletteGroup grass = {kPaletteGroupAddressesKeys[12]}; + PaletteGroup object_3d = {kPaletteGroupAddressesKeys[13]}; + PaletteGroup overworld_mini_map = {kPaletteGroupAddressesKeys[14]}; auto get_group(const std::string &group_name) { if (group_name == "ow_main") { @@ -384,7 +389,7 @@ struct Paletteset { gfx::SnesPalette aux1, gfx::SnesPalette aux2, gfx::SnesColor background, gfx::SnesPalette hud, gfx::SnesPalette spr, gfx::SnesPalette spr2, gfx::SnesPalette comp) - : main(main), + : main_(main), animated(animated), aux1(aux1), aux2(aux2), @@ -394,7 +399,7 @@ struct Paletteset { spr2(spr2), composite(comp) {} - gfx::SnesPalette main; /**< The main palette. */ + gfx::SnesPalette main_; /**< The main palette. */ gfx::SnesPalette animated; /**< The animated palette. */ gfx::SnesPalette aux1; /**< The first auxiliary palette. */ gfx::SnesPalette aux2; /**< The second auxiliary palette. */ @@ -405,18 +410,16 @@ struct Paletteset { gfx::SnesPalette composite; /**< The composite palette. */ }; -} // namespace gfx /** * @brief Shared graphical context across editors. */ class GfxContext { -protected: + protected: // Palettesets for the tile16 individual tiles static std::unordered_map palettesets_; }; - -} // namespace app +} // namespace gfx } // namespace yaze #endif // YAZE_APP_GFX_PALETTE_H diff --git a/src/app/gfx/snes_tile.cc b/src/app/gfx/snes_tile.cc index 940fd6c5..86fc13c7 100644 --- a/src/app/gfx/snes_tile.cc +++ b/src/app/gfx/snes_tile.cc @@ -8,7 +8,6 @@ #include "app/core/constants.h" namespace yaze { -namespace app { namespace gfx { // Bit set for object priority @@ -23,8 +22,8 @@ constexpr ushort TileVFlipBit = 0x8000; // Bits used for tile name constexpr ushort TileNameMask = 0x03FF; -snes_tile8 UnpackBppTile(const std::vector& data, const uint32_t offset, - const uint32_t bpp) { +snes_tile8 UnpackBppTile(const std::vector& data, + const uint32_t offset, const uint32_t bpp) { snes_tile8 tile; assert(bpp >= 1 && bpp <= 8); unsigned int bpp_pos[8]; // More for conveniance and readibility @@ -400,5 +399,5 @@ void CopyTile8bpp16(int x, int y, int tile, std::vector& bitmap, } } // namespace gfx -} // namespace app + } // namespace yaze diff --git a/src/app/gfx/snes_tile.h b/src/app/gfx/snes_tile.h index 46ab72b3..1d78cece 100644 --- a/src/app/gfx/snes_tile.h +++ b/src/app/gfx/snes_tile.h @@ -10,7 +10,6 @@ #include namespace yaze { -namespace app { namespace gfx { constexpr int kTilesheetWidth = 128; @@ -222,7 +221,7 @@ class GraphicsBuffer { }; } // namespace gfx -} // namespace app + } // namespace yaze #endif // YAZE_APP_GFX_SNES_TILE_H diff --git a/src/app/gfx/tilesheet.cc b/src/app/gfx/tilesheet.cc index 5d4d3527..0c3bcdba 100644 --- a/src/app/gfx/tilesheet.cc +++ b/src/app/gfx/tilesheet.cc @@ -4,12 +4,9 @@ #include #include "app/gfx/bitmap.h" -#include "app/gfx/snes_color.h" -#include "app/gfx/snes_palette.h" #include "app/gfx/snes_tile.h" namespace yaze { -namespace app { namespace gfx { absl::StatusOr CreateTilesheetFromGraphicsBuffer( @@ -210,5 +207,4 @@ void Tilesheet::MirrorTileData(std::vector& tile_data, bool mirrorX, } } // namespace gfx -} // namespace app } // namespace yaze diff --git a/src/app/gfx/tilesheet.h b/src/app/gfx/tilesheet.h index 31639b4d..fc77d312 100644 --- a/src/app/gfx/tilesheet.h +++ b/src/app/gfx/tilesheet.h @@ -5,12 +5,10 @@ #include #include "app/gfx/bitmap.h" -#include "app/gfx/snes_color.h" #include "app/gfx/snes_palette.h" #include "app/gfx/snes_tile.h" namespace yaze { -namespace app { namespace gfx { enum class TileType { Tile8, Tile16 }; @@ -45,8 +43,8 @@ class Tilesheet { int sheet_offset = 0); void ModifyTile16(const std::vector& graphics_buffer, const TileInfo& top_left, const TileInfo& top_right, - const TileInfo& bottom_left, const TileInfo& bottom_right, int tile_id, - int sheet_offset = 0); + const TileInfo& bottom_left, const TileInfo& bottom_right, + int tile_id, int sheet_offset = 0); void ComposeAndPlaceTilePart(const std::vector& graphics_buffer, const TileInfo& tile_info, int baseX, int baseY); @@ -61,16 +59,12 @@ class Tilesheet { } Bitmap GetTile16(int tile_id) { - std::cout << "GetTile16: " << tile_id << std::endl; int tiles_per_row = bitmap_->width() / tile_width_; int tile_x = (tile_id % tiles_per_row) * tile_width_; int tile_y = (tile_id / tiles_per_row) * tile_height_; - std::cout << "Tile X: " << tile_x << " Tile Y: " << tile_y << std::endl; - std::vector tile_data(tile_width_ * tile_height_, 0x00); int tile_data_offset = 0; bitmap_->Get16x16Tile(tile_x, tile_y, tile_data, tile_data_offset); - return Bitmap(16, 16, bitmap_->depth(), tile_data); } @@ -138,7 +132,6 @@ absl::StatusOr CreateTilesheetFromGraphicsBuffer( int sheet_id); } // namespace gfx -} // namespace app } // namespace yaze -#endif // YAZE_APP_GFX_TILESHEET_H \ No newline at end of file +#endif // YAZE_APP_GFX_TILESHEET_H diff --git a/src/app/gui/canvas.cc b/src/app/gui/canvas.cc index 7e0a8a3b..78a8cb1e 100644 --- a/src/app/gui/canvas.cc +++ b/src/app/gui/canvas.cc @@ -12,7 +12,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace gui { using core::Renderer; @@ -856,5 +855,4 @@ void BitmapCanvasPipeline(gui::Canvas &canvas, const gfx::Bitmap &bitmap, } } // namespace gui -} // namespace app } // namespace yaze diff --git a/src/app/gui/canvas.h b/src/app/gui/canvas.h index 5bc77ec5..bc94352d 100644 --- a/src/app/gui/canvas.h +++ b/src/app/gui/canvas.h @@ -8,16 +8,15 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { /** - * @namespace yaze::app::gui + * @namespace yaze::gui * @brief Graphical User Interface (GUI) components for the application. */ namespace gui { -using app::gfx::Bitmap; -using app::gfx::BitmapTable; +using gfx::Bitmap; +using gfx::BitmapTable; enum class CanvasType { kTile, kBlock, kMap }; enum class CanvasMode { kPaint, kSelect }; @@ -245,7 +244,6 @@ void BitmapCanvasPipeline(gui::Canvas &canvas, const gfx::Bitmap &bitmap, bool scrollbar, int canvas_id); } // namespace gui -} // namespace app } // namespace yaze #endif diff --git a/src/app/gui/color.cc b/src/app/gui/color.cc index 9e8ea108..ab37f8d3 100644 --- a/src/app/gui/color.cc +++ b/src/app/gui/color.cc @@ -1,19 +1,17 @@ #include "color.h" -#include "imgui/imgui.h" - #include #include #include "app/gfx/bitmap.h" -#include "app/gfx/snes_palette.h" #include "app/gfx/snes_color.h" +#include "app/gfx/snes_palette.h" +#include "imgui/imgui.h" namespace yaze { -namespace app { namespace gui { -ImVec4 ConvertSnesColorToImVec4(const SnesColor& color) { +ImVec4 ConvertSnesColorToImVec4(const gfx::SnesColor& color) { return ImVec4(static_cast(color.rgb().x) / 255.0f, static_cast(color.rgb().y) / 255.0f, static_cast(color.rgb().z) / 255.0f, @@ -22,7 +20,7 @@ ImVec4 ConvertSnesColorToImVec4(const SnesColor& color) { ); } -IMGUI_API bool SnesColorButton(absl::string_view id, SnesColor& color, +IMGUI_API bool SnesColorButton(absl::string_view id, gfx::SnesColor& color, ImGuiColorEditFlags flags, const ImVec2& size_arg) { // Convert the SNES color values to ImGui color values @@ -39,7 +37,7 @@ IMGUI_API bool SnesColorButton(absl::string_view id, SnesColor& color, return pressed; } -IMGUI_API bool SnesColorEdit4(absl::string_view label, SnesColor* color, +IMGUI_API bool SnesColorEdit4(absl::string_view label, gfx::SnesColor* color, ImGuiColorEditFlags flags) { ImVec4 displayColor = ConvertSnesColorToImVec4(*color); @@ -53,7 +51,7 @@ IMGUI_API bool SnesColorEdit4(absl::string_view label, SnesColor* color, return pressed; } -absl::Status DisplayPalette(app::gfx::SnesPalette& palette, bool loaded) { +absl::Status DisplayPalette(gfx::SnesPalette& palette, bool loaded) { static ImVec4 color = ImVec4(0, 0, 0, 255.f); ImGuiColorEditFlags misc_flags = ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_NoDragDrop | @@ -170,5 +168,5 @@ void SelectablePalettePipeline(uint64_t& palette_id, bool& refresh_graphics, } } // namespace gui -} // namespace app + } // namespace yaze diff --git a/src/app/gui/color.h b/src/app/gui/color.h index 8d5134de..9b47886d 100644 --- a/src/app/gui/color.h +++ b/src/app/gui/color.h @@ -1,40 +1,53 @@ #ifndef YAZE_GUI_COLOR_H #define YAZE_GUI_COLOR_H -#include "imgui/imgui.h" - -#include +// #include #include #include "absl/status/status.h" -#include "app/gfx/bitmap.h" #include "app/gfx/snes_palette.h" +#include "imgui/imgui.h" namespace yaze { -namespace app { namespace gui { -using gfx::SnesColor; +struct Color { + float red; + float blue; + float green; + float alpha; +}; + +inline ImVec4 ConvertColorToImVec4(const Color &color) { + return ImVec4(color.red, color.green, color.blue, color.alpha); +} + +inline std::string ColorToHexString(const Color &color) { + return ""; +/* std::format( + "{:02X}{:02X}{:02X}{:02X}", static_cast(color.red * 255), + static_cast(color.green * 255), static_cast(color.blue * 255), + static_cast(color.alpha * 255)); */ +} // A utility function to convert an SnesColor object to an ImVec4 with // normalized color values -ImVec4 ConvertSnesColorToImVec4(const SnesColor& color); +ImVec4 ConvertSnesColorToImVec4(const gfx::SnesColor &color); // The wrapper function for ImGui::ColorButton that takes a SnesColor reference -IMGUI_API bool SnesColorButton(absl::string_view id, SnesColor& color, +IMGUI_API bool SnesColorButton(absl::string_view id, gfx::SnesColor &color, ImGuiColorEditFlags flags = 0, - const ImVec2& size_arg = ImVec2(0, 0)); + const ImVec2 &size_arg = ImVec2(0, 0)); -IMGUI_API bool SnesColorEdit4(absl::string_view label, SnesColor* color, +IMGUI_API bool SnesColorEdit4(absl::string_view label, gfx::SnesColor *color, ImGuiColorEditFlags flags = 0); -absl::Status DisplayPalette(app::gfx::SnesPalette& palette, bool loaded); +absl::Status DisplayPalette(gfx::SnesPalette &palette, bool loaded); -void SelectablePalettePipeline(uint64_t& palette_id, bool& refresh_graphics, - gfx::SnesPalette& palette); +void SelectablePalettePipeline(uint64_t &palette_id, bool &refresh_graphics, + gfx::SnesPalette &palette); } // namespace gui -} // namespace app } // namespace yaze #endif diff --git a/src/app/gui/gui.cmake b/src/app/gui/gui.cmake index 60481956..e3d99d9a 100644 --- a/src/app/gui/gui.cmake +++ b/src/app/gui/gui.cmake @@ -1,6 +1,7 @@ set( YAZE_GUI_SRC app/gui/modules/asset_browser.cc + app/gui/modules/text_editor.cc app/gui/canvas.cc app/gui/input.cc app/gui/style.cc diff --git a/src/app/gui/input.cc b/src/app/gui/input.cc index f3e74bce..a6aa9db7 100644 --- a/src/app/gui/input.cc +++ b/src/app/gui/input.cc @@ -4,16 +4,10 @@ #include #include -#include "ImGuiFileDialog/ImGuiFileDialog.h" #include "absl/strings/string_view.h" -#include "app/gfx/bitmap.h" -#include "app/gfx/snes_palette.h" #include "app/gfx/snes_tile.h" -#include "app/gui/canvas.h" -#include "app/gui/color.h" #include "imgui/imgui.h" #include "imgui/imgui_internal.h" -#include "imgui/misc/cpp/imgui_stdlib.h" namespace ImGui { @@ -133,7 +127,6 @@ bool InputScalarLeft(const char* label, ImGuiDataType data_type, void* p_data, } // namespace ImGui namespace yaze { -namespace app { namespace gui { const int kStepOneHex = 0x01; @@ -263,24 +256,30 @@ bool InputTileInfo(const char* label, gfx::TileInfo* tile_info) { ImGuiID GetID(const std::string& id) { return ImGui::GetID(id.c_str()); } -void FileDialogPipeline(absl::string_view display_key, - absl::string_view file_extensions, - std::optional button_text, - std::function callback) { - if (button_text.has_value() && ImGui::Button(button_text->data())) { - ImGuiFileDialog::Instance()->OpenDialog(display_key.data(), "Choose File", - file_extensions.data(), "."); - } +void AddTableColumn(Table &table, const std::string &label, GuiElement element) { + table.column_labels.push_back(label); + table.column_contents.push_back(element); +} - if (ImGuiFileDialog::Instance()->Display( - display_key.data(), ImGuiWindowFlags_NoCollapse, ImVec2(600, 400))) { - if (ImGuiFileDialog::Instance()->IsOk()) { - callback(); +void DrawTable(Table& params) { + if (ImGui::BeginTable(params.id, params.num_columns, params.flags, params.size)) { + for (int i = 0; i < params.num_columns; ++i) + ImGui::TableSetupColumn(params.column_labels[i].c_str()); + + for (int i = 0; i < params.num_columns; ++i) { + ImGui::TableNextColumn(); + switch (params.column_contents[i].index()) { + case 0: + std::get<0>(params.column_contents[i])(); + break; + case 1: + ImGui::Text("%s", std::get<1>(params.column_contents[i]).c_str()); + break; + } } - ImGuiFileDialog::Instance()->Close(); + ImGui::EndTable(); } } } // namespace gui -} // namespace app } // namespace yaze diff --git a/src/app/gui/input.h b/src/app/gui/input.h index caa4d9bb..ae63cb63 100644 --- a/src/app/gui/input.h +++ b/src/app/gui/input.h @@ -8,45 +8,39 @@ #include #include #include +#include #include #include "absl/strings/string_view.h" -#include "app/gfx/bitmap.h" -#include "app/gfx/snes_palette.h" #include "app/gfx/snes_tile.h" -#include "app/gui/canvas.h" -#include "app/gui/color.h" #include "imgui/imgui.h" -#include "imgui/misc/cpp/imgui_stdlib.h" -#include "imgui_memory_editor.h" namespace yaze { -namespace app { namespace gui { constexpr ImVec2 kDefaultModalSize = ImVec2(200, 0); constexpr ImVec2 kZeroPos = ImVec2(0, 0); -IMGUI_API bool InputHex(const char* label, uint64_t* data); -IMGUI_API bool InputHex(const char* label, int* data, int num_digits = 4, +IMGUI_API bool InputHex(const char *label, uint64_t *data); +IMGUI_API bool InputHex(const char *label, int *data, int num_digits = 4, float input_width = 50.f); -IMGUI_API bool InputHexShort(const char* label, uint32_t* data); -IMGUI_API bool InputHexWord(const char* label, uint16_t* data, +IMGUI_API bool InputHexShort(const char *label, uint32_t *data); +IMGUI_API bool InputHexWord(const char *label, uint16_t *data, float input_width = 50.f, bool no_step = false); -IMGUI_API bool InputHexWord(const char* label, int16_t* data, +IMGUI_API bool InputHexWord(const char *label, int16_t *data, float input_width = 50.f, bool no_step = false); -IMGUI_API bool InputHexByte(const char* label, uint8_t* data, +IMGUI_API bool InputHexByte(const char *label, uint8_t *data, float input_width = 50.f, bool no_step = false); -IMGUI_API bool InputHexByte(const char* label, uint8_t* data, uint8_t max_value, +IMGUI_API bool InputHexByte(const char *label, uint8_t *data, uint8_t max_value, float input_width = 50.f, bool no_step = false); -IMGUI_API bool ListBox(const char* label, int* current_item, - const std::vector& items, +IMGUI_API bool ListBox(const char *label, int *current_item, + const std::vector &items, int height_in_items = -1); -bool InputTileInfo(const char* label, gfx::TileInfo* tile_info); +bool InputTileInfo(const char *label, gfx::TileInfo *tile_info); using ItemLabelFlags = enum ItemLabelFlag { Left = 1u << 0u, @@ -56,15 +50,24 @@ using ItemLabelFlags = enum ItemLabelFlag { IMGUI_API void ItemLabel(absl::string_view title, ItemLabelFlags flags); -IMGUI_API ImGuiID GetID(const std::string& id); +IMGUI_API ImGuiID GetID(const std::string &id); -void FileDialogPipeline(absl::string_view display_key, - absl::string_view file_extensions, - std::optional button_text, - std::function callback); +using GuiElement = std::variant, std::string>; -} // namespace gui -} // namespace app -} // namespace yaze +struct Table { + const char *id; + int num_columns; + ImGuiTableFlags flags; + ImVec2 size; + std::vector column_labels; + std::vector column_contents; +}; + +void AddTableColumn(Table &table, const std::string &label, GuiElement element); + +void DrawTable(Table ¶ms); + +} // namespace gui +} // namespace yaze #endif diff --git a/src/app/gui/modules/asset_browser.cc b/src/app/gui/modules/asset_browser.cc index a584d648..9f36665f 100644 --- a/src/app/gui/modules/asset_browser.cc +++ b/src/app/gui/modules/asset_browser.cc @@ -3,7 +3,6 @@ #include "absl/strings/str_format.h" namespace yaze { -namespace app { namespace gui { using namespace ImGui; @@ -342,5 +341,5 @@ void GfxSheetAssetBrowser::Draw( } } // namespace gui -} // namespace app + } // namespace yaze diff --git a/src/app/gui/modules/asset_browser.h b/src/app/gui/modules/asset_browser.h index 46f57c58..432e896d 100644 --- a/src/app/gui/modules/asset_browser.h +++ b/src/app/gui/modules/asset_browser.h @@ -13,7 +13,6 @@ #define IM_CLAMP(V, MN, MX) ((V) < (MN) ? (MN) : (V) > (MX) ? (MX) : (V)) namespace yaze { -namespace app { namespace gui { // Extra functions to add deletion support to ImGuiSelectionBasicStorage @@ -243,7 +242,7 @@ struct GfxSheetAssetBrowser { }; } // namespace gui -} // namespace app + } // namespace yaze #endif // YAZE_APP_GUI_ASSET_BROWSER_H \ No newline at end of file diff --git a/src/app/gui/modules/text_editor.cc b/src/app/gui/modules/text_editor.cc new file mode 100644 index 00000000..c6b95778 --- /dev/null +++ b/src/app/gui/modules/text_editor.cc @@ -0,0 +1,3766 @@ +#include "text_editor.h" + +#include +#include +#include +#include +#include + +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui.h" // for imGui::GetCurrentWindow() + +// TODO +// - multiline comments vs single-line: latter is blocking start of a ML + +template +bool equals(InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, + BinaryPredicate p) { + for (; first1 != last1 && first2 != last2; ++first1, ++first2) { + if (!p(*first1, *first2)) return false; + } + return first1 == last1 && first2 == last2; +} + +TextEditor::TextEditor() + : mLineSpacing(1.0f), + mUndoIndex(0), + mTabSize(4), + mOverwrite(false), + mReadOnly(false), + mWithinRender(false), + mScrollToCursor(false), + mScrollToTop(false), + mTextChanged(false), + mColorizerEnabled(true), + mTextStart(20.0f), + mLeftMargin(10), + mCursorPositionChanged(false), + mColorRangeMin(0), + mColorRangeMax(0), + mSelectionMode(SelectionMode::Normal), + mCheckComments(true), + mLastClick(-1.0f), + mHandleKeyboardInputs(true), + mHandleMouseInputs(true), + mIgnoreImGuiChild(false), + mShowWhitespaces(true), + mStartTime(std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count()) { + SetPalette(GetDarkPalette()); + SetLanguageDefinition(LanguageDefinition::HLSL()); + mLines.push_back(Line()); +} + +TextEditor::~TextEditor() {} + +void TextEditor::SetLanguageDefinition(const LanguageDefinition& aLanguageDef) { + mLanguageDefinition = aLanguageDef; + mRegexList.clear(); + + for (auto& r : mLanguageDefinition.mTokenRegexStrings) + mRegexList.push_back(std::make_pair( + std::regex(r.first, std::regex_constants::optimize), r.second)); + + Colorize(); +} + +void TextEditor::SetPalette(const Palette& aValue) { mPaletteBase = aValue; } + +std::string TextEditor::GetText(const Coordinates& aStart, + const Coordinates& aEnd) const { + std::string result; + + auto lstart = aStart.mLine; + auto lend = aEnd.mLine; + auto istart = GetCharacterIndex(aStart); + auto iend = GetCharacterIndex(aEnd); + size_t s = 0; + + for (size_t i = lstart; i < lend; i++) s += mLines[i].size(); + + result.reserve(s + s / 8); + + while (istart < iend || lstart < lend) { + if (lstart >= (int)mLines.size()) break; + + auto& line = mLines[lstart]; + if (istart < (int)line.size()) { + result += line[istart].mChar; + istart++; + } else { + istart = 0; + ++lstart; + result += '\n'; + } + } + + return result; +} + +TextEditor::Coordinates TextEditor::GetActualCursorCoordinates() const { + return SanitizeCoordinates(mState.mCursorPosition); +} + +TextEditor::Coordinates TextEditor::SanitizeCoordinates( + const Coordinates& aValue) const { + auto line = aValue.mLine; + auto column = aValue.mColumn; + if (line >= (int)mLines.size()) { + if (mLines.empty()) { + line = 0; + column = 0; + } else { + line = (int)mLines.size() - 1; + column = GetLineMaxColumn(line); + } + return Coordinates(line, column); + } else { + column = mLines.empty() ? 0 : std::min(column, GetLineMaxColumn(line)); + return Coordinates(line, column); + } +} + +// https://en.wikipedia.org/wiki/UTF-8 +// We assume that the char is a standalone character (<128) or a leading byte of +// an UTF-8 code sequence (non-10xxxxxx code) +static int UTF8CharLength(TextEditor::Char c) { + if ((c & 0xFE) == 0xFC) return 6; + if ((c & 0xFC) == 0xF8) return 5; + if ((c & 0xF8) == 0xF0) + return 4; + else if ((c & 0xF0) == 0xE0) + return 3; + else if ((c & 0xE0) == 0xC0) + return 2; + return 1; +} + +// "Borrowed" from ImGui source +static inline int ImTextCharToUtf8(char* buf, int buf_size, unsigned int c) { + if (c < 0x80) { + buf[0] = (char)c; + return 1; + } + if (c < 0x800) { + if (buf_size < 2) return 0; + buf[0] = (char)(0xc0 + (c >> 6)); + buf[1] = (char)(0x80 + (c & 0x3f)); + return 2; + } + if (c >= 0xdc00 && c < 0xe000) { + return 0; + } + if (c >= 0xd800 && c < 0xdc00) { + if (buf_size < 4) return 0; + buf[0] = (char)(0xf0 + (c >> 18)); + buf[1] = (char)(0x80 + ((c >> 12) & 0x3f)); + buf[2] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[3] = (char)(0x80 + ((c) & 0x3f)); + return 4; + } + // else if (c < 0x10000) + { + if (buf_size < 3) return 0; + buf[0] = (char)(0xe0 + (c >> 12)); + buf[1] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[2] = (char)(0x80 + ((c) & 0x3f)); + return 3; + } +} + +void TextEditor::Advance(Coordinates& aCoordinates) const { + if (aCoordinates.mLine < (int)mLines.size()) { + auto& line = mLines[aCoordinates.mLine]; + auto cindex = GetCharacterIndex(aCoordinates); + + if (cindex + 1 < (int)line.size()) { + auto delta = UTF8CharLength(line[cindex].mChar); + cindex = std::min(cindex + delta, (int)line.size() - 1); + } else { + ++aCoordinates.mLine; + cindex = 0; + } + aCoordinates.mColumn = GetCharacterColumn(aCoordinates.mLine, cindex); + } +} + +void TextEditor::DeleteRange(const Coordinates& aStart, + const Coordinates& aEnd) { + assert(aEnd >= aStart); + assert(!mReadOnly); + + // printf("D(%d.%d)-(%d.%d)\n", aStart.mLine, aStart.mColumn, aEnd.mLine, + // aEnd.mColumn); + + if (aEnd == aStart) return; + + auto start = GetCharacterIndex(aStart); + auto end = GetCharacterIndex(aEnd); + + if (aStart.mLine == aEnd.mLine) { + auto& line = mLines[aStart.mLine]; + auto n = GetLineMaxColumn(aStart.mLine); + if (aEnd.mColumn >= n) + line.erase(line.begin() + start, line.end()); + else + line.erase(line.begin() + start, line.begin() + end); + } else { + auto& firstLine = mLines[aStart.mLine]; + auto& lastLine = mLines[aEnd.mLine]; + + firstLine.erase(firstLine.begin() + start, firstLine.end()); + lastLine.erase(lastLine.begin(), lastLine.begin() + end); + + if (aStart.mLine < aEnd.mLine) + firstLine.insert(firstLine.end(), lastLine.begin(), lastLine.end()); + + if (aStart.mLine < aEnd.mLine) RemoveLine(aStart.mLine + 1, aEnd.mLine + 1); + } + + mTextChanged = true; +} + +int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, + const char* aValue) { + assert(!mReadOnly); + + int cindex = GetCharacterIndex(aWhere); + int totalLines = 0; + while (*aValue != '\0') { + assert(!mLines.empty()); + + if (*aValue == '\r') { + // skip + ++aValue; + } else if (*aValue == '\n') { + if (cindex < (int)mLines[aWhere.mLine].size()) { + auto& newLine = InsertLine(aWhere.mLine + 1); + auto& line = mLines[aWhere.mLine]; + newLine.insert(newLine.begin(), line.begin() + cindex, line.end()); + line.erase(line.begin() + cindex, line.end()); + } else { + InsertLine(aWhere.mLine + 1); + } + ++aWhere.mLine; + aWhere.mColumn = 0; + cindex = 0; + ++totalLines; + ++aValue; + } else { + auto& line = mLines[aWhere.mLine]; + auto d = UTF8CharLength(*aValue); + while (d-- > 0 && *aValue != '\0') + line.insert(line.begin() + cindex++, + Glyph(*aValue++, PaletteIndex::Default)); + ++aWhere.mColumn; + } + + mTextChanged = true; + } + + return totalLines; +} + +void TextEditor::AddUndo(UndoRecord& aValue) { + assert(!mReadOnly); + // printf("AddUndo: (@%d.%d) +\'%s' [%d.%d .. %d.%d], -\'%s', [%d.%d .. %d.%d] + // (@%d.%d)\n", aValue.mBefore.mCursorPosition.mLine, + //aValue.mBefore.mCursorPosition.mColumn, aValue.mAdded.c_str(), + //aValue.mAddedStart.mLine, aValue.mAddedStart.mColumn, + //aValue.mAddedEnd.mLine, aValue.mAddedEnd.mColumn, aValue.mRemoved.c_str(), + //aValue.mRemovedStart.mLine, aValue.mRemovedStart.mColumn, + //aValue.mRemovedEnd.mLine, aValue.mRemovedEnd.mColumn, + // aValue.mAfter.mCursorPosition.mLine, + //aValue.mAfter.mCursorPosition.mColumn + // ); + + mUndoBuffer.resize((size_t)(mUndoIndex + 1)); + mUndoBuffer.back() = aValue; + ++mUndoIndex; +} + +TextEditor::Coordinates TextEditor::ScreenPosToCoordinates( + const ImVec2& aPosition) const { + ImVec2 origin = ImGui::GetCursorScreenPos(); + ImVec2 local(aPosition.x - origin.x, aPosition.y - origin.y); + + int lineNo = std::max(0, (int)floor(local.y / mCharAdvance.y)); + + int columnCoord = 0; + + if (lineNo >= 0 && lineNo < (int)mLines.size()) { + auto& line = mLines.at(lineNo); + + int columnIndex = 0; + float columnX = 0.0f; + + while ((size_t)columnIndex < line.size()) { + float columnWidth = 0.0f; + + if (line[columnIndex].mChar == '\t') { + float spaceSize = + ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ") + .x; + float oldX = columnX; + float newColumnX = (1.0f + std::floor((1.0f + columnX) / + (float(mTabSize) * spaceSize))) * + (float(mTabSize) * spaceSize); + columnWidth = newColumnX - oldX; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) break; + columnX = newColumnX; + columnCoord = (columnCoord / mTabSize) * mTabSize + mTabSize; + columnIndex++; + } else { + char buf[7]; + auto d = UTF8CharLength(line[columnIndex].mChar); + int i = 0; + while (i < 6 && d-- > 0) buf[i++] = line[columnIndex++].mChar; + buf[i] = '\0'; + columnWidth = + ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf) + .x; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) break; + columnX += columnWidth; + columnCoord++; + } + } + } + + return SanitizeCoordinates(Coordinates(lineNo, columnCoord)); +} + +TextEditor::Coordinates TextEditor::FindWordStart( + const Coordinates& aFrom) const { + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) return at; + + auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); + + if (cindex >= (int)line.size()) return at; + + while (cindex > 0 && isspace(line[cindex].mChar)) --cindex; + + auto cstart = (PaletteIndex)line[cindex].mColorIndex; + while (cindex > 0) { + auto c = line[cindex].mChar; + if ((c & 0xC0) != 0x80) // not UTF code sequence 10xxxxxx + { + if (c <= 32 && isspace(c)) { + cindex++; + break; + } + if (cstart != (PaletteIndex)line[size_t(cindex - 1)].mColorIndex) break; + } + --cindex; + } + return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex)); +} + +TextEditor::Coordinates TextEditor::FindWordEnd( + const Coordinates& aFrom) const { + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) return at; + + auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); + + if (cindex >= (int)line.size()) return at; + + bool prevspace = (bool)isspace(line[cindex].mChar); + auto cstart = (PaletteIndex)line[cindex].mColorIndex; + while (cindex < (int)line.size()) { + auto c = line[cindex].mChar; + auto d = UTF8CharLength(c); + if (cstart != (PaletteIndex)line[cindex].mColorIndex) break; + + if (prevspace != !!isspace(c)) { + if (isspace(c)) + while (cindex < (int)line.size() && isspace(line[cindex].mChar)) + ++cindex; + break; + } + cindex += d; + } + return Coordinates(aFrom.mLine, GetCharacterColumn(aFrom.mLine, cindex)); +} + +TextEditor::Coordinates TextEditor::FindNextWord( + const Coordinates& aFrom) const { + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) return at; + + // skip to the next non-word character + auto cindex = GetCharacterIndex(aFrom); + bool isword = false; + bool skip = false; + if (cindex < (int)mLines[at.mLine].size()) { + auto& line = mLines[at.mLine]; + isword = isalnum(line[cindex].mChar); + skip = isword; + } + + while (!isword || skip) { + if (at.mLine >= mLines.size()) { + auto l = std::max(0, (int)mLines.size() - 1); + return Coordinates(l, GetLineMaxColumn(l)); + } + + auto& line = mLines[at.mLine]; + if (cindex < (int)line.size()) { + isword = isalnum(line[cindex].mChar); + + if (isword && !skip) + return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex)); + + if (!isword) skip = false; + + cindex++; + } else { + cindex = 0; + ++at.mLine; + skip = false; + isword = false; + } + } + + return at; +} + +int TextEditor::GetCharacterIndex(const Coordinates& aCoordinates) const { + if (aCoordinates.mLine >= mLines.size()) return -1; + auto& line = mLines[aCoordinates.mLine]; + int c = 0; + int i = 0; + for (; i < line.size() && c < aCoordinates.mColumn;) { + if (line[i].mChar == '\t') + c = (c / mTabSize) * mTabSize + mTabSize; + else + ++c; + i += UTF8CharLength(line[i].mChar); + } + return i; +} + +int TextEditor::GetCharacterColumn(int aLine, int aIndex) const { + if (aLine >= mLines.size()) return 0; + auto& line = mLines[aLine]; + int col = 0; + int i = 0; + while (i < aIndex && i < (int)line.size()) { + auto c = line[i].mChar; + i += UTF8CharLength(c); + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + } + return col; +} + +int TextEditor::GetLineCharacterCount(int aLine) const { + if (aLine >= mLines.size()) return 0; + auto& line = mLines[aLine]; + int c = 0; + for (unsigned i = 0; i < line.size(); c++) i += UTF8CharLength(line[i].mChar); + return c; +} + +int TextEditor::GetLineMaxColumn(int aLine) const { + if (aLine >= mLines.size()) return 0; + auto& line = mLines[aLine]; + int col = 0; + for (unsigned i = 0; i < line.size();) { + auto c = line[i].mChar; + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + i += UTF8CharLength(c); + } + return col; +} + +bool TextEditor::IsOnWordBoundary(const Coordinates& aAt) const { + if (aAt.mLine >= (int)mLines.size() || aAt.mColumn == 0) return true; + + auto& line = mLines[aAt.mLine]; + auto cindex = GetCharacterIndex(aAt); + if (cindex >= (int)line.size()) return true; + + if (mColorizerEnabled) + return line[cindex].mColorIndex != line[size_t(cindex - 1)].mColorIndex; + + return isspace(line[cindex].mChar) != isspace(line[cindex - 1].mChar); +} + +void TextEditor::RemoveLine(int aStart, int aEnd) { + assert(!mReadOnly); + assert(aEnd >= aStart); + assert(mLines.size() > (size_t)(aEnd - aStart)); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) { + ErrorMarkers::value_type e(i.first >= aStart ? i.first - 1 : i.first, + i.second); + if (e.first >= aStart && e.first <= aEnd) continue; + etmp.insert(e); + } + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) { + if (i >= aStart && i <= aEnd) continue; + btmp.insert(i >= aStart ? i - 1 : i); + } + mBreakpoints = std::move(btmp); + + mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd); + assert(!mLines.empty()); + + mTextChanged = true; +} + +void TextEditor::RemoveLine(int aIndex) { + assert(!mReadOnly); + assert(mLines.size() > 1); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) { + ErrorMarkers::value_type e(i.first > aIndex ? i.first - 1 : i.first, + i.second); + if (e.first - 1 == aIndex) continue; + etmp.insert(e); + } + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) { + if (i == aIndex) continue; + btmp.insert(i >= aIndex ? i - 1 : i); + } + mBreakpoints = std::move(btmp); + + mLines.erase(mLines.begin() + aIndex); + assert(!mLines.empty()); + + mTextChanged = true; +} + +TextEditor::Line& TextEditor::InsertLine(int aIndex) { + assert(!mReadOnly); + + auto& result = *mLines.insert(mLines.begin() + aIndex, Line()); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + etmp.insert(ErrorMarkers::value_type( + i.first >= aIndex ? i.first + 1 : i.first, i.second)); + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) btmp.insert(i >= aIndex ? i + 1 : i); + mBreakpoints = std::move(btmp); + + return result; +} + +std::string TextEditor::GetWordUnderCursor() const { + auto c = GetCursorPosition(); + return GetWordAt(c); +} + +std::string TextEditor::GetWordAt(const Coordinates& aCoords) const { + auto start = FindWordStart(aCoords); + auto end = FindWordEnd(aCoords); + + std::string r; + + auto istart = GetCharacterIndex(start); + auto iend = GetCharacterIndex(end); + + for (auto it = istart; it < iend; ++it) + r.push_back(mLines[aCoords.mLine][it].mChar); + + return r; +} + +ImU32 TextEditor::GetGlyphColor(const Glyph& aGlyph) const { + if (!mColorizerEnabled) return mPalette[(int)PaletteIndex::Default]; + if (aGlyph.mComment) return mPalette[(int)PaletteIndex::Comment]; + if (aGlyph.mMultiLineComment) + return mPalette[(int)PaletteIndex::MultiLineComment]; + auto const color = mPalette[(int)aGlyph.mColorIndex]; + if (aGlyph.mPreprocessor) { + const auto ppcolor = mPalette[(int)PaletteIndex::Preprocessor]; + const int c0 = ((ppcolor & 0xff) + (color & 0xff)) / 2; + const int c1 = (((ppcolor >> 8) & 0xff) + ((color >> 8) & 0xff)) / 2; + const int c2 = (((ppcolor >> 16) & 0xff) + ((color >> 16) & 0xff)) / 2; + const int c3 = (((ppcolor >> 24) & 0xff) + ((color >> 24) & 0xff)) / 2; + return ImU32(c0 | (c1 << 8) | (c2 << 16) | (c3 << 24)); + } + return color; +} + +void TextEditor::HandleKeyboardInputs() { + ImGuiIO& io = ImGui::GetIO(); + auto shift = io.KeyShift; + auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; + auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; + + if (ImGui::IsWindowFocused()) { + if (ImGui::IsWindowHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput); + // ImGui::CaptureKeyboardFromApp(true); + + io.WantCaptureKeyboard = true; + io.WantTextInput = true; + + if (!IsReadOnly() && ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGuiKey_Z)) + Undo(); + else if (!IsReadOnly() && !ctrl && !shift && alt && + ImGui::IsKeyPressed(ImGuiKey_Backspace)) + Undo(); + else if (!IsReadOnly() && ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGuiKey_Y)) + Redo(); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_UpArrow)) + MoveUp(1, shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_DownArrow)) + MoveDown(1, shift); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) + MoveLeft(1, shift, ctrl); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_RightArrow)) + MoveRight(1, shift, ctrl); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_PageUp)) + MoveUp(GetPageSize() - 4, shift); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_PageDown)) + MoveDown(GetPageSize() - 4, shift); + else if (!alt && ctrl && ImGui::IsKeyPressed(ImGuiKey_Home)) + MoveTop(shift); + else if (ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_End)) + MoveBottom(shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_Home)) + MoveHome(shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_End)) + MoveEnd(shift); + else if (!IsReadOnly() && !ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGuiKey_Delete)) + Delete(); + else if (!IsReadOnly() && !ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGuiKey_Backspace)) + Backspace(); + else if (!ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Insert)) + mOverwrite ^= true; + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Insert)) + Copy(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_C)) + Copy(); + else if (!IsReadOnly() && !ctrl && shift && !alt && + ImGui::IsKeyPressed(ImGuiKey_Insert)) + Paste(); + else if (!IsReadOnly() && ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGuiKey_V)) + Paste(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_X)) + Cut(); + else if (!ctrl && shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Delete)) + Cut(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_A)) + SelectAll(); + else if (!IsReadOnly() && !ctrl && !shift && !alt && + ImGui::IsKeyPressed(ImGuiKey_Enter)) + EnterCharacter('\n', false); + else if (!IsReadOnly() && !ctrl && !alt && + ImGui::IsKeyPressed(ImGuiKey_Tab)) + EnterCharacter('\t', shift); + + if (!IsReadOnly() && !io.InputQueueCharacters.empty()) { + for (int i = 0; i < io.InputQueueCharacters.Size; i++) { + auto c = io.InputQueueCharacters[i]; + if (c != 0 && (c == '\n' || c >= 32)) EnterCharacter(c, shift); + } + io.InputQueueCharacters.resize(0); + } + } +} + +void TextEditor::HandleMouseInputs() { + ImGuiIO& io = ImGui::GetIO(); + auto shift = io.KeyShift; + auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; + auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; + + if (ImGui::IsWindowHovered()) { + if (!shift && !alt) { + auto click = ImGui::IsMouseClicked(0); + auto doubleClick = ImGui::IsMouseDoubleClicked(0); + auto t = ImGui::GetTime(); + auto tripleClick = + click && !doubleClick && + (mLastClick != -1.0f && (t - mLastClick) < io.MouseDoubleClickTime); + + /* + Left mouse button triple click + */ + + if (tripleClick) { + if (!ctrl) { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = + ScreenPosToCoordinates(ImGui::GetMousePos()); + mSelectionMode = SelectionMode::Line; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + + mLastClick = -1.0f; + } + + /* + Left mouse button double click + */ + + else if (doubleClick) { + if (!ctrl) { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = + ScreenPosToCoordinates(ImGui::GetMousePos()); + if (mSelectionMode == SelectionMode::Line) + mSelectionMode = SelectionMode::Normal; + else + mSelectionMode = SelectionMode::Word; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + + mLastClick = (float)ImGui::GetTime(); + } + + /* + Left mouse button click + */ + else if (click) { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = + ScreenPosToCoordinates(ImGui::GetMousePos()); + if (ctrl) + mSelectionMode = SelectionMode::Word; + else + mSelectionMode = SelectionMode::Normal; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + + mLastClick = (float)ImGui::GetTime(); + } + // Mouse left button dragging (=> update selection) + else if (ImGui::IsMouseDragging(0) && ImGui::IsMouseDown(0)) { + io.WantCaptureMouse = true; + mState.mCursorPosition = mInteractiveEnd = + ScreenPosToCoordinates(ImGui::GetMousePos()); + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + } + } +} + +void TextEditor::Render() { + /* Compute mCharAdvance regarding to scaled font size (Ctrl + mouse wheel)*/ + const float fontSize = ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, + -1.0f, "#", nullptr, nullptr) + .x; + mCharAdvance = + ImVec2(fontSize, ImGui::GetTextLineHeightWithSpacing() * mLineSpacing); + + /* Update palette with the current alpha from style */ + for (int i = 0; i < (int)PaletteIndex::Max; ++i) { + auto color = ImGui::ColorConvertU32ToFloat4(mPaletteBase[i]); + color.w *= ImGui::GetStyle().Alpha; + mPalette[i] = ImGui::ColorConvertFloat4ToU32(color); + } + + assert(mLineBuffer.empty()); + + auto contentSize = ImGui::GetWindowContentRegionMax(); + auto drawList = ImGui::GetWindowDrawList(); + float longest(mTextStart); + + if (mScrollToTop) { + mScrollToTop = false; + ImGui::SetScrollY(0.f); + } + + ImVec2 cursorScreenPos = ImGui::GetCursorScreenPos(); + auto scrollX = ImGui::GetScrollX(); + auto scrollY = ImGui::GetScrollY(); + + auto lineNo = (int)floor(scrollY / mCharAdvance.y); + auto globalLineMax = (int)mLines.size(); + auto lineMax = std::max( + 0, std::min( + (int)mLines.size() - 1, + lineNo + (int)floor((scrollY + contentSize.y) / mCharAdvance.y))); + + // Deduce mTextStart by evaluating mLines size (global lineMax) plus two + // spaces as text width + char buf[16]; + snprintf(buf, 16, " %d ", globalLineMax); + mTextStart = ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, + nullptr, nullptr) + .x + + mLeftMargin; + + if (!mLines.empty()) { + float spaceSize = ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, + " ", nullptr, nullptr) + .x; + + while (lineNo <= lineMax) { + ImVec2 lineStartScreenPos = ImVec2( + cursorScreenPos.x, cursorScreenPos.y + lineNo * mCharAdvance.y); + ImVec2 textScreenPos = + ImVec2(lineStartScreenPos.x + mTextStart, lineStartScreenPos.y); + + auto& line = mLines[lineNo]; + longest = std::max(mTextStart + TextDistanceToLineStart(Coordinates( + lineNo, GetLineMaxColumn(lineNo))), + longest); + auto columnNo = 0; + Coordinates lineStartCoord(lineNo, 0); + Coordinates lineEndCoord(lineNo, GetLineMaxColumn(lineNo)); + + // Draw selection for the current line + float sstart = -1.0f; + float ssend = -1.0f; + + assert(mState.mSelectionStart <= mState.mSelectionEnd); + if (mState.mSelectionStart <= lineEndCoord) + sstart = mState.mSelectionStart > lineStartCoord + ? TextDistanceToLineStart(mState.mSelectionStart) + : 0.0f; + if (mState.mSelectionEnd > lineStartCoord) + ssend = TextDistanceToLineStart(mState.mSelectionEnd < lineEndCoord + ? mState.mSelectionEnd + : lineEndCoord); + + if (mState.mSelectionEnd.mLine > lineNo) ssend += mCharAdvance.x; + + if (sstart != -1 && ssend != -1 && sstart < ssend) { + ImVec2 vstart(lineStartScreenPos.x + mTextStart + sstart, + lineStartScreenPos.y); + ImVec2 vend(lineStartScreenPos.x + mTextStart + ssend, + lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(vstart, vend, + mPalette[(int)PaletteIndex::Selection]); + } + + // Draw breakpoints + auto start = ImVec2(lineStartScreenPos.x + scrollX, lineStartScreenPos.y); + + if (mBreakpoints.count(lineNo + 1) != 0) { + auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, + lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, + mPalette[(int)PaletteIndex::Breakpoint]); + } + + // Draw error markers + auto errorIt = mErrorMarkers.find(lineNo + 1); + if (errorIt != mErrorMarkers.end()) { + auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, + lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, + mPalette[(int)PaletteIndex::ErrorMarker]); + + if (ImGui::IsMouseHoveringRect(lineStartScreenPos, end)) { + ImGui::BeginTooltip(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f)); + ImGui::Text("Error at line %d:", errorIt->first); + ImGui::PopStyleColor(); + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.2f, 1.0f)); + ImGui::Text("%s", errorIt->second.c_str()); + ImGui::PopStyleColor(); + ImGui::EndTooltip(); + } + } + + // Draw line number (right aligned) + snprintf(buf, 16, "%d ", lineNo + 1); + + auto lineNoWidth = ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, + -1.0f, buf, nullptr, nullptr) + .x; + drawList->AddText(ImVec2(lineStartScreenPos.x + mTextStart - lineNoWidth, + lineStartScreenPos.y), + mPalette[(int)PaletteIndex::LineNumber], buf); + + if (mState.mCursorPosition.mLine == lineNo) { + auto focused = ImGui::IsWindowFocused(); + + // Highlight the current line (where the cursor is) + if (!HasSelection()) { + auto end = ImVec2(start.x + contentSize.x + scrollX, + start.y + mCharAdvance.y); + drawList->AddRectFilled( + start, end, + mPalette[(int)(focused ? PaletteIndex::CurrentLineFill + : PaletteIndex::CurrentLineFillInactive)]); + drawList->AddRect(start, end, + mPalette[(int)PaletteIndex::CurrentLineEdge], 1.0f); + } + + // Render the cursor + if (focused) { + auto timeEnd = + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + auto elapsed = timeEnd - mStartTime; + if (elapsed > 400) { + float width = 1.0f; + auto cindex = GetCharacterIndex(mState.mCursorPosition); + float cx = TextDistanceToLineStart(mState.mCursorPosition); + + if (mOverwrite && cindex < (int)line.size()) { + auto c = line[cindex].mChar; + if (c == '\t') { + auto x = (1.0f + std::floor((1.0f + cx) / + (float(mTabSize) * spaceSize))) * + (float(mTabSize) * spaceSize); + width = x - cx; + } else { + char buf2[2]; + buf2[0] = line[cindex].mChar; + buf2[1] = '\0'; + width = ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, + -1.0f, buf2) + .x; + } + } + ImVec2 cstart(textScreenPos.x + cx, lineStartScreenPos.y); + ImVec2 cend(textScreenPos.x + cx + width, + lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(cstart, cend, + mPalette[(int)PaletteIndex::Cursor]); + if (elapsed > 800) mStartTime = timeEnd; + } + } + } + + // Render colorized text + auto prevColor = line.empty() ? mPalette[(int)PaletteIndex::Default] + : GetGlyphColor(line[0]); + ImVec2 bufferOffset; + + for (int i = 0; i < line.size();) { + auto& glyph = line[i]; + auto color = GetGlyphColor(glyph); + + if ((color != prevColor || glyph.mChar == '\t' || glyph.mChar == ' ') && + !mLineBuffer.empty()) { + const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, + textScreenPos.y + bufferOffset.y); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + auto textSize = ImGui::GetFont()->CalcTextSizeA( + ImGui::GetFontSize(), FLT_MAX, -1.0f, mLineBuffer.c_str(), + nullptr, nullptr); + bufferOffset.x += textSize.x; + mLineBuffer.clear(); + } + prevColor = color; + + if (glyph.mChar == '\t') { + auto oldX = bufferOffset.x; + bufferOffset.x = (1.0f + std::floor((1.0f + bufferOffset.x) / + (float(mTabSize) * spaceSize))) * + (float(mTabSize) * spaceSize); + ++i; + + if (mShowWhitespaces) { + const auto s = ImGui::GetFontSize(); + const auto x1 = textScreenPos.x + oldX + 1.0f; + const auto x2 = textScreenPos.x + bufferOffset.x - 1.0f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + const ImVec2 p1(x1, y); + const ImVec2 p2(x2, y); + const ImVec2 p3(x2 - s * 0.2f, y - s * 0.2f); + const ImVec2 p4(x2 - s * 0.2f, y + s * 0.2f); + drawList->AddLine(p1, p2, 0x90909090); + drawList->AddLine(p2, p3, 0x90909090); + drawList->AddLine(p2, p4, 0x90909090); + } + } else if (glyph.mChar == ' ') { + if (mShowWhitespaces) { + const auto s = ImGui::GetFontSize(); + const auto x = textScreenPos.x + bufferOffset.x + spaceSize * 0.5f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + drawList->AddCircleFilled(ImVec2(x, y), 1.5f, 0x80808080, 4); + } + bufferOffset.x += spaceSize; + i++; + } else { + auto l = UTF8CharLength(glyph.mChar); + while (l-- > 0) mLineBuffer.push_back(line[i++].mChar); + } + ++columnNo; + } + + if (!mLineBuffer.empty()) { + const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, + textScreenPos.y + bufferOffset.y); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + mLineBuffer.clear(); + } + + ++lineNo; + } + + // Draw a tooltip on known identifiers/preprocessor symbols + if (ImGui::IsMousePosValid()) { + auto id = GetWordAt(ScreenPosToCoordinates(ImGui::GetMousePos())); + if (!id.empty()) { + auto it = mLanguageDefinition.mIdentifiers.find(id); + if (it != mLanguageDefinition.mIdentifiers.end()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(it->second.mDeclaration.c_str()); + ImGui::EndTooltip(); + } else { + auto pi = mLanguageDefinition.mPreprocIdentifiers.find(id); + if (pi != mLanguageDefinition.mPreprocIdentifiers.end()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(pi->second.mDeclaration.c_str()); + ImGui::EndTooltip(); + } + } + } + } + } + + ImGui::Dummy(ImVec2((longest + 2), mLines.size() * mCharAdvance.y)); + + if (mScrollToCursor) { + EnsureCursorVisible(); + ImGui::SetWindowFocus(); + mScrollToCursor = false; + } +} + +void TextEditor::Render(const char* aTitle, const ImVec2& aSize, bool aBorder) { + mWithinRender = true; + mTextChanged = false; + mCursorPositionChanged = false; + + ImGui::PushStyleColor( + ImGuiCol_ChildBg, + ImGui::ColorConvertU32ToFloat4(mPalette[(int)PaletteIndex::Background])); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + if (!mIgnoreImGuiChild) + ImGui::BeginChild(aTitle, aSize, aBorder, + ImGuiWindowFlags_HorizontalScrollbar | + ImGuiWindowFlags_AlwaysHorizontalScrollbar | + ImGuiWindowFlags_NoMove); + + if (mHandleKeyboardInputs) { + HandleKeyboardInputs(); + ImGui::PushAllowKeyboardFocus(true); + } + + if (mHandleMouseInputs) HandleMouseInputs(); + + ColorizeInternal(); + Render(); + + if (mHandleKeyboardInputs) ImGui::PopAllowKeyboardFocus(); + + if (!mIgnoreImGuiChild) ImGui::EndChild(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + + mWithinRender = false; +} + +void TextEditor::SetText(const std::string& aText) { + mLines.clear(); + mLines.emplace_back(Line()); + for (auto chr : aText) { + if (chr == '\r') { + // ignore the carriage return character + } else if (chr == '\n') + mLines.emplace_back(Line()); + else { + mLines.back().emplace_back(Glyph(chr, PaletteIndex::Default)); + } + } + + mTextChanged = true; + mScrollToTop = true; + + mUndoBuffer.clear(); + mUndoIndex = 0; + + Colorize(); +} + +void TextEditor::SetTextLines(const std::vector& aLines) { + mLines.clear(); + + if (aLines.empty()) { + mLines.emplace_back(Line()); + } else { + mLines.resize(aLines.size()); + + for (size_t i = 0; i < aLines.size(); ++i) { + const std::string& aLine = aLines[i]; + + mLines[i].reserve(aLine.size()); + for (size_t j = 0; j < aLine.size(); ++j) + mLines[i].emplace_back(Glyph(aLine[j], PaletteIndex::Default)); + } + } + + mTextChanged = true; + mScrollToTop = true; + + mUndoBuffer.clear(); + mUndoIndex = 0; + + Colorize(); +} + +void TextEditor::EnterCharacter(ImWchar aChar, bool aShift) { + assert(!mReadOnly); + + UndoRecord u; + + u.mBefore = mState; + + if (HasSelection()) { + if (aChar == '\t' && + mState.mSelectionStart.mLine != mState.mSelectionEnd.mLine) { + auto start = mState.mSelectionStart; + auto end = mState.mSelectionEnd; + auto originalEnd = end; + + if (start > end) std::swap(start, end); + start.mColumn = 0; + // end.mColumn = end.mLine < mLines.size() ? + //mLines[end.mLine].size() : 0; + if (end.mColumn == 0 && end.mLine > 0) --end.mLine; + if (end.mLine >= (int)mLines.size()) + end.mLine = mLines.empty() ? 0 : (int)mLines.size() - 1; + end.mColumn = GetLineMaxColumn(end.mLine); + + // if (end.mColumn >= GetLineMaxColumn(end.mLine)) + // end.mColumn = GetLineMaxColumn(end.mLine) - 1; + + u.mRemovedStart = start; + u.mRemovedEnd = end; + u.mRemoved = GetText(start, end); + + bool modified = false; + + for (int i = start.mLine; i <= end.mLine; i++) { + auto& line = mLines[i]; + if (aShift) { + if (!line.empty()) { + if (line.front().mChar == '\t') { + line.erase(line.begin()); + modified = true; + } else { + for (int j = 0; + j < mTabSize && !line.empty() && line.front().mChar == ' '; + j++) { + line.erase(line.begin()); + modified = true; + } + } + } + } else { + line.insert(line.begin(), + Glyph('\t', TextEditor::PaletteIndex::Background)); + modified = true; + } + } + + if (modified) { + start = Coordinates(start.mLine, GetCharacterColumn(start.mLine, 0)); + Coordinates rangeEnd; + if (originalEnd.mColumn != 0) { + end = Coordinates(end.mLine, GetLineMaxColumn(end.mLine)); + rangeEnd = end; + u.mAdded = GetText(start, end); + } else { + end = Coordinates(originalEnd.mLine, 0); + rangeEnd = + Coordinates(end.mLine - 1, GetLineMaxColumn(end.mLine - 1)); + u.mAdded = GetText(start, rangeEnd); + } + + u.mAddedStart = start; + u.mAddedEnd = rangeEnd; + u.mAfter = mState; + + mState.mSelectionStart = start; + mState.mSelectionEnd = end; + AddUndo(u); + + mTextChanged = true; + + EnsureCursorVisible(); + } + + return; + } // c == '\t' + else { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + DeleteSelection(); + } + } // HasSelection + + auto coord = GetActualCursorCoordinates(); + u.mAddedStart = coord; + + assert(!mLines.empty()); + + if (aChar == '\n') { + InsertLine(coord.mLine + 1); + auto& line = mLines[coord.mLine]; + auto& newLine = mLines[coord.mLine + 1]; + + if (mLanguageDefinition.mAutoIndentation) + for (size_t it = 0; it < line.size() && isascii(line[it].mChar) && + isblank(line[it].mChar); + ++it) + newLine.push_back(line[it]); + + const size_t whitespaceSize = newLine.size(); + auto cindex = GetCharacterIndex(coord); + newLine.insert(newLine.end(), line.begin() + cindex, line.end()); + line.erase(line.begin() + cindex, line.begin() + line.size()); + SetCursorPosition( + Coordinates(coord.mLine + 1, + GetCharacterColumn(coord.mLine + 1, (int)whitespaceSize))); + u.mAdded = (char)aChar; + } else { + char buf[7]; + int e = ImTextCharToUtf8(buf, 7, aChar); + if (e > 0) { + buf[e] = '\0'; + auto& line = mLines[coord.mLine]; + auto cindex = GetCharacterIndex(coord); + + if (mOverwrite && cindex < (int)line.size()) { + auto d = UTF8CharLength(line[cindex].mChar); + + u.mRemovedStart = mState.mCursorPosition; + u.mRemovedEnd = Coordinates( + coord.mLine, GetCharacterColumn(coord.mLine, cindex + d)); + + while (d-- > 0 && cindex < (int)line.size()) { + u.mRemoved += line[cindex].mChar; + line.erase(line.begin() + cindex); + } + } + + for (auto p = buf; *p != '\0'; p++, ++cindex) + line.insert(line.begin() + cindex, Glyph(*p, PaletteIndex::Default)); + u.mAdded = buf; + + SetCursorPosition( + Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex))); + } else + return; + } + + mTextChanged = true; + + u.mAddedEnd = GetActualCursorCoordinates(); + u.mAfter = mState; + + AddUndo(u); + + Colorize(coord.mLine - 1, 3); + EnsureCursorVisible(); +} + +void TextEditor::SetReadOnly(bool aValue) { mReadOnly = aValue; } + +void TextEditor::SetColorizerEnable(bool aValue) { mColorizerEnabled = aValue; } + +void TextEditor::SetCursorPosition(const Coordinates& aPosition) { + if (mState.mCursorPosition != aPosition) { + mState.mCursorPosition = aPosition; + mCursorPositionChanged = true; + EnsureCursorVisible(); + } +} + +void TextEditor::SetSelectionStart(const Coordinates& aPosition) { + mState.mSelectionStart = SanitizeCoordinates(aPosition); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); +} + +void TextEditor::SetSelectionEnd(const Coordinates& aPosition) { + mState.mSelectionEnd = SanitizeCoordinates(aPosition); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); +} + +void TextEditor::SetSelection(const Coordinates& aStart, + const Coordinates& aEnd, SelectionMode aMode) { + auto oldSelStart = mState.mSelectionStart; + auto oldSelEnd = mState.mSelectionEnd; + + mState.mSelectionStart = SanitizeCoordinates(aStart); + mState.mSelectionEnd = SanitizeCoordinates(aEnd); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); + + switch (aMode) { + case TextEditor::SelectionMode::Normal: + break; + case TextEditor::SelectionMode::Word: { + mState.mSelectionStart = FindWordStart(mState.mSelectionStart); + if (!IsOnWordBoundary(mState.mSelectionEnd)) + mState.mSelectionEnd = FindWordEnd(FindWordStart(mState.mSelectionEnd)); + break; + } + case TextEditor::SelectionMode::Line: { + const auto lineNo = mState.mSelectionEnd.mLine; + const auto lineSize = + (size_t)lineNo < mLines.size() ? mLines[lineNo].size() : 0; + mState.mSelectionStart = Coordinates(mState.mSelectionStart.mLine, 0); + mState.mSelectionEnd = Coordinates(lineNo, GetLineMaxColumn(lineNo)); + break; + } + default: + break; + } + + if (mState.mSelectionStart != oldSelStart || + mState.mSelectionEnd != oldSelEnd) + mCursorPositionChanged = true; +} + +void TextEditor::SetTabSize(int aValue) { + mTabSize = std::max(0, std::min(32, aValue)); +} + +void TextEditor::InsertText(const std::string& aValue) { + InsertText(aValue.c_str()); +} + +void TextEditor::InsertText(const char* aValue) { + if (aValue == nullptr) return; + + auto pos = GetActualCursorCoordinates(); + auto start = std::min(pos, mState.mSelectionStart); + int totalLines = pos.mLine - start.mLine; + + totalLines += InsertTextAt(pos, aValue); + + SetSelection(pos, pos); + SetCursorPosition(pos); + Colorize(start.mLine - 1, totalLines + 2); +} + +void TextEditor::DeleteSelection() { + assert(mState.mSelectionEnd >= mState.mSelectionStart); + + if (mState.mSelectionEnd == mState.mSelectionStart) return; + + DeleteRange(mState.mSelectionStart, mState.mSelectionEnd); + + SetSelection(mState.mSelectionStart, mState.mSelectionStart); + SetCursorPosition(mState.mSelectionStart); + Colorize(mState.mSelectionStart.mLine, 1); +} + +void TextEditor::MoveUp(int aAmount, bool aSelect) { + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition.mLine = + std::max(0, mState.mCursorPosition.mLine - aAmount); + if (oldPos != mState.mCursorPosition) { + if (aSelect) { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + + EnsureCursorVisible(); + } +} + +void TextEditor::MoveDown(int aAmount, bool aSelect) { + assert(mState.mCursorPosition.mColumn >= 0); + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition.mLine = std::max( + 0, + std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + aAmount)); + + if (mState.mCursorPosition != oldPos) { + if (aSelect) { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + + EnsureCursorVisible(); + } +} + +static bool IsUTFSequence(char c) { return (c & 0xC0) == 0x80; } + +void TextEditor::MoveLeft(int aAmount, bool aSelect, bool aWordMode) { + if (mLines.empty()) return; + + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition = GetActualCursorCoordinates(); + auto line = mState.mCursorPosition.mLine; + auto cindex = GetCharacterIndex(mState.mCursorPosition); + + while (aAmount-- > 0) { + if (cindex == 0) { + if (line > 0) { + --line; + if ((int)mLines.size() > line) + cindex = (int)mLines[line].size(); + else + cindex = 0; + } + } else { + --cindex; + if (cindex > 0) { + if ((int)mLines.size() > line) { + while (cindex > 0 && IsUTFSequence(mLines[line][cindex].mChar)) + --cindex; + } + } + } + + mState.mCursorPosition = + Coordinates(line, GetCharacterColumn(line, cindex)); + if (aWordMode) { + mState.mCursorPosition = FindWordStart(mState.mCursorPosition); + cindex = GetCharacterIndex(mState.mCursorPosition); + } + } + + mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + + assert(mState.mCursorPosition.mColumn >= 0); + if (aSelect) { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection( + mInteractiveStart, mInteractiveEnd, + aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); + + EnsureCursorVisible(); +} + +void TextEditor::MoveRight(int aAmount, bool aSelect, bool aWordMode) { + auto oldPos = mState.mCursorPosition; + + if (mLines.empty() || oldPos.mLine >= mLines.size()) return; + + auto cindex = GetCharacterIndex(mState.mCursorPosition); + while (aAmount-- > 0) { + auto lindex = mState.mCursorPosition.mLine; + auto& line = mLines[lindex]; + + if (cindex >= line.size()) { + if (mState.mCursorPosition.mLine < mLines.size() - 1) { + mState.mCursorPosition.mLine = std::max( + 0, + std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + 1)); + mState.mCursorPosition.mColumn = 0; + } else + return; + } else { + cindex += UTF8CharLength(line[cindex].mChar); + mState.mCursorPosition = + Coordinates(lindex, GetCharacterColumn(lindex, cindex)); + if (aWordMode) + mState.mCursorPosition = FindNextWord(mState.mCursorPosition); + } + } + + if (aSelect) { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = SanitizeCoordinates(mState.mCursorPosition); + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection( + mInteractiveStart, mInteractiveEnd, + aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); + + EnsureCursorVisible(); +} + +void TextEditor::MoveTop(bool aSelect) { + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(0, 0)); + + if (mState.mCursorPosition != oldPos) { + if (aSelect) { + mInteractiveEnd = oldPos; + mInteractiveStart = mState.mCursorPosition; + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::TextEditor::MoveBottom(bool aSelect) { + auto oldPos = GetCursorPosition(); + auto newPos = Coordinates((int)mLines.size() - 1, 0); + SetCursorPosition(newPos); + if (aSelect) { + mInteractiveStart = oldPos; + mInteractiveEnd = newPos; + } else + mInteractiveStart = mInteractiveEnd = newPos; + SetSelection(mInteractiveStart, mInteractiveEnd); +} + +void TextEditor::MoveHome(bool aSelect) { + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, 0)); + + if (mState.mCursorPosition != oldPos) { + if (aSelect) { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::MoveEnd(bool aSelect) { + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, + GetLineMaxColumn(oldPos.mLine))); + + if (mState.mCursorPosition != oldPos) { + if (aSelect) { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::Delete() { + assert(!mReadOnly); + + if (mLines.empty()) return; + + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + DeleteSelection(); + } else { + auto pos = GetActualCursorCoordinates(); + SetCursorPosition(pos); + auto& line = mLines[pos.mLine]; + + if (pos.mColumn == GetLineMaxColumn(pos.mLine)) { + if (pos.mLine == (int)mLines.size() - 1) return; + + u.mRemoved = '\n'; + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + Advance(u.mRemovedEnd); + + auto& nextLine = mLines[pos.mLine + 1]; + line.insert(line.end(), nextLine.begin(), nextLine.end()); + RemoveLine(pos.mLine + 1); + } else { + auto cindex = GetCharacterIndex(pos); + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + u.mRemovedEnd.mColumn++; + u.mRemoved = GetText(u.mRemovedStart, u.mRemovedEnd); + + auto d = UTF8CharLength(line[cindex].mChar); + while (d-- > 0 && cindex < (int)line.size()) + line.erase(line.begin() + cindex); + } + + mTextChanged = true; + + Colorize(pos.mLine, 1); + } + + u.mAfter = mState; + AddUndo(u); +} + +void TextEditor::Backspace() { + assert(!mReadOnly); + + if (mLines.empty()) return; + + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + DeleteSelection(); + } else { + auto pos = GetActualCursorCoordinates(); + SetCursorPosition(pos); + + if (mState.mCursorPosition.mColumn == 0) { + if (mState.mCursorPosition.mLine == 0) return; + + u.mRemoved = '\n'; + u.mRemovedStart = u.mRemovedEnd = + Coordinates(pos.mLine - 1, GetLineMaxColumn(pos.mLine - 1)); + Advance(u.mRemovedEnd); + + auto& line = mLines[mState.mCursorPosition.mLine]; + auto& prevLine = mLines[mState.mCursorPosition.mLine - 1]; + auto prevSize = GetLineMaxColumn(mState.mCursorPosition.mLine - 1); + prevLine.insert(prevLine.end(), line.begin(), line.end()); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + etmp.insert(ErrorMarkers::value_type( + i.first - 1 == mState.mCursorPosition.mLine ? i.first - 1 : i.first, + i.second)); + mErrorMarkers = std::move(etmp); + + RemoveLine(mState.mCursorPosition.mLine); + --mState.mCursorPosition.mLine; + mState.mCursorPosition.mColumn = prevSize; + } else { + auto& line = mLines[mState.mCursorPosition.mLine]; + auto cindex = GetCharacterIndex(pos) - 1; + auto cend = cindex + 1; + while (cindex > 0 && IsUTFSequence(line[cindex].mChar)) --cindex; + + // if (cindex > 0 && UTF8CharLength(line[cindex].mChar) > 1) + // --cindex; + + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + --u.mRemovedStart.mColumn; + --mState.mCursorPosition.mColumn; + + while (cindex < line.size() && cend-- > cindex) { + u.mRemoved += line[cindex].mChar; + line.erase(line.begin() + cindex); + } + } + + mTextChanged = true; + + EnsureCursorVisible(); + Colorize(mState.mCursorPosition.mLine, 1); + } + + u.mAfter = mState; + AddUndo(u); +} + +void TextEditor::SelectWordUnderCursor() { + auto c = GetCursorPosition(); + SetSelection(FindWordStart(c), FindWordEnd(c)); +} + +void TextEditor::SelectAll() { + SetSelection(Coordinates(0, 0), Coordinates((int)mLines.size(), 0)); +} + +bool TextEditor::HasSelection() const { + return mState.mSelectionEnd > mState.mSelectionStart; +} + +void TextEditor::Copy() { + if (HasSelection()) { + ImGui::SetClipboardText(GetSelectedText().c_str()); + } else { + if (!mLines.empty()) { + std::string str; + auto& line = mLines[GetActualCursorCoordinates().mLine]; + for (auto& g : line) str.push_back(g.mChar); + ImGui::SetClipboardText(str.c_str()); + } + } +} + +void TextEditor::Cut() { + if (IsReadOnly()) { + Copy(); + } else { + if (HasSelection()) { + UndoRecord u; + u.mBefore = mState; + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + Copy(); + DeleteSelection(); + + u.mAfter = mState; + AddUndo(u); + } + } +} + +void TextEditor::Paste() { + if (IsReadOnly()) return; + + auto clipText = ImGui::GetClipboardText(); + if (clipText != nullptr && strlen(clipText) > 0) { + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + DeleteSelection(); + } + + u.mAdded = clipText; + u.mAddedStart = GetActualCursorCoordinates(); + + InsertText(clipText); + + u.mAddedEnd = GetActualCursorCoordinates(); + u.mAfter = mState; + AddUndo(u); + } +} + +bool TextEditor::CanUndo() const { return !mReadOnly && mUndoIndex > 0; } + +bool TextEditor::CanRedo() const { + return !mReadOnly && mUndoIndex < (int)mUndoBuffer.size(); +} + +void TextEditor::Undo(int aSteps) { + while (CanUndo() && aSteps-- > 0) mUndoBuffer[--mUndoIndex].Undo(this); +} + +void TextEditor::Redo(int aSteps) { + while (CanRedo() && aSteps-- > 0) mUndoBuffer[mUndoIndex++].Redo(this); +} + +const TextEditor::Palette& TextEditor::GetDarkPalette() { + const static Palette p = {{ + 0xff7f7f7f, // Default + 0xffd69c56, // Keyword + 0xff00ff00, // Number + 0xff7070e0, // String + 0xff70a0e0, // Char literal + 0xffffffff, // Punctuation + 0xff408080, // Preprocessor + 0xffaaaaaa, // Identifier + 0xff9bc64d, // Known identifier + 0xffc040a0, // Preproc identifier + 0xff206020, // Comment (single line) + 0xff406020, // Comment (multi line) + 0xff101010, // Background + 0xffe0e0e0, // Cursor + 0x80a06020, // Selection + 0x800020ff, // ErrorMarker + 0x40f08000, // Breakpoint + 0xff707000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40a0a0a0, // Current line edge + }}; + return p; +} + +const TextEditor::Palette& TextEditor::GetLightPalette() { + const static Palette p = {{ + 0xff7f7f7f, // None + 0xffff0c06, // Keyword + 0xff008000, // Number + 0xff2020a0, // String + 0xff304070, // Char literal + 0xff000000, // Punctuation + 0xff406060, // Preprocessor + 0xff404040, // Identifier + 0xff606010, // Known identifier + 0xffc040a0, // Preproc identifier + 0xff205020, // Comment (single line) + 0xff405020, // Comment (multi line) + 0xffffffff, // Background + 0xff000000, // Cursor + 0x80600000, // Selection + 0xa00010ff, // ErrorMarker + 0x80f08000, // Breakpoint + 0xff505000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40000000, // Current line edge + }}; + return p; +} + +const TextEditor::Palette& TextEditor::GetRetroBluePalette() { + const static Palette p = {{ + 0xff00ffff, // None + 0xffffff00, // Keyword + 0xff00ff00, // Number + 0xff808000, // String + 0xff808000, // Char literal + 0xffffffff, // Punctuation + 0xff008000, // Preprocessor + 0xff00ffff, // Identifier + 0xffffffff, // Known identifier + 0xffff00ff, // Preproc identifier + 0xff808080, // Comment (single line) + 0xff404040, // Comment (multi line) + 0xff800000, // Background + 0xff0080ff, // Cursor + 0x80ffff00, // Selection + 0xa00000ff, // ErrorMarker + 0x80ff8000, // Breakpoint + 0xff808000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40000000, // Current line edge + }}; + return p; +} + +std::string TextEditor::GetText() const { + return GetText(Coordinates(), Coordinates((int)mLines.size(), 0)); +} + +std::vector TextEditor::GetTextLines() const { + std::vector result; + + result.reserve(mLines.size()); + + for (auto& line : mLines) { + std::string text; + + text.resize(line.size()); + + for (size_t i = 0; i < line.size(); ++i) text[i] = line[i].mChar; + + result.emplace_back(std::move(text)); + } + + return result; +} + +std::string TextEditor::GetSelectedText() const { + return GetText(mState.mSelectionStart, mState.mSelectionEnd); +} + +std::string TextEditor::GetCurrentLineText() const { + auto lineLength = GetLineMaxColumn(mState.mCursorPosition.mLine); + return GetText(Coordinates(mState.mCursorPosition.mLine, 0), + Coordinates(mState.mCursorPosition.mLine, lineLength)); +} + +void TextEditor::ProcessInputs() {} + +void TextEditor::Colorize(int aFromLine, int aLines) { + int toLine = aLines == -1 ? (int)mLines.size() + : std::min((int)mLines.size(), aFromLine + aLines); + mColorRangeMin = std::min(mColorRangeMin, aFromLine); + mColorRangeMax = std::max(mColorRangeMax, toLine); + mColorRangeMin = std::max(0, mColorRangeMin); + mColorRangeMax = std::max(mColorRangeMin, mColorRangeMax); + mCheckComments = true; +} + +void TextEditor::ColorizeRange(int aFromLine, int aToLine) { + if (mLines.empty() || aFromLine >= aToLine) return; + + std::string buffer; + std::cmatch results; + std::string id; + + int endLine = std::max(0, std::min((int)mLines.size(), aToLine)); + for (int i = aFromLine; i < endLine; ++i) { + auto& line = mLines[i]; + + if (line.empty()) continue; + + buffer.resize(line.size()); + for (size_t j = 0; j < line.size(); ++j) { + auto& col = line[j]; + buffer[j] = col.mChar; + col.mColorIndex = PaletteIndex::Default; + } + + const char* bufferBegin = &buffer.front(); + const char* bufferEnd = bufferBegin + buffer.size(); + + auto last = bufferEnd; + + for (auto first = bufferBegin; first != last;) { + const char* token_begin = nullptr; + const char* token_end = nullptr; + PaletteIndex token_color = PaletteIndex::Default; + + bool hasTokenizeResult = false; + + if (mLanguageDefinition.mTokenize != nullptr) { + if (mLanguageDefinition.mTokenize(first, last, token_begin, token_end, + token_color)) + hasTokenizeResult = true; + } + + if (hasTokenizeResult == false) { + // todo : remove + // printf("using regex for %.*s\n", first + 10 < last ? 10 : int(last - + // first), first); + + for (auto& p : mRegexList) { + if (std::regex_search(first, last, results, p.first, + std::regex_constants::match_continuous)) { + hasTokenizeResult = true; + + auto& v = *results.begin(); + token_begin = v.first; + token_end = v.second; + token_color = p.second; + break; + } + } + } + + if (hasTokenizeResult == false) { + first++; + } else { + const size_t token_length = token_end - token_begin; + + if (token_color == PaletteIndex::Identifier) { + id.assign(token_begin, token_end); + + // todo : allmost all language definitions use lower case to specify + // keywords, so shouldn't this use ::tolower ? + if (!mLanguageDefinition.mCaseSensitive) + std::transform(id.begin(), id.end(), id.begin(), ::toupper); + + if (!line[first - bufferBegin].mPreprocessor) { + if (mLanguageDefinition.mKeywords.count(id) != 0) + token_color = PaletteIndex::Keyword; + else if (mLanguageDefinition.mIdentifiers.count(id) != 0) + token_color = PaletteIndex::KnownIdentifier; + else if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) + token_color = PaletteIndex::PreprocIdentifier; + } else { + if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) + token_color = PaletteIndex::PreprocIdentifier; + } + } + + for (size_t j = 0; j < token_length; ++j) + line[(token_begin - bufferBegin) + j].mColorIndex = token_color; + + first = token_end; + } + } + } +} + +void TextEditor::ColorizeInternal() { + if (mLines.empty() || !mColorizerEnabled) return; + + if (mCheckComments) { + auto endLine = mLines.size(); + auto endIndex = 0; + auto commentStartLine = endLine; + auto commentStartIndex = endIndex; + auto withinString = false; + auto withinSingleLineComment = false; + auto withinPreproc = false; + auto firstChar = + true; // there is no other non-whitespace characters in the line before + auto concatenate = false; // '\' on the very end of the line + auto currentLine = 0; + auto currentIndex = 0; + while (currentLine < endLine || currentIndex < endIndex) { + auto& line = mLines[currentLine]; + + if (currentIndex == 0 && !concatenate) { + withinSingleLineComment = false; + withinPreproc = false; + firstChar = true; + } + + concatenate = false; + + if (!line.empty()) { + auto& g = line[currentIndex]; + auto c = g.mChar; + + if (c != mLanguageDefinition.mPreprocChar && !isspace(c)) + firstChar = false; + + if (currentIndex == (int)line.size() - 1 && + line[line.size() - 1].mChar == '\\') + concatenate = true; + + bool inComment = (commentStartLine < currentLine || + (commentStartLine == currentLine && + commentStartIndex <= currentIndex)); + + if (withinString) { + line[currentIndex].mMultiLineComment = inComment; + + if (c == '\"') { + if (currentIndex + 1 < (int)line.size() && + line[currentIndex + 1].mChar == '\"') { + currentIndex += 1; + if (currentIndex < (int)line.size()) + line[currentIndex].mMultiLineComment = inComment; + } else + withinString = false; + } else if (c == '\\') { + currentIndex += 1; + if (currentIndex < (int)line.size()) + line[currentIndex].mMultiLineComment = inComment; + } + } else { + if (firstChar && c == mLanguageDefinition.mPreprocChar) + withinPreproc = true; + + if (c == '\"') { + withinString = true; + line[currentIndex].mMultiLineComment = inComment; + } else { + auto pred = [](const char& a, const Glyph& b) { + return a == b.mChar; + }; + auto from = line.begin() + currentIndex; + auto& startStr = mLanguageDefinition.mCommentStart; + auto& singleStartStr = mLanguageDefinition.mSingleLineComment; + + if (singleStartStr.size() > 0 && + currentIndex + singleStartStr.size() <= line.size() && + equals(singleStartStr.begin(), singleStartStr.end(), from, + from + singleStartStr.size(), pred)) { + withinSingleLineComment = true; + } else if (!withinSingleLineComment && + currentIndex + startStr.size() <= line.size() && + equals(startStr.begin(), startStr.end(), from, + from + startStr.size(), pred)) { + commentStartLine = currentLine; + commentStartIndex = currentIndex; + } + + inComment = inComment = (commentStartLine < currentLine || + (commentStartLine == currentLine && + commentStartIndex <= currentIndex)); + + line[currentIndex].mMultiLineComment = inComment; + line[currentIndex].mComment = withinSingleLineComment; + + auto& endStr = mLanguageDefinition.mCommentEnd; + if (currentIndex + 1 >= (int)endStr.size() && + equals(endStr.begin(), endStr.end(), from + 1 - endStr.size(), + from + 1, pred)) { + commentStartIndex = endIndex; + commentStartLine = endLine; + } + } + } + line[currentIndex].mPreprocessor = withinPreproc; + currentIndex += UTF8CharLength(c); + if (currentIndex >= (int)line.size()) { + currentIndex = 0; + ++currentLine; + } + } else { + currentIndex = 0; + ++currentLine; + } + } + mCheckComments = false; + } + + if (mColorRangeMin < mColorRangeMax) { + const int increment = + (mLanguageDefinition.mTokenize == nullptr) ? 10 : 10000; + const int to = std::min(mColorRangeMin + increment, mColorRangeMax); + ColorizeRange(mColorRangeMin, to); + mColorRangeMin = to; + + if (mColorRangeMax == mColorRangeMin) { + mColorRangeMin = std::numeric_limits::max(); + mColorRangeMax = 0; + } + return; + } +} + +float TextEditor::TextDistanceToLineStart(const Coordinates& aFrom) const { + auto& line = mLines[aFrom.mLine]; + float distance = 0.0f; + float spaceSize = ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, + " ", nullptr, nullptr) + .x; + int colIndex = GetCharacterIndex(aFrom); + for (size_t it = 0u; it < line.size() && it < colIndex;) { + if (line[it].mChar == '\t') { + distance = (1.0f + std::floor((1.0f + distance) / + (float(mTabSize) * spaceSize))) * + (float(mTabSize) * spaceSize); + ++it; + } else { + auto d = UTF8CharLength(line[it].mChar); + char tempCString[7]; + int i = 0; + for (; i < 6 && d-- > 0 && it < (int)line.size(); i++, it++) + tempCString[i] = line[it].mChar; + + tempCString[i] = '\0'; + distance += ImGui::GetFont() + ->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, + tempCString, nullptr, nullptr) + .x; + } + } + + return distance; +} + +void TextEditor::EnsureCursorVisible() { + if (!mWithinRender) { + mScrollToCursor = true; + return; + } + + float scrollX = ImGui::GetScrollX(); + float scrollY = ImGui::GetScrollY(); + + auto height = ImGui::GetWindowHeight(); + auto width = ImGui::GetWindowWidth(); + + auto top = 1 + (int)ceil(scrollY / mCharAdvance.y); + auto bottom = (int)ceil((scrollY + height) / mCharAdvance.y); + + auto left = (int)ceil(scrollX / mCharAdvance.x); + auto right = (int)ceil((scrollX + width) / mCharAdvance.x); + + auto pos = GetActualCursorCoordinates(); + auto len = TextDistanceToLineStart(pos); + + if (pos.mLine < top) + ImGui::SetScrollY(std::max(0.0f, (pos.mLine - 1) * mCharAdvance.y)); + if (pos.mLine > bottom - 4) + ImGui::SetScrollY( + std::max(0.0f, (pos.mLine + 4) * mCharAdvance.y - height)); + if (len + mTextStart < left + 4) + ImGui::SetScrollX(std::max(0.0f, len + mTextStart - 4)); + if (len + mTextStart > right - 4) + ImGui::SetScrollX(std::max(0.0f, len + mTextStart + 4 - width)); +} + +int TextEditor::GetPageSize() const { + auto height = ImGui::GetWindowHeight() - 20.0f; + return (int)floor(height / mCharAdvance.y); +} + +TextEditor::UndoRecord::UndoRecord(const std::string& aAdded, + const TextEditor::Coordinates aAddedStart, + const TextEditor::Coordinates aAddedEnd, + const std::string& aRemoved, + const TextEditor::Coordinates aRemovedStart, + const TextEditor::Coordinates aRemovedEnd, + TextEditor::EditorState& aBefore, + TextEditor::EditorState& aAfter) + : mAdded(aAdded), + mAddedStart(aAddedStart), + mAddedEnd(aAddedEnd), + mRemoved(aRemoved), + mRemovedStart(aRemovedStart), + mRemovedEnd(aRemovedEnd), + mBefore(aBefore), + mAfter(aAfter) { + assert(mAddedStart <= mAddedEnd); + assert(mRemovedStart <= mRemovedEnd); +} + +void TextEditor::UndoRecord::Undo(TextEditor* aEditor) { + if (!mAdded.empty()) { + aEditor->DeleteRange(mAddedStart, mAddedEnd); + aEditor->Colorize(mAddedStart.mLine - 1, + mAddedEnd.mLine - mAddedStart.mLine + 2); + } + + if (!mRemoved.empty()) { + auto start = mRemovedStart; + aEditor->InsertTextAt(start, mRemoved.c_str()); + aEditor->Colorize(mRemovedStart.mLine - 1, + mRemovedEnd.mLine - mRemovedStart.mLine + 2); + } + + aEditor->mState = mBefore; + aEditor->EnsureCursorVisible(); +} + +void TextEditor::UndoRecord::Redo(TextEditor* aEditor) { + if (!mRemoved.empty()) { + aEditor->DeleteRange(mRemovedStart, mRemovedEnd); + aEditor->Colorize(mRemovedStart.mLine - 1, + mRemovedEnd.mLine - mRemovedStart.mLine + 1); + } + + if (!mAdded.empty()) { + auto start = mAddedStart; + aEditor->InsertTextAt(start, mAdded.c_str()); + aEditor->Colorize(mAddedStart.mLine - 1, + mAddedEnd.mLine - mAddedStart.mLine + 1); + } + + aEditor->mState = mAfter; + aEditor->EnsureCursorVisible(); +} + +static bool TokenizeCStyleString(const char* in_begin, const char* in_end, + const char*& out_begin, const char*& out_end) { + const char* p = in_begin; + + if (*p == '"') { + p++; + + while (p < in_end) { + // handle end of string + if (*p == '"') { + out_begin = in_begin; + out_end = p + 1; + return true; + } + + // handle escape character for " + if (*p == '\\' && p + 1 < in_end && p[1] == '"') p++; + + p++; + } + } + + return false; +} + +static bool TokenizeCStyleCharacterLiteral(const char* in_begin, + const char* in_end, + const char*& out_begin, + const char*& out_end) { + const char* p = in_begin; + + if (*p == '\'') { + p++; + + // handle escape characters + if (p < in_end && *p == '\\') p++; + + if (p < in_end) p++; + + // handle end of character literal + if (p < in_end && *p == '\'') { + out_begin = in_begin; + out_end = p + 1; + return true; + } + } + + return false; +} + +static bool TokenizeCStyleIdentifier(const char* in_begin, const char* in_end, + const char*& out_begin, + const char*& out_end) { + const char* p = in_begin; + + if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || *p == '_') { + p++; + + while ((p < in_end) && + ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9') || *p == '_')) + p++; + + out_begin = in_begin; + out_end = p; + return true; + } + + return false; +} + +static bool TokenizeCStyleNumber(const char* in_begin, const char* in_end, + const char*& out_begin, const char*& out_end) { + const char* p = in_begin; + + const bool startsWithNumber = *p >= '0' && *p <= '9'; + + if (*p != '+' && *p != '-' && !startsWithNumber) return false; + + p++; + + bool hasNumber = startsWithNumber; + + while (p < in_end && (*p >= '0' && *p <= '9')) { + hasNumber = true; + + p++; + } + + if (hasNumber == false) return false; + + bool isFloat = false; + bool isHex = false; + bool isBinary = false; + + if (p < in_end) { + if (*p == '.') { + isFloat = true; + + p++; + + while (p < in_end && (*p >= '0' && *p <= '9')) p++; + } else if (*p == 'x' || *p == 'X') { + // hex formatted integer of the type 0xef80 + + isHex = true; + + p++; + + while (p < in_end && + ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || + (*p >= 'A' && *p <= 'F'))) + p++; + } else if (*p == 'b' || *p == 'B') { + // binary formatted integer of the type 0b01011101 + + isBinary = true; + + p++; + + while (p < in_end && (*p >= '0' && *p <= '1')) p++; + } + } + + if (isHex == false && isBinary == false) { + // floating point exponent + if (p < in_end && (*p == 'e' || *p == 'E')) { + isFloat = true; + + p++; + + if (p < in_end && (*p == '+' || *p == '-')) p++; + + bool hasDigits = false; + + while (p < in_end && (*p >= '0' && *p <= '9')) { + hasDigits = true; + + p++; + } + + if (hasDigits == false) return false; + } + + // single precision floating point type + if (p < in_end && *p == 'f') p++; + } + + if (isFloat == false) { + // integer size type + while (p < in_end && (*p == 'u' || *p == 'U' || *p == 'l' || *p == 'L')) + p++; + } + + out_begin = in_begin; + out_end = p; + return true; +} + +static bool TokenizeCStylePunctuation(const char* in_begin, const char* in_end, + const char*& out_begin, + const char*& out_end) { + (void)in_end; + + switch (*in_begin) { + case '[': + case ']': + case '{': + case '}': + case '!': + case '%': + case '^': + case '&': + case '*': + case '(': + case ')': + case '-': + case '+': + case '=': + case '~': + case '|': + case '<': + case '>': + case '?': + case ':': + case '/': + case ';': + case ',': + case '.': + out_begin = in_begin; + out_end = in_begin + 1; + return true; + } + + return false; +} + +const TextEditor::LanguageDefinition& +TextEditor::LanguageDefinition::CPlusPlus() { + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) { + static const char* const cppKeywords[] = {"alignas", + "alignof", + "and", + "and_eq", + "asm", + "atomic_cancel", + "atomic_commit", + "atomic_noexcept", + "auto", + "bitand", + "bitor", + "bool", + "break", + "case", + "catch", + "char", + "char16_t", + "char32_t", + "class", + "compl", + "concept", + "const", + "constexpr", + "const_cast", + "continue", + "decltype", + "default", + "delete", + "do", + "double", + "dynamic_cast", + "else", + "enum", + "explicit", + "export", + "extern", + "false", + "float", + "for", + "friend", + "goto", + "if", + "import", + "inline", + "int", + "long", + "module", + "mutable", + "namespace", + "new", + "noexcept", + "not", + "not_eq", + "nullptr", + "operator", + "or", + "or_eq", + "private", + "protected", + "public", + "register", + "reinterpret_cast", + "requires", + "return", + "short", + "signed", + "sizeof", + "static", + "static_assert", + "static_cast", + "struct", + "switch", + "synchronized", + "template", + "this", + "thread_local", + "throw", + "true", + "try", + "typedef", + "typeid", + "typename", + "union", + "unsigned", + "using", + "virtual", + "void", + "volatile", + "wchar_t", + "while", + "xor", + "xor_eq"}; + for (auto& k : cppKeywords) langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", + "atexit", "atof", "atoi", "atol", "ceil", + "clock", "cosh", "ctime", "div", "exit", + "fabs", "floor", "fmod", "getchar", "getenv", + "isalnum", "isalpha", "isdigit", "isgraph", "ispunct", + "isspace", "isupper", "kbhit", "log10", "log2", + "log", "memcmp", "modf", "pow", "printf", + "sprintf", "snprintf", "putchar", "putenv", "puts", + "rand", "remove", "rename", "sinh", "sqrt", + "srand", "strcat", "strcmp", "strerror", "time", + "tolower", "toupper", "std", "string", "vector", + "map", "unordered_map", "set", "unordered_set", "min", + "max"}; + for (auto& k : identifiers) { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char* in_begin, const char* in_end, + const char*& out_begin, const char*& out_end, + PaletteIndex& paletteIndex) -> bool { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } else if (TokenizeCStyleString(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::String; + else if (TokenizeCStyleCharacterLiteral(in_begin, in_end, out_begin, + out_end)) + paletteIndex = PaletteIndex::CharLiteral; + else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeCStyleNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "C++"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::HLSL() { + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) { + static const char* const keywords[] = { + "AppendStructuredBuffer", + "asm", + "asm_fragment", + "BlendState", + "bool", + "break", + "Buffer", + "ByteAddressBuffer", + "case", + "cbuffer", + "centroid", + "class", + "column_major", + "compile", + "compile_fragment", + "CompileShader", + "const", + "continue", + "ComputeShader", + "ConsumeStructuredBuffer", + "default", + "DepthStencilState", + "DepthStencilView", + "discard", + "do", + "double", + "DomainShader", + "dword", + "else", + "export", + "extern", + "false", + "float", + "for", + "fxgroup", + "GeometryShader", + "groupshared", + "half", + "Hullshader", + "if", + "in", + "inline", + "inout", + "InputPatch", + "int", + "interface", + "line", + "lineadj", + "linear", + "LineStream", + "matrix", + "min16float", + "min10float", + "min16int", + "min12int", + "min16uint", + "namespace", + "nointerpolation", + "noperspective", + "NULL", + "out", + "OutputPatch", + "packoffset", + "pass", + "pixelfragment", + "PixelShader", + "point", + "PointStream", + "precise", + "RasterizerState", + "RenderTargetView", + "return", + "register", + "row_major", + "RWBuffer", + "RWByteAddressBuffer", + "RWStructuredBuffer", + "RWTexture1D", + "RWTexture1DArray", + "RWTexture2D", + "RWTexture2DArray", + "RWTexture3D", + "sample", + "sampler", + "SamplerState", + "SamplerComparisonState", + "shared", + "snorm", + "stateblock", + "stateblock_state", + "static", + "string", + "struct", + "switch", + "StructuredBuffer", + "tbuffer", + "technique", + "technique10", + "technique11", + "texture", + "Texture1D", + "Texture1DArray", + "Texture2D", + "Texture2DArray", + "Texture2DMS", + "Texture2DMSArray", + "Texture3D", + "TextureCube", + "TextureCubeArray", + "true", + "typedef", + "triangle", + "triangleadj", + "TriangleStream", + "uint", + "uniform", + "unorm", + "unsigned", + "vector", + "vertexfragment", + "VertexShader", + "void", + "volatile", + "while", + "bool1", + "bool2", + "bool3", + "bool4", + "double1", + "double2", + "double3", + "double4", + "float1", + "float2", + "float3", + "float4", + "int1", + "int2", + "int3", + "int4", + "in", + "out", + "inout", + "uint1", + "uint2", + "uint3", + "uint4", + "dword1", + "dword2", + "dword3", + "dword4", + "half1", + "half2", + "half3", + "half4", + "float1x1", + "float2x1", + "float3x1", + "float4x1", + "float1x2", + "float2x2", + "float3x2", + "float4x2", + "float1x3", + "float2x3", + "float3x3", + "float4x3", + "float1x4", + "float2x4", + "float3x4", + "float4x4", + "half1x1", + "half2x1", + "half3x1", + "half4x1", + "half1x2", + "half2x2", + "half3x2", + "half4x2", + "half1x3", + "half2x3", + "half3x3", + "half4x3", + "half1x4", + "half2x4", + "half3x4", + "half4x4", + }; + for (auto& k : keywords) langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", + "abs", + "acos", + "all", + "AllMemoryBarrier", + "AllMemoryBarrierWithGroupSync", + "any", + "asdouble", + "asfloat", + "asin", + "asint", + "asint", + "asuint", + "asuint", + "atan", + "atan2", + "ceil", + "CheckAccessFullyMapped", + "clamp", + "clip", + "cos", + "cosh", + "countbits", + "cross", + "D3DCOLORtoUBYTE4", + "ddx", + "ddx_coarse", + "ddx_fine", + "ddy", + "ddy_coarse", + "ddy_fine", + "degrees", + "determinant", + "DeviceMemoryBarrier", + "DeviceMemoryBarrierWithGroupSync", + "distance", + "dot", + "dst", + "errorf", + "EvaluateAttributeAtCentroid", + "EvaluateAttributeAtSample", + "EvaluateAttributeSnapped", + "exp", + "exp2", + "f16tof32", + "f32tof16", + "faceforward", + "firstbithigh", + "firstbitlow", + "floor", + "fma", + "fmod", + "frac", + "frexp", + "fwidth", + "GetRenderTargetSampleCount", + "GetRenderTargetSamplePosition", + "GroupMemoryBarrier", + "GroupMemoryBarrierWithGroupSync", + "InterlockedAdd", + "InterlockedAnd", + "InterlockedCompareExchange", + "InterlockedCompareStore", + "InterlockedExchange", + "InterlockedMax", + "InterlockedMin", + "InterlockedOr", + "InterlockedXor", + "isfinite", + "isinf", + "isnan", + "ldexp", + "length", + "lerp", + "lit", + "log", + "log10", + "log2", + "mad", + "max", + "min", + "modf", + "msad4", + "mul", + "noise", + "normalize", + "pow", + "printf", + "Process2DQuadTessFactorsAvg", + "Process2DQuadTessFactorsMax", + "Process2DQuadTessFactorsMin", + "ProcessIsolineTessFactors", + "ProcessQuadTessFactorsAvg", + "ProcessQuadTessFactorsMax", + "ProcessQuadTessFactorsMin", + "ProcessTriTessFactorsAvg", + "ProcessTriTessFactorsMax", + "ProcessTriTessFactorsMin", + "radians", + "rcp", + "reflect", + "refract", + "reversebits", + "round", + "rsqrt", + "saturate", + "sign", + "sin", + "sincos", + "sinh", + "smoothstep", + "sqrt", + "step", + "tan", + "tanh", + "tex1D", + "tex1D", + "tex1Dbias", + "tex1Dgrad", + "tex1Dlod", + "tex1Dproj", + "tex2D", + "tex2D", + "tex2Dbias", + "tex2Dgrad", + "tex2Dlod", + "tex2Dproj", + "tex3D", + "tex3D", + "tex3Dbias", + "tex3Dgrad", + "tex3Dlod", + "tex3Dproj", + "texCUBE", + "texCUBE", + "texCUBEbias", + "texCUBEgrad", + "texCUBElod", + "texCUBEproj", + "transpose", + "trunc"}; + for (auto& k : identifiers) { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back( + std::make_pair("[ \\t]*#[ \\t]*[a-zA-Z_]+", + PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", + PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("\\'\\\\?[^\\']\\'", + PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", + PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/" + "\\;\\,\\.]", + PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "HLSL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::GLSL() { + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) { + static const char* const keywords[] = { + "auto", "break", "case", "char", + "const", "continue", "default", "do", + "double", "else", "enum", "extern", + "float", "for", "goto", "if", + "inline", "int", "long", "register", + "restrict", "return", "short", "signed", + "sizeof", "static", "struct", "switch", + "typedef", "union", "unsigned", "void", + "volatile", "while", "_Alignas", "_Alignof", + "_Atomic", "_Bool", "_Complex", "_Generic", + "_Imaginary", "_Noreturn", "_Static_assert", "_Thread_local"}; + for (auto& k : keywords) langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", + "atof", "atoi", "atol", "ceil", "clock", "cosh", + "ctime", "div", "exit", "fabs", "floor", "fmod", + "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", + "log", "memcmp", "modf", "pow", "putchar", "putenv", + "puts", "rand", "remove", "rename", "sinh", "sqrt", + "srand", "strcat", "strcmp", "strerror", "time", "tolower", + "toupper"}; + for (auto& k : identifiers) { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back( + std::make_pair("[ \\t]*#[ \\t]*[a-zA-Z_]+", + PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", + PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("\\'\\\\?[^\\']\\'", + PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", + PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/" + "\\;\\,\\.]", + PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "GLSL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::C() { + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) { + static const char* const keywords[] = { + "auto", "break", "case", "char", + "const", "continue", "default", "do", + "double", "else", "enum", "extern", + "float", "for", "goto", "if", + "inline", "int", "long", "register", + "restrict", "return", "short", "signed", + "sizeof", "static", "struct", "switch", + "typedef", "union", "unsigned", "void", + "volatile", "while", "_Alignas", "_Alignof", + "_Atomic", "_Bool", "_Complex", "_Generic", + "_Imaginary", "_Noreturn", "_Static_assert", "_Thread_local"}; + for (auto& k : keywords) langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", + "atof", "atoi", "atol", "ceil", "clock", "cosh", + "ctime", "div", "exit", "fabs", "floor", "fmod", + "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", + "log", "memcmp", "modf", "pow", "putchar", "putenv", + "puts", "rand", "remove", "rename", "sinh", "sqrt", + "srand", "strcat", "strcmp", "strerror", "time", "tolower", + "toupper"}; + for (auto& k : identifiers) { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char* in_begin, const char* in_end, + const char*& out_begin, const char*& out_end, + PaletteIndex& paletteIndex) -> bool { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } else if (TokenizeCStyleString(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::String; + else if (TokenizeCStyleCharacterLiteral(in_begin, in_end, out_begin, + out_end)) + paletteIndex = PaletteIndex::CharLiteral; + else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeCStyleNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "C"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::SQL() { + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) { + static const char* const keywords[] = {"ADD", + "EXCEPT", + "PERCENT", + "ALL", + "EXEC", + "PLAN", + "ALTER", + "EXECUTE", + "PRECISION", + "AND", + "EXISTS", + "PRIMARY", + "ANY", + "EXIT", + "PRINT", + "AS", + "FETCH", + "PROC", + "ASC", + "FILE", + "PROCEDURE", + "AUTHORIZATION", + "FILLFACTOR", + "PUBLIC", + "BACKUP", + "FOR", + "RAISERROR", + "BEGIN", + "FOREIGN", + "READ", + "BETWEEN", + "FREETEXT", + "READTEXT", + "BREAK", + "FREETEXTTABLE", + "RECONFIGURE", + "BROWSE", + "FROM", + "REFERENCES", + "BULK", + "FULL", + "REPLICATION", + "BY", + "FUNCTION", + "RESTORE", + "CASCADE", + "GOTO", + "RESTRICT", + "CASE", + "GRANT", + "RETURN", + "CHECK", + "GROUP", + "REVOKE", + "CHECKPOINT", + "HAVING", + "RIGHT", + "CLOSE", + "HOLDLOCK", + "ROLLBACK", + "CLUSTERED", + "IDENTITY", + "ROWCOUNT", + "COALESCE", + "IDENTITY_INSERT", + "ROWGUIDCOL", + "COLLATE", + "IDENTITYCOL", + "RULE", + "COLUMN", + "IF", + "SAVE", + "COMMIT", + "IN", + "SCHEMA", + "COMPUTE", + "INDEX", + "SELECT", + "CONSTRAINT", + "INNER", + "SESSION_USER", + "CONTAINS", + "INSERT", + "SET", + "CONTAINSTABLE", + "INTERSECT", + "SETUSER", + "CONTINUE", + "INTO", + "SHUTDOWN", + "CONVERT", + "IS", + "SOME", + "CREATE", + "JOIN", + "STATISTICS", + "CROSS", + "KEY", + "SYSTEM_USER", + "CURRENT", + "KILL", + "TABLE", + "CURRENT_DATE", + "LEFT", + "TEXTSIZE", + "CURRENT_TIME", + "LIKE", + "THEN", + "CURRENT_TIMESTAMP", + "LINENO", + "TO", + "CURRENT_USER", + "LOAD", + "TOP", + "CURSOR", + "NATIONAL", + "TRAN", + "DATABASE", + "NOCHECK", + "TRANSACTION", + "DBCC", + "NONCLUSTERED", + "TRIGGER", + "DEALLOCATE", + "NOT", + "TRUNCATE", + "DECLARE", + "NULL", + "TSEQUAL", + "DEFAULT", + "NULLIF", + "UNION", + "DELETE", + "OF", + "UNIQUE", + "DENY", + "OFF", + "UPDATE", + "DESC", + "OFFSETS", + "UPDATETEXT", + "DISK", + "ON", + "USE", + "DISTINCT", + "OPEN", + "USER", + "DISTRIBUTED", + "OPENDATASOURCE", + "VALUES", + "DOUBLE", + "OPENQUERY", + "VARYING", + "DROP", + "OPENROWSET", + "VIEW", + "DUMMY", + "OPENXML", + "WAITFOR", + "DUMP", + "OPTION", + "WHEN", + "ELSE", + "OR", + "WHERE", + "END", + "ORDER", + "WHILE", + "ERRLVL", + "OUTER", + "WITH", + "ESCAPE", + "OVER", + "WRITETEXT"}; + + for (auto& k : keywords) langDef.mKeywords.insert(k); + + static const char* const identifiers[] = {"ABS", + "ACOS", + "ADD_MONTHS", + "ASCII", + "ASCIISTR", + "ASIN", + "ATAN", + "ATAN2", + "AVG", + "BFILENAME", + "BIN_TO_NUM", + "BITAND", + "CARDINALITY", + "CASE", + "CAST", + "CEIL", + "CHARTOROWID", + "CHR", + "COALESCE", + "COMPOSE", + "CONCAT", + "CONVERT", + "CORR", + "COS", + "COSH", + "COUNT", + "COVAR_POP", + "COVAR_SAMP", + "CUME_DIST", + "CURRENT_DATE", + "CURRENT_TIMESTAMP", + "DBTIMEZONE", + "DECODE", + "DECOMPOSE", + "DENSE_RANK", + "DUMP", + "EMPTY_BLOB", + "EMPTY_CLOB", + "EXP", + "EXTRACT", + "FIRST_VALUE", + "FLOOR", + "FROM_TZ", + "GREATEST", + "GROUP_ID", + "HEXTORAW", + "INITCAP", + "INSTR", + "INSTR2", + "INSTR4", + "INSTRB", + "INSTRC", + "LAG", + "LAST_DAY", + "LAST_VALUE", + "LEAD", + "LEAST", + "LENGTH", + "LENGTH2", + "LENGTH4", + "LENGTHB", + "LENGTHC", + "LISTAGG", + "LN", + "LNNVL", + "LOCALTIMESTAMP", + "LOG", + "LOWER", + "LPAD", + "LTRIM", + "MAX", + "MEDIAN", + "MIN", + "MOD", + "MONTHS_BETWEEN", + "NANVL", + "NCHR", + "NEW_TIME", + "NEXT_DAY", + "NTH_VALUE", + "NULLIF", + "NUMTODSINTERVAL", + "NUMTOYMINTERVAL", + "NVL", + "NVL2", + "POWER", + "RANK", + "RAWTOHEX", + "REGEXP_COUNT", + "REGEXP_INSTR", + "REGEXP_REPLACE", + "REGEXP_SUBSTR", + "REMAINDER", + "REPLACE", + "ROUND", + "ROWNUM", + "RPAD", + "RTRIM", + "SESSIONTIMEZONE", + "SIGN", + "SIN", + "SINH", + "SOUNDEX", + "SQRT", + "STDDEV", + "SUBSTR", + "SUM", + "SYS_CONTEXT", + "SYSDATE", + "SYSTIMESTAMP", + "TAN", + "TANH", + "TO_CHAR", + "TO_CLOB", + "TO_DATE", + "TO_DSINTERVAL", + "TO_LOB", + "TO_MULTI_BYTE", + "TO_NCLOB", + "TO_NUMBER", + "TO_SINGLE_BYTE", + "TO_TIMESTAMP", + "TO_TIMESTAMP_TZ", + "TO_YMINTERVAL", + "TRANSLATE", + "TRIM", + "TRUNC", + "TZ_OFFSET", + "UID", + "UPPER", + "USER", + "USERENV", + "VAR_POP", + "VAR_SAMP", + "VARIANCE", + "VSIZE "}; + for (auto& k : identifiers) { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back( + std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", + PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("\\\'[^\\\']*\\\'", + PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", + PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/" + "\\;\\,\\.]", + PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = false; + langDef.mAutoIndentation = false; + + langDef.mName = "SQL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& +TextEditor::LanguageDefinition::AngelScript() { + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) { + static const char* const keywords[] = { + "and", "abstract", "auto", "bool", "break", + "case", "cast", "class", "const", "continue", + "default", "do", "double", "else", "enum", + "false", "final", "float", "for", "from", + "funcdef", "function", "get", "if", "import", + "in", "inout", "int", "interface", "int8", + "int16", "int32", "int64", "is", "mixin", + "namespace", "not", "null", "or", "out", + "override", "private", "protected", "return", "set", + "shared", "super", "switch", "this ", "true", + "typedef", "uint", "uint8", "uint16", "uint32", + "uint64", "void", "while", "xor"}; + + for (auto& k : keywords) langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "cos", "sin", "tab", "acos", "asin", + "atan", "atan2", "cosh", "sinh", "tanh", + "log", "log10", "pow", "sqrt", "abs", + "ceil", "floor", "fraction", "closeTo", "fpFromIEEE", + "fpToIEEE", "complex", "opEquals", "opAddAssign", "opSubAssign", + "opMulAssign", "opDivAssign", "opAdd", "opSub", "opMul", + "opDiv"}; + for (auto& k : identifiers) { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back( + std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", + PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("\\'\\\\?[^\\']\\'", + PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", + PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/" + "\\;\\,\\.]", + PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "AngelScript"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Lua() { + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) { + static const char* const keywords[] = { + "and", "break", "do", "", "else", "elseif", "end", "false", + "for", "function", "if", "in", "", "local", "nil", "not", + "or", "repeat", "return", "then", "true", "until", "while"}; + + for (auto& k : keywords) langDef.mKeywords.insert(k); + + static const char* const identifiers[] = {"assert", "collectgarbage", + "dofile", "error", + "getmetatable", "ipairs", + "loadfile", "load", + "loadstring", "next", + "pairs", "pcall", + "print", "rawequal", + "rawlen", "rawget", + "rawset", "select", + "setmetatable", "tonumber", + "tostring", "type", + "xpcall", "_G", + "_VERSION", "arshift", + "band", "bnot", + "bor", "bxor", + "btest", "extract", + "lrotate", "lshift", + "replace", "rrotate", + "rshift", "create", + "resume", "running", + "status", "wrap", + "yield", "isyieldable", + "debug", "getuservalue", + "gethook", "getinfo", + "getlocal", "getregistry", + "getmetatable", "getupvalue", + "upvaluejoin", "upvalueid", + "setuservalue", "sethook", + "setlocal", "setmetatable", + "setupvalue", "traceback", + "close", "flush", + "input", "lines", + "open", "output", + "popen", "read", + "tmpfile", "type", + "write", "close", + "flush", "lines", + "read", "seek", + "setvbuf", "write", + "__gc", "__tostring", + "abs", "acos", + "asin", "atan", + "ceil", "cos", + "deg", "exp", + "tointeger", "floor", + "fmod", "ult", + "log", "max", + "min", "modf", + "rad", "random", + "randomseed", "sin", + "sqrt", "string", + "tan", "type", + "atan2", "cosh", + "sinh", "tanh", + "pow", "frexp", + "ldexp", "log10", + "pi", "huge", + "maxinteger", "mininteger", + "loadlib", "searchpath", + "seeall", "preload", + "cpath", "path", + "searchers", "loaded", + "module", "require", + "clock", "date", + "difftime", "execute", + "exit", "getenv", + "remove", "rename", + "setlocale", "time", + "tmpname", "byte", + "char", "dump", + "find", "format", + "gmatch", "gsub", + "len", "lower", + "match", "rep", + "reverse", "sub", + "upper", "pack", + "packsize", "unpack", + "concat", "maxn", + "insert", "pack", + "unpack", "remove", + "move", "sort", + "offset", "codepoint", + "char", "len", + "codes", "charpattern", + "coroutine", "table", + "io", "os", + "string", "utf8", + "bit32", "math", + "debug", "package"}; + for (auto& k : identifiers) { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back( + std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", + PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("\\\'[^\\\']*\\\'", + PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", + PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back( + std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", + PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back( + std::make_pair( + "[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/" + "\\;\\,\\.]", + PaletteIndex::Punctuation)); + + langDef.mCommentStart = "--[["; + langDef.mCommentEnd = "]]"; + langDef.mSingleLineComment = "--"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = false; + + langDef.mName = "Lua"; + + inited = true; + } + return langDef; +} diff --git a/src/app/gui/modules/text_editor.h b/src/app/gui/modules/text_editor.h new file mode 100644 index 00000000..e26686a5 --- /dev/null +++ b/src/app/gui/modules/text_editor.h @@ -0,0 +1,387 @@ +#ifndef YAZE_APP_GUI_MODULES_TEXT_EDITOR_H +#define YAZE_APP_GUI_MODULES_TEXT_EDITOR_H + +// Originally from ImGuiColorTextEdit/TextEditor.h + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "imgui.h" + +class TextEditor { + public: + enum class PaletteIndex { + Default, + Keyword, + Number, + String, + CharLiteral, + Punctuation, + Preprocessor, + Identifier, + KnownIdentifier, + PreprocIdentifier, + Comment, + MultiLineComment, + Background, + Cursor, + Selection, + ErrorMarker, + Breakpoint, + LineNumber, + CurrentLineFill, + CurrentLineFillInactive, + CurrentLineEdge, + Max + }; + + enum class SelectionMode { Normal, Word, Line }; + + struct Breakpoint { + int mLine; + bool mEnabled; + std::string mCondition; + + Breakpoint() : mLine(-1), mEnabled(false) {} + }; + + // Represents a character coordinate from the user's point of view, + // i. e. consider an uniform grid (assuming fixed-width font) on the + // screen as it is rendered, and each cell has its own coordinate, starting + // from 0. Tabs are counted as [1..mTabSize] count empty spaces, depending on + // how many space is necessary to reach the next tab stop. + // For example, coordinate (1, 5) represents the character 'B' in a line + // "\tABC", when mTabSize = 4, because it is rendered as " ABC" on the + // screen. + struct Coordinates { + int mLine, mColumn; + Coordinates() : mLine(0), mColumn(0) {} + Coordinates(int aLine, int aColumn) : mLine(aLine), mColumn(aColumn) { + assert(aLine >= 0); + assert(aColumn >= 0); + } + static Coordinates Invalid() { + static Coordinates invalid(-1, -1); + return invalid; + } + + bool operator==(const Coordinates& o) const { + return mLine == o.mLine && mColumn == o.mColumn; + } + + bool operator!=(const Coordinates& o) const { + return mLine != o.mLine || mColumn != o.mColumn; + } + + bool operator<(const Coordinates& o) const { + if (mLine != o.mLine) return mLine < o.mLine; + return mColumn < o.mColumn; + } + + bool operator>(const Coordinates& o) const { + if (mLine != o.mLine) return mLine > o.mLine; + return mColumn > o.mColumn; + } + + bool operator<=(const Coordinates& o) const { + if (mLine != o.mLine) return mLine < o.mLine; + return mColumn <= o.mColumn; + } + + bool operator>=(const Coordinates& o) const { + if (mLine != o.mLine) return mLine > o.mLine; + return mColumn >= o.mColumn; + } + }; + + struct Identifier { + Coordinates mLocation; + std::string mDeclaration; + }; + + typedef std::string String; + typedef std::unordered_map Identifiers; + typedef std::unordered_set Keywords; + typedef std::map ErrorMarkers; + typedef std::unordered_set Breakpoints; + typedef std::array Palette; + typedef uint8_t Char; + + struct Glyph { + Char mChar; + PaletteIndex mColorIndex = PaletteIndex::Default; + bool mComment : 1; + bool mMultiLineComment : 1; + bool mPreprocessor : 1; + + Glyph(Char aChar, PaletteIndex aColorIndex) + : mChar(aChar), + mColorIndex(aColorIndex), + mComment(false), + mMultiLineComment(false), + mPreprocessor(false) {} + }; + + typedef std::vector Line; + typedef std::vector Lines; + + struct LanguageDefinition { + typedef std::pair TokenRegexString; + typedef std::vector TokenRegexStrings; + typedef bool (*TokenizeCallback)(const char* in_begin, const char* in_end, + const char*& out_begin, + const char*& out_end, + PaletteIndex& paletteIndex); + + std::string mName; + Keywords mKeywords; + Identifiers mIdentifiers; + Identifiers mPreprocIdentifiers; + std::string mCommentStart, mCommentEnd, mSingleLineComment; + char mPreprocChar; + bool mAutoIndentation; + + TokenizeCallback mTokenize; + + TokenRegexStrings mTokenRegexStrings; + + bool mCaseSensitive; + + LanguageDefinition() + : mPreprocChar('#'), + mAutoIndentation(true), + mTokenize(nullptr), + mCaseSensitive(true) {} + + static const LanguageDefinition& CPlusPlus(); + static const LanguageDefinition& HLSL(); + static const LanguageDefinition& GLSL(); + static const LanguageDefinition& C(); + static const LanguageDefinition& SQL(); + static const LanguageDefinition& AngelScript(); + static const LanguageDefinition& Lua(); + }; + + TextEditor(); + ~TextEditor(); + + void SetLanguageDefinition(const LanguageDefinition& aLanguageDef); + const LanguageDefinition& GetLanguageDefinition() const { + return mLanguageDefinition; + } + + const Palette& GetPalette() const { return mPaletteBase; } + void SetPalette(const Palette& aValue); + + void SetErrorMarkers(const ErrorMarkers& aMarkers) { + mErrorMarkers = aMarkers; + } + void SetBreakpoints(const Breakpoints& aMarkers) { mBreakpoints = aMarkers; } + + void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), + bool aBorder = false); + void SetText(const std::string& aText); + std::string GetText() const; + + void SetTextLines(const std::vector& aLines); + std::vector GetTextLines() const; + + std::string GetSelectedText() const; + std::string GetCurrentLineText() const; + + int GetTotalLines() const { return (int)mLines.size(); } + bool IsOverwrite() const { return mOverwrite; } + + void SetReadOnly(bool aValue); + bool IsReadOnly() const { return mReadOnly; } + bool IsTextChanged() const { return mTextChanged; } + bool IsCursorPositionChanged() const { return mCursorPositionChanged; } + + bool IsColorizerEnabled() const { return mColorizerEnabled; } + void SetColorizerEnable(bool aValue); + + Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); } + void SetCursorPosition(const Coordinates& aPosition); + + inline void SetHandleMouseInputs(bool aValue) { mHandleMouseInputs = aValue; } + inline bool IsHandleMouseInputsEnabled() const { + return mHandleKeyboardInputs; + } + + inline void SetHandleKeyboardInputs(bool aValue) { + mHandleKeyboardInputs = aValue; + } + inline bool IsHandleKeyboardInputsEnabled() const { + return mHandleKeyboardInputs; + } + + inline void SetImGuiChildIgnored(bool aValue) { mIgnoreImGuiChild = aValue; } + inline bool IsImGuiChildIgnored() const { return mIgnoreImGuiChild; } + + inline void SetShowWhitespaces(bool aValue) { mShowWhitespaces = aValue; } + inline bool IsShowingWhitespaces() const { return mShowWhitespaces; } + + void SetTabSize(int aValue); + inline int GetTabSize() const { return mTabSize; } + + void InsertText(const std::string& aValue); + void InsertText(const char* aValue); + + void MoveUp(int aAmount = 1, bool aSelect = false); + void MoveDown(int aAmount = 1, bool aSelect = false); + void MoveLeft(int aAmount = 1, bool aSelect = false, bool aWordMode = false); + void MoveRight(int aAmount = 1, bool aSelect = false, bool aWordMode = false); + void MoveTop(bool aSelect = false); + void MoveBottom(bool aSelect = false); + void MoveHome(bool aSelect = false); + void MoveEnd(bool aSelect = false); + + void SetSelectionStart(const Coordinates& aPosition); + void SetSelectionEnd(const Coordinates& aPosition); + void SetSelection(const Coordinates& aStart, const Coordinates& aEnd, + SelectionMode aMode = SelectionMode::Normal); + void SelectWordUnderCursor(); + void SelectAll(); + bool HasSelection() const; + + void Copy(); + void Cut(); + void Paste(); + void Delete(); + + bool CanUndo() const; + bool CanRedo() const; + void Undo(int aSteps = 1); + void Redo(int aSteps = 1); + + static const Palette& GetDarkPalette(); + static const Palette& GetLightPalette(); + static const Palette& GetRetroBluePalette(); + + private: + typedef std::vector> RegexList; + + struct EditorState { + Coordinates mSelectionStart; + Coordinates mSelectionEnd; + Coordinates mCursorPosition; + }; + + class UndoRecord { + public: + UndoRecord() {} + ~UndoRecord() {} + + UndoRecord(const std::string& aAdded, + const TextEditor::Coordinates aAddedStart, + const TextEditor::Coordinates aAddedEnd, + + const std::string& aRemoved, + const TextEditor::Coordinates aRemovedStart, + const TextEditor::Coordinates aRemovedEnd, + + TextEditor::EditorState& aBefore, + TextEditor::EditorState& aAfter); + + void Undo(TextEditor* aEditor); + void Redo(TextEditor* aEditor); + + std::string mAdded; + Coordinates mAddedStart; + Coordinates mAddedEnd; + + std::string mRemoved; + Coordinates mRemovedStart; + Coordinates mRemovedEnd; + + EditorState mBefore; + EditorState mAfter; + }; + + typedef std::vector UndoBuffer; + + void ProcessInputs(); + void Colorize(int aFromLine = 0, int aCount = -1); + void ColorizeRange(int aFromLine = 0, int aToLine = 0); + void ColorizeInternal(); + float TextDistanceToLineStart(const Coordinates& aFrom) const; + void EnsureCursorVisible(); + int GetPageSize() const; + std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const; + Coordinates GetActualCursorCoordinates() const; + Coordinates SanitizeCoordinates(const Coordinates& aValue) const; + void Advance(Coordinates& aCoordinates) const; + void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd); + int InsertTextAt(Coordinates& aWhere, const char* aValue); + void AddUndo(UndoRecord& aValue); + Coordinates ScreenPosToCoordinates(const ImVec2& aPosition) const; + Coordinates FindWordStart(const Coordinates& aFrom) const; + Coordinates FindWordEnd(const Coordinates& aFrom) const; + Coordinates FindNextWord(const Coordinates& aFrom) const; + int GetCharacterIndex(const Coordinates& aCoordinates) const; + int GetCharacterColumn(int aLine, int aIndex) const; + int GetLineCharacterCount(int aLine) const; + int GetLineMaxColumn(int aLine) const; + bool IsOnWordBoundary(const Coordinates& aAt) const; + void RemoveLine(int aStart, int aEnd); + void RemoveLine(int aIndex); + Line& InsertLine(int aIndex); + void EnterCharacter(ImWchar aChar, bool aShift); + void Backspace(); + void DeleteSelection(); + std::string GetWordUnderCursor() const; + std::string GetWordAt(const Coordinates& aCoords) const; + ImU32 GetGlyphColor(const Glyph& aGlyph) const; + + void HandleKeyboardInputs(); + void HandleMouseInputs(); + void Render(); + + float mLineSpacing; + Lines mLines; + EditorState mState; + UndoBuffer mUndoBuffer; + int mUndoIndex; + + int mTabSize; + bool mOverwrite; + bool mReadOnly; + bool mWithinRender; + bool mScrollToCursor; + bool mScrollToTop; + bool mTextChanged; + bool mColorizerEnabled; + float mTextStart; // position (in pixels) where a code line starts relative + // to the left of the TextEditor. + int mLeftMargin; + bool mCursorPositionChanged; + int mColorRangeMin, mColorRangeMax; + SelectionMode mSelectionMode; + bool mHandleKeyboardInputs; + bool mHandleMouseInputs; + bool mIgnoreImGuiChild; + bool mShowWhitespaces; + + Palette mPaletteBase; + Palette mPalette; + LanguageDefinition mLanguageDefinition; + RegexList mRegexList; + + bool mCheckComments; + Breakpoints mBreakpoints; + ErrorMarkers mErrorMarkers; + ImVec2 mCharAdvance; + Coordinates mInteractiveStart, mInteractiveEnd; + std::string mLineBuffer; + uint64_t mStartTime; + + float mLastClick; +}; + +#endif // YAZE_APP_GUI_MODULES_TEXT_EDITOR_H \ No newline at end of file diff --git a/src/app/gui/style.cc b/src/app/gui/style.cc index 0726dff5..537caac3 100644 --- a/src/app/gui/style.cc +++ b/src/app/gui/style.cc @@ -1,12 +1,334 @@ #include "style.h" +#include "app/core/utils/file_util.h" +#include "gui/color.h" #include "imgui/imgui.h" #include "imgui/imgui_internal.h" namespace yaze { -namespace app { namespace gui { +namespace { +Color ParseColor(const std::string &color) { + Color result; + if (color.size() == 7 && color[0] == '#') { + result.red = std::stoi(color.substr(1, 2), nullptr, 16) / 255.0f; + result.green = std::stoi(color.substr(3, 2), nullptr, 16) / 255.0f; + result.blue = std::stoi(color.substr(5, 2), nullptr, 16) / 255.0f; + } else { + throw std::invalid_argument("Invalid color format: " + color); + } + return result; +} + +absl::Status ParseThemeContents(const std::string &key, + const std::string &value, Theme &theme) { + try { + if (key == "MenuBarBg") { + theme.menu_bar_bg = ParseColor(value); + } else if (key == "TitleBgActive") { + theme.title_bg_active = ParseColor(value); + } else if (key == "TitleBgCollapsed") { + theme.title_bg_collapsed = ParseColor(value); + } else if (key == "Tab") { + theme.tab = ParseColor(value); + } else if (key == "TabHovered") { + theme.tab_hovered = ParseColor(value); + } else if (key == "TabActive") { + theme.tab_active = ParseColor(value); + } + } catch (const std::exception &e) { + return absl::InvalidArgumentError(e.what()); + } + return absl::OkStatus(); +} + +} // namespace + +absl::StatusOr LoadTheme(const std::string &filename) { + std::string theme_contents; + try { + theme_contents = core::LoadFile(filename); + } catch (const std::exception &e) { + return absl::InternalError(e.what()); + } + + Theme theme; + std::istringstream theme_stream(theme_contents); + while (theme_stream.good()) { + std::string line; + std::getline(theme_stream, line); + if (line.empty()) { + continue; + } + + std::istringstream line_stream(line); + std::string key; + std::string value; + std::getline(line_stream, key, '='); + std::getline(line_stream, value); + RETURN_IF_ERROR(ParseThemeContents(key, value, theme)); + } + return theme; +} + +absl::Status SaveTheme(const Theme &theme) { + std::ostringstream theme_stream; + theme_stream << theme.name << "Theme\n"; + theme_stream << "MenuBarBg=#" << gui::ColorToHexString(theme.menu_bar_bg) + << "\n"; + theme_stream << "TitleBg=#" << gui::ColorToHexString(theme.title_bar_bg) << "\n"; + theme_stream << "Header=#" << gui::ColorToHexString(theme.header) << "\n"; + theme_stream << "HeaderHovered=#" << gui::ColorToHexString(theme.header_hovered) << "\n"; + theme_stream << "HeaderActive=#" << gui::ColorToHexString(theme.header_active) << "\n"; + theme_stream << "TitleBgActive=#" << gui::ColorToHexString(theme.title_bg_active) << "\n"; + theme_stream << "TitleBgCollapsed=#" << gui::ColorToHexString(theme.title_bg_collapsed) << "\n"; + theme_stream << "Tab=#" << gui::ColorToHexString(theme.tab) << "\n"; + theme_stream << "TabHovered=#" << gui::ColorToHexString(theme.tab_hovered) << "\n"; + theme_stream << "TabActive=#" << gui::ColorToHexString(theme.tab_active) << "\n"; + theme_stream << "Button=#" << gui::ColorToHexString(theme.button) << "\n"; + theme_stream << "ButtonHovered=#" << gui::ColorToHexString(theme.button_hovered) << "\n"; + theme_stream << "ButtonActive=#" << gui::ColorToHexString(theme.button_active) << "\n"; + + // Save the theme to a file. + + return absl::OkStatus(); +} + +void ApplyTheme(const Theme &theme) { + ImGuiStyle *style = &ImGui::GetStyle(); + ImVec4 *colors = style->Colors; + + colors[ImGuiCol_MenuBarBg] = gui::ConvertColorToImVec4(theme.menu_bar_bg); + colors[ImGuiCol_TitleBg] = gui::ConvertColorToImVec4(theme.title_bar_bg); + colors[ImGuiCol_Header] = gui::ConvertColorToImVec4(theme.header); + colors[ImGuiCol_HeaderHovered] = gui::ConvertColorToImVec4(theme.header_hovered); + colors[ImGuiCol_HeaderActive] = gui::ConvertColorToImVec4(theme.header_active); + colors[ImGuiCol_TitleBgActive] = gui::ConvertColorToImVec4(theme.title_bg_active); + colors[ImGuiCol_TitleBgCollapsed] = gui::ConvertColorToImVec4(theme.title_bg_collapsed); + colors[ImGuiCol_Tab] = gui::ConvertColorToImVec4(theme.tab); + colors[ImGuiCol_TabHovered] = gui::ConvertColorToImVec4(theme.tab_hovered); + colors[ImGuiCol_TabActive] = gui::ConvertColorToImVec4(theme.tab_active); + colors[ImGuiCol_Button] = gui::ConvertColorToImVec4(theme.button); + colors[ImGuiCol_ButtonHovered] = gui::ConvertColorToImVec4(theme.button_hovered); + colors[ImGuiCol_ButtonActive] = gui::ConvertColorToImVec4(theme.button_active); +} + +void ColorsYaze() { + ImGuiStyle *style = &ImGui::GetStyle(); + ImVec4 *colors = style->Colors; + + style->WindowPadding = ImVec2(10.f, 10.f); + style->FramePadding = ImVec2(10.f, 2.f); + style->CellPadding = ImVec2(4.f, 5.f); + style->ItemSpacing = ImVec2(10.f, 5.f); + style->ItemInnerSpacing = ImVec2(5.f, 5.f); + style->TouchExtraPadding = ImVec2(0.f, 0.f); + style->IndentSpacing = 20.f; + style->ScrollbarSize = 14.f; + style->GrabMinSize = 15.f; + + style->WindowBorderSize = 0.f; + style->ChildBorderSize = 1.f; + style->PopupBorderSize = 1.f; + style->FrameBorderSize = 0.f; + style->TabBorderSize = 0.f; + + style->WindowRounding = 0.f; + style->ChildRounding = 0.f; + style->FrameRounding = 5.f; + style->PopupRounding = 0.f; + style->ScrollbarRounding = 5.f; + + auto alttpDarkGreen = ImVec4(0.18f, 0.26f, 0.18f, 1.0f); + auto alttpMidGreen = ImVec4(0.28f, 0.36f, 0.28f, 1.0f); + auto allttpLightGreen = ImVec4(0.36f, 0.45f, 0.36f, 1.0f); + auto allttpLightestGreen = ImVec4(0.49f, 0.57f, 0.49f, 1.0f); + + colors[ImGuiCol_MenuBarBg] = alttpDarkGreen; + colors[ImGuiCol_TitleBg] = alttpMidGreen; + + colors[ImGuiCol_Header] = alttpDarkGreen; + colors[ImGuiCol_HeaderHovered] = allttpLightGreen; + colors[ImGuiCol_HeaderActive] = alttpMidGreen; + + colors[ImGuiCol_TitleBgActive] = alttpDarkGreen; + colors[ImGuiCol_TitleBgCollapsed] = alttpMidGreen; + + colors[ImGuiCol_Tab] = alttpDarkGreen; + colors[ImGuiCol_TabHovered] = alttpMidGreen; + colors[ImGuiCol_TabActive] = ImVec4(0.347f, 0.466f, 0.347f, 1.000f); + + colors[ImGuiCol_Button] = alttpMidGreen; + colors[ImGuiCol_ButtonHovered] = allttpLightestGreen; + colors[ImGuiCol_ButtonActive] = allttpLightGreen; + + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.36f, 0.45f, 0.36f, 0.60f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.36f, 0.45f, 0.36f, 0.30f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.36f, 0.45f, 0.36f, 0.40f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.36f, 0.45f, 0.36f, 0.60f); + + colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.85f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.11f, 0.11f, 0.14f, 0.92f); + colors[ImGuiCol_Border] = allttpLightGreen; + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + + colors[ImGuiCol_FrameBg] = ImVec4(0.43f, 0.43f, 0.43f, 0.39f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.28f, 0.36f, 0.28f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.28f, 0.36f, 0.28f, 0.69f); + + colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 0.50f); + colors[ImGuiCol_SliderGrab] = ImVec4(1.00f, 1.00f, 1.00f, 0.30f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.36f, 0.45f, 0.36f, 0.60f); + + colors[ImGuiCol_Separator] = ImVec4(0.50f, 0.50f, 0.50f, 0.60f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.60f, 0.60f, 0.70f, 1.00f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.70f, 0.70f, 0.90f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(1.00f, 1.00f, 1.00f, 0.10f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.78f, 0.82f, 1.00f, 0.60f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.78f, 0.82f, 1.00f, 0.90f); + + colors[ImGuiCol_TabUnfocused] = + ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabUnfocusedActive] = + ImLerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = alttpDarkGreen; + colors[ImGuiCol_TableBorderStrong] = alttpMidGreen; + colors[ImGuiCol_TableBorderLight] = + ImVec4(0.26f, 0.26f, 0.28f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.07f); + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.00f, 1.00f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); + colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); +} + +void DrawBitmapViewer(const std::vector &bitmaps, float scale, + int ¤t_bitmap_id) { + if (bitmaps.empty()) { + ImGui::Text("No bitmaps available."); + return; + } + + // Display the current bitmap index and total count. + ImGui::Text("Viewing Bitmap %d / %zu", current_bitmap_id + 1, bitmaps.size()); + + // Buttons to navigate through bitmaps. + if (ImGui::Button("<- Prev")) { + if (current_bitmap_id > 0) { + --current_bitmap_id; + } + } + ImGui::SameLine(); + if (ImGui::Button("Next ->")) { + if (current_bitmap_id < bitmaps.size() - 1) { + ++current_bitmap_id; + } + } + + // Display the current bitmap. + const gfx::Bitmap ¤t_bitmap = bitmaps[current_bitmap_id]; + // Assuming Bitmap has a function to get its texture ID, and width and + // height. + ImTextureID tex_id = (ImTextureID)(intptr_t)current_bitmap.texture(); + ImVec2 size(current_bitmap.width() * scale, current_bitmap.height() * scale); + // ImGui::Image(tex_id, size); + + // Scroll if the image is larger than the display area. + if (ImGui::BeginChild("BitmapScrollArea", ImVec2(0, 0), false, + ImGuiWindowFlags_HorizontalScrollbar)) { + ImGui::Image(tex_id, size); + ImGui::EndChild(); + } +} + +static const char *const kKeywords[] = { + "ADC", "AND", "ASL", "BCC", "BCS", "BEQ", "BIT", "BMI", "BNE", "BPL", + "BRA", "BRL", "BVC", "BVS", "CLC", "CLD", "CLI", "CLV", "CMP", "CPX", + "CPY", "DEC", "DEX", "DEY", "EOR", "INC", "INX", "INY", "JMP", "JSR", + "JSL", "LDA", "LDX", "LDY", "LSR", "MVN", "NOP", "ORA", "PEA", "PER", + "PHA", "PHB", "PHD", "PHP", "PHX", "PHY", "PLA", "PLB", "PLD", "PLP", + "PLX", "PLY", "REP", "ROL", "ROR", "RTI", "RTL", "RTS", "SBC", "SEC", + "SEI", "SEP", "STA", "STP", "STX", "STY", "STZ", "TAX", "TAY", "TCD", + "TCS", "TDC", "TRB", "TSB", "TSC", "TSX", "TXA", "TXS", "TXY", "TYA", + "TYX", "WAI", "WDM", "XBA", "XCE", "ORG", "LOROM", "HIROM"}; + +static const char *const kIdentifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", + "atof", "atoi", "atol", "ceil", "clock", "cosh", + "ctime", "div", "exit", "fabs", "floor", "fmod", + "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", + "log", "memcmp", "modf", "pow", "putchar", "putenv", + "puts", "rand", "remove", "rename", "sinh", "sqrt", + "srand", "strcat", "strcmp", "strerror", "time", "tolower", + "toupper"}; + +TextEditor::LanguageDefinition GetAssemblyLanguageDef() { + TextEditor::LanguageDefinition language_65816; + for (auto &k : kKeywords) + language_65816.mKeywords.emplace(k); + + for (auto &k : kIdentifiers) { + TextEditor::Identifier id; + id.mDeclaration = "Built-in function"; + language_65816.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + language_65816.mTokenRegexStrings.push_back( + std::make_pair( + "[ \\t]*#[ \\t]*[a-zA-Z_]+", TextEditor::PaletteIndex::Preprocessor)); + language_65816.mTokenRegexStrings.push_back( + std::make_pair( + "L?\\\"(\\\\.|[^\\\"])*\\\"", TextEditor::PaletteIndex::String)); + language_65816.mTokenRegexStrings.push_back( + std::make_pair( + "\\'\\\\?[^\\']\\'", TextEditor::PaletteIndex::CharLiteral)); + language_65816.mTokenRegexStrings.push_back( + std::make_pair( + "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", + TextEditor::PaletteIndex::Number)); + language_65816.mTokenRegexStrings.push_back( + std::make_pair( + "[+-]?[0-9]+[Uu]?[lL]?[lL]?", TextEditor::PaletteIndex::Number)); + language_65816.mTokenRegexStrings.push_back( + std::make_pair( + "0[0-7]+[Uu]?[lL]?[lL]?", TextEditor::PaletteIndex::Number)); + language_65816.mTokenRegexStrings.push_back( + std::make_pair( + "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", + TextEditor::PaletteIndex::Number)); + language_65816.mTokenRegexStrings.push_back( + std::make_pair( + "[a-zA-Z_][a-zA-Z0-9_]*", TextEditor::PaletteIndex::Identifier)); + language_65816.mTokenRegexStrings.push_back( + std::make_pair( + "[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/" + "\\;\\,\\.]", + TextEditor::PaletteIndex::Punctuation)); + + language_65816.mCommentStart = "/*"; + language_65816.mCommentEnd = "*/"; + language_65816.mSingleLineComment = ";"; + + language_65816.mCaseSensitive = false; + language_65816.mAutoIndentation = true; + + language_65816.mName = "65816"; + + return language_65816; +} + // TODO: Add more display settings to popup windows. void BeginWindowWithDisplaySettings(const char *id, bool *active, const ImVec2 &size, @@ -427,227 +749,5 @@ void TextWithSeparators(const absl::string_view &text) { ImGui::Separator(); } -// TODO: Make the ColorsYaze style into a configuration file. -void ColorsYaze() { - ImGuiStyle *style = &ImGui::GetStyle(); - ImVec4 *colors = style->Colors; - - style->WindowPadding = ImVec2(10.f, 10.f); - style->FramePadding = ImVec2(10.f, 2.f); - style->CellPadding = ImVec2(4.f, 5.f); - style->ItemSpacing = ImVec2(10.f, 5.f); - style->ItemInnerSpacing = ImVec2(5.f, 5.f); - style->TouchExtraPadding = ImVec2(0.f, 0.f); - style->IndentSpacing = 20.f; - style->ScrollbarSize = 14.f; - style->GrabMinSize = 15.f; - - style->WindowBorderSize = 0.f; - style->ChildBorderSize = 1.f; - style->PopupBorderSize = 1.f; - style->FrameBorderSize = 0.f; - style->TabBorderSize = 0.f; - - style->WindowRounding = 0.f; - style->ChildRounding = 0.f; - style->FrameRounding = 5.f; - style->PopupRounding = 0.f; - style->ScrollbarRounding = 5.f; - - auto alttpDarkGreen = ImVec4(0.18f, 0.26f, 0.18f, 1.0f); - auto alttpMidGreen = ImVec4(0.28f, 0.36f, 0.28f, 1.0f); - auto allttpLightGreen = ImVec4(0.36f, 0.45f, 0.36f, 1.0f); - auto allttpLightestGreen = ImVec4(0.49f, 0.57f, 0.49f, 1.0f); - - colors[ImGuiCol_MenuBarBg] = alttpDarkGreen; - colors[ImGuiCol_TitleBg] = alttpMidGreen; - - colors[ImGuiCol_Header] = alttpDarkGreen; - colors[ImGuiCol_HeaderHovered] = allttpLightGreen; - colors[ImGuiCol_HeaderActive] = alttpMidGreen; - - colors[ImGuiCol_TitleBgActive] = alttpDarkGreen; - colors[ImGuiCol_TitleBgCollapsed] = alttpMidGreen; - - colors[ImGuiCol_Tab] = alttpDarkGreen; - colors[ImGuiCol_TabHovered] = alttpMidGreen; - colors[ImGuiCol_TabActive] = ImVec4(0.347f, 0.466f, 0.347f, 1.000f); - - colors[ImGuiCol_Button] = alttpMidGreen; - colors[ImGuiCol_ButtonHovered] = allttpLightestGreen; - colors[ImGuiCol_ButtonActive] = allttpLightGreen; - - colors[ImGuiCol_ScrollbarBg] = ImVec4(0.36f, 0.45f, 0.36f, 0.60f); - colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.36f, 0.45f, 0.36f, 0.30f); - colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.36f, 0.45f, 0.36f, 0.40f); - colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.36f, 0.45f, 0.36f, 0.60f); - - colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); - colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); - colors[ImGuiCol_WindowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.85f); - colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_PopupBg] = ImVec4(0.11f, 0.11f, 0.14f, 0.92f); - colors[ImGuiCol_Border] = allttpLightGreen; - colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - - colors[ImGuiCol_FrameBg] = ImVec4(0.43f, 0.43f, 0.43f, 0.39f); - colors[ImGuiCol_FrameBgHovered] = ImVec4(0.28f, 0.36f, 0.28f, 0.40f); - colors[ImGuiCol_FrameBgActive] = ImVec4(0.28f, 0.36f, 0.28f, 0.69f); - - colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 0.50f); - colors[ImGuiCol_SliderGrab] = ImVec4(1.00f, 1.00f, 1.00f, 0.30f); - colors[ImGuiCol_SliderGrabActive] = ImVec4(0.36f, 0.45f, 0.36f, 0.60f); - - colors[ImGuiCol_Separator] = ImVec4(0.50f, 0.50f, 0.50f, 0.60f); - colors[ImGuiCol_SeparatorHovered] = ImVec4(0.60f, 0.60f, 0.70f, 1.00f); - colors[ImGuiCol_SeparatorActive] = ImVec4(0.70f, 0.70f, 0.90f, 1.00f); - colors[ImGuiCol_ResizeGrip] = ImVec4(1.00f, 1.00f, 1.00f, 0.10f); - colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.78f, 0.82f, 1.00f, 0.60f); - colors[ImGuiCol_ResizeGripActive] = ImVec4(0.78f, 0.82f, 1.00f, 0.90f); - - colors[ImGuiCol_TabUnfocused] = - ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); - colors[ImGuiCol_TabUnfocusedActive] = - ImLerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); - colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); - colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); - colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); - colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); - colors[ImGuiCol_TableHeaderBg] = alttpDarkGreen; - colors[ImGuiCol_TableBorderStrong] = alttpMidGreen; - colors[ImGuiCol_TableBorderLight] = - ImVec4(0.26f, 0.26f, 0.28f, 1.00f); // Prefer using Alpha=1.0 here - colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.07f); - colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.00f, 1.00f, 0.35f); - colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); - colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; - colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); - colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); - colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); -} - -void DrawBitmapViewer(const std::vector &bitmaps, float scale, - int ¤t_bitmap_id) { - if (bitmaps.empty()) { - ImGui::Text("No bitmaps available."); - return; - } - - // Display the current bitmap index and total count. - ImGui::Text("Viewing Bitmap %d / %zu", current_bitmap_id + 1, bitmaps.size()); - - // Buttons to navigate through bitmaps. - if (ImGui::Button("<- Prev")) { - if (current_bitmap_id > 0) { - --current_bitmap_id; - } - } - ImGui::SameLine(); - if (ImGui::Button("Next ->")) { - if (current_bitmap_id < bitmaps.size() - 1) { - ++current_bitmap_id; - } - } - - // Display the current bitmap. - const gfx::Bitmap ¤t_bitmap = bitmaps[current_bitmap_id]; - // Assuming Bitmap has a function to get its texture ID, and width and - // height. - ImTextureID tex_id = (ImTextureID)(intptr_t)current_bitmap.texture(); - ImVec2 size(current_bitmap.width() * scale, current_bitmap.height() * scale); - // ImGui::Image(tex_id, size); - - // Scroll if the image is larger than the display area. - if (ImGui::BeginChild("BitmapScrollArea", ImVec2(0, 0), false, - ImGuiWindowFlags_HorizontalScrollbar)) { - ImGui::Image(tex_id, size); - ImGui::EndChild(); - } -} - -// ============================================================================ -// 65816 LanguageDefinition -// ============================================================================ - -static const char *const kKeywords[] = { - "ADC", "AND", "ASL", "BCC", "BCS", "BEQ", "BIT", "BMI", "BNE", - "BPL", "BRA", "BRL", "BVC", "BVS", "CLC", "CLD", "CLI", "CLV", - "CMP", "CPX", "CPY", "DEC", "DEX", "DEY", "EOR", "INC", "INX", - "INY", "JMP", "JSR", "JSL", "LDA", "LDX", "LDY", "LSR", "MVN", - "NOP", "ORA", "PEA", "PER", "PHA", "PHB", "PHD", "PHP", "PHX", - "PHY", "PLA", "PLB", "PLD", "PLP", "PLX", "PLY", "REP", "ROL", - "ROR", "RTI", "RTL", "RTS", "SBC", "SEC", "SEI", "SEP", "STA", - "STP", "STX", "STY", "STZ", "TAX", "TAY", "TCD", "TCS", "TDC", - "TRB", "TSB", "TSC", "TSX", "TXA", "TXS", "TXY", "TYA", "TYX", - "WAI", "WDM", "XBA", "XCE", "ORG", "LOROM", "HIROM", "NAMESPACE", "DB"}; - -static const char *const kIdentifiers[] = { - "abort", "abs", "acos", "asin", "atan", "atexit", - "atof", "atoi", "atol", "ceil", "clock", "cosh", - "ctime", "div", "exit", "fabs", "floor", "fmod", - "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", - "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", - "log", "memcmp", "modf", "pow", "putchar", "putenv", - "puts", "rand", "remove", "rename", "sinh", "sqrt", - "srand", "strcat", "strcmp", "strerror", "time", "tolower", - "toupper"}; - -TextEditor::LanguageDefinition GetAssemblyLanguageDef() { - TextEditor::LanguageDefinition language_65816; - for (auto &k : kKeywords) - language_65816.mKeywords.emplace(k); - - for (auto &k : kIdentifiers) { - TextEditor::Identifier id; - id.mDeclaration = "Built-in function"; - language_65816.mIdentifiers.insert(std::make_pair(std::string(k), id)); - } - - language_65816.mTokenRegexStrings.push_back( - std::make_pair( - "[ \\t]*#[ \\t]*[a-zA-Z_]+", TextEditor::PaletteIndex::Preprocessor)); - language_65816.mTokenRegexStrings.push_back( - std::make_pair( - "L?\\\"(\\\\.|[^\\\"])*\\\"", TextEditor::PaletteIndex::String)); - language_65816.mTokenRegexStrings.push_back( - std::make_pair( - "\\'\\\\?[^\\']\\'", TextEditor::PaletteIndex::CharLiteral)); - language_65816.mTokenRegexStrings.push_back( - std::make_pair( - "[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", - TextEditor::PaletteIndex::Number)); - language_65816.mTokenRegexStrings.push_back( - std::make_pair( - "[+-]?[0-9]+[Uu]?[lL]?[lL]?", TextEditor::PaletteIndex::Number)); - language_65816.mTokenRegexStrings.push_back( - std::make_pair( - "0[0-7]+[Uu]?[lL]?[lL]?", TextEditor::PaletteIndex::Number)); - language_65816.mTokenRegexStrings.push_back( - std::make_pair( - "0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", - TextEditor::PaletteIndex::Number)); - language_65816.mTokenRegexStrings.push_back( - std::make_pair( - "[a-zA-Z_][a-zA-Z0-9_]*", TextEditor::PaletteIndex::Identifier)); - language_65816.mTokenRegexStrings.push_back( - std::make_pair( - "[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/" - "\\;\\,\\.]", - TextEditor::PaletteIndex::Punctuation)); - - language_65816.mCommentStart = "/*"; - language_65816.mCommentEnd = "*/"; - language_65816.mSingleLineComment = ";"; - - language_65816.mCaseSensitive = false; - language_65816.mAutoIndentation = true; - - language_65816.mName = "65816"; - - return language_65816; -} - } // namespace gui -} // namespace app } // namespace yaze diff --git a/src/app/gui/style.h b/src/app/gui/style.h index 051a1f88..d21a7144 100644 --- a/src/app/gui/style.h +++ b/src/app/gui/style.h @@ -1,21 +1,51 @@ #ifndef YAZE_APP_CORE_STYLE_H #define YAZE_APP_CORE_STYLE_H -#include +#include #include -#include "ImGuiColorTextEdit/TextEditor.h" -#include "absl/status/status.h" #include "absl/strings/string_view.h" -#include "app/core/constants.h" #include "app/gfx/bitmap.h" +#include "app/gui/color.h" +#include "app/gui/modules/text_editor.h" #include "imgui/imgui.h" -#include "imgui/misc/cpp/imgui_stdlib.h" namespace yaze { -namespace app { namespace gui { +struct Theme { + std::string name; + + Color menu_bar_bg; + Color title_bar_bg; + + Color header; + Color header_hovered; + Color header_active; + + Color title_bg_active; + Color title_bg_collapsed; + + Color tab; + Color tab_hovered; + Color tab_active; + + Color button; + Color button_hovered; + Color button_active; +}; + +absl::StatusOr LoadTheme(const std::string &filename); +absl::Status SaveTheme(const Theme &theme); +void ApplyTheme(const Theme &theme); + +void ColorsYaze(); + +TextEditor::LanguageDefinition GetAssemblyLanguageDef(); + +void DrawBitmapViewer(const std::vector &bitmaps, float scale, + int ¤t_bitmap); + void BeginWindowWithDisplaySettings(const char *id, bool *active, const ImVec2 &size = ImVec2(0, 0), ImGuiWindowFlags flags = 0); @@ -36,15 +66,6 @@ void DrawDisplaySettings(ImGuiStyle *ref = nullptr); void TextWithSeparators(const absl::string_view &text); -void ColorsYaze(); - -TextEditor::LanguageDefinition GetAssemblyLanguageDef(); - -void DrawBitmapViewer(const std::vector &bitmaps, float scale, - int ¤t_bitmap); - -// ============================================================================ - static const char *ExampleNames[] = { "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", @@ -75,7 +96,7 @@ struct MultiSelectWithClipper { clipper.Begin(ITEMS_COUNT); if (ms_io->RangeSrcItem != -1) clipper.IncludeItemByIndex( - (int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped. + (int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped. while (clipper.Step()) { for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) { char label[64]; @@ -95,8 +116,7 @@ struct MultiSelectWithClipper { } }; -} // namespace gui -} // namespace app -} // namespace yaze +} // namespace gui +} // namespace yaze #endif diff --git a/src/app/gui/zeml.cc b/src/app/gui/zeml.cc index 5e94e097..a83210a4 100644 --- a/src/app/gui/zeml.cc +++ b/src/app/gui/zeml.cc @@ -15,7 +15,6 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { namespace gui { namespace zeml { @@ -618,5 +617,5 @@ std::string LoadFile(const std::string& filename) { } // namespace zeml } // namespace gui -} // namespace app + } // namespace yaze diff --git a/src/app/gui/zeml.h b/src/app/gui/zeml.h index 2ba4f594..1ddbaac1 100644 --- a/src/app/gui/zeml.h +++ b/src/app/gui/zeml.h @@ -11,11 +11,10 @@ #include namespace yaze { -namespace app { namespace gui { /** - * @namespace yaze::app::gui::zeml + * @namespace yaze::gui::zeml * @brief Zelda Editor Markup Language Functions */ namespace zeml { @@ -207,7 +206,7 @@ std::string LoadFile(const std::string& filename); } // namespace zeml } // namespace gui -} // namespace app + } // namespace yaze #endif // YAZE_APP_GUI_YAZON_H_ diff --git a/src/app/main.cc b/src/app/main.cc index 80f827f5..b5980ff7 100644 --- a/src/app/main.cc +++ b/src/app/main.cc @@ -1,6 +1,4 @@ -#if defined(_WIN32) -#define main SDL_main -#elif __APPLE__ +#if __APPLE__ #include "app/core/platform/app_delegate.h" #endif @@ -9,14 +7,11 @@ #include "app/core/controller.h" /** - * @namespace yaze::app - * @brief Main namespace for the ImGui application. + * @namespace yaze + * @brief Main namespace for the application. */ -using namespace yaze::app; +using namespace yaze; -/** - * @brief Main entry point for the application. - */ int main(int argc, char** argv) { absl::InitializeSymbolizer(argv[0]); @@ -35,20 +30,23 @@ int main(int argc, char** argv) { #ifdef __APPLE__ yaze_run_cocoa_app_delegate(rom_filename.c_str()); return EXIT_SUCCESS; +#elif defined(_WIN32) + // We set SDL_MAIN_HANDLED for Win32 to avoid SDL hijacking main() + SDL_SetMainReady(); #endif - core::Controller controller; - EXIT_IF_ERROR(controller.OnEntry(rom_filename)) + auto controller = std::make_unique(); + EXIT_IF_ERROR(controller->OnEntry(rom_filename)) - while (controller.IsActive()) { - controller.OnInput(); - if (auto status = controller.OnLoad(); !status.ok()) { + while (controller->IsActive()) { + controller->OnInput(); + if (auto status = controller->OnLoad(); !status.ok()) { std::cerr << status.message() << std::endl; break; } - controller.DoRender(); + controller->DoRender(); } - controller.OnExit(); + controller->OnExit(); return EXIT_SUCCESS; } diff --git a/src/app/rom.cc b/src/app/rom.cc index 220b65ce..3ffcaa05 100644 --- a/src/app/rom.cc +++ b/src/app/rom.cc @@ -23,8 +23,6 @@ #include "app/gfx/snes_tile.h" namespace yaze { -namespace app { - using core::Renderer; constexpr int Uncompressed3BPPSize = 0x0600; @@ -36,17 +34,17 @@ int GetGraphicsAddress(const uchar *data, uint8_t addr, uint32_t ptr1, } } // namespace -absl::StatusOr> Rom::Load2BppGraphics() { +absl::StatusOr> Load2BppGraphics(const Rom &rom) { std::vector sheet; const uint8_t sheets[] = {113, 114, 218, 219, 220, 221}; for (const auto &sheet_id : sheets) { - auto offset = GetGraphicsAddress(data(), sheet_id, - version_constants().kOverworldGfxPtr1, - version_constants().kOverworldGfxPtr2, - version_constants().kOverworldGfxPtr3); + auto offset = GetGraphicsAddress(rom.data(), sheet_id, + rom.version_constants().kOverworldGfxPtr1, + rom.version_constants().kOverworldGfxPtr2, + rom.version_constants().kOverworldGfxPtr3); ASSIGN_OR_RETURN(auto decomp_sheet, - gfx::lc_lz2::DecompressV2(data(), offset)) + gfx::lc_lz2::DecompressV2(rom.data(), offset)) auto converted_sheet = gfx::SnesTo8bppSheet(decomp_sheet, 2); for (const auto &each_pixel : converted_sheet) { sheet.push_back(each_pixel); @@ -136,23 +134,27 @@ absl::Status Rom::LoadAllGraphicsData(bool defer_render) { absl::Status Rom::SaveAllGraphicsData() { for (int i = 0; i < kNumGfxSheets; i++) { if (graphics_sheets_[i].is_active()) { - int from_bpp = 8; int to_bpp = 3; std::vector final_data; bool compressed = true; if (i >= 115 && i <= 126) { - to_bpp = 3; compressed = false; } else if (i == 113 || i == 114 || i >= 218) { to_bpp = 2; + continue; } + std::cout << "Sheet ID " << i << " BPP: " << to_bpp << std::endl; auto sheet_data = graphics_sheets_[i].vector(); - final_data = gfx::ConvertBpp(sheet_data, from_bpp, to_bpp); + std::cout << "Sheet data size: " << sheet_data.size() << std::endl; + final_data = gfx::Bpp8SnesToIndexed(sheet_data, 8); + int size = 0; if (compressed) { - ASSIGN_OR_RETURN( - final_data, - gfx::lc_lz2::CompressV2(final_data.data(), 0, final_data.size())); + auto compressed_data = gfx::HyruleMagicCompress( + final_data.data(), final_data.size(), &size, 1); + for (int j = 0; j < size; j++) { + sheet_data[j] = compressed_data[j]; + } } auto offset = GetGraphicsAddress(data(), i, version_constants().kOverworldGfxPtr1, @@ -242,6 +244,7 @@ absl::Status Rom::LoadZelda3() { // Copy ROM title constexpr uint32_t kTitleStringOffset = 0x7FC0; constexpr uint32_t kTitleStringLength = 20; + title_.resize(kTitleStringLength); std::copy(rom_data_.begin() + kTitleStringOffset, rom_data_.begin() + kTitleStringOffset + kTitleStringLength, title_.begin()); @@ -312,9 +315,12 @@ absl::Status Rom::SaveToFile(bool backup, bool save_new, std::string filename) { } // Run the other save functions - if (flags()->kSaveAllPalettes) RETURN_IF_ERROR(SaveAllPalettes()); - if (flags()->kSaveGfxGroups) RETURN_IF_ERROR(SaveGroupsToRom()); - if (flags()->kSaveGraphicsSheet) RETURN_IF_ERROR(SaveAllGraphicsData()); + if (core::ExperimentFlags::get().kSaveAllPalettes) + RETURN_IF_ERROR(SaveAllPalettes()); + if (core::ExperimentFlags::get().kSaveGfxGroups) + RETURN_IF_ERROR(SaveGroupsToRom()); + if (core::ExperimentFlags::get().kSaveGraphicsSheet) + RETURN_IF_ERROR(SaveAllGraphicsData()); if (save_new) { // Create a file of the same name and append the date between the filename @@ -464,5 +470,4 @@ absl::Status Rom::SaveGroupsToRom() { std::shared_ptr SharedRom::shared_rom_ = nullptr; -} // namespace app } // namespace yaze diff --git a/src/app/rom.h b/src/app/rom.h index c99ebd92..ebf7850c 100644 --- a/src/app/rom.h +++ b/src/app/rom.h @@ -27,8 +27,6 @@ #include "app/gfx/snes_tile.h" namespace yaze { -namespace app { - /** * @brief Different versions of the game supported by the Rom class. */ @@ -48,10 +46,10 @@ struct VersionConstants { uint32_t kOverworldGfxGroups2; uint32_t kCompressedAllMap32PointersHigh; uint32_t kCompressedAllMap32PointersLow; - uint32_t overworldMapPaletteGroup; - uint32_t overlayPointers; - uint32_t overlayPointersBank; - uint32_t overworldTilesType; + uint32_t kOverworldMapPaletteGroup; + uint32_t kOverlayPointers; + uint32_t kOverlayPointersBank; + uint32_t kOverworldTilesType; uint32_t kOverworldGfxPtr1; uint32_t kOverworldGfxPtr2; uint32_t kOverworldGfxPtr3; @@ -74,10 +72,10 @@ static const std::map kVersionConstantsMap = { 0x6073, // kOverworldGfxGroups2 0x1794D, // kCompressedAllMap32PointersHigh 0x17B2D, // kCompressedAllMap32PointersLow - 0x75504, // overworldMapPaletteGroup - 0x77664, // overlayPointers - 0x0E, // overlayPointersBank - 0x71459, // overworldTilesType + 0x75504, // kOverworldMapPaletteGroup + 0x77664, // kOverlayPointers + 0x0E, // kOverlayPointersBank + 0x71459, // kOverworldTilesType 0x4F80, // kOverworldGfxPtr1 0x505F, // kOverworldGfxPtr2 0x513E, // kOverworldGfxPtr3 @@ -95,10 +93,10 @@ static const std::map kVersionConstantsMap = { 0x60B3, // kOverworldGfxGroups2 0x176B1, // kCompressedAllMap32PointersHigh 0x17891, // kCompressedAllMap32PointersLow - 0x67E74, // overworldMapPaletteGroup - 0x3FAF4, // overlayPointers - 0x07, // overlayPointersBank - 0x7FD94, // overworldTilesType + 0x67E74, // kOverworldMapPaletteGroup + 0x3FAF4, // kOverlayPointers + 0x07, // kOverlayPointersBank + 0x7FD94, // kOverworldTilesType 0x4FC0, // kOverworldGfxPtr1 0x509F, // kOverworldGfxPtr2 0x517E, // kOverworldGfxPtr3 @@ -133,18 +131,8 @@ constexpr uint32_t kMaxGraphics = 0xC3FB5; /** * @brief The Rom class is used to load, save, and modify Rom data. */ -class Rom : public core::ExperimentFlags { +class Rom { public: - /** - * @brief Loads 2bpp graphics from Rom data. - * - * This function loads 2bpp graphics from Rom data by iterating over a list of - * sheet IDs, decompressing the sheet data, converting it to 8bpp format, and - * appending the converted sheet data to a byte vector. - * - */ - absl::StatusOr> Load2BppGraphics(); - /** * @brief Loads the players 4bpp graphics sheet from Rom data. */ @@ -340,9 +328,7 @@ class Rom : public core::ExperimentFlags { value, addr)); } rom_data_[addr] = value; - std::string log_str = absl::StrFormat("WriteByte: %#06X: %s", addr, - core::UppercaseHexByte(value).data()); - core::Logger::log(log_str); + core::logf("WriteByte: %#06X: %s", addr, core::HexByte(value).data()); return absl::OkStatus(); } @@ -355,8 +341,7 @@ class Rom : public core::ExperimentFlags { } rom_data_[addr] = (uint8_t)(value & 0xFF); rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF); - core::Logger::log(absl::StrFormat("WriteWord: %#06X: %s", addr, - core::UppercaseHexWord(value))); + core::logf("WriteWord: %#06X: %s", addr, core::HexWord(value).data()); return absl::OkStatus(); } @@ -369,8 +354,7 @@ class Rom : public core::ExperimentFlags { } rom_data_[addr] = (uint8_t)(value & 0xFF); rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF); - core::Logger::log(absl::StrFormat("WriteShort: %#06X: %s", addr, - core::UppercaseHexWord(value))); + core::logf("WriteShort: %#06X: %s", addr, core::HexWord(value).data()); return absl::OkStatus(); } @@ -384,8 +368,7 @@ class Rom : public core::ExperimentFlags { rom_data_[addr] = (uint8_t)(value & 0xFF); rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF); rom_data_[addr + 2] = (uint8_t)((value >> 16) & 0xFF); - core::Logger::log(absl::StrFormat("WriteLong: %#06X: %s", addr, - core::UppercaseHexLong(value))); + core::logf("WriteLong: %#06X: %s", addr, core::HexLong(value).data()); return absl::OkStatus(); } @@ -399,8 +382,7 @@ class Rom : public core::ExperimentFlags { for (int i = 0; i < static_cast(data.size()); i++) { rom_data_[addr + i] = data[i]; } - core::Logger::log(absl::StrFormat("WriteVector: %#06X: %s", addr, - core::UppercaseHexByte(data[0]))); + core::logf("WriteVector: %#06X: %s", addr, core::HexByte(data[0]).data()); return absl::OkStatus(); } @@ -409,8 +391,7 @@ class Rom : public core::ExperimentFlags { ((color.snes() & 0x1F) << 10) | (color.snes() & 0x7C00); // Write the 16-bit color value to the ROM at the specified address - core::Logger::log(absl::StrFormat("WriteColor: %#06X: %s", address, - core::UppercaseHexWord(bgr))); + core::logf("WriteColor: %#06X: %s", address, core::HexWord(bgr).data()); return WriteShort(address, bgr); } @@ -458,9 +439,12 @@ class Rom : public core::ExperimentFlags { auto title() const { return title_; } auto size() const { return size_; } + auto data() const { return rom_data_.data(); } + auto mutable_data() { return rom_data_.data(); } + auto begin() { return rom_data_.begin(); } auto end() { return rom_data_.end(); } - auto data() { return rom_data_.data(); } + auto vector() const { return rom_data_; } auto version() const { return version_; } auto filename() const { return filename_; } @@ -570,6 +554,16 @@ class Rom : public core::ExperimentFlags { Z3_Version version_ = Z3_Version::US; }; +/** + * @brief Loads 2bpp graphics from Rom data. + * + * This function loads 2bpp graphics from Rom data by iterating over a list of + * sheet IDs, decompressing the sheet data, converting it to 8bpp format, and + * appending the converted sheet data to a byte vector. + * + */ +absl::StatusOr> Load2BppGraphics(const Rom& rom); + /** * @brief A class to hold a shared pointer to a Rom object. */ @@ -597,7 +591,6 @@ class SharedRom { static std::shared_ptr shared_rom_; }; -} // namespace app } // namespace yaze #endif diff --git a/src/app/zelda3/common.h b/src/app/zelda3/common.h index 152d9369..34fd8721 100644 --- a/src/app/zelda3/common.h +++ b/src/app/zelda3/common.h @@ -1,33 +1,18 @@ #ifndef YAZE_APP_ZELDA3_COMMON_H #define YAZE_APP_ZELDA3_COMMON_H -#include #include #include +#include namespace yaze { -namespace app { + /** - * @namespace yaze::app::zelda3 + * @namespace yaze::zelda3 * @brief Zelda 3 specific classes and functions. */ namespace zelda3 { -/** - * @brief Represents tile32 data for the overworld. - */ -using OWBlockset = std::vector>; - -/** - * @brief Overworld map tile32 data. - */ -struct OWMapTiles { - OWBlockset light_world; // 64 maps - OWBlockset dark_world; // 64 maps - OWBlockset special_world; // 32 maps -}; -using OWMapTiles = struct OWMapTiles; - /** * @class GameEntity * @brief Base class for all overworld and dungeon entities. @@ -60,6 +45,141 @@ class GameEntity { virtual void UpdateMapProperties(uint16_t map_id) = 0; }; +constexpr std::string_view kEntranceNames[] = { + "Link's House Intro", + "Link's House Post-intro", + "Sanctuary", + "Hyrule Castle West", + "Hyrule Castle Central", + "Hyrule Castle East", + "Death Mountain Express (Lower)", + "Death Mountain Express (Upper)", + "Eastern Palace", + "Desert Palace Central", + "Desert Palace East", + "Desert Palace West", + "Desert Palace Boss Lair", + "Kakariko Elder's House West", + "Kakariko Elder's House East", + "Kakariko Angry Bros West", + "Kakariko Angry Bros East", + "Mad Batter Lair", + "Under Lumberjacks' Weird Tree", + "Death Mountain Maze 0000", + "Death Mountain Maze 0001", + "Turtle Rock Mountainface 1", + "Death Mountain Cape Heart Piece Cave (Lower)", + "Death Mountain Cape Heart Piece Cave (Upper)", + "Turtle Rock Mountainface 2", + "Turtle Rock Mountainface 3", + "Death Mountain Maze 0002", + "Death Mountain Maze 0003", + "Death Mountain Maze 0004", + "Death Mountain Maze 0005", + "Death Mountain Maze 0006", + "Death Mountain Maze 0007", + "Death Mountain Maze 0008", + "Spectacle Rock Maze 1", + "Spectacle Rock Maze 2", + "Spectacle Rock Maze 3", + "Hyrule Castle Tower", + "Swamp Palace", + "Palace of Darkness", + "Misery Mire", + "Skull Woods 1", + "Skull Woods 2", + "Skull Woods Big Chest", + "Skull Woods Boss Lair", + "Lost Woods Thieves' Lair", + "Ice Palace", + "Death Mountain Escape West", + "Death Mountain Escape East", + "Death Mountain Elder's Cave (Lower)", + "Death Mountain Elder's Cave (Upper)", + "Hyrule Castle Secret Cellar", + "Tower of Hera", + "Thieves's Town", + "Turtle Rock Main", + "Ganon's Pyramid Sanctum (Lower)", + "Ganon's Tower", + "Fairy Cave 1", + "Kakariko Western Well", + "Death Mountain Maze 0009", + "Death Mountain Maze 0010", + "Treasure Shell Game 1", + "Storyteller Cave 1", + "Snitch House 1", + "Snitch House 2", + "SickBoy House", + "Byrna Gauntlet", + "Kakariko Pub South", + "Kakariko Pub North", + "Kakariko Inn", + "Sahasrahlah's Disco Infernum", + "Kakariko's Lame Shop", + "Village of Outcasts Chest Game", + "Village of Outcasts Orphanage", + "Kakariko Library", + "Kakariko Storage Shed", + "Kakariko Sweeper Lady's House", + "Potion Shop", + "Aginah's Desert Cottage", + "Watergate", + "Death Mountain Maze 0011", + "Fairy Cave 2", + "Refill Cave 0001", + "Refill Cave 0002", + "The Bomb \"Shop\"", + "Village of Outcasts Retirement Center", + "Fairy Cave 3", + "Good Bee Cave", + "General Store 1", + "General Store 2", + "Archery Game", + "Storyteller Cave 2", + "Hall of the Invisibility Cape", + "Pond of Wishing", + "Pond of Happiness", + "Fairy Cave 4", + "Swamp of Evil Heart Piece Hall", + "General Store 3", + "Blind's Old Hideout", + "Storyteller Cave 3", + "Warped Pond of Wishing", + "Chez Smithies", + "Fortune Teller 1", + "Fortune Teller 2", + "Chest Shell Game 2", + "Storyteller Cave 4", + "Storyteller Cave 5", + "Storyteller Cave 6", + "Village House 1", + "Thief Hideout 1", + "Thief Hideout 2", + "Heart Piece Cave 1", + "Thief Hideout 3", + "Refill Cave 3", + "Fairy Cave 5", + "Heart Piece Cave 2", + "Hyrule Castle Prison", + "Hyrule Castle Throne Room", + "Hyrule Tower Agahnim's Sanctum", + "Skull Woods 3 (Drop In)", + "Skull Woods 4 (Drop In)", + "Skull Woods 5 (Drop In)", + "Skull Woods 6 (Drop In)", + "Lost Woods Thieves' Hideout (Drop In)", + "Ganon's Pyramid Sanctum (Upper)", + "Fairy Cave 6 (Drop In)", + "Hyrule Castle Secret Cellar (Drop In)", + "Mad Batter Lair (Drop In)", + "Under Lumberjacks' Weird Tree (Drop In)", + "Kakariko Western Well (Drop In)", + "Hyrule Sewers Goodies Room (Drop In)", + "Chris Houlihan Room (Drop In)", + "Heart Piece Cave 3 (Drop In)", + "Ice Rod Cave"}; + static const std::string TileTypeNames[] = { "$00 Nothing (standard floor)", "$01 Collision", @@ -319,7 +439,6 @@ static const std::string TileTypeNames[] = { "$FF Door X top? (unused?)"}; } // namespace zelda3 -} // namespace app } // namespace yaze #endif // YAZE_APP_ZELDA3_COMMON_H diff --git a/src/app/zelda3/dungeon/object_names.h b/src/app/zelda3/dungeon/object_names.h index cc24aeea..d7756546 100644 --- a/src/app/zelda3/dungeon/object_names.h +++ b/src/app/zelda3/dungeon/object_names.h @@ -4,9 +4,8 @@ #include "absl/strings/string_view.h" namespace yaze { -namespace app { namespace zelda3 { -namespace dungeon { + constexpr static inline absl::string_view Type1RoomObjectNames[] = { "Ceiling ↔", @@ -457,9 +456,10 @@ constexpr static inline absl::string_view Type3RoomObjectNames[] = { "Nothing", }; -} // namespace dungeon + + } // namespace zelda3 -} // namespace app + } // namespace yaze #endif // YAZE_APP_ZELDA3_DUNGEON_OBJECT_NAMES_H \ No newline at end of file diff --git a/src/app/zelda3/dungeon/object_renderer.cc b/src/app/zelda3/dungeon/object_renderer.cc index fd88b6af..7cf77ba2 100644 --- a/src/app/zelda3/dungeon/object_renderer.cc +++ b/src/app/zelda3/dungeon/object_renderer.cc @@ -1,9 +1,8 @@ #include "app/zelda3/dungeon/object_renderer.h" namespace yaze { -namespace app { namespace zelda3 { -namespace dungeon { + void DungeonObjectRenderer::LoadObject(uint32_t routine_ptr, std::array& sheet_ids) { @@ -112,7 +111,8 @@ void DungeonObjectRenderer::UpdateObjectBitmap() { bitmap_.Create(256, 256, 8, tilemap_); } -} // namespace dungeon + + } // namespace zelda3 -} // namespace app + } // namespace yaze diff --git a/src/app/zelda3/dungeon/object_renderer.h b/src/app/zelda3/dungeon/object_renderer.h index ef74c3c5..c83990d5 100644 --- a/src/app/zelda3/dungeon/object_renderer.h +++ b/src/app/zelda3/dungeon/object_renderer.h @@ -14,9 +14,7 @@ #include "app/zelda3/dungeon/object_names.h" namespace yaze { -namespace app { namespace zelda3 { -namespace dungeon { struct PseudoVram { std::array sheets; @@ -43,15 +41,13 @@ class DungeonObjectRenderer : public SharedRom { PseudoVram vram_; emu::ClockImpl clock_; - emu::memory::MemoryImpl memory_; - emu::memory::CpuCallbacks cpu_callbacks_; - emu::video::Ppu ppu{memory_, clock_}; + emu::MemoryImpl memory_; + emu::CpuCallbacks cpu_callbacks_; + emu::Ppu ppu{memory_, clock_}; emu::Cpu cpu{memory_, clock_, cpu_callbacks_}; gfx::Bitmap bitmap_; }; -} // namespace dungeon } // namespace zelda3 -} // namespace app } // namespace yaze \ No newline at end of file diff --git a/src/app/zelda3/dungeon/room.cc b/src/app/zelda3/dungeon/room.cc index 71a2f92b..aeadfbd8 100644 --- a/src/app/zelda3/dungeon/room.cc +++ b/src/app/zelda3/dungeon/room.cc @@ -16,9 +16,8 @@ #include "app/zelda3/sprite/sprite.h" namespace yaze { -namespace app { namespace zelda3 { -namespace dungeon { + void Room::LoadHeader() { // Address of the room header @@ -486,7 +485,8 @@ void Room::LoadChests() { } } -} // namespace dungeon + + } // namespace zelda3 -} // namespace app + } // namespace yaze diff --git a/src/app/zelda3/dungeon/room.h b/src/app/zelda3/dungeon/room.h index 533aa57d..33e884a1 100644 --- a/src/app/zelda3/dungeon/room.h +++ b/src/app/zelda3/dungeon/room.h @@ -4,24 +4,17 @@ #include #include -#include -#include #include #include #include "app/core/constants.h" #include "app/gfx/bitmap.h" -#include "app/gfx/snes_palette.h" -#include "app/gfx/snes_tile.h" -#include "app/gui/canvas.h" #include "app/rom.h" #include "app/zelda3/dungeon/room_object.h" #include "app/zelda3/sprite/sprite.h" namespace yaze { -namespace app { namespace zelda3 { -namespace dungeon { // room_object_layout_pointer 0x882D // room_object_pointer 0x874C @@ -113,11 +106,6 @@ class Room : public SharedRom { auto room_size_ptr() const { return room_size_pointer_; } auto set_room_size(uint64_t size) { room_size_ = size; } - RoomObject AddObject(short oid, uint8_t x, uint8_t y, uint8_t size, - uint8_t layer) { - return RoomObject(oid, x, y, size, layer); - } - uint8_t blockset = 0; uint8_t spriteset = 0; uint8_t palette = 0; @@ -476,144 +464,7 @@ constexpr std::string_view kRoomNames[] = { "Mazeblock Cave", "Smith Peg Cave"}; -constexpr std::string_view kEntranceNames[] = { - "Link's House Intro", - "Link's House Post-intro", - "Sanctuary", - "Hyrule Castle West", - "Hyrule Castle Central", - "Hyrule Castle East", - "Death Mountain Express (Lower)", - "Death Mountain Express (Upper)", - "Eastern Palace", - "Desert Palace Central", - "Desert Palace East", - "Desert Palace West", - "Desert Palace Boss Lair", - "Kakariko Elder's House West", - "Kakariko Elder's House East", - "Kakariko Angry Bros West", - "Kakariko Angry Bros East", - "Mad Batter Lair", - "Under Lumberjacks' Weird Tree", - "Death Mountain Maze 0000", - "Death Mountain Maze 0001", - "Turtle Rock Mountainface 1", - "Death Mountain Cape Heart Piece Cave (Lower)", - "Death Mountain Cape Heart Piece Cave (Upper)", - "Turtle Rock Mountainface 2", - "Turtle Rock Mountainface 3", - "Death Mountain Maze 0002", - "Death Mountain Maze 0003", - "Death Mountain Maze 0004", - "Death Mountain Maze 0005", - "Death Mountain Maze 0006", - "Death Mountain Maze 0007", - "Death Mountain Maze 0008", - "Spectacle Rock Maze 1", - "Spectacle Rock Maze 2", - "Spectacle Rock Maze 3", - "Hyrule Castle Tower", - "Swamp Palace", - "Palace of Darkness", - "Misery Mire", - "Skull Woods 1", - "Skull Woods 2", - "Skull Woods Big Chest", - "Skull Woods Boss Lair", - "Lost Woods Thieves' Lair", - "Ice Palace", - "Death Mountain Escape West", - "Death Mountain Escape East", - "Death Mountain Elder's Cave (Lower)", - "Death Mountain Elder's Cave (Upper)", - "Hyrule Castle Secret Cellar", - "Tower of Hera", - "Thieves's Town", - "Turtle Rock Main", - "Ganon's Pyramid Sanctum (Lower)", - "Ganon's Tower", - "Fairy Cave 1", - "Kakariko Western Well", - "Death Mountain Maze 0009", - "Death Mountain Maze 0010", - "Treasure Shell Game 1", - "Storyteller Cave 1", - "Snitch House 1", - "Snitch House 2", - "SickBoy House", - "Byrna Gauntlet", - "Kakariko Pub South", - "Kakariko Pub North", - "Kakariko Inn", - "Sahasrahlah's Disco Infernum", - "Kakariko's Lame Shop", - "Village of Outcasts Chest Game", - "Village of Outcasts Orphanage", - "Kakariko Library", - "Kakariko Storage Shed", - "Kakariko Sweeper Lady's House", - "Potion Shop", - "Aginah's Desert Cottage", - "Watergate", - "Death Mountain Maze 0011", - "Fairy Cave 2", - "Refill Cave 0001", - "Refill Cave 0002", - "The Bomb \"Shop\"", - "Village of Outcasts Retirement Center", - "Fairy Cave 3", - "Good Bee Cave", - "General Store 1", - "General Store 2", - "Archery Game", - "Storyteller Cave 2", - "Hall of the Invisibility Cape", - "Pond of Wishing", - "Pond of Happiness", - "Fairy Cave 4", - "Swamp of Evil Heart Piece Hall", - "General Store 3", - "Blind's Old Hideout", - "Storyteller Cave 3", - "Warped Pond of Wishing", - "Chez Smithies", - "Fortune Teller 1", - "Fortune Teller 2", - "Chest Shell Game 2", - "Storyteller Cave 4", - "Storyteller Cave 5", - "Storyteller Cave 6", - "Village House 1", - "Thief Hideout 1", - "Thief Hideout 2", - "Heart Piece Cave 1", - "Thief Hideout 3", - "Refill Cave 3", - "Fairy Cave 5", - "Heart Piece Cave 2", - "Hyrule Castle Prison", - "Hyrule Castle Throne Room", - "Hyrule Tower Agahnim's Sanctum", - "Skull Woods 3 (Drop In)", - "Skull Woods 4 (Drop In)", - "Skull Woods 5 (Drop In)", - "Skull Woods 6 (Drop In)", - "Lost Woods Thieves' Hideout (Drop In)", - "Ganon's Pyramid Sanctum (Upper)", - "Fairy Cave 6 (Drop In)", - "Hyrule Castle Secret Cellar (Drop In)", - "Mad Batter Lair (Drop In)", - "Under Lumberjacks' Weird Tree (Drop In)", - "Kakariko Western Well (Drop In)", - "Hyrule Sewers Goodies Room (Drop In)", - "Chris Houlihan Room (Drop In)", - "Heart Piece Cave 3 (Drop In)", - "Ice Rod Cave"}; - -} // namespace dungeon } // namespace zelda3 -} // namespace app } // namespace yaze #endif diff --git a/src/app/zelda3/dungeon/room_entrance.h b/src/app/zelda3/dungeon/room_entrance.h index 074d9c42..978c8487 100644 --- a/src/app/zelda3/dungeon/room_entrance.h +++ b/src/app/zelda3/dungeon/room_entrance.h @@ -6,13 +6,7 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace zelda3 { -namespace dungeon { - -// ============================================================================ -// Dungeon Entrances Related Variables -// ============================================================================ // 0x14577 word value for each room constexpr int kEntranceRoom = 0x14813; @@ -350,9 +344,7 @@ class RoomEntrance { uint8_t camera_boundary_fe_; }; -} // namespace dungeon } // namespace zelda3 -} // namespace app } // namespace yaze #endif // YAZE_APP_ZELDA3_DUNGEON_ROOM_ENTRANCE_H diff --git a/src/app/zelda3/dungeon/room_object.cc b/src/app/zelda3/dungeon/room_object.cc index 8aeb6e71..796bbe91 100644 --- a/src/app/zelda3/dungeon/room_object.cc +++ b/src/app/zelda3/dungeon/room_object.cc @@ -1,9 +1,8 @@ #include "room_object.h" namespace yaze { -namespace app { namespace zelda3 { -namespace dungeon { + ObjectOption operator|(ObjectOption lhs, ObjectOption rhs) { return static_cast(static_cast(lhs) | @@ -131,7 +130,8 @@ void RoomObject::DrawTile(gfx::Tile16 t, int xx, int yy, } } -} // namespace dungeon + + } // namespace zelda3 -} // namespace app + } // namespace yaze diff --git a/src/app/zelda3/dungeon/room_object.h b/src/app/zelda3/dungeon/room_object.h index 497bbe4e..f6a33d3a 100644 --- a/src/app/zelda3/dungeon/room_object.h +++ b/src/app/zelda3/dungeon/room_object.h @@ -5,19 +5,13 @@ #include #include -#include "app/emu/cpu/cpu.h" -#include "app/emu/memory/memory.h" -#include "app/emu/video/ppu.h" -#include "app/gfx/snes_palette.h" #include "app/gfx/snes_tile.h" #include "app/rom.h" -#include "app/zelda3/dungeon/object_names.h" #include "app/zelda3/dungeon/object_renderer.h" namespace yaze { -namespace app { namespace zelda3 { -namespace dungeon { + struct SubtypeInfo { uint32_t subtype_ptr; @@ -207,9 +201,10 @@ class Subtype3 : public RoomObject { } }; -} // namespace dungeon + + } // namespace zelda3 -} // namespace app + } // namespace yaze #endif // YAZE_APP_ZELDA3_DUNGEON_ROOM_OBJECT_H diff --git a/src/app/zelda3/dungeon/room_tag.h b/src/app/zelda3/dungeon/room_tag.h index a6f5a331..d1a139f6 100644 --- a/src/app/zelda3/dungeon/room_tag.h +++ b/src/app/zelda3/dungeon/room_tag.h @@ -4,9 +4,7 @@ #include namespace yaze { -namespace app { namespace zelda3 { -namespace dungeon { static const std::string RoomEffect[] = {"Nothing", "Nothing", @@ -86,9 +84,9 @@ static const std::string RoomTag[] = {"Nothing", "Light Torches for Chest", "Kill Boss Again"}; -} // namespace dungeon + + } // namespace zelda3 -} // namespace app } // namespace yaze #endif // YAZE_APP_ZELDA3_DUNGEON_ROOM_TAG_H \ No newline at end of file diff --git a/src/app/zelda3/music/tracker.cc b/src/app/zelda3/music/tracker.cc index 5753051f..454ed4cc 100644 --- a/src/app/zelda3/music/tracker.cc +++ b/src/app/zelda3/music/tracker.cc @@ -17,7 +17,6 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace zelda3 { namespace { @@ -65,7 +64,7 @@ unsigned char *Tracker::GetSpcAddr(Rom &rom, unsigned short addr, short bank) { spcbank = bank + 1; again: - rom_ptr = rom.data() + sbank_ofs[spcbank]; + rom_ptr = rom.mutable_data() + sbank_ofs[spcbank]; for (;;) { a = *(unsigned short *)rom_ptr; @@ -425,7 +424,7 @@ void Tracker::LoadSongs(Rom &rom) { srsize = 0; song_range_ = 0; sp_mark = 0; - b = rom.data(); + b = rom.mutable_data(); sbank_ofs[1] = (b[0x91c] << 15) + ((b[0x918] & 127) << 8) + b[0x914]; sbank_ofs[3] = (b[0x93a] << 15) + ((b[0x936] & 127) << 8) + b[0x932]; @@ -551,7 +550,7 @@ void Tracker::LoadSongs(Rom &rom) { numinst = spclen / 6; b = GetSpcAddr(rom, 0x3e00, 0); - m_ofs = b - rom.data() + spclen; + m_ofs = b - rom.mutable_data() + spclen; sndinsts = (ZeldaSfxInstrument *)malloc(spclen); memcpy(sndinsts, b, spclen); numsndinst = spclen / 9; @@ -785,7 +784,7 @@ short Tracker::SaveSpcCommand(Rom &rom, short num, short songtime, int Tracker::WriteSpcData(Rom &rom, void *buf, int len, int addr, int spc, int limit) { - unsigned char *rom_data = rom.data(); + unsigned char *rom_data = rom.mutable_data(); if (!len) return addr; @@ -873,7 +872,7 @@ void Tracker::SaveSongs(Rom &rom) { // set it so the music has not been modified. (reset the status) m_modf = 0; - rom_data = rom.data(); + rom_data = rom.mutable_data(); // SetCursor(wait_cursor); @@ -1241,7 +1240,7 @@ void Tracker::SaveSongs(Rom &rom) { m_modf = 1; return; } - memcpy(rom.data() + n, stbl->buf, stbl->len); + memcpy(rom.mutable_data() + n, stbl->buf, stbl->len); n += stbl->len; free(stbl->relocs); free(stbl->buf); @@ -1249,13 +1248,13 @@ void Tracker::SaveSongs(Rom &rom) { ssblt[i] = 0; } if (n > l + 4) { - *(short *)(rom.data() + l) = n - l - 4; - *(short *)(rom.data() + l + 2) = o ? bank_lwr[k] : 0xd000; + *(short *)(rom.mutable_data() + l) = n - l - 4; + *(short *)(rom.mutable_data() + l + 2) = o ? bank_lwr[k] : 0xd000; l = n; } } - *(short *)(rom.data() + l) = 0; - *(short *)(rom.data() + l + 2) = 0x800; + *(short *)(rom.mutable_data() + l) = 0; + *(short *)(rom.mutable_data() + l + 2) = 0x800; if (k == 1) m = l + 4; } free(ssblt); @@ -1337,5 +1336,5 @@ void Tracker::NewSR(Rom &rom, int bank) { } // namespace music } // namespace zelda3 -} // namespace app + } // namespace yaze diff --git a/src/app/zelda3/music/tracker.h b/src/app/zelda3/music/tracker.h index 65048b2a..61e01e5d 100644 --- a/src/app/zelda3/music/tracker.h +++ b/src/app/zelda3/music/tracker.h @@ -7,11 +7,10 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace zelda3 { /** - * @namespace yaze::app::zelda3::music + * @namespace yaze::zelda3::music * @brief Contains classes and functions for handling music data in Zelda 3. * * Based off of the HyruleMagic tracker code. @@ -254,7 +253,7 @@ class Tracker { } // namespace music } // namespace zelda3 -} // namespace app + } // namespace yaze #endif diff --git a/src/app/zelda3/overworld/overworld.cc b/src/app/zelda3/overworld/overworld.cc index aee8dc06..a63c0288 100644 --- a/src/app/zelda3/overworld/overworld.cc +++ b/src/app/zelda3/overworld/overworld.cc @@ -1,25 +1,18 @@ #include "overworld.h" #include -#include #include -#include #include #include #include "absl/status/status.h" #include "app/core/constants.h" -#include "app/gfx/bitmap.h" #include "app/gfx/compression.h" #include "app/gfx/snes_tile.h" #include "app/rom.h" -#include "app/zelda3/overworld/overworld_map.h" -#include "app/zelda3/sprite/sprite.h" namespace yaze { -namespace app { namespace zelda3 { -namespace overworld { absl::Status Overworld::Load(Rom &rom) { rom_ = rom; @@ -28,7 +21,8 @@ absl::Status Overworld::Load(Rom &rom) { AssembleMap16Tiles(); RETURN_IF_ERROR(DecompressAllMapTiles()) - const bool load_custom_overworld = flags()->overworld.kLoadCustomOverworld; + const bool load_custom_overworld = + core::ExperimentFlags::get().overworld.kLoadCustomOverworld; for (int map_index = 0; map_index < kNumOverworldMaps; ++map_index) overworld_maps_.emplace_back(map_index, rom_, load_custom_overworld); @@ -54,11 +48,9 @@ void Overworld::FetchLargeMaps() { overworld_maps_[138].SetAsLargeMap(129, 3); overworld_maps_[136].SetAsSmallMap(); - std::vector map_checked; - map_checked.reserve(0x40); - for (int i = 0; i < 64; i++) { - map_checked[i] = false; - } + std::array map_checked; + std::fill(map_checked.begin(), map_checked.end(), false); + int xx = 0; int yy = 0; while (true) { @@ -116,7 +108,8 @@ absl::Status Overworld::AssembleMap32Tiles() { rom_.version_constants().kMap32TileTR, rom_.version_constants().kMap32TileBL, rom_.version_constants().kMap32TileBR}; - if (rom()->data()[0x01772E] != 0x04) { + if (rom()->data()[kMap32ExpandedFlagPos] != 0x04 && + core::ExperimentFlags::get().overworld.kLoadCustomOverworld) { map32address[0] = rom_.version_constants().kMap32TileTL; map32address[1] = kMap32TileTRExpanded; map32address[2] = kMap32TileBLExpanded; @@ -126,23 +119,23 @@ absl::Status Overworld::AssembleMap32Tiles() { } // Loop through each 32x32 pixel tile in the rom - for (int i = 0; i < kMap32TilesLength; i += 6) { + for (int i = 0; i < num_tile32; i += 6) { // Loop through each quadrant of the 32x32 pixel tile. for (int k = 0; k < 4; k++) { // Generate the 16-bit tile for the current quadrant of the current // 32x32 pixel tile. - ASSIGN_OR_RETURN(uint16_t tl, - GetTile16ForTile32(i, k, (int)Dimension::map32TilesTL, - map32address)); - ASSIGN_OR_RETURN(uint16_t tr, - GetTile16ForTile32(i, k, (int)Dimension::map32TilesTR, - map32address)); - ASSIGN_OR_RETURN(uint16_t bl, - GetTile16ForTile32(i, k, (int)Dimension::map32TilesBL, - map32address)); - ASSIGN_OR_RETURN(uint16_t br, - GetTile16ForTile32(i, k, (int)Dimension::map32TilesBR, - map32address)); + ASSIGN_OR_RETURN( + uint16_t tl, + GetTile16ForTile32(i, k, (int)Dimension::map32TilesTL, map32address)); + ASSIGN_OR_RETURN( + uint16_t tr, + GetTile16ForTile32(i, k, (int)Dimension::map32TilesTR, map32address)); + ASSIGN_OR_RETURN( + uint16_t bl, + GetTile16ForTile32(i, k, (int)Dimension::map32TilesBL, map32address)); + ASSIGN_OR_RETURN( + uint16_t br, + GetTile16ForTile32(i, k, (int)Dimension::map32TilesBR, map32address)); // Add the generated 16-bit tiles to the tiles32 vector. tiles32_unique_.emplace_back(gfx::Tile32(tl, tr, bl, br)); @@ -164,7 +157,8 @@ absl::Status Overworld::AssembleMap32Tiles() { void Overworld::AssembleMap16Tiles() { int tpos = kMap16Tiles; int num_tile16 = kNumTile16Individual; - if (rom()->data()[0x02FD28] != 0x0F) { + if (rom()->data()[kMap16ExpandedFlagPos] != 0x0F && + core::ExperimentFlags::get().overworld.kLoadCustomOverworld) { tpos = kMap16TilesExpanded; num_tile16 = NumberOfMap16Ex; expanded_tile16_ = true; @@ -184,7 +178,7 @@ void Overworld::AssembleMap16Tiles() { } void Overworld::AssignWorldTiles(int x, int y, int sx, int sy, int tpos, - OWBlockset &world) { + OverworldBlockset &world) { int position_x1 = (x * 2) + (sx * 32); int position_y1 = (y * 2) + (sy * 32); int position_x2 = (x * 2) + 1 + (sx * 32); @@ -223,12 +217,15 @@ absl::Status Overworld::DecompressAllMapTiles() { return core::SnesToPc(p); }; - uint32_t lowest = 0x0FFFFF; - uint32_t highest = 0x0F8000; + constexpr uint32_t kBaseLowest = 0x0FFFFF; + constexpr uint32_t kBaseHighest = 0x0F8000; + + uint32_t lowest = kBaseLowest; + uint32_t highest = kBaseHighest; int sx = 0; int sy = 0; int c = 0; - for (int i = 0; i < 160; i++) { + for (int i = 0; i < kNumOverworldMaps; i++) { auto p1 = get_ow_map_gfx_ptr( i, rom()->version_constants().kCompressedAllMap32PointersHigh); auto p2 = get_ow_map_gfx_ptr( @@ -239,24 +236,12 @@ absl::Status Overworld::DecompressAllMapTiles() { if (p1 >= highest) highest = p1; if (p2 >= highest) highest = p2; - if (p1 <= lowest && p1 > 0x0F8000) lowest = p1; - if (p2 <= lowest && p2 > 0x0F8000) lowest = p2; + if (p1 <= lowest && p1 > kBaseHighest) lowest = p1; + if (p2 <= lowest && p2 > kBaseHighest) lowest = p2; - std::vector bytes, bytes2; int size1, size2; - auto decomp = gfx::lc_lz2::Uncompress(rom()->data() + p2, &size1, 1); - bytes.resize(size1); - for (int j = 0; j < size1; j++) { - bytes[j] = decomp[j]; - } - free(decomp); - decomp = gfx::lc_lz2::Uncompress(rom()->data() + p1, &size2, 1); - bytes2.resize(size2); - for (int j = 0; j < size2; j++) { - bytes2[j] = decomp[j]; - } - free(decomp); - + auto bytes = gfx::HyruleMagicDecompress(rom()->data() + p2, &size1, 1); + auto bytes2 = gfx::HyruleMagicDecompress(rom()->data() + p1, &size2, 1); OrganizeMapTiles(bytes, bytes2, i, sx, sy, ttpos); sx++; @@ -294,30 +279,28 @@ absl::Status Overworld::LoadOverworldMaps() { // Wait for all tasks to complete and check their results for (auto &future : futures) { - absl::Status status = future.get(); - if (!status.ok()) { - return status; - } + RETURN_IF_ERROR(future.get()); } return absl::OkStatus(); } void Overworld::LoadTileTypes() { - for (int i = 0; i < 0x200; i++) { + for (int i = 0; i < kNumTileTypes; ++i) { all_tiles_types_[i] = - rom()->data()[rom()->version_constants().overworldTilesType + i]; + rom()->data()[rom()->version_constants().kOverworldTilesType + i]; } } void Overworld::LoadEntrances() { - int ow_entrance_map_ptr = OWEntranceMap; - int ow_entrance_pos_ptr = OWEntrancePos; - int ow_entrance_id_ptr = OWEntranceEntranceId; + int ow_entrance_map_ptr = kOverworldEntranceMap; + int ow_entrance_pos_ptr = kOverworldEntrancePos; + int ow_entrance_id_ptr = kOverworldEntranceEntranceId; int num_entrances = 129; - if (rom()->data()[0x0DB895] != 0xB8) { - ow_entrance_map_ptr = 0x0DB55F; - ow_entrance_pos_ptr = 0x0DB35F; - ow_entrance_id_ptr = 0x0DB75F; + if (rom()->data()[kOverworldEntranceExpandedFlagPos] != 0xB8 && + core::ExperimentFlags::get().overworld.kLoadCustomOverworld) { + ow_entrance_map_ptr = kOverworldEntranceMapExpanded; + ow_entrance_pos_ptr = kOverworldEntrancePosExpanded; + ow_entrance_id_ptr = kOverworldEntranceEntranceIdExpanded; expanded_entrances_ = true; } @@ -339,11 +322,11 @@ void Overworld::LoadEntrances() { } for (int i = 0; i < 0x13; i++) { - auto map_id = (short)((rom_[OWHoleArea + (i * 2) + 1] << 8) + - (rom_[OWHoleArea + (i * 2)])); - auto map_pos = (short)((rom_[OWHolePos + (i * 2) + 1] << 8) + - (rom_[OWHolePos + (i * 2)])); - uint8_t entrance_id = (rom_[OWHoleEntrance + i]); + auto map_id = (short)((rom_[kOverworldHoleArea + (i * 2) + 1] << 8) + + (rom_[kOverworldHoleArea + (i * 2)])); + auto map_pos = (short)((rom_[kOverworldHolePos + (i * 2) + 1] << 8) + + (rom_[kOverworldHolePos + (i * 2)])); + uint8_t entrance_id = (rom_[kOverworldHoleEntrance + i]); int p = (map_pos + 0x400) >> 1; int x = (p % 64); int y = (p >> 6); @@ -389,7 +372,7 @@ absl::Status Overworld::LoadExits() { uint16_t px = (uint16_t)((rom_data[OWExitXPlayer + (i * 2) + 1] << 8) + rom_data[OWExitXPlayer + (i * 2)]); - if (rom()->flags()->kLogToConsole) { + if (core::ExperimentFlags::get().kLogToConsole) { std::cout << "Exit: " << i << " RoomID: " << exit_room_id << " MapID: " << exit_map_id << " VRAM: " << exit_vram << " YScroll: " << exit_y_scroll @@ -413,7 +396,7 @@ absl::Status Overworld::LoadExits() { absl::Status Overworld::LoadItems() { ASSIGN_OR_RETURN(uint32_t pointer, - rom()->ReadLong(zelda3::overworld::kOverworldItemsAddress)); + rom()->ReadLong(zelda3::kOverworldItemsAddress)); uint32_t pointer_pc = core::SnesToPc(pointer); // 1BC2F9 -> 0DC2F9 for (int i = 0; i < 128; i++) { ASSIGN_OR_RETURN(uint16_t word_address, @@ -466,9 +449,9 @@ absl::Status Overworld::LoadSprites() { all_sprites_.emplace_back(); } - RETURN_IF_ERROR(LoadSpritesFromMap(overworldSpritesBegining, 64, 0)); - RETURN_IF_ERROR(LoadSpritesFromMap(overworldSpritesZelda, 144, 1)); - RETURN_IF_ERROR(LoadSpritesFromMap(overworldSpritesAgahnim, 144, 2)); + RETURN_IF_ERROR(LoadSpritesFromMap(kOverworldSpritesBeginning, 64, 0)); + RETURN_IF_ERROR(LoadSpritesFromMap(kOverworldSpritesZelda, 144, 1)); + RETURN_IF_ERROR(LoadSpritesFromMap(kOverworldSpritesAgahnim, 144, 2)); return absl::OkStatus(); } @@ -512,8 +495,6 @@ absl::Status Overworld::LoadSpritesFromMap(int sprites_per_gamestate_ptr, return absl::OkStatus(); } -// --------------------------------------------------------------------------- - absl::Status Overworld::Save(Rom &rom) { rom_ = rom; if (expanded_tile16_) RETURN_IF_ERROR(SaveMap16Expanded()) @@ -527,15 +508,15 @@ absl::Status Overworld::Save(Rom &rom) { } absl::Status Overworld::SaveOverworldMaps() { - core::Logger::log("Saving Overworld Maps"); + core::logf("Saving Overworld Maps"); // Initialize map pointers std::fill(map_pointers1_id.begin(), map_pointers1_id.end(), -1); std::fill(map_pointers2_id.begin(), map_pointers2_id.end(), -1); // Compress and save each map - int pos = 0x058000; - for (int i = 0; i < 160; i++) { + int pos = kOverworldCompressedMapPos; + for (int i = 0; i < kNumOverworldMaps; i++) { std::vector single_map_1(512); std::vector single_map_2(512); @@ -550,24 +531,13 @@ absl::Status Overworld::SaveOverworldMaps() { } } - std::vector a, b; int size_a, size_b; // Compress single_map_1 and single_map_2 - auto a_char = gfx::lc_lz2::Compress(single_map_1.data(), 256, &size_a, 1); - auto b_char = gfx::lc_lz2::Compress(single_map_2.data(), 256, &size_b, 1); - if (a_char == nullptr || b_char == nullptr) { + auto a = gfx::HyruleMagicCompress(single_map_1.data(), 256, &size_a, 1); + auto b = gfx::HyruleMagicCompress(single_map_2.data(), 256, &size_b, 1); + if (a.empty() || b.empty()) { return absl::AbortedError("Error compressing map gfx."); } - // Copy the compressed data to a and b - a.resize(size_a); - b.resize(size_b); - // Copy the arrays manually - for (int k = 0; k < size_a; k++) { - a[k] = a_char[k]; - } - for (int k = 0; k < size_b; k++) { - b[k] = b_char[k]; - } // Save compressed data and pointers map_data_p1[i] = std::vector(size_a); @@ -578,14 +548,13 @@ absl::Status Overworld::SaveOverworldMaps() { } if ((pos + size_a) >= 0x6411F && (pos + size_a) <= 0x70000) { - core::Logger::log("Pos set to overflow region for map " + - std::to_string(i) + " at " + - core::UppercaseHexLong(pos)); - pos = OverworldMapDataOverflow; // 0x0F8780; + core::logf("Pos set to overflow region for map %s at %s", + std::to_string(i), core::HexLong(pos)); + pos = kOverworldMapDataOverflow; // 0x0F8780; } - auto compareArray = [](const std::vector &array1, - const std::vector &array2) -> bool { + const auto compare_array = [](const std::vector &array1, + const std::vector &array2) -> bool { if (array1.size() != array2.size()) { return false; } @@ -600,12 +569,12 @@ absl::Status Overworld::SaveOverworldMaps() { }; for (int j = 0; j < i; j++) { - if (compareArray(a, map_data_p1[j])) { + if (compare_array(a, map_data_p1[j])) { // Reuse pointer id j for P1 (a) map_pointers1_id[i] = j; } - if (compareArray(b, map_data_p2[j])) { + if (compare_array(b, map_data_p2[j])) { map_pointers2_id[i] = j; // Reuse pointer id j for P2 (b) } @@ -616,9 +585,8 @@ absl::Status Overworld::SaveOverworldMaps() { std::copy(a.begin(), a.end(), map_data_p1[i].begin()); int snes_pos = core::PcToSnes(pos); map_pointers1[i] = snes_pos; - core::Logger::log("Saving map pointers1 and compressed data for map " + - core::UppercaseHexByte(i) + " at " + - core::UppercaseHexLong(snes_pos)); + core::logf("Saving map pointers1 and compressed data for map %s at %s", + core::HexByte(i), core::HexLong(snes_pos)); RETURN_IF_ERROR(rom()->WriteLong( rom()->version_constants().kCompressedAllMap32PointersLow + (3 * i), snes_pos)); @@ -627,9 +595,8 @@ absl::Status Overworld::SaveOverworldMaps() { } else { // Save pointer for map1 int snes_pos = map_pointers1[map_pointers1_id[i]]; - core::Logger::log("Saving map pointers1 for map " + - core::UppercaseHexByte(i) + " at " + - core::UppercaseHexLong(snes_pos)); + core::logf("Saving map pointers1 for map %s at %s", core::HexByte(i), + core::HexLong(snes_pos)); RETURN_IF_ERROR(rom()->WriteLong( rom()->version_constants().kCompressedAllMap32PointersLow + (3 * i), snes_pos)); @@ -640,10 +607,9 @@ absl::Status Overworld::SaveOverworldMaps() { } if ((pos + b.size()) >= 0x6411F && (pos + b.size()) <= 0x70000) { - core::Logger::log("Pos set to overflow region for map " + - core::UppercaseHexByte(i) + " at " + - core::UppercaseHexLong(pos)); - pos = OverworldMapDataOverflow; + core::logf("Pos set to overflow region for map %s at %s", + core::HexByte(i), core::HexLong(pos)); + pos = kOverworldMapDataOverflow; } if (map_pointers2_id[i] == -1) { @@ -651,9 +617,8 @@ absl::Status Overworld::SaveOverworldMaps() { std::copy(b.begin(), b.end(), map_data_p2[i].begin()); int snes_pos = core::PcToSnes(pos); map_pointers2[i] = snes_pos; - core::Logger::log("Saving map pointers2 and compressed data for map " + - core::UppercaseHexByte(i) + " at " + - core::UppercaseHexLong(snes_pos)); + core::logf("Saving map pointers2 and compressed data for map %s at %s", + core::HexByte(i), core::HexLong(snes_pos)); RETURN_IF_ERROR(rom()->WriteLong( rom()->version_constants().kCompressedAllMap32PointersHigh + (3 * i), snes_pos)); @@ -662,9 +627,8 @@ absl::Status Overworld::SaveOverworldMaps() { } else { // Save pointer for map2 int snes_pos = map_pointers2[map_pointers2_id[i]]; - core::Logger::log("Saving map pointers2 for map " + - core::UppercaseHexByte(i) + " at " + - core::UppercaseHexLong(snes_pos)); + core::logf("Saving map pointers2 for map %s at %s", core::HexByte(i), + core::HexLong(snes_pos)); RETURN_IF_ERROR(rom()->WriteLong( rom()->version_constants().kCompressedAllMap32PointersHigh + (3 * i), snes_pos)); @@ -672,8 +636,8 @@ absl::Status Overworld::SaveOverworldMaps() { } // Check if too many maps data - if (pos > 0x137FFF) { - std::cerr << "Too many maps data " << std::hex << pos << std::endl; + if (pos > kOverworldCompressedOverflowPos) { + core::logf("Too many maps data %s", core::HexLong(pos)); return absl::AbortedError("Too many maps data " + std::to_string(pos)); } @@ -684,7 +648,7 @@ absl::Status Overworld::SaveOverworldMaps() { } absl::Status Overworld::SaveLargeMaps() { - core::Logger::log("Saving Large Maps"); + core::logf("Saving Large Maps"); std::vector checked_map; for (int i = 0; i < 0x40; i++) { @@ -695,7 +659,7 @@ absl::Status Overworld::SaveLargeMaps() { // Always write the map parent since it should not matter RETURN_IF_ERROR( - rom()->Write(overworldMapParentId + i, overworld_maps_[i].parent())) + rom()->Write(kOverworldMapParentId + i, overworld_maps_[i].parent())) if (std::find(checked_map.begin(), checked_map.end(), i) != checked_map.end()) { @@ -708,93 +672,99 @@ absl::Status Overworld::SaveLargeMaps() { const uint8_t large_map_offsets[] = {0, 1, 8, 9}; for (const auto &offset : large_map_offsets) { // Check 1 - RETURN_IF_ERROR(rom()->WriteByte(overworldMapSize + i + offset, 0x20)); + RETURN_IF_ERROR(rom()->WriteByte(kOverworldMapSize + i + offset, 0x20)); // Check 2 RETURN_IF_ERROR( - rom()->WriteByte(overworldMapSizeHighByte + i + offset, 0x03)); + rom()->WriteByte(kOverworldMapSizeHighByte + i + offset, 0x03)); // Check 3 RETURN_IF_ERROR( - rom()->WriteByte(overworldScreenSize + i + offset, 0x00)); + rom()->WriteByte(kOverworldScreenSize + i + offset, 0x00)); RETURN_IF_ERROR( - rom()->WriteByte(overworldScreenSize + i + offset + 64, 0x00)); + rom()->WriteByte(kOverworldScreenSize + i + offset + 64, 0x00)); // Check 4 - RETURN_IF_ERROR( - rom()->WriteByte(OverworldScreenSizeForLoading + i + offset, 0x04)); RETURN_IF_ERROR(rom()->WriteByte( - OverworldScreenSizeForLoading + i + offset + 64, 0x04)); + kOverworldScreenSizeForLoading + i + offset, 0x04)); RETURN_IF_ERROR(rom()->WriteByte( - OverworldScreenSizeForLoading + i + offset + 128, 0x04)); + kOverworldScreenSizeForLoading + i + offset + 64, 0x04)); + RETURN_IF_ERROR(rom()->WriteByte( + kOverworldScreenSizeForLoading + i + offset + 128, 0x04)); } // Check 5 and 6 RETURN_IF_ERROR( - rom()->WriteShort(transition_target_north + (i * 2), + rom()->WriteShort(kTransitionTargetNorth + (i * 2), (uint16_t)((parent_y_pos * 0x200) - 0xE0))); RETURN_IF_ERROR( - rom()->WriteShort(transition_target_west + (i * 2), + rom()->WriteShort(kTransitionTargetWest + (i * 2), (uint16_t)((parent_x_pos * 0x200) - 0x100))); RETURN_IF_ERROR( - rom()->WriteShort(transition_target_north + (i * 2) + 2, + rom()->WriteShort(kTransitionTargetNorth + (i * 2) + 2, (uint16_t)((parent_y_pos * 0x200) - 0xE0))); RETURN_IF_ERROR( - rom()->WriteShort(transition_target_west + (i * 2) + 2, + rom()->WriteShort(kTransitionTargetWest + (i * 2) + 2, (uint16_t)((parent_x_pos * 0x200) - 0x100))); RETURN_IF_ERROR( - rom()->WriteShort(transition_target_north + (i * 2) + 16, + rom()->WriteShort(kTransitionTargetNorth + (i * 2) + 16, (uint16_t)((parent_y_pos * 0x200) - 0xE0))); RETURN_IF_ERROR( - rom()->WriteShort(transition_target_west + (i * 2) + 16, + rom()->WriteShort(kTransitionTargetWest + (i * 2) + 16, (uint16_t)((parent_x_pos * 0x200) - 0x100))); RETURN_IF_ERROR( - rom()->WriteShort(transition_target_north + (i * 2) + 18, + rom()->WriteShort(kTransitionTargetNorth + (i * 2) + 18, (uint16_t)((parent_y_pos * 0x200) - 0xE0))); RETURN_IF_ERROR( - rom()->WriteShort(transition_target_west + (i * 2) + 18, + rom()->WriteShort(kTransitionTargetWest + (i * 2) + 18, (uint16_t)((parent_x_pos * 0x200) - 0x100))); // Check 7 and 8 - RETURN_IF_ERROR(rom()->WriteShort(overworldTransitionPositionX + (i * 2), + RETURN_IF_ERROR(rom()->WriteShort(kOverworldTransitionPositionX + (i * 2), (parent_x_pos * 0x200))); - RETURN_IF_ERROR(rom()->WriteShort(overworldTransitionPositionY + (i * 2), + RETURN_IF_ERROR(rom()->WriteShort(kOverworldTransitionPositionY + (i * 2), (parent_y_pos * 0x200))); - RETURN_IF_ERROR(rom()->WriteShort( - overworldTransitionPositionX + (i * 2) + 02, (parent_x_pos * 0x200))); - RETURN_IF_ERROR(rom()->WriteShort( - overworldTransitionPositionY + (i * 2) + 02, (parent_y_pos * 0x200))); + RETURN_IF_ERROR( + rom()->WriteShort(kOverworldTransitionPositionX + (i * 2) + 02, + (parent_x_pos * 0x200))); + RETURN_IF_ERROR( + rom()->WriteShort(kOverworldTransitionPositionY + (i * 2) + 02, + (parent_y_pos * 0x200))); // problematic - RETURN_IF_ERROR(rom()->WriteShort( - overworldTransitionPositionX + (i * 2) + 16, (parent_x_pos * 0x200))); - RETURN_IF_ERROR(rom()->WriteShort( - overworldTransitionPositionY + (i * 2) + 16, (parent_y_pos * 0x200))); + RETURN_IF_ERROR( + rom()->WriteShort(kOverworldTransitionPositionX + (i * 2) + 16, + (parent_x_pos * 0x200))); + RETURN_IF_ERROR( + rom()->WriteShort(kOverworldTransitionPositionY + (i * 2) + 16, + (parent_y_pos * 0x200))); - RETURN_IF_ERROR(rom()->WriteShort( - overworldTransitionPositionX + (i * 2) + 18, (parent_x_pos * 0x200))); - RETURN_IF_ERROR(rom()->WriteShort( - overworldTransitionPositionY + (i * 2) + 18, (parent_y_pos * 0x200))); + RETURN_IF_ERROR( + rom()->WriteShort(kOverworldTransitionPositionX + (i * 2) + 18, + (parent_x_pos * 0x200))); + RETURN_IF_ERROR( + rom()->WriteShort(kOverworldTransitionPositionY + (i * 2) + 18, + (parent_y_pos * 0x200))); // Check 9 RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen1 + (i * 2) + 00, 0x0060)); + kOverworldScreenTileMapChangeByScreen1 + (i * 2) + 00, 0x0060)); RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen1 + (i * 2) + 02, 0x0060)); + kOverworldScreenTileMapChangeByScreen1 + (i * 2) + 02, 0x0060)); // If parentX == 0 then lower submaps == 0x0060 too if (parent_x_pos == 0) { RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen1 + (i * 2) + 16, 0x0060)); + kOverworldScreenTileMapChangeByScreen1 + (i * 2) + 16, 0x0060)); RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen1 + (i * 2) + 18, 0x0060)); + kOverworldScreenTileMapChangeByScreen1 + (i * 2) + 18, 0x0060)); } else { // Otherwise lower submaps == 0x1060 RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen1 + (i * 2) + 16, 0x1060)); + kOverworldScreenTileMapChangeByScreen1 + (i * 2) + 16, 0x1060)); RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen1 + (i * 2) + 18, 0x1060)); + kOverworldScreenTileMapChangeByScreen1 + (i * 2) + 18, 0x1060)); // If the area to the left is a large map, we don't need to add an // offset to it. otherwise leave it the same. Just to make sure where @@ -805,7 +775,7 @@ absl::Status Overworld::SaveLargeMaps() { // If the area to the left is the bottom right of a large area. if (overworld_maps_[i - 1].large_index() == 1) { RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen1 + (i * 2) + 16, + kOverworldScreenTileMapChangeByScreen1 + (i * 2) + 16, 0x0060)); } } @@ -814,14 +784,14 @@ absl::Status Overworld::SaveLargeMaps() { // Always 0x0080 RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen2 + (i * 2) + 00, 0x0080)); + kOverworldScreenTileMapChangeByScreen2 + (i * 2) + 00, 0x0080)); RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen2 + (i * 2) + 2, 0x0080)); + kOverworldScreenTileMapChangeByScreen2 + (i * 2) + 2, 0x0080)); // Lower always 0x1080 RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen2 + (i * 2) + 16, 0x1080)); + kOverworldScreenTileMapChangeByScreen2 + (i * 2) + 16, 0x1080)); RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen2 + (i * 2) + 18, 0x1080)); + kOverworldScreenTileMapChangeByScreen2 + (i * 2) + 18, 0x1080)); // If the area to the right is a large map, we don't need to add an offset // to it. otherwise leave it the same. Just to make sure where don't try @@ -832,21 +802,21 @@ absl::Status Overworld::SaveLargeMaps() { // If the area to the right is the top left of a large area. if (overworld_maps_[i + 2].large_index() == 0) { RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen2 + (i * 2) + 18, 0x0080)); + kOverworldScreenTileMapChangeByScreen2 + (i * 2) + 18, 0x0080)); } } } // Always 0x1800 RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen3 + (i * 2), 0x1800)); + kOverworldScreenTileMapChangeByScreen3 + (i * 2), 0x1800)); RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen3 + (i * 2) + 16, 0x1800)); + kOverworldScreenTileMapChangeByScreen3 + (i * 2) + 16, 0x1800)); // Right side is always 0x1840 RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen3 + (i * 2) + 2, 0x1840)); + kOverworldScreenTileMapChangeByScreen3 + (i * 2) + 2, 0x1840)); RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen3 + (i * 2) + 18, 0x1840)); + kOverworldScreenTileMapChangeByScreen3 + (i * 2) + 18, 0x1840)); // If the area above is a large map, we don't need to add an offset to it. // otherwise leave it the same. @@ -857,21 +827,21 @@ absl::Status Overworld::SaveLargeMaps() { // If the area just above us is the bottom left of a large area. if (overworld_maps_[i - 8].large_index() == 2) { RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen3 + (i * 2) + 02, 0x1800)); + kOverworldScreenTileMapChangeByScreen3 + (i * 2) + 02, 0x1800)); } } } // Always 0x2000 RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen4 + (i * 2) + 00, 0x2000)); + kOverworldScreenTileMapChangeByScreen4 + (i * 2) + 00, 0x2000)); RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen4 + (i * 2) + 16, 0x2000)); + kOverworldScreenTileMapChangeByScreen4 + (i * 2) + 16, 0x2000)); // Right side always 0x2040 RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen4 + (i * 2) + 2, 0x2040)); + kOverworldScreenTileMapChangeByScreen4 + (i * 2) + 2, 0x2040)); RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen4 + (i * 2) + 18, 0x2040)); + kOverworldScreenTileMapChangeByScreen4 + (i * 2) + 18, 0x2040)); // If the area below is a large map, we don't need to add an offset to it. // otherwise leave it the same. @@ -882,7 +852,7 @@ absl::Status Overworld::SaveLargeMaps() { // If the area just below us is the top left of a large area. if (overworld_maps_[i + 16].large_index() == 0) { RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen4 + (i * 2) + 18, 0x2000)); + kOverworldScreenTileMapChangeByScreen4 + (i * 2) + 18, 0x2000)); } } } @@ -893,21 +863,21 @@ absl::Status Overworld::SaveLargeMaps() { checked_map.emplace_back((i + 9)); } else { - RETURN_IF_ERROR(rom()->WriteByte(overworldMapSize + i, 0x00)); - RETURN_IF_ERROR(rom()->WriteByte(overworldMapSizeHighByte + i, 0x01)); + RETURN_IF_ERROR(rom()->WriteByte(kOverworldMapSize + i, 0x00)); + RETURN_IF_ERROR(rom()->WriteByte(kOverworldMapSizeHighByte + i, 0x01)); - RETURN_IF_ERROR(rom()->WriteByte(overworldScreenSize + i, 0x01)); - RETURN_IF_ERROR(rom()->WriteByte(overworldScreenSize + i + 64, 0x01)); + RETURN_IF_ERROR(rom()->WriteByte(kOverworldScreenSize + i, 0x01)); + RETURN_IF_ERROR(rom()->WriteByte(kOverworldScreenSize + i + 64, 0x01)); RETURN_IF_ERROR( - rom()->WriteByte(OverworldScreenSizeForLoading + i, 0x02)); + rom()->WriteByte(kOverworldScreenSizeForLoading + i, 0x02)); RETURN_IF_ERROR( - rom()->WriteByte(OverworldScreenSizeForLoading + i + 64, 0x02)); + rom()->WriteByte(kOverworldScreenSizeForLoading + i + 64, 0x02)); RETURN_IF_ERROR( - rom()->WriteByte(OverworldScreenSizeForLoading + i + 128, 0x02)); + rom()->WriteByte(kOverworldScreenSizeForLoading + i + 128, 0x02)); RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen1 + (i * 2), 0x0060)); + kOverworldScreenTileMapChangeByScreen1 + (i * 2), 0x0060)); // If the area to the left is a large map, we don't need to add an offset // to it. otherwise leave it the same. @@ -916,25 +886,25 @@ absl::Status Overworld::SaveLargeMaps() { if (overworld_maps_[i - 1].is_large_map()) { if (overworld_maps_[i - 1].large_index() == 3) { RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen1 + (i * 2), 0xF060)); + kOverworldScreenTileMapChangeByScreen1 + (i * 2), 0xF060)); } } } RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen2 + (i * 2), 0x0040)); + kOverworldScreenTileMapChangeByScreen2 + (i * 2), 0x0040)); if (i + 1 < 64 && parent_x_pos != 7) { if (overworld_maps_[i + 1].is_large_map()) { if (overworld_maps_[i + 1].large_index() == 2) { RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen2 + (i * 2), 0xF040)); + kOverworldScreenTileMapChangeByScreen2 + (i * 2), 0xF040)); } } } RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen3 + (i * 2), 0x1800)); + kOverworldScreenTileMapChangeByScreen3 + (i * 2), 0x1800)); // If the area above is a large map, we don't need to add an offset to it. // otherwise leave it the same. @@ -945,13 +915,13 @@ absl::Status Overworld::SaveLargeMaps() { // If we are under the bottom right of the large area. if (overworld_maps_[i - 8].large_index() == 3) { RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen3 + (i * 2), 0x17C0)); + kOverworldScreenTileMapChangeByScreen3 + (i * 2), 0x17C0)); } } } RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen4 + (i * 2), 0x1000)); + kOverworldScreenTileMapChangeByScreen4 + (i * 2), 0x1000)); // If the area below is a large map, we don't need to add an offset to it. // otherwise leave it the same. @@ -962,19 +932,19 @@ absl::Status Overworld::SaveLargeMaps() { // If we are on top of the top right of the large area. if (overworld_maps_[i + 8].large_index() == 1) { RETURN_IF_ERROR(rom()->WriteShort( - OverworldScreenTileMapChangeByScreen4 + (i * 2), 0x0FC0)); + kOverworldScreenTileMapChangeByScreen4 + (i * 2), 0x0FC0)); } } } - RETURN_IF_ERROR(rom()->WriteShort(transition_target_north + (i * 2), + RETURN_IF_ERROR(rom()->WriteShort(kTransitionTargetNorth + (i * 2), (uint16_t)((y_pos * 0x200) - 0xE0))); - RETURN_IF_ERROR(rom()->WriteShort(transition_target_west + (i * 2), + RETURN_IF_ERROR(rom()->WriteShort(kTransitionTargetWest + (i * 2), (uint16_t)((x_pos * 0x200) - 0x100))); - RETURN_IF_ERROR(rom()->WriteShort(overworldTransitionPositionX + (i * 2), + RETURN_IF_ERROR(rom()->WriteShort(kOverworldTransitionPositionX + (i * 2), (x_pos * 0x200))); - RETURN_IF_ERROR(rom()->WriteShort(overworldTransitionPositionY + (i * 2), + RETURN_IF_ERROR(rom()->WriteShort(kOverworldTransitionPositionY + (i * 2), (y_pos * 0x200))); checked_map.emplace_back(i); @@ -996,13 +966,13 @@ absl::Status Overworld::SaveLargeMaps() { } namespace { -std::vector GetAllTile16(OWMapTiles &map_tiles_) { +std::vector GetAllTile16(OverworldMapTiles &map_tiles_) { std::vector all_tile_16; // Ensure it's 64 bits int sx = 0; int sy = 0; int c = 0; - OWBlockset tiles_used; + OverworldBlockset tiles_used; for (int i = 0; i < kNumOverworldMaps; i++) { if (i < 64) { tiles_used = map_tiles_.light_world; @@ -1087,7 +1057,7 @@ absl::Status Overworld::CreateTile32Tilemap() { unique_tiles.size(), LimitOfMap32)); } - if (flags()->kLogToConsole) { + if (core::ExperimentFlags::get().kLogToConsole) { std::cout << "Number of unique Tiles32: " << tiles32_unique_.size() << " Saved:" << tiles32_unique_.size() << " Out of: " << LimitOfMap32 << std::endl; @@ -1154,7 +1124,7 @@ absl::Status Overworld::SaveMap32Expanded() { } absl::Status Overworld::SaveMap32Tiles() { - core::Logger::log("Saving Map32 Tiles"); + core::logf("Saving Map32 Tiles"); constexpr int kMaxUniqueTiles = 0x4540; constexpr int kTilesPer32x32Tile = 6; @@ -1361,7 +1331,7 @@ absl::Status Overworld::SaveMap16Expanded() { } absl::Status Overworld::SaveMap16Tiles() { - core::Logger::log("Saving Map16 Tiles"); + core::logf("Saving Map16 Tiles"); int tpos = kMap16Tiles; // 3760 for (int i = 0; i < NumberOfMap16; i += 1) { @@ -1382,30 +1352,30 @@ absl::Status Overworld::SaveMap16Tiles() { } absl::Status Overworld::SaveEntrances() { - core::Logger::log("Saving Entrances"); + core::logf("Saving Entrances"); for (int i = 0; i < 129; i++) { - RETURN_IF_ERROR( - rom()->WriteShort(OWEntranceMap + (i * 2), all_entrances_[i].map_id_)) - RETURN_IF_ERROR( - rom()->WriteShort(OWEntrancePos + (i * 2), all_entrances_[i].map_pos_)) - RETURN_IF_ERROR(rom()->WriteByte(OWEntranceEntranceId + i, + RETURN_IF_ERROR(rom()->WriteShort(kOverworldEntranceMap + (i * 2), + all_entrances_[i].map_id_)) + RETURN_IF_ERROR(rom()->WriteShort(kOverworldEntrancePos + (i * 2), + all_entrances_[i].map_pos_)) + RETURN_IF_ERROR(rom()->WriteByte(kOverworldEntranceEntranceId + i, all_entrances_[i].entrance_id_)) } for (int i = 0; i < 0x13; i++) { RETURN_IF_ERROR( - rom()->WriteShort(OWHoleArea + (i * 2), all_holes_[i].map_id_)) + rom()->WriteShort(kOverworldHoleArea + (i * 2), all_holes_[i].map_id_)) RETURN_IF_ERROR( - rom()->WriteShort(OWHolePos + (i * 2), all_holes_[i].map_pos_)) - RETURN_IF_ERROR( - rom()->WriteByte(OWHoleEntrance + i, all_holes_[i].entrance_id_)) + rom()->WriteShort(kOverworldHolePos + (i * 2), all_holes_[i].map_pos_)) + RETURN_IF_ERROR(rom()->WriteByte(kOverworldHoleEntrance + i, + all_holes_[i].entrance_id_)) } return absl::OkStatus(); } absl::Status Overworld::SaveExits() { - core::Logger::log("Saving Exits"); + core::logf("Saving Exits"); for (int i = 0; i < 0x4F; i++) { RETURN_IF_ERROR( rom()->WriteShort(OWExitRoomId + (i * 2), all_exits_[i].room_id_)); @@ -1439,7 +1409,7 @@ absl::Status Overworld::SaveExits() { namespace { -bool compareItemsArrays(std::vector item_array1, +bool CompareItemsArrays(std::vector item_array1, std::vector item_array2) { if (item_array1.size() != item_array2.size()) { return false; @@ -1484,12 +1454,10 @@ absl::Status Overworld::SaveItems() { } } - int data_pos = overworldItemsPointers + 0x100; - + int data_pos = kOverworldItemsPointers + 0x100; int item_pointers[128]; int item_pointers_reuse[128]; int empty_pointer = 0; - for (int i = 0; i < 128; i++) { item_pointers_reuse[i] = -1; for (int ci = 0; ci < i; ci++) { @@ -1499,7 +1467,7 @@ absl::Status Overworld::SaveItems() { } // Copy into separator vectors from i to ci, then ci to end - if (compareItemsArrays( + if (CompareItemsArrays( std::vector(room_items[i].begin(), room_items[i].end()), std::vector(room_items[ci].begin(), @@ -1535,14 +1503,14 @@ absl::Status Overworld::SaveItems() { int snesaddr = core::PcToSnes(item_pointers[i]); RETURN_IF_ERROR( - rom()->WriteWord(overworldItemsPointers + (i * 2), snesaddr)); + rom()->WriteWord(kOverworldItemsPointers + (i * 2), snesaddr)); } - if (data_pos > overworldItemsEndData) { + if (data_pos > kOverworldItemsEndData) { return absl::AbortedError("Too many items"); } - if (flags()->kLogToConsole) { + if (core::ExperimentFlags::get().kLogToConsole) { std::cout << "End of Items : " << data_pos << std::endl; } @@ -1550,17 +1518,17 @@ absl::Status Overworld::SaveItems() { } absl::Status Overworld::SaveMapProperties() { - core::Logger::log("Saving Map Properties"); + core::logf("Saving Map Properties"); for (int i = 0; i < 64; i++) { RETURN_IF_ERROR(rom()->WriteByte(kAreaGfxIdPtr + i, overworld_maps_[i].area_graphics())); RETURN_IF_ERROR(rom()->WriteByte(kOverworldMapPaletteIds + i, overworld_maps_[i].area_palette())); - RETURN_IF_ERROR(rom()->WriteByte(overworldSpriteset + i, + RETURN_IF_ERROR(rom()->WriteByte(kOverworldSpriteset + i, overworld_maps_[i].sprite_graphics(0))); - RETURN_IF_ERROR(rom()->WriteByte(overworldSpriteset + 64 + i, + RETURN_IF_ERROR(rom()->WriteByte(kOverworldSpriteset + 64 + i, overworld_maps_[i].sprite_graphics(1))); - RETURN_IF_ERROR(rom()->WriteByte(overworldSpriteset + 128 + i, + RETURN_IF_ERROR(rom()->WriteByte(kOverworldSpriteset + 128 + i, overworld_maps_[i].sprite_graphics(2))); RETURN_IF_ERROR(rom()->WriteByte(kOverworldSpritePaletteIds + i, overworld_maps_[i].sprite_palette(0))); @@ -1573,11 +1541,11 @@ absl::Status Overworld::SaveMapProperties() { for (int i = 64; i < 128; i++) { RETURN_IF_ERROR(rom()->WriteByte(kAreaGfxIdPtr + i, overworld_maps_[i].area_graphics())); - RETURN_IF_ERROR(rom()->WriteByte(overworldSpriteset + i, + RETURN_IF_ERROR(rom()->WriteByte(kOverworldSpriteset + i, overworld_maps_[i].sprite_graphics(0))); - RETURN_IF_ERROR(rom()->WriteByte(overworldSpriteset + 64 + i, + RETURN_IF_ERROR(rom()->WriteByte(kOverworldSpriteset + 64 + i, overworld_maps_[i].sprite_graphics(1))); - RETURN_IF_ERROR(rom()->WriteByte(overworldSpriteset + 128 + i, + RETURN_IF_ERROR(rom()->WriteByte(kOverworldSpriteset + 128 + i, overworld_maps_[i].sprite_graphics(2))); RETURN_IF_ERROR(rom()->WriteByte(kOverworldMapPaletteIds + i, overworld_maps_[i].area_palette())); @@ -1592,7 +1560,5 @@ absl::Status Overworld::SaveMapProperties() { return absl::OkStatus(); } -} // namespace overworld } // namespace zelda3 -} // namespace app } // namespace yaze diff --git a/src/app/zelda3/overworld/overworld.h b/src/app/zelda3/overworld/overworld.h index 030547ed..2ba5daaf 100644 --- a/src/app/zelda3/overworld/overworld.h +++ b/src/app/zelda3/overworld/overworld.h @@ -10,405 +10,51 @@ #include "app/gfx/snes_tile.h" #include "app/rom.h" #include "app/zelda3/common.h" +#include "app/zelda3/overworld/overworld_entrance.h" +#include "app/zelda3/overworld/overworld_exit.h" +#include "app/zelda3/overworld/overworld_item.h" #include "app/zelda3/overworld/overworld_map.h" #include "app/zelda3/sprite/sprite.h" namespace yaze { -namespace app { namespace zelda3 { -/** - * @namespace yaze::app::zelda3::overworld - * @brief Represents the Overworld data. - */ -namespace overworld { - -constexpr int GravesYTilePos = 0x49968; // short (0x0F entries) -constexpr int GravesXTilePos = 0x49986; // short (0x0F entries) -constexpr int GravesTilemapPos = 0x499A4; // short (0x0F entries) -constexpr int GravesGFX = 0x499C2; // short (0x0F entries) - -constexpr int GravesXPos = 0x4994A; // short (0x0F entries) -constexpr int GravesYLine = 0x4993A; // short (0x08 entries) -constexpr int GravesCountOnY = 0x499E0; // Byte 0x09 entries - +constexpr int GravesYTilePos = 0x49968; // short (0x0F entries) +constexpr int GravesXTilePos = 0x49986; // short (0x0F entries) +constexpr int GravesTilemapPos = 0x499A4; // short (0x0F entries) +constexpr int GravesGFX = 0x499C2; // short (0x0F entries) +constexpr int GravesXPos = 0x4994A; // short (0x0F entries) +constexpr int GravesYLine = 0x4993A; // short (0x08 entries) +constexpr int GravesCountOnY = 0x499E0; // Byte 0x09 entries constexpr int GraveLinkSpecialHole = 0x46DD9; // short constexpr int GraveLinkSpecialStairs = 0x46DE0; // short -// List of secret item names -const std::vector kSecretItemNames = { - "Nothing", // 0 - "Green Rupee", // 1 - "Rock hoarder", // 2 - "Bee", // 3 - "Health pack", // 4 - "Bomb", // 5 - "Heart ", // 6 - "Blue Rupee", // 7 - "Key", // 8 - "Arrow", // 9 - "Bomb", // 10 - "Heart", // 11 - "Magic", // 12 - "Full Magic", // 13 - "Cucco", // 14 - "Green Soldier", // 15 - "Bush Stal", // 16 - "Blue Soldier", // 17 - "Landmine", // 18 - "Heart", // 19 - "Fairy", // 20 - "Heart", // 21 - "Nothing ", // 22 - "Hole", // 23 - "Warp", // 24 - "Staircase", // 25 - "Bombable", // 26 - "Switch" // 27 -}; - -constexpr int overworldItemsPointers = 0xDC2F9; -constexpr int kOverworldItemsAddress = 0xDC8B9; // 1BC2F9 -constexpr int overworldItemsBank = 0xDC8BF; -constexpr int overworldItemsEndData = 0xDC89C; // 0DC89E - -class OverworldItem : public GameEntity { - public: - bool bg2_ = false; - uint8_t id_; - uint8_t game_x_; - uint8_t game_y_; - uint16_t room_map_id_; - int unique_id = 0; - bool deleted = false; - OverworldItem() = default; - - OverworldItem(uint8_t id, uint16_t room_map_id, int x, int y, bool bg2) { - this->id_ = id; - this->x_ = x; - this->y_ = y; - this->bg2_ = bg2; - this->room_map_id_ = room_map_id; - this->map_id_ = room_map_id; - this->entity_id_ = id; - this->entity_type_ = kItem; - - int map_x = room_map_id - ((room_map_id / 8) * 8); - int map_y = room_map_id / 8; - - game_x_ = static_cast(std::abs(x - (map_x * 512)) / 16); - game_y_ = static_cast(std::abs(y - (map_y * 512)) / 16); - } - - void UpdateMapProperties(uint16_t room_map_id) override { - room_map_id_ = room_map_id; - - if (room_map_id_ >= 64) { - room_map_id_ -= 64; - } - - int map_x = room_map_id_ - ((room_map_id_ / 8) * 8); - int map_y = room_map_id_ / 8; - - game_x_ = static_cast(std::abs(x_ - (map_x * 512)) / 16); - game_y_ = static_cast(std::abs(y_ - (map_y * 512)) / 16); - - std::cout << "Item: " << std::hex << std::setw(2) << std::setfill('0') - << static_cast(id_) << " MapId: " << std::hex << std::setw(2) - << std::setfill('0') << static_cast(room_map_id_) - << " X: " << static_cast(game_x_) - << " Y: " << static_cast(game_y_) << std::endl; - } -}; - -constexpr int OWExitRoomId = 0x15D8A; // 0x15E07 Credits sequences -// 105C2 Ending maps -// 105E2 Sprite Group Table for Ending -constexpr int OWExitMapId = 0x15E28; -constexpr int OWExitVram = 0x15E77; -constexpr int OWExitYScroll = 0x15F15; -constexpr int OWExitXScroll = 0x15FB3; -constexpr int OWExitYPlayer = 0x16051; -constexpr int OWExitXPlayer = 0x160EF; -constexpr int OWExitYCamera = 0x1618D; -constexpr int OWExitXCamera = 0x1622B; -constexpr int OWExitDoorPosition = 0x15724; -constexpr int OWExitUnk1 = 0x162C9; -constexpr int OWExitUnk2 = 0x16318; -constexpr int OWExitDoorType1 = 0x16367; -constexpr int OWExitDoorType2 = 0x16405; - -constexpr int OWExitMapIdWhirlpool = 0x16AE5; // JP = ;016849 -constexpr int OWExitVramWhirlpool = 0x16B07; // JP = ;01686B -constexpr int OWExitYScrollWhirlpool = 0x16B29; // JP = ;01688D -constexpr int OWExitXScrollWhirlpool = 0x16B4B; // JP = ;016DE7 -constexpr int OWExitYPlayerWhirlpool = 0x16B6D; // JP = ;016E09 -constexpr int OWExitXPlayerWhirlpool = 0x16B8F; // JP = ;016E2B -constexpr int OWExitYCameraWhirlpool = 0x16BB1; // JP = ;016E4D -constexpr int OWExitXCameraWhirlpool = 0x16BD3; // JP = ;016E6F -constexpr int OWExitUnk1Whirlpool = 0x16BF5; // JP = ;016E91 -constexpr int OWExitUnk2Whirlpool = 0x16C17; // JP = ;016EB3 -constexpr int OWWhirlpoolPosition = 0x16CF8; // JP = ;016F94 - -class OverworldExit : public GameEntity { - public: - uint16_t y_scroll_; - uint16_t x_scroll_; - uchar y_player_; - uchar x_player_; - uchar y_camera_; - uchar x_camera_; - uchar scroll_mod_y_; - uchar scroll_mod_x_; - uint16_t door_type_1_; - uint16_t door_type_2_; - uint16_t room_id_; - uint16_t map_pos_; // Position in the vram - uchar entrance_id_; - uchar area_x_; - uchar area_y_; - bool is_hole_ = false; - bool deleted_ = false; - bool is_automatic_ = false; - bool large_map_ = false; - - OverworldExit() = default; - OverworldExit(uint16_t room_id, uchar map_id, uint16_t vram_location, - uint16_t y_scroll, uint16_t x_scroll, uint16_t player_y, - uint16_t player_x, uint16_t camera_y, uint16_t camera_x, - uchar scroll_mod_y, uchar scroll_mod_x, uint16_t door_type_1, - uint16_t door_type_2, bool deleted = false) - : map_pos_(vram_location), - entrance_id_(0), - area_x_(0), - area_y_(0), - room_id_(room_id), - y_scroll_(y_scroll), - x_scroll_(x_scroll), - y_player_(player_y), - x_player_(player_x), - y_camera_(camera_y), - x_camera_(camera_x), - scroll_mod_y_(scroll_mod_y), - scroll_mod_x_(scroll_mod_x), - door_type_1_(door_type_1), - door_type_2_(door_type_2), - is_hole_(false), - deleted_(deleted) { - // Initialize entity variables - x_ = player_x; - y_ = player_y; - map_id_ = map_id; - entity_type_ = kExit; - - int mapX = (map_id_ - ((map_id_ / 8) * 8)); - int mapY = (map_id_ / 8); - - area_x_ = (uchar)((std::abs(x_ - (mapX * 512)) / 16)); - area_y_ = (uchar)((std::abs(y_ - (mapY * 512)) / 16)); - - if (door_type_1 != 0) { - int p = (door_type_1 & 0x7FFF) >> 1; - entrance_id_ = (uchar)(p % 64); - area_y_ = (uchar)(p >> 6); - } - - if (door_type_2 != 0) { - int p = (door_type_2 & 0x7FFF) >> 1; - entrance_id_ = (uchar)(p % 64); - area_y_ = (uchar)(p >> 6); - } - - if (map_id_ >= 64) { - map_id_ -= 64; - } - - mapX = (map_id_ - ((map_id_ / 8) * 8)); - mapY = (map_id_ / 8); - - area_x_ = (uchar)((std::abs(x_ - (mapX * 512)) / 16)); - area_y_ = (uchar)((std::abs(y_ - (mapY * 512)) / 16)); - - map_pos_ = (uint16_t)((((area_y_) << 6) | (area_x_ & 0x3F)) << 1); - } - - // Overworld overworld - void UpdateMapProperties(uint16_t map_id) override { - map_id_ = map_id; - - int large = 256; - int mapid = map_id; - - if (map_id < 128) { - large = large_map_ ? 768 : 256; - // if (overworld.overworld_map(map_id)->Parent() != map_id) { - // mapid = overworld.overworld_map(map_id)->Parent(); - // } - } - - int mapX = map_id - ((map_id / 8) * 8); - int mapY = map_id / 8; - - area_x_ = (uchar)((std::abs(x_ - (mapX * 512)) / 16)); - area_y_ = (uchar)((std::abs(y_ - (mapY * 512)) / 16)); - - if (map_id >= 64) { - map_id -= 64; - } - - int mapx = (map_id & 7) << 9; - int mapy = (map_id & 56) << 6; - - if (is_automatic_) { - x_ = x_ - 120; - y_ = y_ - 80; - - if (x_ < mapx) { - x_ = mapx; - } - - if (y_ < mapy) { - y_ = mapy; - } - - if (x_ > mapx + large) { - x_ = mapx + large; - } - - if (y_ > mapy + large + 32) { - y_ = mapy + large + 32; - } - - x_camera_ = x_player_ + 0x07; - y_camera_ = y_player_ + 0x1F; - - if (x_camera_ < mapx + 127) { - x_camera_ = mapx + 127; - } - - if (y_camera_ < mapy + 111) { - y_camera_ = mapy + 111; - } - - if (x_camera_ > mapx + 127 + large) { - x_camera_ = mapx + 127 + large; - } - - if (y_camera_ > mapy + 143 + large) { - y_camera_ = mapy + 143 + large; - } - } - - short vram_x_scroll = (short)(x_ - mapx); - short vram_y_scroll = (short)(y_ - mapy); - - map_pos_ = (uint16_t)(((vram_y_scroll & 0xFFF0) << 3) | - ((vram_x_scroll & 0xFFF0) >> 3)); - - std::cout << "Exit: " << room_id_ << " MapId: " << std::hex << mapid - << " X: " << static_cast(area_x_) - << " Y: " << static_cast(area_y_) << std::endl; - } -}; - -constexpr int OWEntranceMap = 0xDB96F; -constexpr int OWEntrancePos = 0xDBA71; -constexpr int OWEntranceEntranceId = 0xDBB73; - -// (0x13 entries, 2 bytes each) modified(less 0x400) -// map16 coordinates for each hole -constexpr int OWHolePos = 0xDB800; - -// (0x13 entries, 2 bytes each) corresponding -// area numbers for each hole -constexpr int OWHoleArea = 0xDB826; - -//(0x13 entries, 1 byte each) corresponding entrance numbers -constexpr int OWHoleEntrance = 0xDB84C; - -// OWEntrances Expansion - -// Instructions for editors -// if byte at (PC) address 0xDB895 == B8 then it is vanilla -// Load normal overworld entrances data -// Otherwise load from the expanded space -// When saving just save in expanded space 256 values for each -// (PC Addresses) - you can find snes address at the orgs below -// 0x0DB35F = (short) Map16 tile address (mapPos in ZS) -// 0x0DB55F = (short) Screen ID (MapID in ZS) -// 0x0DB75F = (byte) Entrance leading to (EntranceID in ZS) - -// *Important* the Screen ID now also require bit 0x8000 (15) being set to tell -// entrance is a hole -class OverworldEntrance : public GameEntity { - public: - uint16_t map_pos_; - uchar entrance_id_; - uchar area_x_; - uchar area_y_; - bool is_hole_ = false; - bool deleted = false; - - OverworldEntrance() = default; - OverworldEntrance(int x, int y, uchar entrance_id, short map_id, - uint16_t map_pos, bool hole) - : map_pos_(map_pos), entrance_id_(entrance_id), is_hole_(hole) { - x_ = x; - y_ = y; - map_id_ = map_id; - entity_id_ = entrance_id; - entity_type_ = kEntrance; - - int mapX = (map_id_ - ((map_id_ / 8) * 8)); - int mapY = (map_id_ / 8); - area_x_ = (uchar)((std::abs(x - (mapX * 512)) / 16)); - area_y_ = (uchar)((std::abs(y - (mapY * 512)) / 16)); - } - - void UpdateMapProperties(uint16_t map_id) override { - map_id_ = map_id; - - if (map_id_ >= 64) { - map_id_ -= 64; - } - - int mapX = (map_id_ - ((map_id_ / 8) * 8)); - int mapY = (map_id_ / 8); - - area_x_ = (uchar)((std::abs(x_ - (mapX * 512)) / 16)); - area_y_ = (uchar)((std::abs(y_ - (mapY * 512)) / 16)); - - map_pos_ = (uint16_t)((((area_y_) << 6) | (area_x_ & 0x3F)) << 1); - } -}; - constexpr int kOverworldMapPaletteIds = 0x7D1C; constexpr int kOverworldSpritePaletteIds = 0x7B41; -constexpr int overworldMapPaletteGroup = 0x75504; -constexpr int overworldSpritePaletteGroup = 0x75580; -constexpr int overworldSpriteset = 0x7A41; -constexpr int overworldSpecialGFXGroup = 0x16821; -constexpr int overworldSpecialPALGroup = 0x16831; -constexpr int overworldSpritesBegining = 0x4C881; -constexpr int overworldSpritesAgahnim = 0x4CA21; -constexpr int overworldSpritesZelda = 0x4C901; +constexpr int kOverworldSpritePaletteGroup = 0x75580; +constexpr int kOverworldSpriteset = 0x7A41; +constexpr int kOverworldSpecialGfxGroup = 0x16821; +constexpr int kOverworldSpecialPalGroup = 0x16831; +constexpr int kOverworldSpritesBeginning = 0x4C881; +constexpr int kOverworldSpritesAgahnim = 0x4CA21; +constexpr int kOverworldSpritesZelda = 0x4C901; constexpr int kAreaGfxIdPtr = 0x7C9C; constexpr int kOverworldMessageIds = 0x3F51D; -constexpr int overworldMusicBegining = 0x14303; -constexpr int overworldMusicZelda = 0x14303 + 0x40; -constexpr int overworldMusicMasterSword = 0x14303 + 0x80; -constexpr int overworldMusicAgahim = 0x14303 + 0xC0; -constexpr int overworldMusicDW = 0x14403; -constexpr int overworldEntranceAllowedTilesLeft = 0xDB8C1; -constexpr int overworldEntranceAllowedTilesRight = 0xDB917; +constexpr int kOverworldMusicBeginning = 0x14303; +constexpr int kOverworldMusicZelda = 0x14303 + 0x40; +constexpr int kOverworldMusicMasterSword = 0x14303 + 0x80; +constexpr int kOverworldMusicAgahnim = 0x14303 + 0xC0; +constexpr int kOverworldMusicDarkWorld = 0x14403; +constexpr int kOverworldEntranceAllowedTilesLeft = 0xDB8C1; +constexpr int kOverworldEntranceAllowedTilesRight = 0xDB917; // 0x00 = small maps, 0x20 = large maps -constexpr int overworldMapSize = 0x12844; +constexpr int kOverworldMapSize = 0x12844; // 0x01 = small maps, 0x03 = large maps -constexpr int overworldMapSizeHighByte = 0x12884; +constexpr int kOverworldMapSizeHighByte = 0x12884; // relative to the WORLD + 0x200 per map // large map that are not == parent id = same position as their parent! @@ -416,22 +62,21 @@ constexpr int overworldMapSizeHighByte = 0x12884; // 0000, 0200, 0400, 0600, 0800, 0A00, 0C00, 0E00 // all Large map would be : // 0000, 0000, 0400, 0400, 0800, 0800, 0C00, 0C00 -constexpr int overworldMapParentId = 0x125EC; -constexpr int overworldTransitionPositionY = 0x128C4; -constexpr int overworldTransitionPositionX = 0x12944; -constexpr int overworldScreenSize = 0x1788D; -constexpr int OverworldScreenSizeForLoading = 0x4C635; +constexpr int kOverworldMapParentId = 0x125EC; +constexpr int kOverworldTransitionPositionY = 0x128C4; +constexpr int kOverworldTransitionPositionX = 0x12944; +constexpr int kOverworldScreenSize = 0x1788D; +constexpr int kOverworldScreenSizeForLoading = 0x4C635; -// constexpr int OverworldScreenTileMapChangeByScreen = 0x12634; -constexpr int OverworldScreenTileMapChangeByScreen1 = 0x12634; -constexpr int OverworldScreenTileMapChangeByScreen2 = 0x126B4; -constexpr int OverworldScreenTileMapChangeByScreen3 = 0x12734; -constexpr int OverworldScreenTileMapChangeByScreen4 = 0x127B4; +constexpr int kOverworldScreenTileMapChangeByScreen1 = 0x12634; +constexpr int kOverworldScreenTileMapChangeByScreen2 = 0x126B4; +constexpr int kOverworldScreenTileMapChangeByScreen3 = 0x12734; +constexpr int kOverworldScreenTileMapChangeByScreen4 = 0x127B4; -constexpr int OverworldMapDataOverflow = 0x130000; +constexpr int kOverworldMapDataOverflow = 0x130000; -constexpr int transition_target_north = 0x13EE2; -constexpr int transition_target_west = 0x13F62; +constexpr int kTransitionTargetNorth = 0x13EE2; +constexpr int kTransitionTargetWest = 0x13F62; constexpr int overworldCustomMosaicASM = 0x1301D0; constexpr int overworldCustomMosaicArray = 0x1301F0; @@ -441,7 +86,14 @@ constexpr int kMap32TileTRExpanded = 0x020000; constexpr int kMap32TileBLExpanded = 0x1F0000; constexpr int kMap32TileBRExpanded = 0x1F8000; constexpr int kMap32TileCountExpanded = 0x0067E0; +constexpr int kMap32ExpandedFlagPos = 0x01772E; // 0x04 +constexpr int kMap16ExpandedFlagPos = 0x02FD28; // 0x0F +constexpr int kOverworldEntranceExpandedFlagPos = 0x0DB895; // 0xB8 +constexpr int kOverworldCompressedMapPos = 0x058000; +constexpr int kOverworldCompressedOverflowPos = 0x137FFF; + +constexpr int kNumTileTypes = 0x200; constexpr int kMap16Tiles = 0x78000; constexpr int kNumOverworldMaps = 160; constexpr int kNumTile16Individual = 4096; @@ -458,7 +110,7 @@ constexpr int NumberOfMap32 = Map32PerScreen * kNumOverworldMaps; * This class is responsible for loading and saving the overworld data, * as well as creating the tilesets and tilemaps for the overworld. */ -class Overworld : public SharedRom, public core::ExperimentFlags { +class Overworld : public SharedRom { public: absl::Status Load(Rom &rom); absl::Status LoadOverworldMaps(); @@ -508,7 +160,7 @@ class Overworld : public SharedRom, public core::ExperimentFlags { } } - OWBlockset &GetMapTiles(int world_type) { + OverworldBlockset &GetMapTiles(int world_type) { switch (world_type) { case 0: return map_tiles_.light_world; @@ -572,12 +224,14 @@ class Overworld : public SharedRom, public core::ExperimentFlags { absl::Status AssembleMap32Tiles(); void AssembleMap16Tiles(); void AssignWorldTiles(int x, int y, int sx, int sy, int tpos, - OWBlockset &world); + OverworldBlockset &world); void OrganizeMapTiles(std::vector &bytes, std::vector &bytes2, int i, int sx, int sy, int &ttpos); absl::Status DecompressAllMapTiles(); + Rom rom_; + bool is_loaded_ = false; bool expanded_tile16_ = false; bool expanded_tile32_ = false; @@ -586,13 +240,11 @@ class Overworld : public SharedRom, public core::ExperimentFlags { int game_state_ = 0; int current_map_ = 0; int current_world_ = 0; - uchar map_parent_[160]; - Rom rom_; - OWMapTiles map_tiles_; - - uint8_t all_tiles_types_[0x200]; + OverworldMapTiles map_tiles_; + std::array map_parent_; + std::array all_tiles_types_; std::vector tiles16_; std::vector tiles32_; std::vector tiles32_list_; @@ -603,9 +255,7 @@ class Overworld : public SharedRom, public core::ExperimentFlags { std::vector all_exits_; std::vector all_items_; std::vector> all_sprites_; - std::vector deleted_entrances_; - std::vector> map_data_p1 = std::vector>(kNumOverworldMaps); std::vector> map_data_p2 = @@ -613,16 +263,13 @@ class Overworld : public SharedRom, public core::ExperimentFlags { std::vector map_pointers1_id = std::vector(kNumOverworldMaps); std::vector map_pointers2_id = std::vector(kNumOverworldMaps); - std::vector map_pointers1 = std::vector(kNumOverworldMaps); std::vector map_pointers2 = std::vector(kNumOverworldMaps); std::vector> usage_stats_; }; -} // namespace overworld } // namespace zelda3 -} // namespace app } // namespace yaze #endif diff --git a/src/app/zelda3/overworld/overworld_entrance.h b/src/app/zelda3/overworld/overworld_entrance.h new file mode 100644 index 00000000..da3c9947 --- /dev/null +++ b/src/app/zelda3/overworld/overworld_entrance.h @@ -0,0 +1,112 @@ +#ifndef YAZE_APP_ZELDA3_OVERWORLD_ENTRANCE_H +#define YAZE_APP_ZELDA3_OVERWORLD_ENTRANCE_H + +#include + +#include "app/core/constants.h" +#include "app/rom.h" +#include "app/zelda3/common.h" + +namespace yaze { +namespace zelda3 { + +constexpr int kOverworldEntranceMap = 0xDB96F; +constexpr int kOverworldEntrancePos = 0xDBA71; +constexpr int kOverworldEntranceEntranceId = 0xDBB73; + +constexpr int kOverworldEntranceMapExpanded = 0x0DB55F; +constexpr int kOverworldEntrancePosExpanded = 0x0DB35F; +constexpr int kOverworldEntranceEntranceIdExpanded = 0x0DB75F; + +// (0x13 entries, 2 bytes each) modified(less 0x400) +// map16 coordinates for each hole +constexpr int kOverworldHolePos = 0xDB800; + +// (0x13 entries, 2 bytes each) corresponding +// area numbers for each hole +constexpr int kOverworldHoleArea = 0xDB826; + +//(0x13 entries, 1 byte each) corresponding entrance numbers +constexpr int kOverworldHoleEntrance = 0xDB84C; + +// OWEntrances Expansion + +// Instructions for editors +// if byte at (PC) address 0xDB895 == B8 then it is vanilla +// Load normal overworld entrances data +// Otherwise load from the expanded space +// When saving just save in expanded space 256 values for each +// (PC Addresses) - you can find snes address at the orgs below +// 0x0DB35F = (short) Map16 tile address (mapPos in ZS) +// 0x0DB55F = (short) Screen ID (MapID in ZS) +// 0x0DB75F = (byte) Entrance leading to (EntranceID in ZS) + +// *Important* the Screen ID now also require bit 0x8000 (15) being set to tell +// entrance is a hole +class OverworldEntrance : public GameEntity { + public: + uint16_t map_pos_; + uchar entrance_id_; + uchar area_x_; + uchar area_y_; + bool is_hole_ = false; + bool deleted = false; + + OverworldEntrance() = default; + OverworldEntrance(int x, int y, uchar entrance_id, short map_id, + uint16_t map_pos, bool hole) + : map_pos_(map_pos), entrance_id_(entrance_id), is_hole_(hole) { + x_ = x; + y_ = y; + map_id_ = map_id; + entity_id_ = entrance_id; + entity_type_ = kEntrance; + + int mapX = (map_id_ - ((map_id_ / 8) * 8)); + int mapY = (map_id_ / 8); + area_x_ = (uchar)((std::abs(x - (mapX * 512)) / 16)); + area_y_ = (uchar)((std::abs(y - (mapY * 512)) / 16)); + } + + void UpdateMapProperties(uint16_t map_id) override { + map_id_ = map_id; + + if (map_id_ >= 64) { + map_id_ -= 64; + } + + int mapX = (map_id_ - ((map_id_ / 8) * 8)); + int mapY = (map_id_ / 8); + + area_x_ = (uchar)((std::abs(x_ - (mapX * 512)) / 16)); + area_y_ = (uchar)((std::abs(y_ - (mapY * 512)) / 16)); + + map_pos_ = (uint16_t)((((area_y_) << 6) | (area_x_ & 0x3F)) << 1); + } +}; +constexpr int kEntranceTileTypePtrLow = 0xDB8BF; +constexpr int kEntranceTileTypePtrHigh = 0xDB917; +constexpr int kNumEntranceTileTypes = 0x2C; + +struct OverworldEntranceTileTypes { + std::array low; + std::array high; +}; + +inline absl::StatusOr LoadEntranceTileTypes( + Rom &rom) { + OverworldEntranceTileTypes tiletypes; + for (int i = 0; i < kNumEntranceTileTypes; i++) { + ASSIGN_OR_RETURN(auto value_low, rom.ReadWord(kEntranceTileTypePtrLow + i)); + tiletypes.low[i] = value_low; + ASSIGN_OR_RETURN(auto value_high, + rom.ReadWord(kEntranceTileTypePtrHigh + i)); + tiletypes.high[i] = value_high; + } + return tiletypes; +} + +} // namespace zelda3 +} // namespace yaze + +#endif diff --git a/src/app/zelda3/overworld/overworld_exit.h b/src/app/zelda3/overworld/overworld_exit.h new file mode 100644 index 00000000..f663b917 --- /dev/null +++ b/src/app/zelda3/overworld/overworld_exit.h @@ -0,0 +1,206 @@ +#ifndef YAZE_APP_ZELDA3_OVERWORLD_EXIT_H +#define YAZE_APP_ZELDA3_OVERWORLD_EXIT_H + +#include +#include + +#include "app/core/constants.h" +#include "app/zelda3/common.h" + +namespace yaze { +namespace zelda3 { + +constexpr int OWExitRoomId = 0x15D8A; // 0x15E07 Credits sequences +// 105C2 Ending maps +// 105E2 Sprite Group Table for Ending +constexpr int OWExitMapId = 0x15E28; +constexpr int OWExitVram = 0x15E77; +constexpr int OWExitYScroll = 0x15F15; +constexpr int OWExitXScroll = 0x15FB3; +constexpr int OWExitYPlayer = 0x16051; +constexpr int OWExitXPlayer = 0x160EF; +constexpr int OWExitYCamera = 0x1618D; +constexpr int OWExitXCamera = 0x1622B; +constexpr int OWExitDoorPosition = 0x15724; +constexpr int OWExitUnk1 = 0x162C9; +constexpr int OWExitUnk2 = 0x16318; +constexpr int OWExitDoorType1 = 0x16367; +constexpr int OWExitDoorType2 = 0x16405; + +constexpr int OWExitMapIdWhirlpool = 0x16AE5; // JP = ;016849 +constexpr int OWExitVramWhirlpool = 0x16B07; // JP = ;01686B +constexpr int OWExitYScrollWhirlpool = 0x16B29; // JP = ;01688D +constexpr int OWExitXScrollWhirlpool = 0x16B4B; // JP = ;016DE7 +constexpr int OWExitYPlayerWhirlpool = 0x16B6D; // JP = ;016E09 +constexpr int OWExitXPlayerWhirlpool = 0x16B8F; // JP = ;016E2B +constexpr int OWExitYCameraWhirlpool = 0x16BB1; // JP = ;016E4D +constexpr int OWExitXCameraWhirlpool = 0x16BD3; // JP = ;016E6F +constexpr int OWExitUnk1Whirlpool = 0x16BF5; // JP = ;016E91 +constexpr int OWExitUnk2Whirlpool = 0x16C17; // JP = ;016EB3 +constexpr int OWWhirlpoolPosition = 0x16CF8; // JP = ;016F94 + +class OverworldExit : public GameEntity { + public: + uint16_t y_scroll_; + uint16_t x_scroll_; + uchar y_player_; + uchar x_player_; + uchar y_camera_; + uchar x_camera_; + uchar scroll_mod_y_; + uchar scroll_mod_x_; + uint16_t door_type_1_; + uint16_t door_type_2_; + uint16_t room_id_; + uint16_t map_pos_; // Position in the vram + uchar entrance_id_; + uchar area_x_; + uchar area_y_; + bool is_hole_ = false; + bool deleted_ = false; + bool is_automatic_ = false; + bool large_map_ = false; + + OverworldExit() = default; + OverworldExit(uint16_t room_id, uchar map_id, uint16_t vram_location, + uint16_t y_scroll, uint16_t x_scroll, uint16_t player_y, + uint16_t player_x, uint16_t camera_y, uint16_t camera_x, + uchar scroll_mod_y, uchar scroll_mod_x, uint16_t door_type_1, + uint16_t door_type_2, bool deleted = false) + : map_pos_(vram_location), + entrance_id_(0), + area_x_(0), + area_y_(0), + room_id_(room_id), + y_scroll_(y_scroll), + x_scroll_(x_scroll), + y_player_(player_y), + x_player_(player_x), + y_camera_(camera_y), + x_camera_(camera_x), + scroll_mod_y_(scroll_mod_y), + scroll_mod_x_(scroll_mod_x), + door_type_1_(door_type_1), + door_type_2_(door_type_2), + is_hole_(false), + deleted_(deleted) { + // Initialize entity variables + x_ = player_x; + y_ = player_y; + map_id_ = map_id; + entity_type_ = kExit; + + int mapX = (map_id_ - ((map_id_ / 8) * 8)); + int mapY = (map_id_ / 8); + + area_x_ = (uchar)((std::abs(x_ - (mapX * 512)) / 16)); + area_y_ = (uchar)((std::abs(y_ - (mapY * 512)) / 16)); + + if (door_type_1 != 0) { + int p = (door_type_1 & 0x7FFF) >> 1; + entrance_id_ = (uchar)(p % 64); + area_y_ = (uchar)(p >> 6); + } + + if (door_type_2 != 0) { + int p = (door_type_2 & 0x7FFF) >> 1; + entrance_id_ = (uchar)(p % 64); + area_y_ = (uchar)(p >> 6); + } + + if (map_id_ >= 64) { + map_id_ -= 64; + } + + mapX = (map_id_ - ((map_id_ / 8) * 8)); + mapY = (map_id_ / 8); + + area_x_ = (uchar)((std::abs(x_ - (mapX * 512)) / 16)); + area_y_ = (uchar)((std::abs(y_ - (mapY * 512)) / 16)); + + map_pos_ = (uint16_t)((((area_y_) << 6) | (area_x_ & 0x3F)) << 1); + } + + // Overworld overworld + void UpdateMapProperties(uint16_t map_id) override { + map_id_ = map_id; + + int large = 256; + int mapid = map_id; + + if (map_id < 128) { + large = large_map_ ? 768 : 256; + // if (overworld.overworld_map(map_id)->Parent() != map_id) { + // mapid = overworld.overworld_map(map_id)->Parent(); + // } + } + + int mapX = map_id - ((map_id / 8) * 8); + int mapY = map_id / 8; + + area_x_ = (uchar)((std::abs(x_ - (mapX * 512)) / 16)); + area_y_ = (uchar)((std::abs(y_ - (mapY * 512)) / 16)); + + if (map_id >= 64) { + map_id -= 64; + } + + int mapx = (map_id & 7) << 9; + int mapy = (map_id & 56) << 6; + + if (is_automatic_) { + x_ = x_ - 120; + y_ = y_ - 80; + + if (x_ < mapx) { + x_ = mapx; + } + + if (y_ < mapy) { + y_ = mapy; + } + + if (x_ > mapx + large) { + x_ = mapx + large; + } + + if (y_ > mapy + large + 32) { + y_ = mapy + large + 32; + } + + x_camera_ = x_player_ + 0x07; + y_camera_ = y_player_ + 0x1F; + + if (x_camera_ < mapx + 127) { + x_camera_ = mapx + 127; + } + + if (y_camera_ < mapy + 111) { + y_camera_ = mapy + 111; + } + + if (x_camera_ > mapx + 127 + large) { + x_camera_ = mapx + 127 + large; + } + + if (y_camera_ > mapy + 143 + large) { + y_camera_ = mapy + 143 + large; + } + } + + short vram_x_scroll = (short)(x_ - mapx); + short vram_y_scroll = (short)(y_ - mapy); + + map_pos_ = (uint16_t)(((vram_y_scroll & 0xFFF0) << 3) | + ((vram_x_scroll & 0xFFF0) >> 3)); + + std::cout << "Exit: " << room_id_ << " MapId: " << std::hex << mapid + << " X: " << static_cast(area_x_) + << " Y: " << static_cast(area_y_) << std::endl; + } +}; + +} // namespace zelda3 +} // namespace yaze + +#endif // YAZE_APP_ZELDA3_OVERWORLD_EXIT_H_ diff --git a/src/app/zelda3/overworld/overworld_item.h b/src/app/zelda3/overworld/overworld_item.h new file mode 100644 index 00000000..e95fb30b --- /dev/null +++ b/src/app/zelda3/overworld/overworld_item.h @@ -0,0 +1,101 @@ +#ifndef YAZE_APP_ZELDA3_OVERWORLD_ITEM_H_ +#define YAZE_APP_ZELDA3_OVERWORLD_ITEM_H_ + +#include +#include +#include +#include +#include + +#include "app/zelda3/common.h" + +namespace yaze { +namespace zelda3 { + +constexpr int kOverworldItemsPointers = 0xDC2F9; +constexpr int kOverworldItemsAddress = 0xDC8B9; // 1BC2F9 +constexpr int kOverworldItemsBank = 0xDC8BF; +constexpr int kOverworldItemsEndData = 0xDC89C; // 0DC89E + +class OverworldItem : public GameEntity { + public: + OverworldItem() = default; + OverworldItem(uint8_t id, uint16_t room_map_id, int x, int y, bool bg2) + : bg2_(bg2), id_(id), room_map_id_(room_map_id) { + x_ = x; + y_ = y; + map_id_ = room_map_id; + entity_id_ = id; + entity_type_ = kItem; + + int map_x = room_map_id - ((room_map_id / 8) * 8); + int map_y = room_map_id / 8; + + game_x_ = static_cast(std::abs(x - (map_x * 512)) / 16); + game_y_ = static_cast(std::abs(y - (map_y * 512)) / 16); + } + + void UpdateMapProperties(uint16_t room_map_id) override { + room_map_id_ = room_map_id; + + if (room_map_id_ >= 64) { + room_map_id_ -= 64; + } + + int map_x = room_map_id_ - ((room_map_id_ / 8) * 8); + int map_y = room_map_id_ / 8; + + game_x_ = static_cast(std::abs(x_ - (map_x * 512)) / 16); + game_y_ = static_cast(std::abs(y_ - (map_y * 512)) / 16); + + std::cout << "Item: " << std::hex << std::setw(2) << std::setfill('0') + << static_cast(id_) << " MapId: " << std::hex << std::setw(2) + << std::setfill('0') << static_cast(room_map_id_) + << " X: " << static_cast(game_x_) + << " Y: " << static_cast(game_y_) << std::endl; + } + + bool bg2_ = false; + uint8_t id_; + uint8_t game_x_; + uint8_t game_y_; + uint16_t room_map_id_; + int unique_id = 0; + bool deleted = false; +}; + +const std::vector kSecretItemNames = { + "Nothing", // 0 + "Green Rupee", // 1 + "Rock hoarder", // 2 + "Bee", // 3 + "Health pack", // 4 + "Bomb", // 5 + "Heart ", // 6 + "Blue Rupee", // 7 + "Key", // 8 + "Arrow", // 9 + "Bomb", // 10 + "Heart", // 11 + "Magic", // 12 + "Full Magic", // 13 + "Cucco", // 14 + "Green Soldier", // 15 + "Bush Stal", // 16 + "Blue Soldier", // 17 + "Landmine", // 18 + "Heart", // 19 + "Fairy", // 20 + "Heart", // 21 + "Nothing ", // 22 + "Hole", // 23 + "Warp", // 24 + "Staircase", // 25 + "Bombable", // 26 + "Switch" // 27 +}; + +} // namespace zelda3 +} // namespace yaze + +#endif // YAZE_APP_ZELDA3_OVERWORLD_ITEM_H_ diff --git a/src/app/zelda3/overworld/overworld_map.cc b/src/app/zelda3/overworld/overworld_map.cc index 3ff96b98..7c0b3ce8 100644 --- a/src/app/zelda3/overworld/overworld_map.cc +++ b/src/app/zelda3/overworld/overworld_map.cc @@ -10,9 +10,7 @@ #include "app/zelda3/overworld/overworld.h" namespace yaze { -namespace app { namespace zelda3 { -namespace overworld { OverworldMap::OverworldMap(int index, Rom& rom, bool load_custom_data) : index_(index), parent_(index), rom_(rom) { @@ -32,14 +30,14 @@ OverworldMap::OverworldMap(int index, Rom& rom, bool load_custom_data) absl::Status OverworldMap::BuildMap(int count, int game_state, int world, std::vector& tiles16, - OWBlockset& world_blockset) { + OverworldBlockset& world_blockset) { game_state_ = game_state; world_ = world; if (large_map_) { if (parent_ != index_ && !initialized_) { if (index_ >= 0x80 && index_ <= 0x8A && index_ != 0x88) { - area_graphics_ = rom_[overworldSpecialGFXGroup + (parent_ - 0x80)]; - area_palette_ = rom_[overworldSpecialPALGroup + 1]; + area_graphics_ = rom_[kOverworldSpecialGfxGroup + (parent_ - 0x80)]; + area_palette_ = rom_[kOverworldSpecialPalGroup + 1]; } else if (index_ == 0x88) { area_graphics_ = 0x51; area_palette_ = 0x00; @@ -64,7 +62,7 @@ absl::Status OverworldMap::BuildMap(int count, int game_state, int world, void OverworldMap::LoadAreaInfo() { if (index_ != 0x80) { if (index_ <= 128) - large_map_ = (rom_[overworldMapSize + (index_ & 0x3F)] != 0); + large_map_ = (rom_[kOverworldMapSize + (index_ & 0x3F)] != 0); else { large_map_ = index_ == 129 || index_ == 130 || index_ == 137 || index_ == 138; @@ -77,14 +75,14 @@ void OverworldMap::LoadAreaInfo() { area_graphics_ = rom_[kAreaGfxIdPtr + parent_]; area_palette_ = rom_[kOverworldMapPaletteIds + parent_]; - area_music_[0] = rom_[overworldMusicBegining + parent_]; - area_music_[1] = rom_[overworldMusicZelda + parent_]; - area_music_[2] = rom_[overworldMusicMasterSword + parent_]; - area_music_[3] = rom_[overworldMusicAgahim + parent_]; + area_music_[0] = rom_[kOverworldMusicBeginning + parent_]; + area_music_[1] = rom_[kOverworldMusicZelda + parent_]; + area_music_[2] = rom_[kOverworldMusicMasterSword + parent_]; + area_music_[3] = rom_[kOverworldMusicAgahnim + parent_]; - sprite_graphics_[0] = rom_[overworldSpriteset + parent_]; - sprite_graphics_[1] = rom_[overworldSpriteset + parent_ + 0x40]; - sprite_graphics_[2] = rom_[overworldSpriteset + parent_ + 0x80]; + sprite_graphics_[0] = rom_[kOverworldSpriteset + parent_]; + sprite_graphics_[1] = rom_[kOverworldSpriteset + parent_ + 0x40]; + sprite_graphics_[2] = rom_[kOverworldSpriteset + parent_ + 0x80]; sprite_palette_[0] = rom_[kOverworldSpritePaletteIds + parent_]; sprite_palette_[1] = rom_[kOverworldSpritePaletteIds + parent_ + 0x40]; @@ -92,11 +90,11 @@ void OverworldMap::LoadAreaInfo() { } else if (index_ < 0x80) { area_graphics_ = rom_[kAreaGfxIdPtr + parent_]; area_palette_ = rom_[kOverworldMapPaletteIds + parent_]; - area_music_[0] = rom_[overworldMusicDW + (parent_ - 64)]; + area_music_[0] = rom_[kOverworldMusicDarkWorld + (parent_ - 64)]; - sprite_graphics_[0] = rom_[overworldSpriteset + parent_ + 0x80]; - sprite_graphics_[1] = rom_[overworldSpriteset + parent_ + 0x80]; - sprite_graphics_[2] = rom_[overworldSpriteset + parent_ + 0x80]; + sprite_graphics_[0] = rom_[kOverworldSpriteset + parent_ + 0x80]; + sprite_graphics_[1] = rom_[kOverworldSpriteset + parent_ + 0x80]; + sprite_graphics_[2] = rom_[kOverworldSpriteset + parent_ + 0x80]; sprite_palette_[0] = rom_[kOverworldSpritePaletteIds + parent_ + 0x80]; sprite_palette_[1] = rom_[kOverworldSpritePaletteIds + parent_ + 0x80]; @@ -125,11 +123,11 @@ void OverworldMap::LoadAreaInfo() { parent_ = 129; } - area_palette_ = rom_[overworldSpecialPALGroup + parent_ - 0x80]; + area_palette_ = rom_[kOverworldSpecialPalGroup + parent_ - 0x80]; if ((index_ >= 0x80 && index_ <= 0x8A && index_ != 0x88) || index_ == 0x94) { - area_graphics_ = rom_[overworldSpecialGFXGroup + (parent_ - 0x80)]; - area_palette_ = rom_[overworldSpecialPALGroup + 1]; + area_graphics_ = rom_[kOverworldSpecialGfxGroup + (parent_ - 0x80)]; + area_palette_ = rom_[kOverworldSpecialPalGroup + 1]; } else if (index_ == 0x88) { area_graphics_ = 0x51; area_palette_ = 0x00; @@ -139,9 +137,9 @@ void OverworldMap::LoadAreaInfo() { area_palette_ = rom_[kOverworldMapPaletteIds + parent_]; } - sprite_graphics_[0] = rom_[overworldSpriteset + parent_ + 0x80]; - sprite_graphics_[1] = rom_[overworldSpriteset + parent_ + 0x80]; - sprite_graphics_[2] = rom_[overworldSpriteset + parent_ + 0x80]; + sprite_graphics_[0] = rom_[kOverworldSpriteset + parent_ + 0x80]; + sprite_graphics_[1] = rom_[kOverworldSpriteset + parent_ + 0x80]; + sprite_graphics_[2] = rom_[kOverworldSpriteset + parent_ + 0x80]; sprite_palette_[0] = rom_[kOverworldSpritePaletteIds + parent_ + 0x80]; sprite_palette_[1] = rom_[kOverworldSpritePaletteIds + parent_ + 0x80]; @@ -338,8 +336,6 @@ void OverworldMap::SetupCustomTileset(uint8_t asm_version) { rom_[OverworldCustomSubscreenOverlayArray + (index_ * 2)]; } -// ============================================================================ - void OverworldMap::LoadMainBlocksetId() { if (parent_ < 0x40) { main_gfx_id_ = 0x20; @@ -379,12 +375,12 @@ void OverworldMap::LoadMainBlocksets() { // of the 5A sheet, so this will need some special manipulation to make work // during the BuildBitmap step (or a new one specifically for animating). void OverworldMap::DrawAnimatedTiles() { - std::cout << "static_graphics_[6] = " - << core::UppercaseHexByte(static_graphics_[6]) << std::endl; - std::cout << "static_graphics_[7] = " - << core::UppercaseHexByte(static_graphics_[7]) << std::endl; - std::cout << "static_graphics_[8] = " - << core::UppercaseHexByte(static_graphics_[8]) << std::endl; + std::cout << "static_graphics_[6] = " << core::HexByte(static_graphics_[6]) + << std::endl; + std::cout << "static_graphics_[7] = " << core::HexByte(static_graphics_[7]) + << std::endl; + std::cout << "static_graphics_[8] = " << core::HexByte(static_graphics_[8]) + << std::endl; if (static_graphics_[7] == 0x5B) { static_graphics_[7] = 0x5A; } else { @@ -438,7 +434,7 @@ absl::Status SetColorsPalette(Rom& rom, int index, gfx::SnesPalette& current, // contains 7 colors width wide) There is 16 color per line so 16*Y // Left side of the palette - Main, Animated - std::vector new_palette(256); + std::array new_palette = {}; // Main Palette, Location 0,2 : 35 colors [7x5] int k = 0; @@ -489,8 +485,7 @@ absl::Status SetColorsPalette(Rom& rom, int index, gfx::SnesPalette& current, k = 0; for (int y = 8; y < 9; y++) { for (int x = 1; x < 8; x++) { - auto pal_group = rom.palette_group().sprites_aux1; - new_palette[x + (16 * y)] = pal_group[1][k]; + new_palette[x + (16 * y)] = rom.palette_group().sprites_aux1[1][k]; k++; } } @@ -499,8 +494,7 @@ absl::Status SetColorsPalette(Rom& rom, int index, gfx::SnesPalette& current, k = 0; for (int y = 8; y < 9; y++) { for (int x = 9; x < 16; x++) { - auto pal_group = rom.palette_group().sprites_aux3; - new_palette[x + (16 * y)] = pal_group[0][k]; + new_palette[x + (16 * y)] = rom.palette_group().sprites_aux3[0][k]; k++; } } @@ -509,8 +503,7 @@ absl::Status SetColorsPalette(Rom& rom, int index, gfx::SnesPalette& current, k = 0; for (int y = 9; y < 13; y++) { for (int x = 1; x < 16; x++) { - auto pal_group = rom.palette_group().global_sprites; - new_palette[x + (16 * y)] = pal_group[0][k]; + new_palette[x + (16 * y)] = rom.palette_group().global_sprites[0][k]; k++; } } @@ -537,8 +530,7 @@ absl::Status SetColorsPalette(Rom& rom, int index, gfx::SnesPalette& current, k = 0; for (int y = 15; y < 16; y++) { for (int x = 1; x < 16; x++) { - auto pal_group = rom.palette_group().armors; - new_palette[x + (16 * y)] = pal_group[0][k]; + new_palette[x + (16 * y)] = rom.palette_group().armors[0][k]; k++; } } @@ -557,7 +549,7 @@ absl::StatusOr OverworldMap::GetPalette( const gfx::PaletteGroup& palette_group, int index, int previous_index, int limit) { if (index == 255) { - index = rom_[rom_.version_constants().overworldMapPaletteGroup + + index = rom_[rom_.version_constants().kOverworldMapPaletteGroup + (previous_index * 4)]; } if (index >= limit) { @@ -575,15 +567,15 @@ absl::Status OverworldMap::LoadPalette() { area_palette_ = std::min((int)area_palette_, 0xA3); uchar pal0 = 0; - uchar pal1 = rom_[rom_.version_constants().overworldMapPaletteGroup + + uchar pal1 = rom_[rom_.version_constants().kOverworldMapPaletteGroup + (area_palette_ * 4)]; - uchar pal2 = rom_[rom_.version_constants().overworldMapPaletteGroup + + uchar pal2 = rom_[rom_.version_constants().kOverworldMapPaletteGroup + (area_palette_ * 4) + 1]; - uchar pal3 = rom_[rom_.version_constants().overworldMapPaletteGroup + + uchar pal3 = rom_[rom_.version_constants().kOverworldMapPaletteGroup + (area_palette_ * 4) + 2]; uchar pal4 = - rom_[overworldSpritePaletteGroup + (sprite_palette_[game_state_] * 2)]; - uchar pal5 = rom_[overworldSpritePaletteGroup + + rom_[kOverworldSpritePaletteGroup + (sprite_palette_[game_state_] * 2)]; + uchar pal5 = rom_[kOverworldSpritePaletteGroup + (sprite_palette_[game_state_] * 2) + 1]; auto grass_pal_group = rom_.palette_group().grass; @@ -597,7 +589,7 @@ absl::Status OverworldMap::LoadPalette() { // Additional handling of `pal3` and `parent_` if (pal3 == 255) { - pal3 = rom_[rom_.version_constants().overworldMapPaletteGroup + + pal3 = rom_[rom_.version_constants().kOverworldMapPaletteGroup + (previousPalId * 4) + 2]; } @@ -720,7 +712,7 @@ absl::Status OverworldMap::BuildTiles16Gfx(std::vector& tiles16, return absl::OkStatus(); } -absl::Status OverworldMap::BuildBitmap(OWBlockset& world_blockset) { +absl::Status OverworldMap::BuildBitmap(OverworldBlockset& world_blockset) { if (bitmap_data_.size() != 0) { bitmap_data_.clear(); } @@ -743,7 +735,5 @@ absl::Status OverworldMap::BuildBitmap(OWBlockset& world_blockset) { return absl::OkStatus(); } -} // namespace overworld } // namespace zelda3 -} // namespace app } // namespace yaze diff --git a/src/app/zelda3/overworld/overworld_map.h b/src/app/zelda3/overworld/overworld_map.h index f4158a04..9575bd68 100644 --- a/src/app/zelda3/overworld/overworld_map.h +++ b/src/app/zelda3/overworld/overworld_map.h @@ -1,6 +1,7 @@ #ifndef YAZE_APP_ZELDA3_OVERWORLD_MAP_H #define YAZE_APP_ZELDA3_OVERWORLD_MAP_H +#include #include #include #include @@ -12,9 +13,7 @@ #include "app/zelda3/common.h" namespace yaze { -namespace app { namespace zelda3 { -namespace overworld { static constexpr int kTileOffsets[] = {0, 8, 4096, 4104}; @@ -56,23 +55,37 @@ constexpr int OverworldCustomTileGFXGroupArray = 0x140480; // 1 byte, not 0 if enabled constexpr int OverworldCustomTileGFXGroupEnabled = 0x140148; +/** + * @brief Represents tile32 data for the overworld. + */ +using OverworldBlockset = std::vector>; + +/** + * @brief Overworld map tile32 data. + */ +typedef struct OverworldMapTiles { + OverworldBlockset light_world; // 64 maps + OverworldBlockset dark_world; // 64 maps + OverworldBlockset special_world; // 32 maps +} OverworldMapTiles; + /** * @brief Represents a single Overworld map screen. */ -class OverworldMap : public GfxContext { +class OverworldMap : public gfx::GfxContext { public: OverworldMap() = default; OverworldMap(int index, Rom& rom, bool load_custom_data = false); absl::Status BuildMap(int count, int game_state, int world, std::vector& tiles16, - OWBlockset& world_blockset); + OverworldBlockset& world_blockset); void LoadAreaGraphics(); absl::Status LoadPalette(); absl::Status BuildTileset(); absl::Status BuildTiles16Gfx(std::vector& tiles16, int count); - absl::Status BuildBitmap(OWBlockset& world_blockset); + absl::Status BuildBitmap(OverworldBlockset& world_blockset); void DrawAnimatedTiles(); @@ -150,10 +163,11 @@ class OverworldMap : public GfxContext { int index, int previous_index, int limit); + Rom rom_; + bool built_ = false; bool large_map_ = false; bool initialized_ = false; - bool mosaic_ = false; int index_ = 0; // Map index @@ -169,25 +183,22 @@ class OverworldMap : public GfxContext { uint8_t animated_gfx_ = 0; // Custom Overworld Animated ID uint16_t subscreen_overlay_ = 0; // Custom Overworld Subscreen Overlay ID - uint8_t custom_gfx_ids_[8]; - uchar sprite_graphics_[3]; - uchar sprite_palette_[3]; - uchar area_music_[4]; - uchar static_graphics_[16]; + std::array custom_gfx_ids_; + std::array sprite_graphics_; + std::array sprite_palette_; + std::array area_music_; + std::array static_graphics_; - Rom rom_; std::vector all_gfx_; std::vector current_blockset_; std::vector current_gfx_; std::vector bitmap_data_; - OWMapTiles map_tiles_; + OverworldMapTiles map_tiles_; gfx::SnesPalette current_palette_; }; -} // namespace overworld } // namespace zelda3 -} // namespace app } // namespace yaze #endif diff --git a/src/app/zelda3/screen/dungeon_map.cc b/src/app/zelda3/screen/dungeon_map.cc new file mode 100644 index 00000000..b2cac1ff --- /dev/null +++ b/src/app/zelda3/screen/dungeon_map.cc @@ -0,0 +1,53 @@ +#include "dungeon_map.h" + +#include +#include + +#include "app/core/platform/file_dialog.h" +#include "app/core/platform/renderer.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_tile.h" + +namespace yaze { +namespace zelda3 { +namespace screen { + +absl::Status LoadDungeonMapGfxFromBinary(Rom &rom, + std::array &sheets, + gfx::Tilesheet &tile16_sheet, + std::vector &gfx_bin_data) { + std::string bin_file = core::FileDialogWrapper::ShowOpenFileDialog(); + if (bin_file.empty()) { + return absl::InternalError("No file selected"); + } + + std::ifstream file(bin_file, std::ios::binary); + if (file.is_open()) { + // Read the gfx data into a buffer + std::vector bin_data((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + auto converted_bin = gfx::SnesTo8bppSheet(bin_data, 4, 4); + gfx_bin_data = converted_bin; + tile16_sheet.clear(); + if (LoadDungeonMapTile16(converted_bin, true).ok()) { + std::vector> gfx_sheets; + for (int i = 0; i < 4; i++) { + gfx_sheets.emplace_back(converted_bin.begin() + (i * 0x1000), + converted_bin.begin() + ((i + 1) * 0x1000)); + sheets[i] = gfx::Bitmap(128, 32, 8, gfx_sheets[i]); + sheets[i].ApplyPalette(*rom.mutable_dungeon_palette(3)); + core::Renderer::GetInstance().RenderBitmap(&sheets[i]); + } + } else { + return absl::InternalError("Failed to load dungeon map tile16"); + } + file.close(); + } + + return absl::OkStatus(); +} + +} // namespace screen +} // namespace zelda3 + +} // namespace yaze diff --git a/src/app/zelda3/screen/dungeon_map.h b/src/app/zelda3/screen/dungeon_map.h index eb4f19f8..f8a73f38 100644 --- a/src/app/zelda3/screen/dungeon_map.h +++ b/src/app/zelda3/screen/dungeon_map.h @@ -4,8 +4,11 @@ #include #include +#include "absl/status/status.h" +#include "app/gfx/tilesheet.h" +#include "app/rom.h" + namespace yaze { -namespace app { namespace zelda3 { namespace screen { @@ -38,8 +41,8 @@ struct DungeonMap { DungeonMap(unsigned short boss_room, unsigned char nbr_of_floor, unsigned char nbr_of_basement, - const std::vector>& floor_rooms, - const std::vector>& floor_gfx) + const std::vector> &floor_rooms, + const std::vector> &floor_gfx) : boss_room(boss_room), nbr_of_floor(nbr_of_floor), nbr_of_basement(nbr_of_basement), @@ -47,9 +50,17 @@ struct DungeonMap { floor_gfx(floor_gfx) {} }; +absl::Status LoadDungeonMapTile16(const std::vector &gfx_data, + bool bin_mode); + +absl::Status LoadDungeonMapGfxFromBinary(Rom &rom, + std::array &sheets, + gfx::Tilesheet &tile16_sheet, + std::vector &gfx_bin_data); + } // namespace screen } // namespace zelda3 -} // namespace app + } // namespace yaze #endif // YAZE_APP_ZELDA3_SCREEN_DUNGEON_MAP_H diff --git a/src/app/zelda3/screen/inventory.cc b/src/app/zelda3/screen/inventory.cc index d494daaa..caf4208c 100644 --- a/src/app/zelda3/screen/inventory.cc +++ b/src/app/zelda3/screen/inventory.cc @@ -7,7 +7,6 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace zelda3 { namespace screen { @@ -75,7 +74,7 @@ absl::Status Inventory::Create() { absl::Status Inventory::BuildTileset() { tilesheets_.reserve(6 * 0x2000); for (int i = 0; i < 6 * 0x2000; i++) tilesheets_.push_back(0xFF); - ASSIGN_OR_RETURN(tilesheets_, rom()->Load2BppGraphics()) + ASSIGN_OR_RETURN(tilesheets_, Load2BppGraphics(*rom())) std::vector test; for (int i = 0; i < 0x4000; i++) { test_.push_back(tilesheets_[i]); @@ -93,5 +92,5 @@ absl::Status Inventory::BuildTileset() { } // namespace screen } // namespace zelda3 -} // namespace app + } // namespace yaze \ No newline at end of file diff --git a/src/app/zelda3/screen/inventory.h b/src/app/zelda3/screen/inventory.h index 4cae3a47..9ca0ff0d 100644 --- a/src/app/zelda3/screen/inventory.h +++ b/src/app/zelda3/screen/inventory.h @@ -8,7 +8,6 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace zelda3 { namespace screen { @@ -40,7 +39,7 @@ class Inventory : public SharedRom { } // namespace screen } // namespace zelda3 -} // namespace app + } // namespace yaze #endif // YAZE_APP_ZELDA3_INVENTORY_H diff --git a/src/app/zelda3/screen/title_screen.cc b/src/app/zelda3/screen/title_screen.cc index db2f4b3c..f0a06dee 100644 --- a/src/app/zelda3/screen/title_screen.cc +++ b/src/app/zelda3/screen/title_screen.cc @@ -7,7 +7,6 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace zelda3 { namespace screen { @@ -124,5 +123,5 @@ void TitleScreen::LoadTitleScreen() { } // namespace screen } // namespace zelda3 -} // namespace app + } // namespace yaze diff --git a/src/app/zelda3/screen/title_screen.h b/src/app/zelda3/screen/title_screen.h index 49befd5f..7b08d332 100644 --- a/src/app/zelda3/screen/title_screen.h +++ b/src/app/zelda3/screen/title_screen.h @@ -9,7 +9,6 @@ #include "app/rom.h" namespace yaze { -namespace app { namespace zelda3 { namespace screen { @@ -77,7 +76,7 @@ class TitleScreen { } // namespace screen } // namespace zelda3 -} // namespace app + } // namespace yaze #endif // YAZE_APP_ZELDA3_SCREEN_H diff --git a/src/app/zelda3/sprite/overlord.h b/src/app/zelda3/sprite/overlord.h index 44314104..e559eccf 100644 --- a/src/app/zelda3/sprite/overlord.h +++ b/src/app/zelda3/sprite/overlord.h @@ -4,7 +4,6 @@ #include namespace yaze { -namespace app { namespace zelda3 { static const std::string kOverlordNames[] = { @@ -37,7 +36,6 @@ static const std::string kOverlordNames[] = { }; } -} // namespace app } // namespace yaze #endif // YAZE_APP_ZELDA3_SPRITE_OVERLORD_H diff --git a/src/app/zelda3/sprite/sprite.cc b/src/app/zelda3/sprite/sprite.cc index a9f1d940..32c2d1ec 100644 --- a/src/app/zelda3/sprite/sprite.cc +++ b/src/app/zelda3/sprite/sprite.cc @@ -3,7 +3,6 @@ #include "app/zelda3/overworld/overworld.h" namespace yaze { -namespace app { namespace zelda3 { void Sprite::UpdateMapProperties(uint16_t map_id) { @@ -708,7 +707,9 @@ void Sprite::Draw() { } else if (id_ == 0x8E) // Terrorpin { DrawSpriteTile((x * 16), (y * 16), 14, 24, 12); - } else if (id_ == 0x8F) // Slime + } + + if (id_ == 0x8F) // Slime { DrawSpriteTile((x * 16), (y * 16), 0, 20, 12); } else if (id_ == 0x90) // Wall master @@ -911,5 +912,4 @@ void Sprite::DrawSpriteTile(int x, int y, int srcx, int srcy, int pal, } } // namespace zelda3 -} // namespace app } // namespace yaze diff --git a/src/app/zelda3/sprite/sprite.h b/src/app/zelda3/sprite/sprite.h index 02fcc39b..c0a8896b 100644 --- a/src/app/zelda3/sprite/sprite.h +++ b/src/app/zelda3/sprite/sprite.h @@ -11,7 +11,6 @@ #include "app/zelda3/sprite/overlord.h" namespace yaze { -namespace app { namespace zelda3 { static const std::string kSpriteDefaultNames[]{ @@ -391,7 +390,6 @@ class Sprite : public GameEntity { }; } // namespace zelda3 -} // namespace app } // namespace yaze #endif diff --git a/src/app/zelda3/sprite/sprite_builder.cc b/src/app/zelda3/sprite/sprite_builder.cc index 8b069411..c329fe8e 100644 --- a/src/app/zelda3/sprite/sprite_builder.cc +++ b/src/app/zelda3/sprite/sprite_builder.cc @@ -4,7 +4,6 @@ #include namespace yaze { -namespace app { namespace zelda3 { SpriteBuilder SpriteBuilder::Create(const std::string& spriteName) { @@ -149,5 +148,4 @@ SpriteInstruction SpriteInstruction::JumpToFunction( } } // namespace zelda3 -} // namespace app } // namespace yaze diff --git a/src/app/zelda3/sprite/sprite_builder.h b/src/app/zelda3/sprite/sprite_builder.h index 313c2eee..d5ccc88d 100644 --- a/src/app/zelda3/sprite/sprite_builder.h +++ b/src/app/zelda3/sprite/sprite_builder.h @@ -6,7 +6,6 @@ #include namespace yaze { -namespace app { namespace zelda3 { class SpriteInstruction { @@ -125,7 +124,6 @@ class SpriteBuilder { }; } // namespace zelda3 -} // namespace app } // namespace yaze #endif // YAZE_APP_ZELDA3_SPRITE_SPRITE_BUILDER_H_ \ No newline at end of file diff --git a/src/cli/handlers/compress.cc b/src/cli/handlers/compress.cc index 95b014ec..d9795576 100644 --- a/src/cli/handlers/compress.cc +++ b/src/cli/handlers/compress.cc @@ -1,4 +1,4 @@ -#include "cli/command.h" +#include "cli/z3ed.h" namespace yaze { namespace cli { diff --git a/src/cli/handlers/patch.cc b/src/cli/handlers/patch.cc index 4aedf52e..402bf2da 100644 --- a/src/cli/handlers/patch.cc +++ b/src/cli/handlers/patch.cc @@ -1,4 +1,6 @@ -#include "cli/command.h" +#include "cli/z3ed.h" + +#include "asar-dll-bindings/c/asar.h" namespace yaze { namespace cli { @@ -15,7 +17,7 @@ absl::Status ApplyPatch::handle(const std::vector& arg_vec) { // Apply patch std::vector patched; - app::core::ApplyBpsPatch(source, patch, patched); + core::ApplyBpsPatch(source, patch, patched); // Save patched file std::ofstream patched_rom("patched.sfc", std::ios::binary); @@ -48,7 +50,7 @@ absl::Status CreatePatch::handle(const std::vector& arg_vec) { std::vector target; std::vector patch; // Create patch - app::core::CreateBpsPatch(source, target, patch); + core::CreateBpsPatch(source, target, patch); // Save patch to file // std::ofstream patchFile("patch.bps", ios::binary); diff --git a/src/cli/handlers/tile16_transfer.cc b/src/cli/handlers/tile16_transfer.cc index 9b874919..991b6ee8 100644 --- a/src/cli/handlers/tile16_transfer.cc +++ b/src/cli/handlers/tile16_transfer.cc @@ -5,13 +5,11 @@ #include "app/core/common.h" #include "app/core/constants.h" #include "app/rom.h" -#include "cli/command.h" +#include "cli/z3ed.h" namespace yaze { namespace cli { -using namespace app; - absl::Status Tile16Transfer::handle(const std::vector& arg_vec) { // Load the source rom RETURN_IF_ERROR(rom_.LoadFromFile(arg_vec[0])) diff --git a/src/cli/tui.cc b/src/cli/tui.cc new file mode 100644 index 00000000..77857a7c --- /dev/null +++ b/src/cli/tui.cc @@ -0,0 +1,70 @@ +#include "tui.h" + +#include +#include +#include +#include + +namespace yaze { +namespace cli { + +using namespace ftxui; + +namespace { +bool HandleInput(ftxui::Event &event, int &selected) { + if (event == Event::ArrowDown || event == Event::Character('j')) { + selected++; + return true; + } + if (event == Event::ArrowUp || event == Event::Character('k')) { + if (selected != 0) + selected--; + return true; + } + return false; +} +} // namespace + +void ShowMain() { + Context context; + + std::vector entries = { + "Palette Editor", "Tile Editor", "Sprite Editor", "Map Editor", "Exit", + }; + int selected = 0; + + MenuOption option; + auto menu = Menu(&entries, &selected, option); + menu = CatchEvent( + menu, [&selected](Event event) { return HandleInput(event, selected); }); + + auto layout = Container::Vertical({ + menu, + }); + auto main_component = Renderer(layout, [&] { + return vbox({ + menu->Render(), + }); + }); + auto screen = ScreenInteractive::FitComponent(); + + // Exit the loop when "Exit" is selected + main_component = CatchEvent(main_component, [&](Event event) { + if (event == Event::Return && selected == 4) { + screen.ExitLoopClosure()(); + return true; + } + if (event == Event::Character('q')) { + screen.ExitLoopClosure()(); + return true; + } + return false; + }); + + screen.Loop(main_component); +} + +void DrawPaletteEditor(Rom *rom) { auto palette_groups = rom->palette_group(); } + +} // namespace cli +} // namespace yaze diff --git a/src/cli/tui.h b/src/cli/tui.h new file mode 100644 index 00000000..70de8329 --- /dev/null +++ b/src/cli/tui.h @@ -0,0 +1,27 @@ +#ifndef YAZE_CLI_TUI_H +#define YAZE_CLI_TUI_H + +#include +#include +#include + +#include "app/rom.h" + +namespace yaze { +namespace cli { + +struct Context { + bool is_loaded = false; + + ftxui::Component layout; + ftxui::Component main_component; +}; + +void ShowMain(); + +void DrawPaletteEditor(Rom *rom); + +} // namespace cli +} // namespace yaze + +#endif // YAZE_CLI_TUI_H diff --git a/src/cli/z3ed.cc b/src/cli/z3ed.cc index 1f7319bf..c8ce7ac5 100644 --- a/src/cli/z3ed.cc +++ b/src/cli/z3ed.cc @@ -7,13 +7,13 @@ #include "absl/flags/flag.h" #include "app/core/constants.h" -#include "cli/command.h" +#include "cli/z3ed.h" +#include "tui.h" ABSL_FLAG(bool, verbose, false, "Enable verbose output"); ABSL_FLAG(bool, debug, false, "Enable debug output"); namespace yaze { - /** * @namespace yaze::cli * @brief Namespace for the command line interface. @@ -21,12 +21,13 @@ namespace yaze { namespace cli { namespace { +ColorModifier ylw(ColorCode::FG_YELLOW); +ColorModifier mag(ColorCode::FG_MAGENTA); +ColorModifier red(ColorCode::FG_RED); +ColorModifier reset(ColorCode::FG_RESET); +ColorModifier underline(ColorCode::FG_UNDERLINE); + void HelpCommand() { - ColorModifier ylw(ColorCode::FG_YELLOW); - ColorModifier mag(ColorCode::FG_MAGENTA); - ColorModifier red(ColorCode::FG_RED); - ColorModifier reset(ColorCode::FG_RESET); - ColorModifier underline(ColorCode::FG_UNDERLINE); std::cout << "\n"; std::cout << ylw << " ▲ " << reset << " z3ed\n"; std::cout << ylw << "▲ ▲ " << reset << " by " << mag << "scawful\n\n" @@ -57,7 +58,7 @@ void HelpCommand() { std::cout << "\n"; } -int RunCommandHandler(int argc, char* argv[]) { +int RunCommandHandler(int argc, char *argv[]) { if (argc == 1) { HelpCommand(); return EXIT_SUCCESS; @@ -88,6 +89,8 @@ int RunCommandHandler(int argc, char* argv[]) { } // namespace cli } // namespace yaze -int main(int argc, char* argv[]) { - return yaze::cli::RunCommandHandler(argc, argv); +int main(int argc, char *argv[]) { + yaze::cli::ShowMain(); + return EXIT_SUCCESS; + // return yaze::cli::RunCommandHandler(argc, argv); } diff --git a/src/cli/z3ed.cmake b/src/cli/z3ed.cmake index fd3b7772..592aa217 100644 --- a/src/cli/z3ed.cmake +++ b/src/cli/z3ed.cmake @@ -1,6 +1,20 @@ +include(FetchContent) + +FetchContent_Declare(ftxui + GIT_REPOSITORY https://github.com/ArthurSonzogni/ftxui + GIT_TAG v5.0.0 +) + +FetchContent_GetProperties(ftxui) +if(NOT ftxui_POPULATED) + FetchContent_Populate(ftxui) + add_subdirectory(${ftxui_SOURCE_DIR} ${ftxui_BINARY_DIR} EXCLUDE_FROM_ALL) +endif() + add_executable( z3ed cli/z3ed.cc + cli/tui.cc cli/handlers/compress.cc cli/handlers/patch.cc cli/handlers/tile16_transfer.cc @@ -8,6 +22,7 @@ add_executable( app/core/common.cc app/core/project.cc app/core/platform/file_path.mm + app/core/utils/file_util.cc ${YAZE_APP_EMU_SRC} ${YAZE_APP_GFX_SRC} ${YAZE_APP_ZELDA3_SRC} @@ -26,11 +41,15 @@ target_include_directories( ${PNG_INCLUDE_DIRS} ${SDL2_INCLUDE_DIR} ${GLEW_INCLUDE_DIRS} + ${PROJECT_BINARY_DIR} ) target_link_libraries( z3ed PUBLIC asar-static + ftxui::component + ftxui::screen + ftxui::dom ${ABSL_TARGETS} ${SDL_TARGETS} ${PNG_LIBRARIES} @@ -39,4 +58,4 @@ target_link_libraries( ${CMAKE_DL_LIBS} ImGuiTestEngine ImGui -) \ No newline at end of file +) diff --git a/src/cli/command.h b/src/cli/z3ed.h similarity index 96% rename from src/cli/command.h rename to src/cli/z3ed.h index 941d25f0..670c943f 100644 --- a/src/cli/command.h +++ b/src/cli/z3ed.h @@ -1,5 +1,5 @@ -#ifndef YAZE_CLI_COMMAND_HANDLER_H -#define YAZE_CLI_COMMAND_HANDLER_H +#ifndef YAZE_CLI_Z3ED_H +#define YAZE_CLI_Z3ED_H #include #include @@ -13,7 +13,6 @@ #include "app/core/common.h" #include "app/core/constants.h" #include "app/rom.h" -#include "asar-dll-bindings/c/asar.h" namespace yaze { namespace cli { @@ -48,7 +47,7 @@ class CommandHandler { virtual ~CommandHandler() = default; virtual absl::Status handle(const std::vector& arg_vec) = 0; - app::Rom rom_; + Rom rom_; }; class ApplyPatch : public CommandHandler { @@ -130,7 +129,7 @@ class SnesToPc : public CommandHandler { std::stringstream ss(arg.data()); uint32_t snes_address; ss >> std::hex >> snes_address; - uint32_t pc_address = app::core::SnesToPc(snes_address); + uint32_t pc_address = core::SnesToPc(snes_address); std::cout << std::hex << pc_address << std::endl; return absl::OkStatus(); } @@ -149,7 +148,7 @@ class PcToSnes : public CommandHandler { std::stringstream ss(arg.data()); uint32_t pc_address; ss >> std::hex >> pc_address; - uint32_t snes_address = app::core::PcToSnes(pc_address); + uint32_t snes_address = core::PcToSnes(pc_address); ColorModifier blue(ColorCode::FG_BLUE); std::cout << "SNES LoROM Address: "; std::cout << blue << "$" << std::uppercase << std::hex << snes_address diff --git a/src/ios/main.mm b/src/ios/main.mm index dcf7e85d..46d39aa0 100644 --- a/src/ios/main.mm +++ b/src/ios/main.mm @@ -352,9 +352,9 @@ // Size of the data size_t size = [data length]; - PRINT_IF_ERROR(yaze::app::SharedRom::shared_rom_->LoadFromPointer(bytes, size)); + PRINT_IF_ERROR(yaze::SharedRom::shared_rom_->LoadFromPointer(bytes, size)); std::string filename = std::string([selectedFileURL.path UTF8String]); - yaze::app::SharedRom::shared_rom_->set_filename(filename); + yaze::SharedRom::shared_rom_->set_filename(filename); [selectedFileURL stopAccessingSecurityScopedResource]; } else { diff --git a/src/ios/yaze.xcodeproj/project.pbxproj b/src/ios/yaze.xcodeproj/project.pbxproj index 731caab6..514f0c31 100644 --- a/src/ios/yaze.xcodeproj/project.pbxproj +++ b/src/ios/yaze.xcodeproj/project.pbxproj @@ -66,8 +66,6 @@ E318D9222C59C08300091322 /* entity.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D8762C59C08300091322 /* entity.cc */; }; E318D9252C59C08300091322 /* sprite_editor.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D87A2C59C08300091322 /* sprite_editor.cc */; }; E318D9262C59C08300091322 /* sprite_editor.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D87A2C59C08300091322 /* sprite_editor.cc */; }; - E318D9272C59C08300091322 /* gfx_context.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D8802C59C08300091322 /* gfx_context.cc */; }; - E318D9282C59C08300091322 /* gfx_context.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D8802C59C08300091322 /* gfx_context.cc */; }; E318D9332C59C08300091322 /* addressing.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D88E2C59C08300091322 /* addressing.cc */; }; E318D9342C59C08300091322 /* addressing.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D88E2C59C08300091322 /* addressing.cc */; }; E318D9352C59C08300091322 /* instructions.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D88F2C59C08300091322 /* instructions.cc */; }; @@ -139,7 +137,6 @@ E318D9802C59C08300091322 /* sprite.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D8EE2C59C08300091322 /* sprite.cc */; }; E318D9872C59C08300091322 /* rom.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D8F62C59C08300091322 /* rom.cc */; }; E318D9882C59C08300091322 /* rom.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D8F62C59C08300091322 /* rom.cc */; }; - E318D98A2C59C08300091322 /* yaze.cc in Sources */ = {isa = PBXBuildFile; fileRef = E318D8F82C59C08300091322 /* yaze.cc */; }; E318D98D2C59CBBB00091322 /* TextEditor.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E318D98B2C59CBBB00091322 /* TextEditor.cpp */; }; E318D98E2C59CBBB00091322 /* TextEditor.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E318D98B2C59CBBB00091322 /* TextEditor.cpp */; }; E318D9952C59CDF800091322 /* imgui_impl_sdl2.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E318D9942C59CDF800091322 /* imgui_impl_sdl2.cpp */; }; @@ -741,6 +738,8 @@ E384E2D62C76C6E800147029 /* message_data.cc in Sources */ = {isa = PBXBuildFile; fileRef = E384E2D52C76C6C800147029 /* message_data.cc */; }; E38A97F72C6C4CE3005FB662 /* extension_manager.cc in Sources */ = {isa = PBXBuildFile; fileRef = E38A97F22C6C4CE3005FB662 /* extension_manager.cc */; }; E38A97F82C6C4CE3005FB662 /* settings_editor.cc in Sources */ = {isa = PBXBuildFile; fileRef = E38A97F42C6C4CE3005FB662 /* settings_editor.cc */; }; + E3A5CEE52CF61F1200259DE8 /* main.cc in Sources */ = {isa = PBXBuildFile; fileRef = E3A5CEE32CF61F1200259DE8 /* main.cc */; }; + E3A5CEE72CF61F2300259DE8 /* editor.cc in Sources */ = {isa = PBXBuildFile; fileRef = E3A5CEE62CF61F2300259DE8 /* editor.cc */; }; E3B864952C8214B500122951 /* asset_browser.cc in Sources */ = {isa = PBXBuildFile; fileRef = E3B864902C82144A00122951 /* asset_browser.cc */; }; E3BE958D2C68379B008DD1E7 /* editor_manager.cc in Sources */ = {isa = PBXBuildFile; fileRef = E3BE958B2C68379B008DD1E7 /* editor_manager.cc */; }; E3BE95902C6837C8008DD1E7 /* overworld_editor.cc in Sources */ = {isa = PBXBuildFile; fileRef = E3BE958F2C6837C8008DD1E7 /* overworld_editor.cc */; }; @@ -917,11 +916,6 @@ E318D87A2C59C08300091322 /* sprite_editor.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = sprite_editor.cc; sourceTree = ""; }; E318D87B2C59C08300091322 /* sprite_editor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sprite_editor.h; sourceTree = ""; }; E318D87C2C59C08300091322 /* zsprite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = zsprite.h; sourceTree = ""; }; - E318D87E2C59C08300091322 /* editor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = editor.h; sourceTree = ""; }; - E318D87F2C59C08300091322 /* flags.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = flags.h; sourceTree = ""; }; - E318D8802C59C08300091322 /* gfx_context.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = gfx_context.cc; sourceTree = ""; }; - E318D8812C59C08300091322 /* gfx_context.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = gfx_context.h; sourceTree = ""; }; - E318D8832C59C08300091322 /* recent_files.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = recent_files.h; sourceTree = ""; }; E318D88E2C59C08300091322 /* addressing.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = addressing.cc; sourceTree = ""; }; E318D88F2C59C08300091322 /* instructions.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = instructions.cc; sourceTree = ""; }; E318D8902C59C08300091322 /* opcodes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opcodes.h; sourceTree = ""; }; @@ -1005,7 +999,6 @@ E318D8F22C59C08300091322 /* common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = common.h; sourceTree = ""; }; E318D8F62C59C08300091322 /* rom.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = rom.cc; sourceTree = ""; }; E318D8F72C59C08300091322 /* rom.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = rom.h; sourceTree = ""; }; - E318D8F82C59C08300091322 /* yaze.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = yaze.cc; sourceTree = ""; }; E318D98B2C59CBBB00091322 /* TextEditor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = TextEditor.cpp; path = ../../yaze/src/lib/ImGuiColorTextEdit/TextEditor.cpp; sourceTree = ""; }; E318D98C2C59CBBB00091322 /* TextEditor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TextEditor.h; path = ../../yaze/src/lib/ImGuiColorTextEdit/TextEditor.h; sourceTree = ""; }; E318D9942C59CDF800091322 /* imgui_impl_sdl2.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = imgui_impl_sdl2.cpp; path = ../../yaze/src/lib/imgui/backends/imgui_impl_sdl2.cpp; sourceTree = ""; }; @@ -2422,6 +2415,9 @@ E38A97F32C6C4CE3005FB662 /* extension_manager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = extension_manager.h; sourceTree = ""; }; E38A97F42C6C4CE3005FB662 /* settings_editor.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = settings_editor.cc; sourceTree = ""; }; E38A97F52C6C4CE3005FB662 /* settings_editor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = settings_editor.h; sourceTree = ""; }; + E3A5CEE32CF61F1200259DE8 /* main.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = main.cc; sourceTree = ""; }; + E3A5CEE62CF61F2300259DE8 /* editor.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = editor.cc; sourceTree = ""; }; + E3A5CEE82CF61F3100259DE8 /* editor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = editor.h; sourceTree = ""; }; E3B8648F2C82144A00122951 /* asset_browser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = asset_browser.h; sourceTree = ""; }; E3B864902C82144A00122951 /* asset_browser.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = asset_browser.cc; sourceTree = ""; }; E3BE958B2C68379B008DD1E7 /* editor_manager.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = editor_manager.cc; sourceTree = ""; }; @@ -2668,18 +2664,6 @@ path = sprite; sourceTree = ""; }; - E318D8842C59C08300091322 /* utils */ = { - isa = PBXGroup; - children = ( - E318D87E2C59C08300091322 /* editor.h */, - E318D87F2C59C08300091322 /* flags.h */, - E318D8802C59C08300091322 /* gfx_context.cc */, - E318D8812C59C08300091322 /* gfx_context.h */, - E318D8832C59C08300091322 /* recent_files.h */, - ); - path = utils; - sourceTree = ""; - }; E318D88D2C59C08300091322 /* editor */ = { isa = PBXGroup; children = ( @@ -2690,10 +2674,11 @@ E318D8752C59C08300091322 /* music */, E318D8792C59C08300091322 /* overworld */, E318D87D2C59C08300091322 /* sprite */, - E318D8842C59C08300091322 /* utils */, E38A97F62C6C4CE3005FB662 /* system */, E3BE958B2C68379B008DD1E7 /* editor_manager.cc */, E3BE958C2C68379B008DD1E7 /* editor_manager.h */, + E3A5CEE62CF61F2300259DE8 /* editor.cc */, + E3A5CEE82CF61F3100259DE8 /* editor.h */, ); path = editor; sourceTree = ""; @@ -2915,7 +2900,7 @@ E318D8F32C59C08300091322 /* zelda3 */, E318D8F62C59C08300091322 /* rom.cc */, E318D8F72C59C08300091322 /* rom.h */, - E318D8F82C59C08300091322 /* yaze.cc */, + E3A5CEE32CF61F1200259DE8 /* main.cc */, ); name = app; path = ../app; @@ -5546,11 +5531,11 @@ E318E1F32C5A4FC200091322 /* string_view.cc in Sources */, 07A82ED82139413D0078D120 /* imgui_widgets.cpp in Sources */, E318E1A52C5A4FC200091322 /* utf8.cc in Sources */, - E318D9272C59C08300091322 /* gfx_context.cc in Sources */, E318D91F2C59C08300091322 /* music_editor.cc in Sources */, E318D9672C59C08300091322 /* input.cc in Sources */, 8309BDA5253CCC070045E2A1 /* main.mm in Sources */, E318E6F32C5A4FC900091322 /* get_current_time_posix.inc in Sources */, + E3A5CEE72CF61F2300259DE8 /* editor.cc in Sources */, E318E71B2C5A4FC900091322 /* time.cc in Sources */, E318D9172C59C08300091322 /* screen_editor.cc in Sources */, E318E0652C5A4FBF00091322 /* usage.cc in Sources */, @@ -5642,7 +5627,6 @@ E318DF382C5A4FBE00091322 /* cycleclock.cc in Sources */, E318E7342C5A4FC900091322 /* span_test.cc in Sources */, E318DF702C5A4FBE00091322 /* unscaledcycleclock.cc in Sources */, - E318D9282C59C08300091322 /* gfx_context.cc in Sources */, E318E0962C5A4FC000091322 /* bits_benchmark.cc in Sources */, E318D9342C59C08300091322 /* addressing.cc in Sources */, E318DF2E2C5A4FBD00091322 /* container_test.cc in Sources */, @@ -5681,7 +5665,6 @@ E318E07C2C5A4FC000091322 /* low_level_hash.cc in Sources */, E318E21E2C5A4FC200091322 /* mutex.cc in Sources */, E318DF822C5A4FBE00091322 /* inline_variable_test.cc in Sources */, - E318D98A2C59C08300091322 /* yaze.cc in Sources */, E318D99E2C59D0E600091322 /* imgui_stdlib.cpp in Sources */, E318DF4C2C5A4FBE00091322 /* scoped_set_env.cc in Sources */, E318E0562C5A4FBF00091322 /* marshalling_test.cc in Sources */, @@ -5710,6 +5693,7 @@ E318E0622C5A4FBF00091322 /* usage_config_test.cc in Sources */, E318E6FE2C5A4FC900091322 /* civil_time_test.cc in Sources */, E318E1EC2C5A4FC200091322 /* str_split_test.cc in Sources */, + E3A5CEE52CF61F1200259DE8 /* main.cc in Sources */, E318E70C2C5A4FC900091322 /* duration_test.cc in Sources */, E318E7AE2C5A548C00091322 /* png.c in Sources */, E318E7382C5A4FC900091322 /* variant_exception_safety_test.cc in Sources */, @@ -6181,6 +6165,7 @@ SYSTEM_HEADER_SEARCH_PATHS = ( "\"$(SRCROOT)/../lib/SDL/include\"", /Users/scawful/Code/lpng1643, + "\"$(SRCROOT)/../../incl\"", ); TARGETED_DEVICE_FAMILY = "1,2"; USER_HEADER_SEARCH_PATHS = ( @@ -6227,6 +6212,7 @@ SYSTEM_HEADER_SEARCH_PATHS = ( "\"$(SRCROOT)/../lib/SDL/include\"", /Users/scawful/Code/lpng1643, + "\"$(SRCROOT)/../../incl\"", ); TARGETED_DEVICE_FAMILY = "1,2"; USER_HEADER_SEARCH_PATHS = ( diff --git a/src/ios/yaze.xcodeproj/project.xcworkspace/xcuserdata/scawful.xcuserdatad/UserInterfaceState.xcuserstate b/src/ios/yaze.xcodeproj/project.xcworkspace/xcuserdata/scawful.xcuserdatad/UserInterfaceState.xcuserstate index 80bffebbf79f10b674e4c2ebb2c8a195c8e38e97..f74fdb31d77eea31d1978522b58faff5165b42f1 100644 GIT binary patch literal 462719 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 literal 407072 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ⅅophfpf>x>!zXfErDpRz^d) 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

!3cU(~?Y#w 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.xcscheme_^#shared#^_ orderHint - 4 + 7 yaze_macos.xcscheme_^#shared#^_ orderHint - 7 + 4 diff --git a/src/lib/ImGuiColorTextEdit b/src/lib/ImGuiColorTextEdit deleted file mode 160000 index 0a88824f..00000000 --- a/src/lib/ImGuiColorTextEdit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0a88824f7de8d0bd11d8419066caa7d3469395c4 diff --git a/src/lib/ImGuiFileDialog b/src/lib/ImGuiFileDialog deleted file mode 160000 index 8371c861..00000000 --- a/src/lib/ImGuiFileDialog +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8371c8612ddfe9b23464ba9b02770ea046e81f98 diff --git a/src/lib/imgui b/src/lib/imgui index 08400f5b..6982ce43 160000 --- a/src/lib/imgui +++ b/src/lib/imgui @@ -1 +1 @@ -Subproject commit 08400f5be7fa8a83682b99d611ab7cdc66258425 +Subproject commit 6982ce43f5b143c5dce5fab0ce07dd4867b705ae diff --git a/src/lib/imgui_test_engine b/src/lib/imgui_test_engine index a6b76971..2e2d83a7 160000 --- a/src/lib/imgui_test_engine +++ b/src/lib/imgui_test_engine @@ -1 +1 @@ -Subproject commit a6b76971146d0cd148860156a8382497982943aa +Subproject commit 2e2d83a791facb7e7fa139582daa6c212c1ee3e2 diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index d2601731..909903f0 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -32,6 +32,7 @@ target_include_directories( ${ASAR_INCLUDE_DIR} ${SDL2_INCLUDE_DIR} ${PNG_INCLUDE_DIRS} + ${PROJECT_BINARY_DIR} ) target_link_libraries( diff --git a/src/test/emu/cpu_test.cc b/src/test/emu/cpu_test.cc index 296443bc..93179e68 100644 --- a/src/test/emu/cpu_test.cc +++ b/src/test/emu/cpu_test.cc @@ -3,21 +3,18 @@ #include #include -#include "app/emu/cpu/clock.h" -#include "app/emu/cpu/internal/opcodes.h" #include "app/emu/memory/asm_parser.h" #include "app/emu/memory/memory.h" #include "test/mocks/mock_memory.h" namespace yaze { namespace test { -namespace emu { -using yaze::app::emu::AsmParser; -using yaze::app::emu::Cpu; -using yaze::app::emu::memory::CpuCallbacks; -using yaze::app::emu::memory::MockClock; -using yaze::app::emu::memory::MockMemory; +using yaze::emu::AsmParser; +using yaze::emu::Cpu; +using yaze::emu::CpuCallbacks; +using yaze::emu::MockClock; +using yaze::emu::MockMemory; /** * \test Test fixture for CPU unit tests @@ -4198,6 +4195,5 @@ TEST_F(CpuTest, XCESwitchBackAndForth) { EXPECT_FALSE(cpu.E); // Emulation mode flag should be cleared } -} // namespace emu } // namespace test } // namespace yaze diff --git a/src/test/emu/ppu_test.cc b/src/test/emu/ppu_test.cc index 3099f801..a7491cbd 100644 --- a/src/test/emu/ppu_test.cc +++ b/src/test/emu/ppu_test.cc @@ -2,22 +2,17 @@ #include -#include "app/emu/cpu/clock.h" -#include "app/emu/memory/memory.h" #include "test/mocks/mock_memory.h" namespace yaze { namespace test { -namespace emu { -using yaze::app::emu::Clock; -using yaze::app::emu::memory::MockClock; -using yaze::app::emu::memory::MockMemory; -using yaze::app::emu::video::BackgroundMode; -using yaze::app::emu::video::PpuInterface; -using yaze::app::emu::video::SpriteAttributes; -using yaze::app::emu::video::Tilemap; -using yaze::app::gfx::Bitmap; +using yaze::emu::MockClock; +using yaze::emu::MockMemory; +using yaze::emu::BackgroundMode; +using yaze::emu::PpuInterface; +using yaze::emu::SpriteAttributes; +using yaze::emu::Tilemap; /** * @brief Mock Ppu class for testing @@ -57,6 +52,5 @@ class PpuTest : public ::testing::Test { } }; -} // namespace emu } // namespace test -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/test/emu/spc700_test.cc b/src/test/emu/spc700_test.cc index 16625fae..05b4bbe7 100644 --- a/src/test/emu/spc700_test.cc +++ b/src/test/emu/spc700_test.cc @@ -6,13 +6,12 @@ namespace yaze { namespace test { -namespace emu { using testing::_; using testing::Return; -using yaze::app::emu::audio::ApuCallbacks; -using yaze::app::emu::audio::AudioRam; -using yaze::app::emu::audio::Spc700; +using yaze::emu::ApuCallbacks; +using yaze::emu::AudioRam; +using yaze::emu::Spc700; /** * @brief MockAudioRam is a mock class for the AudioRam class. @@ -471,6 +470,5 @@ TEST_F(Spc700Test, BootIplRomOk) { // EXPECT_EQ(spc700.PC, 0xFFC1 + 0x3F); } -} // namespace emu } // namespace test } // namespace yaze diff --git a/src/test/gfx/compression_test.cc b/src/test/gfx/compression_test.cc index f7875f1d..1a058282 100644 --- a/src/test/gfx/compression_test.cc +++ b/src/test/gfx/compression_test.cc @@ -12,20 +12,19 @@ namespace yaze { namespace test { -namespace gfx { -using yaze::app::Rom; -using yaze::app::gfx::lc_lz2::CompressionContext; -using yaze::app::gfx::lc_lz2::CompressionPiece; -using yaze::app::gfx::lc_lz2::CompressV2; -using yaze::app::gfx::lc_lz2::CompressV3; -using yaze::app::gfx::lc_lz2::DecompressV2; -using yaze::app::gfx::lc_lz2::kCommandByteFill; -using yaze::app::gfx::lc_lz2::kCommandDirectCopy; -using yaze::app::gfx::lc_lz2::kCommandIncreasingFill; -using yaze::app::gfx::lc_lz2::kCommandLongLength; -using yaze::app::gfx::lc_lz2::kCommandRepeatingBytes; -using yaze::app::gfx::lc_lz2::kCommandWordFill; +using yaze::Rom; +using yaze::gfx::lc_lz2::CompressionContext; +using yaze::gfx::lc_lz2::CompressionPiece; +using yaze::gfx::lc_lz2::CompressV2; +using yaze::gfx::lc_lz2::CompressV3; +using yaze::gfx::lc_lz2::DecompressV2; +using yaze::gfx::lc_lz2::kCommandByteFill; +using yaze::gfx::lc_lz2::kCommandDirectCopy; +using yaze::gfx::lc_lz2::kCommandIncreasingFill; +using yaze::gfx::lc_lz2::kCommandLongLength; +using yaze::gfx::lc_lz2::kCommandRepeatingBytes; +using yaze::gfx::lc_lz2::kCommandWordFill; using ::testing::ElementsAre; using ::testing::ElementsAreArray; @@ -81,8 +80,9 @@ void AssertCompressionQuality( EXPECT_THAT(compressed_data, ElementsAreArray(expected_compressed_data)); } -std::vector ExpectCompressV3Ok(const std::vector& uncompressed_data, - const std::vector& expected_compressed_data) { +std::vector ExpectCompressV3Ok( + const std::vector& uncompressed_data, + const std::vector& expected_compressed_data) { absl::StatusOr> result = CompressV3(uncompressed_data, 0, uncompressed_data.size(), 0, false); EXPECT_TRUE(result.ok()); @@ -397,7 +397,8 @@ TEST(CheckIncByteV3Test, NotAnIncreasingSequence) { TEST(LC_LZ2_CompressionTest, DecompressionValidCommand) { Rom rom; - std::vector simple_copy_input = {BUILD_HEADER(0x00, 0x02), 0x2A, 0x45, 0xFF}; + std::vector simple_copy_input = {BUILD_HEADER(0x00, 0x02), 0x2A, + 0x45, 0xFF}; uchar simple_copy_output[2] = {0x2A, 0x45}; auto decomp_result = ExpectDecompressBytesOk(rom, simple_copy_input); EXPECT_THAT(simple_copy_output, ElementsAreArray(decomp_result.data(), 2)); @@ -421,6 +422,5 @@ TEST(LC_LZ2_CompressionTest, DecompressionMixingCommand) { EXPECT_THAT(random1_o, ElementsAreArray(decomp_result.data(), 9)); } -} // namespace gfx } // namespace test -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/test/gfx/snes_palette_test.cc b/src/test/gfx/snes_palette_test.cc index 2e962290..81a74dab 100644 --- a/src/test/gfx/snes_palette_test.cc +++ b/src/test/gfx/snes_palette_test.cc @@ -7,13 +7,12 @@ namespace yaze { namespace test { -namespace gfx { using ::testing::ElementsAreArray; -using yaze::app::gfx::ConvertRgbToSnes; -using yaze::app::gfx::ConvertSnestoRGB; -using yaze::app::gfx::Extract; -using yaze::app::gfx::SnesPalette; +using yaze::gfx::ConvertRgbToSnes; +using yaze::gfx::ConvertSnesToRgb; +using yaze::gfx::Extract; +using yaze::gfx::SnesPalette; namespace { unsigned int test_convert(snes_color col) { @@ -26,8 +25,8 @@ unsigned int test_convert(snes_color col) { } // namespace TEST(SnesPaletteTest, AddColor) { - yaze::app::gfx::SnesPalette palette; - yaze::app::gfx::SnesColor color; + yaze::gfx::SnesPalette palette; + yaze::gfx::SnesColor color; palette.AddColor(color); ASSERT_EQ(palette.size(), 1); } @@ -40,7 +39,7 @@ TEST(SnesColorTest, ConvertRgbToSnes) { TEST(SnesColorTest, ConvertSnestoRGB) { uint16_t snes = 0x4210; - snes_color color = ConvertSnestoRGB(snes); + snes_color color = ConvertSnesToRgb(snes); ASSERT_EQ(color.red, 132); ASSERT_EQ(color.green, 132); ASSERT_EQ(color.blue, 132); @@ -53,13 +52,13 @@ TEST(SnesColorTest, ConvertSnesToRGB_Binary) { uint16_t purple = 0b0111110000011111; snes_color testcolor; - testcolor = ConvertSnestoRGB(red); + testcolor = ConvertSnesToRgb(red); ASSERT_EQ(0xFF0000, test_convert(testcolor)); - testcolor = ConvertSnestoRGB(green); + testcolor = ConvertSnesToRgb(green); ASSERT_EQ(0x00FF00, test_convert(testcolor)); - testcolor = ConvertSnestoRGB(blue); + testcolor = ConvertSnesToRgb(blue); ASSERT_EQ(0x0000FF, test_convert(testcolor)); - testcolor = ConvertSnestoRGB(purple); + testcolor = ConvertSnesToRgb(purple); ASSERT_EQ(0xFF00FF, test_convert(testcolor)); } @@ -88,11 +87,10 @@ TEST(SnesColorTest, Convert) { static_cast(0xFF), 0x1F}; auto pal = Extract(data, 0, 5); - auto snes_string = yaze::app::gfx::Convert(pal); + auto snes_string = yaze::gfx::Convert(pal); EXPECT_EQ(10, snes_string.size()); EXPECT_THAT(data, ElementsAreArray(snes_string.data(), 10)); } -} // namespace gfx } // namespace test -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/test/integration/test_editor.cc b/src/test/integration/test_editor.cc index 53411800..45cfbba3 100644 --- a/src/test/integration/test_editor.cc +++ b/src/test/integration/test_editor.cc @@ -45,7 +45,7 @@ void TestEditor::RegisterTests(ImGuiTestEngine* engine) { int RunIntegrationTest() { yaze::test::integration::TestEditor test_editor; - yaze::app::core::Controller controller; + yaze::core::Controller controller; controller.init_test_editor(&test_editor); if (!controller.CreateWindow().ok()) { @@ -68,16 +68,15 @@ int RunIntegrationTest() { // Initialize ImGui for SDL ImGui_ImplSDL2_InitForSDLRenderer( - controller.window(), yaze::app::core::Renderer::GetInstance().renderer()); - ImGui_ImplSDLRenderer2_Init( - yaze::app::core::Renderer::GetInstance().renderer()); + controller.window(), yaze::core::Renderer::GetInstance().renderer()); + ImGui_ImplSDLRenderer2_Init(yaze::core::Renderer::GetInstance().renderer()); test_editor.RegisterTests(engine); ImGuiTestEngine_Start(engine, ImGui::GetCurrentContext()); controller.set_active(true); // Set the default style - yaze::app::gui::ColorsYaze(); + yaze::gui::ColorsYaze(); // Build a new ImGui frame ImGui_ImplSDLRenderer2_NewFrame(); diff --git a/src/test/integration/test_editor.h b/src/test/integration/test_editor.h index c555bd32..87c92f6d 100644 --- a/src/test/integration/test_editor.h +++ b/src/test/integration/test_editor.h @@ -10,7 +10,7 @@ namespace yaze { namespace test { namespace integration { -class TestEditor : public yaze::app::editor::Editor { +class TestEditor : public yaze::editor::Editor { public: TestEditor() = default; ~TestEditor() = default; diff --git a/src/test/mocks/mock_memory.h b/src/test/mocks/mock_memory.h index 44e8ac5e..7a97311a 100644 --- a/src/test/mocks/mock_memory.h +++ b/src/test/mocks/mock_memory.h @@ -9,9 +9,7 @@ #include "app/emu/memory/memory.h" namespace yaze { -namespace app { namespace emu { -namespace memory { /** * @brief Mock CPU class for testing @@ -221,9 +219,7 @@ class MockMemory : public Memory { uint16_t SP_ = 0x01FF; }; -} // namespace memory } // namespace emu -} // namespace app } // namespace yaze -#endif // YAZE_TEST_MOCK_MOCK_MEMORY_H \ No newline at end of file +#endif // YAZE_TEST_MOCK_MOCK_MEMORY_H diff --git a/src/test/rom_test.cc b/src/test/rom_test.cc index 99de8782..4dc4da0a 100644 --- a/src/test/rom_test.cc +++ b/src/test/rom_test.cc @@ -6,13 +6,11 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "test/core/testing.h" -#include "app/gfx/snes_color.h" namespace yaze { namespace test { using ::testing::_; -using ::testing::DoAll; using ::testing::Return; const static std::vector kMockRomData = { @@ -21,7 +19,7 @@ const static std::vector kMockRomData = { 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, }; -class MockRom : public app::Rom { +class MockRom : public Rom { public: MOCK_METHOD(absl::Status, WriteHelper, (const WriteAction&), (override)); @@ -36,7 +34,7 @@ class MockRom : public app::Rom { class RomTest : public ::testing::Test { protected: - app::Rom rom_; + Rom rom_; }; TEST_F(RomTest, Uninitialized) { @@ -202,9 +200,9 @@ TEST_F(RomTest, WriteTransactionSuccess) { .WillRepeatedly(Return(absl::OkStatus())); EXPECT_OK(mock_rom.WriteTransaction( - app::Rom::WriteAction{0x1000, uint8_t{0xFF}}, - app::Rom::WriteAction{0x1001, uint16_t{0xABCD}}, - app::Rom::WriteAction{0x1002, std::vector{0x12, 0x34}})); + Rom::WriteAction{0x1000, uint8_t{0xFF}}, + Rom::WriteAction{0x1001, uint16_t{0xABCD}}, + Rom::WriteAction{0x1002, std::vector{0x12, 0x34}})); } TEST_F(RomTest, WriteTransactionFailure) { @@ -215,10 +213,10 @@ TEST_F(RomTest, WriteTransactionFailure) { .WillOnce(Return(absl::OkStatus())) .WillOnce(Return(absl::InternalError("Write failed"))); - EXPECT_EQ(mock_rom.WriteTransaction( - app::Rom::WriteAction{0x1000, uint8_t{0xFF}}, - app::Rom::WriteAction{0x1001, uint16_t{0xABCD}}), - absl::InternalError("Write failed")); + EXPECT_EQ( + mock_rom.WriteTransaction(Rom::WriteAction{0x1000, uint8_t{0xFF}}, + Rom::WriteAction{0x1001, uint16_t{0xABCD}}), + absl::InternalError("Write failed")); } TEST_F(RomTest, ReadTransactionSuccess) { diff --git a/src/test/yaze_test.cc b/src/test/yaze_test.cc index f80414ac..8534ae60 100644 --- a/src/test/yaze_test.cc +++ b/src/test/yaze_test.cc @@ -1,19 +1,11 @@ #define SDL_MAIN_HANDLED -#include "yaze.h" - #include #include "absl/debugging/failure_signal_handler.h" #include "absl/debugging/symbolize.h" #include "test/integration/test_editor.h" -namespace yaze { -namespace test { - -} // namespace test -} // namespace yaze - int main(int argc, char* argv[]) { absl::InitializeSymbolizer(argv[0]); diff --git a/src/test/zelda3/dungeon_room_test.cc b/src/test/zelda3/dungeon_room_test.cc index c2211997..29c611be 100644 --- a/src/test/zelda3/dungeon_room_test.cc +++ b/src/test/zelda3/dungeon_room_test.cc @@ -1,17 +1,14 @@ -#include "app/zelda3/dungeon/room.h" - -#include "gtest/gtest.h" #include #include #include "app/rom.h" +#include "app/zelda3/dungeon/room.h" namespace yaze { namespace test { -namespace zelda3 { -class DungeonRoomTest : public ::testing::Test, public app::SharedRom { -protected: +class DungeonRoomTest : public ::testing::Test, public SharedRom { + protected: void SetUp() override { // Skip tests on Linux for automated github builds #if defined(__linux__) @@ -26,12 +23,11 @@ protected: }; TEST_F(DungeonRoomTest, SingleRoomLoadOk) { - app::zelda3::dungeon::Room test_room(/*room_id=*/0); + zelda3::Room test_room(/*room_id=*/0); test_room.LoadHeader(); // Do some assertions based on the output in ZS test_room.LoadRoomFromROM(); } -} // namespace zelda3 -} // namespace test -} // namespace yaze +} // namespace test +} // namespace yaze diff --git a/src/test/zelda3/message_test.cc b/src/test/zelda3/message_test.cc index c593d187..bf22d2fa 100644 --- a/src/test/zelda3/message_test.cc +++ b/src/test/zelda3/message_test.cc @@ -6,9 +6,8 @@ namespace yaze { namespace test { -namespace zelda3 { -class MessageTest : public ::testing::Test, public app::SharedRom { +class MessageTest : public ::testing::Test, public SharedRom { protected: void SetUp() override { #if defined(__linux__) @@ -17,8 +16,8 @@ class MessageTest : public ::testing::Test, public app::SharedRom { } void TearDown() override {} - app::editor::MessageEditor message_editor_; - std::vector dictionary_; + editor::MessageEditor message_editor_; + std::vector dictionary_; }; TEST_F(MessageTest, LoadMessagesFromRomOk) { @@ -44,6 +43,5 @@ TEST_F(MessageTest, VerifySingleMessageFromRomOk) { // TODO - Implement this test } -} // namespace zelda3 } // namespace test } // namespace yaze diff --git a/src/test/zelda3/overworld_test.cc b/src/test/zelda3/overworld_test.cc index fac567c2..8e27e18c 100644 --- a/src/test/zelda3/overworld_test.cc +++ b/src/test/zelda3/overworld_test.cc @@ -10,9 +10,8 @@ namespace yaze { namespace test { -namespace zelda3 { -class OverworldTest : public ::testing::Test, public app::SharedRom { +class OverworldTest : public ::testing::Test, public SharedRom { protected: void SetUp() override { // Skip tests on Linux for automated github builds @@ -22,12 +21,12 @@ class OverworldTest : public ::testing::Test, public app::SharedRom { } void TearDown() override {} - app::zelda3::overworld::Overworld overworld_; + zelda3::Overworld overworld_; }; TEST_F(OverworldTest, OverworldLoadNoRomDataError) { // Arrange - app::Rom rom; + Rom rom; // Act auto status = overworld_.Load(rom); @@ -48,11 +47,10 @@ TEST_F(OverworldTest, OverworldLoadRomDataOk) { // Assert EXPECT_TRUE(status.ok()); EXPECT_EQ(overworld_.overworld_maps().size(), - app::zelda3::overworld::kNumOverworldMaps); + zelda3::kNumOverworldMaps); EXPECT_EQ(overworld_.tiles16().size(), - app::zelda3::overworld::kNumTile16Individual); + zelda3::kNumTile16Individual); } -} // namespace zelda3 } // namespace test } // namespace yaze diff --git a/src/test/zelda3/sprite_builder_test.cc b/src/test/zelda3/sprite_builder_test.cc index f97ad8c2..56d2fcf8 100644 --- a/src/test/zelda3/sprite_builder_test.cc +++ b/src/test/zelda3/sprite_builder_test.cc @@ -5,9 +5,8 @@ namespace yaze { namespace test { -namespace zelda3 { -using namespace yaze::app::zelda3; +using namespace yaze::zelda3; class SpriteBuilderTest : public ::testing::Test { protected: @@ -61,6 +60,5 @@ TEST_F(SpriteBuilderTest, BuildSpritePropertiesOk) { )")); } -} // namespace zelda3_test } // namespace test -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/yaze.cc b/src/yaze.cc index 5af7e32a..08d10b15 100644 --- a/src/yaze.cc +++ b/src/yaze.cc @@ -1,21 +1,28 @@ #include "yaze.h" -#include +#include +#include #include "app/rom.h" #include "app/zelda3/overworld/overworld.h" +#include "dungeon.h" +#include "yaze_config.h" -void yaze_check_version(const char* version) { - printf("Yaze version: %s\n", version); - auto version_check = yaze::app::core::CheckVersion(version); - if (!version_check.ok()) { - printf("%s\n", version_check.status().message().data()); +void yaze_check_version(const char *version) { + std::string current_version; + std::stringstream ss; + ss << YAZE_VERSION_MAJOR << "." << YAZE_VERSION_MINOR << "." + << YAZE_VERSION_PATCH; + ss >> current_version; + + if (version != current_version) { + std::cout << "Yaze version mismatch: expected " << current_version + << ", got " << version << std::endl; exit(1); } - return; } -int yaze_init(yaze_editor_context* yaze_ctx) { +int yaze_init(yaze_editor_context *yaze_ctx) { if (yaze_ctx->project->rom_filename == nullptr) { return -1; } @@ -28,27 +35,27 @@ int yaze_init(yaze_editor_context* yaze_ctx) { return 0; } -void yaze_cleanup(yaze_editor_context* yaze_ctx) { +void yaze_cleanup(yaze_editor_context *yaze_ctx) { if (yaze_ctx->rom) { yaze_unload_rom(yaze_ctx->rom); } } -yaze_project yaze_load_project(const char* filename) { +yaze_project yaze_load_project(const char *filename) { yaze_project project; project.filepath = filename; return project; } -z3_rom* yaze_load_rom(const char* filename) { - yaze::app::Rom* internal_rom; - internal_rom = new yaze::app::Rom(); +z3_rom *yaze_load_rom(const char *filename) { + yaze::Rom *internal_rom; + internal_rom = new yaze::Rom(); if (!internal_rom->LoadFromFile(filename).ok()) { delete internal_rom; return nullptr; } - z3_rom* rom = new z3_rom(); + z3_rom *rom = new z3_rom(); rom->filename = filename; rom->impl = internal_rom; rom->data = internal_rom->data(); @@ -56,9 +63,9 @@ z3_rom* yaze_load_rom(const char* filename) { return rom; } -void yaze_unload_rom(z3_rom* rom) { +void yaze_unload_rom(z3_rom *rom) { if (rom->impl) { - delete static_cast(rom->impl); + delete static_cast(rom->impl); } if (rom) { @@ -66,7 +73,7 @@ void yaze_unload_rom(z3_rom* rom) { } } -yaze_bitmap yaze_load_bitmap(const char* filename) { +yaze_bitmap yaze_load_bitmap(const char *filename) { yaze_bitmap bitmap; bitmap.width = 0; bitmap.height = 0; @@ -75,7 +82,7 @@ yaze_bitmap yaze_load_bitmap(const char* filename) { return bitmap; } -snes_color yaze_get_color_from_paletteset(const z3_rom* rom, int palette_set, +snes_color yaze_get_color_from_paletteset(const z3_rom *rom, int palette_set, int palette, int color) { snes_color color_struct; color_struct.red = 0; @@ -83,10 +90,10 @@ snes_color yaze_get_color_from_paletteset(const z3_rom* rom, int palette_set, color_struct.blue = 0; if (rom->impl) { - yaze::app::Rom* internal_rom = static_cast(rom->impl); + yaze::Rom *internal_rom = static_cast(rom->impl); auto get_color = internal_rom->palette_group() - .get_group(yaze::app::gfx::kPaletteGroupAddressesKeys[palette_set]) + .get_group(yaze::gfx::kPaletteGroupAddressesKeys[palette_set]) ->palette(palette) .GetColor(color); if (!get_color.ok()) { @@ -100,24 +107,33 @@ snes_color yaze_get_color_from_paletteset(const z3_rom* rom, int palette_set, return color_struct; } -z3_overworld* yaze_load_overworld(const z3_rom* rom) { +z3_overworld *yaze_load_overworld(const z3_rom *rom) { if (rom->impl == nullptr) { return nullptr; } - yaze::app::Rom* internal_rom = static_cast(rom->impl); - auto internal_overworld = new yaze::app::zelda3::overworld::Overworld(); + yaze::Rom *internal_rom = static_cast(rom->impl); + auto internal_overworld = new yaze::zelda3::Overworld(); if (!internal_overworld->Load(*internal_rom).ok()) { return nullptr; } - z3_overworld* overworld = new z3_overworld(); + z3_overworld *overworld = new z3_overworld(); overworld->impl = internal_overworld; int map_id = 0; - for (const auto& ow_map : internal_overworld->overworld_maps()) { + for (const auto &ow_map : internal_overworld->overworld_maps()) { overworld->maps[map_id] = new z3_overworld_map(); overworld->maps[map_id]->id = map_id; map_id++; } return overworld; } + +z3_dungeon_room *yaze_load_all_rooms(const z3_rom *rom) { + if (rom->impl == nullptr) { + return nullptr; + } + yaze::Rom *internal_rom = static_cast(rom->impl); + z3_dungeon_room *rooms = new z3_dungeon_room[256]; + return rooms; +} diff --git a/src/yaze_config.h.in b/src/yaze_config.h.in new file mode 100644 index 00000000..511af1a4 --- /dev/null +++ b/src/yaze_config.h.in @@ -0,0 +1,4 @@ +// yaze config file +#define YAZE_VERSION_MAJOR @yaze_VERSION_MAJOR@ +#define YAZE_VERSION_MINOR @yaze_VERSION_MINOR@ +#define YAZE_VERSION_PATCH @yaze_VERSION_PATCH@