diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 2d0431d7..73b2469c 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -2,8 +2,14 @@ name: CMake on: push: + paths: + - 'src/**' + - 'test/**' branches: [ "master" ] pull_request: + paths: + - 'src/**' + - 'test/**' branches: [ "master" ] env: @@ -18,12 +24,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v2 with: submodules: recursive - name: Install Video Libs - run: sudo apt install libglew-dev + run: sudo apt install libglew-dev + + - name: Install Audio Libs + run: sudo apt install libwavpack-dev - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d4fa03c..3c4e3205 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,15 +1,15 @@ -# CMake Specifications -------------------------------------------------------- +# CMake Specifications cmake_minimum_required(VERSION 3.10) # Yet Another Zelda3 Editor # by scawful project(yaze VERSION 0.01) -# C++ Standard Specifications ------------------------------------------------- +# C++ Standard Specifications set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS ON) -set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) @@ -18,46 +18,22 @@ set(CMAKE_MODULE_LINKER_FLAGS \"-Wl,--no-undefined -Wl,--no-undefined\") set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) set(BUILD_SHARED_LIBS ON) -# Abseil Standard Specifications ---------------------------------------------- +# Abseil Standard Specifications include(cmake/absl.cmake) add_subdirectory(src/lib/abseil-cpp) include(cmake/openssl.cmake) -set(Protobuf_PROTOC_EXECUTABLE "/Users/scawful/code/protobuf/bin/protoc") -set(Protobuf_INCLUDE_DIRS "/Users/scawful/code/protobuf/include/") -set(PROTOBUF_INCLUDE_PATH ${CMAKE_CURRENT_BINARY_DIR} - CACHE INTERNAL "Path to generated protobuf files.") -include_directories(${PROTOBUF_INCLUDE_PATH}) -# Video Libraries ------------------------------------------------------------- +# Video Libraries find_package(PNG REQUIRED) find_package(OpenGL REQUIRED) -find_package(GLEW REQUIRED) +include(cmake/sdl2.cmake) -# SDL2 ------------------------------------------------------------------------ -if (UNIX) - add_subdirectory(src/lib/SDL) -else() - find_package(SDL2) -endif() -set(SDL2MIXER_OPUS OFF) -set(SDL2MIXER_FLAC OFF) -set(SDL2MIXER_MOD OFF) -set(SDL2MIXER_MIDI_FLUIDSYNTH OFF) -find_library(SDL_MIXER_LIBRARY - NAMES SDL_mixer - HINTS - ENV SDLMIXERDIR - ENV SDLDIR - PATH_SUFFIXES lib ${VC_LIB_PATH_SUFFIX} - ) -add_subdirectory(src/lib/SDL_mixer) +# Asar +# add_subdirectory(src/lib/asar/src) +# include(cmake/asar.cmake) -# Asar ------------------------------------------------------------------------ -add_subdirectory(src/lib/asar/src) -include(cmake/asar.cmake) - -# snes-spc -------------------------------------------------------------------- +# snes-spc ADD_DEFINITIONS(-DSNES_SPC_EXPORTS) set(SNES_SPC_SOURCES "../src/lib/snes_spc/snes_spc/spc.cpp" @@ -71,9 +47,9 @@ set(SNES_SPC_SOURCES "../src/lib/snes_spc/demo/demo_util.c" ) include_directories(src/lib/snes_spc/snes_spc) -ADD_LIBRARY(snes_spc STATIC ${SNES_SPC_SOURCES} src/app/spc700/spc700.def) +ADD_LIBRARY(snes_spc STATIC ${SNES_SPC_SOURCES} src/app/zelda3/music/spc700.def) -# ImGui ----------------------------------------------------------------------- +# ImGui include(cmake/imgui.cmake) # Project Files diff --git a/README.md b/README.md index b0c941af..7dde2535 100644 --- a/README.md +++ b/README.md @@ -15,24 +15,38 @@ Building and installation 1. Clone the repository - git clone --recurse-submodules https://github.com/scawful/yaze.git +``` + git clone --recurse-submodules https://github.com/scawful/yaze.git +``` 2. Create the build directory and configuration - cmake -S . -B build +``` + cmake -S . -B build +``` 3. Build and run. - cmake --build build +``` + cmake --build build +``` + +## Documentation + +- For users, please refer to [getting_started.md](docs/getting-started.md) for instructions on how to use yaze. +- For developers, please refer to [infrastructure.md](docs/infrastructure.md) for information on the project's infrastructure. License -------- YAZE is distributed under the [GNU GPLv3](https://www.gnu.org/licenses/gpl-3.0.txt) license. -SDL2, ImGUI and Abseil are subject to respective licenses. +SDL2, ImGui and Abseil are subject to respective licenses. Screenshots -------- ![image](https://user-images.githubusercontent.com/47263509/194669806-2b0da68d-9d38-4f52-bcce-c60ee861092c.png) +![image](https://github.com/scawful/yaze/assets/47263509/8913f7ff-6345-4295-ae05-782fd3949eb5) + +![image](https://github.com/scawful/yaze/assets/47263509/e1cf3edb-a59e-4f0a-b4e0-d68925803e58) diff --git a/assets/asm/alttp-hacker-workspace b/assets/asm/alttp-hacker-workspace index ea81eb34..2520fb70 160000 --- a/assets/asm/alttp-hacker-workspace +++ b/assets/asm/alttp-hacker-workspace @@ -1 +1 @@ -Subproject commit ea81eb34251c4e77703791f710f5b60a8a063d21 +Subproject commit 2520fb70c3a47f9f29b5aa61413c4e99defd1b71 diff --git a/assets/font/IBMPlexSansJP-Bold.ttf b/assets/font/IBMPlexSansJP-Bold.ttf new file mode 100644 index 00000000..527267ad Binary files /dev/null and b/assets/font/IBMPlexSansJP-Bold.ttf differ diff --git a/assets/font/NotoSansJP.ttf b/assets/font/NotoSansJP.ttf new file mode 100644 index 00000000..505477e0 Binary files /dev/null and b/assets/font/NotoSansJP.ttf differ diff --git a/cmake/imgui.cmake b/cmake/imgui.cmake index cf434b89..b594b913 100644 --- a/cmake/imgui.cmake +++ b/cmake/imgui.cmake @@ -27,8 +27,8 @@ set( ${IMGUI_PATH}/imgui_demo.cpp ${IMGUI_PATH}/imgui_draw.cpp ${IMGUI_PATH}/imgui_widgets.cpp - ${IMGUI_PATH}/backends/imgui_impl_sdl.cpp - ${IMGUI_PATH}/backends/imgui_impl_sdlrenderer.cpp + ${IMGUI_PATH}/backends/imgui_impl_sdl2.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 diff --git a/cmake/sdl2.cmake b/cmake/sdl2.cmake new file mode 100644 index 00000000..3b2299e2 --- /dev/null +++ b/cmake/sdl2.cmake @@ -0,0 +1,19 @@ +# SDL2, SDL2_image and SDL2_mixer +if (UNIX) + add_subdirectory(src/lib/SDL) +else() + find_package(SDL2) +endif() +set(SDL2MIXER_OPUS OFF) +set(SDL2MIXER_FLAC OFF) +set(SDL2MIXER_MOD OFF) +set(SDL2MIXER_MIDI_FLUIDSYNTH OFF) +find_library(SDL_MIXER_LIBRARY + NAMES SDL_mixer + HINTS + ENV SDLMIXERDIR + ENV SDLDIR + PATH_SUFFIXES lib ${VC_LIB_PATH_SUFFIX} + ) +add_subdirectory(src/lib/SDL_mixer) +find_package(SDL2_image) \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md deleted file mode 100644 index 5e74e4a0..00000000 --- a/docs/changelog.md +++ /dev/null @@ -1,26 +0,0 @@ -## September 2022 - -- Drawing Overworld maps to the screen -- Drawing entrance data on the overworld -- Drawing 2bpp inventory graphics data -- Started the YazeDelta project for version control. - -## August 2022 - -- Added ValidateCompressionResults to ROM::Compress -- Improved Overworld systems in preparation for drawing maps. - -## July 2022 - -- Display current overworld map graphics tile sheets. -- Added CreateAllGraphicsData to the ROM class -- Added Google Abseil C++ library for error handling, string manipulation -- Refactor ROM class to use smart pointers and STL containers - -## June 2022 - -- Implemented LC_LZ2 Decompression -- Created Bitmap class for displaying SNES Graphics -- Added Overworld and OverworldMap class definitions -- Built user interface using ImGui and SDL2 -- Started YAZE \ No newline at end of file diff --git a/docs/compression.md b/docs/compression.md new file mode 100644 index 00000000..0a380eb7 --- /dev/null +++ b/docs/compression.md @@ -0,0 +1,67 @@ +# LC_LZ2 Compression + +The compression algorithm has multiple implementations with varying levels of quality, based primarily on the implementations made in skarsnik/sneshacking, Zarby89/ZScreamDungeon and ZCompress with optimizations made for C++. + +## Key Definitions + +### Constants and Macros: +- `BUILD_HEADER(command, length)`: Macro to build a header from a command and a length. +- Command Constants: Constants to represent different commands like `kCommandDirectCopy`, `kCommandByteFill`, etc. +- Length and Mode Constants: Such as `kMaxLengthNormalHeader`, `kNintendoMode1`, etc. + +### Data Structures: + +#### 1. CompressionCommand: + - **arguments**: 2D array representing the command arguments for each possible command. + - **cmd_size**: Array storing the size of each possible command. + - **data_size**: Array storing the size of the data processed by each possible command. + +#### 2. CompressionPiece: + - **command**: Represents the compression command. + - **length**: Length of the compressed data piece. + - **argument_length**: Length of the argument. + - **argument**: Argument as a string. + - **next**: Pointer to the next compression piece. + +#### 3. CompressionContext (for Compression V3): + - Contains vectors to store raw and compressed data, compression pieces, and compression string. + - Various counters and flags for compression control. + - Current compression command details. + +## Compression Functions + +### Version 1: +- **Byte Repeat**: `CheckByteRepeat` +- **Word Repeat**: `CheckWordRepeat` +- **Increasing Byte**: `CheckIncByte` +- **Intra Copy**: `CheckIntraCopy` +- **Validation and Alternatives**: `ValidateForByteGain` & `CompressionCommandAlternative` + +### Version 2: +- **Byte Repeat**: `CheckByteRepeatV2` +- **Word Repeat**: `CheckWordRepeatV2` +- **Increasing Byte**: `CheckIncByteV2` +- **Intra Copy**: `CheckIntraCopyV2` +- **Validation and Alternatives**: `ValidateForByteGainV2` & `CompressionCommandAlternativeV2` + +### Version 3: +Using `CompressionContext` to handle compression. +- **Initialization**: `InitializeCompression` +- **Command Checks**: Such as `CheckByteRepeatV3` +- **Determining Best Compression**: `DetermineBestCompression` +- **Handling Direct Copy**: `HandleDirectCopy` +- **Adding Compression to Chain**: `AddCompressionToChain` + +## Decompression Functions: +- `SetBuffer`: Prepares a buffer from data. +- `memfill`: Fills memory. +- **Decompression**: Such as `DecompressV2`, `DecompressGraphics`, and `DecompressOverworld`. + +## Utility Functions: +- **Printing**: Such as `PrintCompressionPiece` and `PrintCompressionChain`. +- **Compression String Creation**: `CreateCompressionString` +- **Compression Result Validation**: Such as `ValidateCompressionResult` and its V3 variant. +- **Compression Piece Manipulation**: Like `SplitCompressionPiece` and its V3 variant. + +## Final Notes +The YAZE's LC_LZ2 compression scheme provides three versions of compression methodologies with comprehensive support for various commands and modes. It ensures versatility and adaptability for different compression needs. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..be311e78 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,32 @@ +# Getting Started with YAZE + +This software allows you to modify the classic SNES game "The Legend of Zelda: A Link to the Past" by editing its ROM file. With this editor, you can change various aspects of the game, such as the maps, sprites, items, and more. + +Please note that this project is currently a work in progress, and some features may not be fully implemented or may be subject to change. + +## Prerequisites +Before you start using YAZE, make sure you have the following: + +- A copy of "The Legend of Zelda: A Link to the Past" ROM file (US or JP) +- Basic knowledge of hexadecimal and binary data + +## Installation +To install YAZE, follow these steps based on your platform: + +### Windows + +### MacOS + +### GNU/Linux + +## Usage +To use the Link to the Past ROM Editor, follow these steps: + +Open the "ALTTP.sfc" ROM file using the "File" menu. + +... + +Save your changes using the "File" menu. +Backup files are enabled by default. Each save will produce a timestamped copy of your ROM before you last saved. + +That's it! With these instructions, you should be able to get started with using YAZE. Happy editing! \ No newline at end of file diff --git a/docs/infrastructure.md b/docs/infrastructure.md new file mode 100644 index 00000000..b066f30f --- /dev/null +++ b/docs/infrastructure.md @@ -0,0 +1,209 @@ +# YAZE Infrastructure Overview + +For developers to reference. + +## Directory Structure + +- **.github/workflows**: Contains `yaze_test` workflow config. +- **assets**: Hosts assets like fonts. +- **cmake**: Contains CMake configurations. +- **docs**: Contains documentation for users and developers. + - [Getting Started](./getting-started.md) + - [LC_LZ2 Compression](./compression.md) +- **src**: Contains source files. + - **app**: Contains the GUI editor `yaze` + - **cli**: Contains the command line interface `z3ed` + - **lib**: Contains git submodule dependencies. + - Abseil-cpp + - Asar + - ImGui + - ImGuiFileDialog + - ImGuiColorTextEdit + - imgui_memory_editor + - SDL2 +- **test**: Contains testing interface `yaze_test` + +## App Organization + +- **Core Namespace**: + - Contains fundamental functionalities. + - [Common](../src/app/core/common.h) + - [Constants](../src/app/core/constants.h) + - [Controller](../src/app/core/controller.h) + - [Editor](../src/app/core/editor.h) + - [Pipeline](../src/app/gui/pipeline.h) +- **Editor Namespace**: + - Editors are responsible for representing the GUI view and handling user input. + - These classes are all controlled by [MasterEditor](../src/app/editor/master_editor.h) + - [DungeonEditor](../src/app/editor/dungeon_editor.h) + - [GraphicsEditor](../src/app/editor/graphics_editor.h) + - [OverworldEditor](../src/app/editor/overworld_editor.h) + - [ScreenEditor](../src/app/editor/screen_editor.h) + - [SpriteEditor](../src/app/editor/sprite_editor.h) + - **Modules** + - [AssemblyEditor](../src/app/editor/modules/assembly_editor.h) + - [MusicEditor](../src/app/editor/modules/music_editor.h) + - [GfxGroupEditor](../src/app/editor/modules/gfx_group_editor.h) + - [Tile16Editor](../src/app/editor/modules/tile16_editor.h) +- **Emu Namespace**: + - Contains business logic for `core::emulator` + - [Audio](../src/app/emu/audio/) + - [Debug](../src/app/emu/debug/) + - [Memory](../src/app/emu/memory/) + - [Video](../src/app/emu/video/) + - [Emulator](../src/app/emu/emulator.h) +- **Gfx Namespace**: + - Handles graphics related tasks. + - [Bitmap](../src/app/gfx/bitmap.h) + - [Compression](../src/app/gfx/compression.h) + - [SCAD Format](../src/app/gfx/scad_format.h) + - [SNES Palette](../src/app/gfx/snes_palette.h) + - [SNES Tile](../src/app/gfx/snes_tile.h) +- **Gui Namespace**: + - Manages GUI elements. + - [Canvas](../src/app/gui/canvas.h) + - [Color](../src/app/gui/color.h) + - [Icons](../src/app/gui/icons.h) + - [Input](../src/app/gui/input.h) + - [Style](../src/app/gui/style.h) + - [Widgets](../src/app/gui/widgets.h) +- **Zelda3 Namespace**: + - Holds business logic specific to Zelda3. + - [Dungeon](../src/app/zelda3/dungeon/) + - [Music](../src/app/zelda3/music/) + - [Screen](../src/app/zelda3/screen/) + - [Sprite](../src/app/zelda3/sprite/) + - [OverworldMap](../src/app/zelda3/overworld_map.h) + - [Overworld](../src/app/zelda3/overworld.h) + +### Flow of Control + +- [app/yaze.cc](../src/app/yaze.cc) + - Initializes `absl::FailureSignalHandler` for stack tracing. + - Runs the `core::Controller` loop. +- [app/core/controller.cc](../src/app/core/controller.cc) + - Initializes SDLRenderer and SDLWindow + - Initializes ImGui, fonts, themes, and clipboard. + - Handles user input from keyboard and mouse. + - Updates `editor::MasterEditor` + - Renders the output to the screen. + - Handles the teardown of SDL and ImGui resources. +- [app/editor/master_editor.cc](../src/app/editor/master_editor.cc) + - Handles the main menu bar. + - File + - Open - [app::ROM::LoadFromFile](../src/app/rom.cc#l=90) + - Save - [app::ROM::SaveToFile](../src/app/rom.cc#l=301) + - Edit + - View + - Emulator + - HEX Editor + - ASM Editor + - Palette Editor + - Memory Viewer + - ImGui Demo + - GUI Tools + - Runtime Metrics + - Style Editor + - Help + - Handles `absl::Status` errors as popups delivered to the user. + - Update all the editors in a tab view. + - [app/editor/assembly_editor.cc](../src/app/editor/assembly_editor.cc) + - [app/editor/dungeon_editor.cc](../src/app/editor/dungeon_editor.cc) + - [app/editor/graphics_editor.cc](../src/app/editor/graphics_editor.cc) + - [app/editor/music_editor.cc](../src/app/editor/music_editor.cc) + - [app/editor/overworld_editor.cc](../src/app/editor/overworld_editor.cc) + - [app/editor/screen_editor.cc](../src/app/editor/screen_editor.cc) + - [app/editor/sprite_editor.cc](../src/app/editor/sprite_editor.cc) + +## ROM +- [app/rom.cc](../src/app/rom.cc) +- [app/rom.h](../src/app/rom.h) +--- + +This `ROM` class provides methods to manipulate and access data from a ROM. + +- **Key Methods**: + - `Load2BppGraphics()`: Loads 2BPP graphics data from specified sheets. + - `LoadAllGraphicsData()`: Loads all graphics data, both compressed and uncompressed, converting where necessary. + - `LoadFromFile(const absl::string_view& filename, bool z3_load)`: Loads ROM data from a file. It also handles headers and Zelda 3 specific data if requested. + - `LoadFromPointer(uchar* data, size_t length)`: Loads ROM data from a provided pointer. + - `LoadFromBytes(const Bytes& data)`: Loads ROM data from bytes. + - `LoadAllPalettes()`: Loads all color palettes used in the ROM. This includes palettes for various elements like sprites, shields, swords, etc. + - `UpdatePaletteColor(...)`: Updates a specific color within a named palette group. + +- **Internal Data Structures**: + - `rom_data_`: A container that holds the ROM data. + - `graphics_bin_`: Holds the graphics data. + - `palette_groups_`: A map containing various palette groups, each having its own set of color palettes. + +- **Special Notes**: + - The class interacts with various external functionalities, such as decompression algorithms (`gfx::DecompressV2`) and color conversion (`gfx::SnesTo8bppSheet`). + - Headers in the ROM data, if present, are identified and removed. + - Specific Zelda 3 data can be loaded if specified. + - Palettes are categorized into multiple groups (e.g., `ow_main`, `ow_aux`, `hud`, etc.) and loaded accordingly. + + +## Overworld + +- [app/zelda3/overworld.cc](../src/app/zelda3/overworld.cc) +- [app/zelda3/overworld.h](../src/app/zelda3/overworld.h) +- [app/zelda3/overworld_map.cc](../src/app/zelda3/overworld_map.cc) +- [app/zelda3/overworld_map.h](../src/app/zelda3/overworld_map.h) + +--- + +- **Construction of Tile16 and Tile32** +- **Save and Load Resources** + - Sprites + - Entrances + - Tilemaps + +## Bitmap + +- [app/gfx/bitmap.cc](../src/app/gfx/bitmap.cc) +- [app/gfx/bitmap.h](../src/app/gfx/bitmap.cc) +--- + +This class is responsible for creating, managing, and manipulating bitmap data, which can be displayed on the screen using the ImGui library. + +### Key Attributes: + +1. **Width, Height, Depth, and Data Size**: These represent the dimensions and data size of the bitmap. +2. **Pixel Data**: Points to the raw data of the bitmap. +3. **Texture and Surface**: Use SDL to manage the graphical representation of the bitmap data. Both these attributes have custom deleters, ensuring proper resource management. + +### Main Functions: + +1. **Constructors**: Multiple constructors allow for different ways to create a Bitmap instance, like specifying width, height, depth, and data. +2. **Create**: This set of overloaded functions provides ways to create a bitmap from different data sources. +3. **CreateFromSurface**: Allows for the creation of a bitmap from an SDL_Surface. +4. **Apply**: Changes the bitmap's data to a new set of Bytes. +5. **Texture Operations**: + - **CreateTexture**: Creates an SDL_Texture from the bitmap's data for rendering. + - **UpdateTexture**: Updates the SDL_Texture with the latest bitmap data. +6. **SaveSurfaceToFile**: Saves the SDL_Surface to a file. +7. **SetSurface**: Assigns a new SDL_Surface to the bitmap. +8. **Palette Functions**: + - **ApplyPalette (Overloaded)**: This allows for the application of a SNESPalette or a standard SDL_Color palette to the bitmap. +9. **WriteToPixel**: Directly writes a value to a specified position in the pixel data. + +## Z3ED cli + +| Command | Arg | Params | Status | +|---------|-----|--------|--------| +| Apply BPS Patch | -a | rom_file bps_file | In progress | +| Create BPS Patch | -c | bps_file src_file modified_file | Not started | +| Open ROM | -o | rom_file | Complete | +| Backup ROM | -b | rom_file [new_file] | In progress | +| Expand ROM | -x | rom_file file_size | Not started | +| Transfer Tile16 | -t | src_rom dest_rom tile32_id_list(csv) | Complete | +| Export Graphics | -e | rom_file bin_file | In progress | +| Import Graphics | -i | bin_file rom_file | Not started | +| SNES to PC Address | -s | address | Complete | +| PC to SNES Address | -p | address | Complete | + + +## Further Development Ideas +- Extend `zelda3` namespace with additional functionalities. +- Optimize program performance. +- Introduce new features in the GUI editor. diff --git a/docs/macos-build.md b/docs/macos-build.md new file mode 100644 index 00000000..7cce401b --- /dev/null +++ b/docs/macos-build.md @@ -0,0 +1,6 @@ +# macOS Build Settings + +- Clang 15.0.1 x86_64-apple-darrwin22.5.0 +- SDL2 Source v2.26.5 +- Removed snes_spc +- Removed asar_static \ No newline at end of file diff --git a/docs/manual/manual.pdf b/docs/manual/manual.pdf deleted file mode 100644 index 974fd6fc..00000000 Binary files a/docs/manual/manual.pdf and /dev/null differ diff --git a/docs/manual/manual.tex b/docs/manual/manual.tex deleted file mode 100644 index 4574d023..00000000 --- a/docs/manual/manual.tex +++ /dev/null @@ -1,40 +0,0 @@ -\documentclass[12pt, oneside]{report} -\title{Yet Another Zelda3 Editor} -\author{Justin Scofield\thanks{Special thanks to JaredBrian, Zarby89}} -\date{June 2022 - October 2022} -\pagestyle{headings} - -\begin{document} -\maketitle - -\tableofcontents - -\chapter{Introduction} - -{\bf Yet Another Zelda3 Editor} is a multi-purpose editor for the retro video game title {\it {"The Legend of Zelda: A Link to the Past"}} for the Super Nintendo Entertainment System. The editor only supports the US version. - -\section{Getting Started} -\section{Loading from ROM} -\section{Saving to ROM} - -\chapter{Overworld} - - The editor provides an interface for the user to make various changes to the overworld maps. These changes include the manpulation of the maps tiles, palettes, entrances, exits, sprites, area music, and other properties. Here we will explain the basics of the tile system. - - \section{Tile System} - \section{Map Toolset} - \section{Map Canvas} - -\chapter{Dungeons} - -\chapter{Palettes} - -\chapter{Sprites} - -\chapter{Screens} - \section{Inventory} - \section{Heads-up Display} - -\chapter{Modules} - -\end{document} \ No newline at end of file diff --git a/docs/dev-setup-windows.md b/docs/windows-build.md similarity index 100% rename from docs/dev-setup-windows.md rename to docs/windows-build.md diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e3b147ad..ce6df54a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,3 @@ -# yaze source files ----------------------------------------------------------- set( YAZE_APP_CORE_SRC app/core/common.cc @@ -7,94 +6,98 @@ set( set( YAZE_APP_EDITOR_SRC - app/editor/assembly_editor.cc app/editor/dungeon_editor.cc + app/editor/graphics_editor.cc app/editor/master_editor.cc - app/editor/music_editor.cc app/editor/overworld_editor.cc - app/editor/palette_editor.cc app/editor/screen_editor.cc + app/editor/sprite_editor.cc + app/editor/modules/music_editor.cc + app/editor/modules/palette_editor.cc + app/editor/modules/assembly_editor.cc + app/editor/modules/tile16_editor.cc + app/editor/modules/gfx_group_editor.cc + app/editor/context/gfx_context.cc ) set( YAZE_APP_GFX_SRC app/gfx/bitmap.cc + app/gfx/compression.cc + app/gfx/scad_format.cc app/gfx/snes_palette.cc app/gfx/snes_tile.cc ) set( YAZE_APP_ZELDA3_SRC - app/zelda3/inventory.cc app/zelda3/overworld_map.cc app/zelda3/overworld.cc - app/zelda3/title_screen.cc - app/zelda3/sprite.cc + app/zelda3/screen/inventory.cc + app/zelda3/screen/title_screen.cc + app/zelda3/sprite/sprite.cc + app/zelda3/music/tracker.cc + app/zelda3/dungeon/room.cc + app/zelda3/dungeon/room_object.cc ) set( YAZE_GUI_SRC - gui/canvas.cc - gui/input.cc - gui/style.cc - gui/widgets.cc - gui/color.cc + app/gui/canvas.cc + app/gui/input.cc + app/gui/style.cc + app/gui/widgets.cc + app/gui/color.cc + app/gui/pipeline.cc ) -# executable creation --------------------------------------------------------- - -add_executable( - yaze - app/yaze.cc - app/rom.cc - ${YAZE_APP_CORE_SRC} - ${YAZE_APP_EDITOR_SRC} - ${YAZE_APP_GFX_SRC} - ${YAZE_APP_ZELDA3_SRC} - ${YAZE_GUI_SRC} - ${ASAR_STATIC_SRC} - ${SNES_SPC_SOURCES} - ${IMGUI_SRC} -) - -# including libraries --------------------------------------------------------- - -target_include_directories( - yaze PUBLIC - lib/ - app/ - ${CMAKE_SOURCE_DIR}/src/ - ${PNG_INCLUDE_DIRS} - ${SDL2_INCLUDE_DIR} - lib/SDL_mixer/include/ - ${GLEW_INCLUDE_DIRS} - lib/asar/src/asar/ - lib/snes_spc/snes_spc/ +set( + YAZE_APP_EMU_SRC + app/emu/emulator.cc + app/emu/audio/apu.cc + app/emu/audio/spc700.cc + app/emu/audio/dsp.cc + app/emu/audio/internal/addressing.cc + app/emu/audio/internal/instructions.cc + app/emu/cpu/internal/addressing.cc + app/emu/cpu/internal/instructions.cc + app/emu/cpu/cpu.cc + app/emu/video/ppu.cc + app/emu/memory/dma.cc + app/emu/memory/memory.cc + app/emu/snes.cc ) set(SDL_TARGETS SDL2::SDL2) if(WIN32 OR MINGW) - list(PREPEND SDL_TARGETS SDL2::SDL2main) + list(PREPEND SDL_TARGETS SDL2::SDL2main ws2_32) add_definitions(-DSDL_MAIN_HANDLED) endif() -# linking libraries ----------------------------------------------------------- +if (WIN32 OR MINGW OR UNIX) + list(APPEND YAZE_APP_CORE_SRC + app/core/platform/font_loader.cc + ) +endif() -target_link_libraries( - yaze PUBLIC - ${ABSL_TARGETS} - ${SDL_TARGETS} - ${SDLMIXER_LIBRARY} - SDL2_mixer - ${PNG_LIBRARIES} - ${GLEW_LIBRARIES} - ${OPENGL_LIBRARIES} - ${CMAKE_DL_LIBS} - asar-static - snes_spc - ImGui -) +if(APPLE) + list(APPEND YAZE_APP_CORE_SRC + app/core/platform/file_dialog.mm + app/core/platform/app_delegate.mm + app/core/platform/font_loader.mm + app/core/platform/clipboard.mm + ) + + find_library(COCOA_LIBRARY Cocoa) + if(NOT COCOA_LIBRARY) + message(FATAL_ERROR "Cocoa not found") + endif() + set(CMAKE_EXE_LINKER_FLAGS "-framework ServiceManagement -framework Foundation -framework Cocoa") +endif() + +include(app/CMakeLists.txt) +include(cli/CMakeLists.txt) if (UNIX) target_compile_definitions(yaze PRIVATE "linux") @@ -109,7 +112,7 @@ if(MACOS) ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" - # MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/cmake/yaze.plist.in + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/cmake/yaze.plist.in ) elseif(UNIX) set_target_properties(yaze @@ -127,57 +130,4 @@ set_target_properties(yaze RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" LINK_FLAGS "${CMAKE_CURRENT_SOURCE_DIR}/yaze.res" ) -endif() - -# add_subdirectory(app/delta) - -# add_executable( -# yaze_delta -# app/delta/delta.cc -# app/delta/viewer.cc -# app/delta/service.cc -# app/delta/client.cc -# app/rom.cc -# ${YAZE_APP_ASM_SRC} -# ${YAZE_APP_CORE_SRC} -# ${YAZE_APP_EDITOR_SRC} -# ${YAZE_APP_GFX_SRC} -# ${YAZE_APP_ZELDA3_SRC} -# ${YAZE_GUI_SRC} -# ${IMGUI_SRC} -# ${ASAR_STATIC_SRC} -# ) - -# target_include_directories( -# yaze_delta PUBLIC -# lib/ -# app/ -# lib/asar/src/ -# ${ASAR_INCLUDE_DIR} -# ${CMAKE_SOURCE_DIR}/src/ -# ${PNG_INCLUDE_DIRS} -# ${SDL2_INCLUDE_DIR} -# ${GLEW_INCLUDE_DIRS} -# ) - -# target_link_libraries( -# yaze_delta PUBLIC -# ${ABSL_TARGETS} -# ${SDL_TARGETS} -# ${PNG_LIBRARIES} -# ${GLEW_LIBRARIES} -# ${OPENGL_LIBRARIES} -# ${CMAKE_DL_LIBS} -# delta-service -# asar-static -# ImGui -# ) -# target_compile_definitions(yaze_delta PRIVATE "linux") -# target_compile_definitions(yaze_delta PRIVATE "stricmp=strcasecmp") - -# set (source "${CMAKE_SOURCE_DIR}/assets") -# set (destination "${CMAKE_CURRENT_BINARY_DIR}/assets") -# add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD -# COMMAND ${CMAKE_COMMAND} -E create_symlink ${source} ${destination} -# DEPENDS ${destination} -# COMMENT "symbolic link resources folder from ${source} => ${destination}") \ No newline at end of file +endif() \ No newline at end of file diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt new file mode 100644 index 00000000..4e9dd430 --- /dev/null +++ b/src/app/CMakeLists.txt @@ -0,0 +1,36 @@ +add_executable( + yaze + app/yaze.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} +) + +target_include_directories( + yaze PUBLIC + lib/ + app/ + lib/SDL_mixer/include/ + ${CMAKE_SOURCE_DIR}/src/ + ${PNG_INCLUDE_DIRS} + ${SDL2_INCLUDE_DIR} +) + +target_link_libraries( + yaze PUBLIC + ${ABSL_TARGETS} + ${SDL_TARGETS} + ${PNG_LIBRARIES} + ${CMAKE_DL_LIBS} + SDL2_mixer + ImGui +) + +if (APPLE) + target_link_libraries(yaze PUBLIC ${COCOA_LIBRARY}) +endif() \ No newline at end of file diff --git a/src/app/core/common.cc b/src/app/core/common.cc index a8d0ee85..d3f4dcb8 100644 --- a/src/app/core/common.cc +++ b/src/app/core/common.cc @@ -1,20 +1,40 @@ #include "common.h" +#include + +#include #include +#include +#include +#include #include namespace yaze { namespace app { namespace core { -unsigned int SnesToPc(unsigned int addr) { +std::shared_ptr ExperimentFlags::flags_; + +uint32_t SnesToPc(uint32_t addr) { if (addr >= 0x808000) { addr -= 0x808000; } - unsigned int temp = (addr & 0x7FFF) + ((addr / 2) & 0xFF8000); + uint32_t temp = (addr & 0x7FFF) + ((addr / 2) & 0xFF8000); return (temp + 0x0); } +uint32_t PcToSnes(uint32_t addr) { + if (addr >= 0x400000) return -1; + addr = ((addr << 1) & 0x7F0000) | (addr & 0x7FFF) | 0x8000; + return addr; +} + +uint32_t MapBankToWordAddress(uint8_t bank, uint16_t addr) { + uint32_t result = 0; + result = (bank << 16) | addr; + return result; +} + int AddressFromBytes(uint8_t addr1, uint8_t addr2, uint8_t addr3) { return (addr1 << 16) | (addr2 << 8) | addr3; } @@ -58,6 +78,76 @@ bool StringReplace(std::string &str, const std::string &from, return true; } +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; +} + +void stle0(uint8_t *const p_arr, unsigned const p_val) { + stle(p_arr, 0, p_val); +} + +void stle1(uint8_t *const p_arr, unsigned const p_val) { + stle(p_arr, 1, p_val); +} + +void stle2(uint8_t *const p_arr, unsigned const p_val) { + stle(p_arr, 2, p_val); +} + +void stle3(uint8_t *const p_arr, unsigned const p_val) { + stle(p_arr, 3, p_val); +} +void stle16b(uint8_t *const p_arr, uint16_t const p_val) { + stle0(p_arr, p_val); + stle1(p_arr, p_val); +} +// "Store little endian 16-bit value using a byte pointer, offset by an +// index before dereferencing" +void stle16b_i(uint8_t *const p_arr, size_t const p_index, + uint16_t const p_val) { + stle16b(p_arr + (p_index * 2), p_val); +} +// "load little endian value at the given byte offset and shift to get its +// value relative to the base offset (powers of 256, essentially)" +unsigned ldle(uint8_t const *const p_arr, unsigned const p_index) { + uint32_t v = p_arr[p_index]; + + v <<= (8 * p_index); + + return v; +} + +// Helper function to get the first byte in a little endian number +uint32_t ldle0(uint8_t const *const p_arr) { return ldle(p_arr, 0); } + +// Helper function to get the second byte in a little endian number +uint32_t ldle1(uint8_t const *const p_arr) { return ldle(p_arr, 1); } + +// Helper function to get the third byte in a little endian number +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); } +// Load little endian halfword (16-bit) dereferenced from +uint16_t ldle16b(uint8_t const *const p_arr) { + uint16_t v = 0; + + v |= (ldle0(p_arr) | ldle1(p_arr)); + + return v; +} +// Load little endian halfword (16-bit) dereferenced from an arrays of bytes. +// This version provides an index that will be multiplied by 2 and added to the +// base address. +uint16_t ldle16b_i(uint8_t const *const p_arr, size_t const p_index) { + return ldle16b(p_arr + (2 * p_index)); +} + +// Initialize the static member +std::stack ImGuiIdIssuer::idStack; + } // namespace core } // namespace app } // namespace yaze diff --git a/src/app/core/common.h b/src/app/core/common.h index 4a6969a5..c8c4e699 100644 --- a/src/app/core/common.h +++ b/src/app/core/common.h @@ -1,19 +1,235 @@ #ifndef YAZE_CORE_COMMON_H #define YAZE_CORE_COMMON_H +#include + +#include #include +#include +#include +#include #include namespace yaze { namespace app { namespace core { -unsigned int SnesToPc(unsigned int addr); +class ExperimentFlags { + public: + struct Flags { + // Load and render overworld sprites to the screen. Unstable. + bool kDrawOverworldSprites = false; + + // Bitmap manager abstraction to manage graphics bin of ROM. + bool kUseBitmapManager = true; + + // Log instructions to the GUI debugger. + bool kLogInstructions = false; + + // 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; + + // Flag to enable the change queue, which could have any anonymous + // save routine for the ROM. In practice, just the overworld tilemap + // and tile32 save. + bool kSaveWithChangeQueue = false; + + // Attempt to run the dungeon room draw routine when opening a room. + bool kDrawDungeonRoomGraphics = true; + + // Use the new platform specific file dialog wrappers. + bool kNewFileDialogWrapper = true; + + // Platform specific loading of fonts from the system. Currently + // only supports macOS. + bool kLoadSystemFonts = true; + + bool kLoadTexturesAsStreaming = false; + }; + + 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(); + } + + private: + static std::shared_ptr flags_; +}; + +template +class NotifyValue { + public: + NotifyValue() : value_(), modified_(false), temp_value_() {} + NotifyValue(const T &value) + : value_(value), modified_(false), temp_value_() {} + + void set(const T &value) { + value_ = value; + modified_ = true; + } + + const T &get() { + modified_ = false; + return value_; + } + + T &mutable_get() { + modified_ = false; + temp_value_ = value_; + return temp_value_; + } + + void apply_changes() { + if (temp_value_ != value_) { + value_ = temp_value_; + modified_ = true; + } + } + + void operator=(const T &value) { set(value); } + operator T() { return get(); } + + bool modified() const { return modified_; } + + private: + T value_; + bool modified_; + T temp_value_; +}; + +struct TaskCheckpoint { + int task_index = 0; + bool complete = false; + // You can add more internal data or state-related variables here as needed +}; + +class TaskTimer { + public: + // Starts the timer + void StartTimer() { start_time_ = std::chrono::steady_clock::now(); } + + // Checks if the task should finish based on the given timeout in seconds + bool ShouldFinishTask(int timeout_seconds) { + auto current_time = std::chrono::steady_clock::now(); + auto elapsed_time = std::chrono::duration_cast( + current_time - start_time_); + return elapsed_time.count() >= timeout_seconds; + } + + private: + std::chrono::steady_clock::time_point start_time_; +}; + +template +class TaskManager { + public: + TaskManager() = default; + ~TaskManager() = default; + + TaskManager(int totalTasks, int timeoutSeconds) + : total_tasks_(totalTasks), + timeout_seconds_(timeoutSeconds), + task_index_(0), + task_complete_(false) {} + + void ExecuteTasks(const TFunc &taskFunc) { + if (task_complete_) { + return; + } + + StartTimer(); + + for (; task_index_ < total_tasks_; ++task_index_) { + taskFunc(task_index_); + + if (ShouldFinishTask()) { + break; + } + } + + if (task_index_ == total_tasks_) { + task_complete_ = true; + } + } + + bool IsTaskComplete() const { return task_complete_; } + void SetTimeout(int timeout) { timeout_seconds_ = timeout; } + + private: + int total_tasks_; + int timeout_seconds_; + int task_index_; + bool task_complete_; + std::chrono::steady_clock::time_point start_time_; + + void StartTimer() { start_time_ = std::chrono::steady_clock::now(); } + + bool ShouldFinishTask() { + auto current_time = std::chrono::steady_clock::now(); + auto elapsed_time = std::chrono::duration_cast( + current_time - start_time_); + return elapsed_time.count() >= timeout_seconds_; + } +}; + +class ImGuiIdIssuer { + private: + static std::stack idStack; + + public: + // Generate and push a new ID onto the stack + static ImGuiID GetNewID() { + static int counter = 1; // Start from 1 to ensure uniqueness + ImGuiID child_id = ImGui::GetID((void *)(intptr_t)counter++); + idStack.push(child_id); + return child_id; + } + + // Pop all IDs from the stack (can be called explicitly or upon program exit) + static void Cleanup() { + while (!idStack.empty()) { + idStack.pop(); + } + } +}; + +uint32_t SnesToPc(uint32_t addr); +uint32_t PcToSnes(uint32_t addr); + +uint32_t MapBankToWordAddress(uint8_t bank, uint16_t addr); + int AddressFromBytes(uint8_t addr1, uint8_t addr2, uint8_t addr3); int HexToDec(char *input, int length); + bool StringReplace(std::string &str, const std::string &from, const std::string &to); +void stle16b_i(uint8_t *const p_arr, size_t const p_index, + uint16_t const p_val); +uint16_t ldle16b_i(uint8_t const *const p_arr, size_t const p_index); + +void stle16b(uint8_t *const p_arr, uint16_t const p_val); +void stle32b(uint8_t *const p_arr, uint32_t const p_val); + +void stle32b_i(uint8_t *const p_arr, size_t const p_index, + uint32_t const p_val); + } // namespace core } // namespace app } // namespace yaze diff --git a/src/app/core/constants.h b/src/app/core/constants.h index 94f957ba..54107f5c 100644 --- a/src/app/core/constants.h +++ b/src/app/core/constants.h @@ -5,6 +5,8 @@ #include "absl/strings/string_view.h" +#define IMGUI_DEFINE_MATH_OPERATORS + #define BASIC_BUTTON(w) if (ImGui::Button(w)) #define TAB_BAR(w) if (ImGui::BeginTabBar(w)) { @@ -33,6 +35,22 @@ ImGui::TableNextColumn(); \ ImGui::Text(w); +#define BEGIN_TABLE(l, n, f) if (ImGui::BeginTable(l, n, f, ImVec2(0, 0))) { +#define SETUP_COLUMN(l) ImGui::TableSetupColumn(l); + +#define TABLE_HEADERS() \ + ImGui::TableHeadersRow(); \ + ImGui::TableNextRow(); + +#define NEXT_COLUMN() ImGui::TableNextColumn(); + +#define END_TABLE() \ + ImGui::EndTable(); \ + } + +#define HOVER_HINT(string) \ + if (ImGui::IsItemHovered()) ImGui::SetTooltip(string); + #define PRINT_IF_ERROR(expression) \ { \ auto error = expression; \ @@ -41,6 +59,15 @@ } \ } +#define EXIT_IF_ERROR(expression) \ + { \ + auto error = expression; \ + if (!error.ok()) { \ + std::cout << error.ToString() << std::endl; \ + return EXIT_FAILURE; \ + } \ + } + #define RETURN_IF_ERROR(expression) \ { \ auto error = expression; \ @@ -60,11 +87,37 @@ } \ type_variable_name = std::move(*error_or_value); +#define ASSIGN_OR_LOG_ERROR(type_variable_name, expression) \ + ASSIGN_OR_LOG_ERROR_IMPL(APPEND_NUMBER(error_or_value, __LINE__), \ + type_variable_name, expression) + +#define ASSIGN_OR_LOG_ERROR_IMPL(error_or_value, type_variable_name, \ + expression) \ + auto error_or_value = expression; \ + if (!error_or_value.ok()) { \ + std::cout << error_or_value.status().ToString() << std::endl; \ + } \ + type_variable_name = std::move(*error_or_value); + #define APPEND_NUMBER(expression, number) \ APPEND_NUMBER_INNER(expression, number) #define APPEND_NUMBER_INNER(expression, number) expression##number +#define TEXT_WITH_SEPARATOR(text) \ + ImGui::Text(text); \ + ImGui::Separator(); + +#define TABLE_BORDERS_RESIZABLE \ + ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable + +#define CLEAR_AND_RETURN_STATUS(status) \ + if (!status.ok()) { \ + auto temp = status; \ + status = absl::OkStatus(); \ + return temp; \ + } + using ushort = unsigned short; using uint = unsigned int; using uchar = unsigned char; @@ -82,39 +135,7 @@ namespace yaze { namespace app { namespace core { -// ============================================================================ -// Window Variables -// ============================================================================ - -constexpr int kScreenWidth = 1200; -constexpr int kScreenHeight = 800; - -// ============================================================================ -// 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"}; +constexpr float kYazeVersion = 0.05; // ============================================================================ // Magic numbers @@ -135,15 +156,8 @@ constexpr ushort TileNameMask = 0x03FF; constexpr int Uncompressed3BPPSize = 0x0600; constexpr int UncompressedSheetSize = 0x0800; -constexpr int NumberOfSheets = 223; -constexpr int LimitOfMap32 = 8864; constexpr int NumberOfRooms = 296; -constexpr int kNumOverworldMaps = 160; -constexpr int Map32PerScreen = 256; -constexpr int NumberOfMap16 = 3752; // 4096 -constexpr int NumberOfMap32 = Map32PerScreen * kNumOverworldMaps; -constexpr int NumberOfOWSprites = 352; constexpr int NumberOfColors = 3143; // ============================================================================ @@ -156,12 +170,7 @@ constexpr int subtype1_tiles = 0x8000; // JP = Same constexpr int subtype2_tiles = 0x83F0; // JP = Same constexpr int subtype3_tiles = 0x84F0; // JP = Same constexpr int gfx_animated_pointer = 0x10275; // JP 0x10624 //long pointer -constexpr int overworldgfxGroups2 = 0x6073; // 0x60B3 -// 2 byte pointer bank 00 pc -> 0x4320 -constexpr int gfx_1_pointer = 0x6790; // CF80 ; 004F80 -constexpr int gfx_2_pointer = 0x6795; // D05F ; 00505F -constexpr int gfx_3_pointer = 0x679A; // D13E ; 00513E constexpr int hud_palettes = 0xDD660; constexpr int maxGfx = 0xC3FB5; @@ -169,174 +178,6 @@ constexpr int kTilesheetWidth = 128; constexpr int kTilesheetHeight = 32; constexpr int kTilesheetDepth = 8; -// ============================================================================ -// Overworld Related Variables -// ============================================================================ - -constexpr int compressedAllMap32PointersHigh = 0x1794D; -constexpr int compressedAllMap32PointersLow = 0x17B2D; -constexpr int overworldgfxGroups = 0x05D97; -constexpr int map16Tiles = 0x78000; -constexpr int map32TilesTL = 0x18000; -constexpr int map32TilesTR = 0x1B400; -constexpr int map32TilesBL = 0x20000; -constexpr int map32TilesBR = 0x23400; -constexpr int overworldPalGroup1 = 0xDE6C8; -constexpr int overworldPalGroup2 = 0xDE86C; -constexpr int overworldPalGroup3 = 0xDE604; -constexpr int overworldMapPalette = 0x7D1C; -constexpr int overworldSpritePalette = 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 overworldItemsPointers = 0xDC2F9; -constexpr int overworldItemsAddress = 0xDC8B9; // 1BC2F9 -constexpr int overworldItemsBank = 0xDC8BF; -constexpr int overworldItemsEndData = 0xDC89C; // 0DC89E - -constexpr int mapGfx = 0x7C9C; -constexpr int overlayPointers = 0x77664; -constexpr int overlayPointersBank = 0x0E; - -constexpr int overworldTilesType = 0x71459; -constexpr int overworldMessages = 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; - -// 0x00 = small maps, 0x20 = large maps -constexpr int overworldMapSize = 0x12844; - -// 0x01 = small maps, 0x03 = large maps -constexpr int overworldMapSizeHighByte = 0x12884; - -// relative to the WORLD + 0x200 per map -// large map that are not == parent id = same position as their parent! -// eg for X position small maps : -// 0000, 0200, 0400, 0600, 0800, 0A00, 0C00, 0E00 -// all Large map would be : -// 0000, 0000, 0400, 0400, 0800, 0800, 0C00, 0C00 - -constexpr int overworldTransitionPositionY = 0x128C4; -constexpr int overworldTransitionPositionX = 0x12944; - -constexpr int overworldScreenSize = 0x1788D; - -// ============================================================================ -// Overworld Exits/Entrances Variables -// ============================================================================ -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 OWEntranceMap = 0xDB96F; -constexpr int OWEntrancePos = 0xDBA71; -constexpr int OWEntranceEntranceId = 0xDBB73; -constexpr int OWHolePos = 0xDB800; //(0x13 entries, 2 bytes each) modified(less - // 0x400) map16 coordinates for each hole -constexpr int OWHoleArea = - 0xDB826; //(0x13 entries, 2 bytes each) corresponding - // area numbers for each hole -constexpr int OWHoleEntrance = - 0xDB84C; //(0x13 entries, 1 byte each) corresponding entrance numbers - -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 - -// ============================================================================ -// Dungeon Related Variables -// ============================================================================ - -// That could be turned into a pointer : -constexpr int dungeons_palettes_groups = 0x75460; // JP 0x67DD0 -constexpr int dungeons_main_bg_palette_pointers = 0xDEC4B; // JP Same -constexpr int dungeons_palettes = - 0xDD734; // JP Same (where all dungeons palettes are) - -// That could be turned into a pointer : -constexpr int room_items_pointers = 0xDB69; // JP 0xDB67 - -constexpr int rooms_sprite_pointer = 0x4C298; // JP Same //2byte bank 09D62E -constexpr int room_header_pointer = 0xB5DD; // LONG -constexpr int room_header_pointers_bank = 0xB5E7; // JP Same - -constexpr int gfx_groups_pointer = 0x6237; -constexpr int room_object_layout_pointer = 0x882D; - -constexpr int room_object_pointer = 0x874C; // Long pointer - -constexpr int chests_length_pointer = 0xEBF6; -constexpr int chests_data_pointer1 = 0xEBFB; -// constexpr int chests_data_pointer2 = 0xEC0A; //Disabled for now could be used -// for expansion constexpr int chests_data_pointer3 = 0xEC10; //Disabled for now -// could be used for expansion - -constexpr int blocks_length = 0x8896; // word value -constexpr int blocks_pointer1 = 0x15AFA; -constexpr int blocks_pointer2 = 0x15B01; -constexpr int blocks_pointer3 = 0x15B08; -constexpr int blocks_pointer4 = 0x15B0F; - -constexpr int torch_data = 0x2736A; // JP 0x2704A -constexpr int torches_length_pointer = 0x88C1; - -constexpr int kSpriteBlocksetPointer = 0x5B57; -constexpr int sprites_data = - 0x4D8B0; // It use the unused pointers to have more space //Save purpose -constexpr int sprites_data_empty_room = 0x4D8AE; -constexpr int sprites_end_data = 0x4EC9E; - -constexpr int pit_pointer = 0x394AB; -constexpr int pit_count = 0x394A6; - -constexpr int doorPointers = 0xF83C0; - -// doors -constexpr int door_gfx_up = 0x4D9E; -constexpr int door_gfx_down = 0x4E06; -constexpr int door_gfx_cavexit_down = 0x4E06; -constexpr int door_gfx_left = 0x4E66; -constexpr int door_gfx_right = 0x4EC6; - -constexpr int door_pos_up = 0x197E; -constexpr int door_pos_down = 0x1996; -constexpr int door_pos_left = 0x19AE; -constexpr int door_pos_right = 0x19C6; - // TEXT EDITOR RELATED CONSTANTS constexpr int gfx_font = 0x70000; // 2bpp format constexpr int text_data = 0xE0000; @@ -482,7 +323,7 @@ constexpr int customAreaSpecificBGPalette = constexpr int customAreaSpecificBGASM = 0x140150; constexpr int customAreaSpecificBGEnabled = 0x140140; // 1 byte, not 0 if enabled -constexpr int overworldCustomMosaicArray = 0x1301F0; + // ============================================================================ // Dungeon Map Related Variables // ============================================================================ @@ -602,455 +443,6 @@ static const absl::string_view SecretItemNames[] = { "Hole", "Warp", "Staircase", "Bombable", "Switch"}; -static const absl::string_view Type1RoomObjectNames[] = { - "Ceiling ↔", - "Wall (top, north) ↔", - "Wall (top, south) ↔", - "Wall (bottom, north) ↔", - "Wall (bottom, south) ↔", - "Wall columns (north) ↔", - "Wall columns (south) ↔", - "Deep wall (north) ↔", - "Deep wall (south) ↔", - "Diagonal wall A ◤ (top) ↔", - "Diagonal wall A ◣ (top) ↔", - "Diagonal wall A ◥ (top) ↔", - "Diagonal wall A ◢ (top) ↔", - "Diagonal wall B ◤ (top) ↔", - "Diagonal wall B ◣ (top) ↔", - "Diagonal wall B ◥ (top) ↔", - "Diagonal wall B ◢ (top) ↔", - "Diagonal wall C ◤ (top) ↔", - "Diagonal wall C ◣ (top) ↔", - "Diagonal wall C ◥ (top) ↔", - "Diagonal wall C ◢ (top) ↔", - "Diagonal wall A ◤ (bottom) ↔", - "Diagonal wall A ◣ (bottom) ↔", - "Diagonal wall A ◥ (bottom) ↔", - "Diagonal wall A ◢ (bottom) ↔", - "Diagonal wall B ◤ (bottom) ↔", - "Diagonal wall B ◣ (bottom) ↔", - "Diagonal wall B ◥ (bottom) ↔", - "Diagonal wall B ◢ (bottom) ↔", - "Diagonal wall C ◤ (bottom) ↔", - "Diagonal wall C ◣ (bottom) ↔", - "Diagonal wall C ◥ (bottom) ↔", - "Diagonal wall C ◢ (bottom) ↔", - "Platform stairs ↔", - "Rail ↔", - "Pit edge ┏━┓ A (north) ↔", - "Pit edge ┏━┓ B (north) ↔", - "Pit edge ┏━┓ C (north) ↔", - "Pit edge ┏━┓ D (north) ↔", - "Pit edge ┏━┓ E (north) ↔", - "Pit edge ┗━┛ (south) ↔", - "Pit edge ━━━ (south) ↔", - "Pit edge ━━━ (north) ↔", - "Pit edge ━━┛ (south) ↔", - "Pit edge ┗━━ (south) ↔", - "Pit edge ━━┓ (north) ↔", - "Pit edge ┏━━ (north) ↔", - "Rail wall (north) ↔", - "Rail wall (south) ↔", - "Nothing", - "Nothing", - "Carpet ↔", - "Carpet trim ↔", - "Weird door", // TODO: WEIRD DOOR OBJECT NEEDS INVESTIGATION - "Drapes (north) ↔", - "Drapes (west, odd) ↔", - "Statues ↔", - "Columns ↔", - "Wall decors (north) ↔", - "Wall decors (south) ↔", - "Chairs in pairs ↔", - "Tall torches ↔", - "Supports (north) ↔", - "Water edge ┏━┓ (concave) ↔", - "Water edge ┗━┛ (concave) ↔", - "Water edge ┏━┓ (convex) ↔", - "Water edge ┗━┛ (convex) ↔", - "Water edge ┏━┛ (concave) ↔", - "Water edge ┗━┓ (concave) ↔", - "Water edge ┗━┓ (convex) ↔", - "Water edge ┏━┛ (convex) ↔", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Supports (south) ↔", - "Bar ↔", - "Shelf A ↔", - "Shelf B ↔", - "Shelf C ↔", - "Somaria path ↔", - "Cannon hole A (north) ↔", - "Cannon hole A (south) ↔", - "Pipe path ↔", - "Nothing", - "Wall torches (north) ↔", - "Wall torches (south) ↔", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Cannon hole B (north) ↔", - "Cannon hole B (south) ↔", - "Thick rail ↔", - "Blocks ↔", - "Long rail ↔", - "Ceiling ↕", - "Wall (top, west) ↕", - "Wall (top, east) ↕", - "Wall (bottom, west) ↕", - "Wall (bottom, east) ↕", - "Wall columns (west) ↕", - "Wall columns (east) ↕", - "Deep wall (west) ↕", - "Deep wall (east) ↕", - "Rail ↕", - "Pit edge (west) ↕", - "Pit edge (east) ↕", - "Rail wall (west) ↕", - "Rail wall (east) ↕", - "Nothing", - "Nothing", - "Carpet ↕", - "Carpet trim ↕", - "Nothing", - "Drapes (west) ↕", - "Drapes (east) ↕", - "Columns ↕", - "Wall decors (west) ↕", - "Wall decors (east) ↕", - "Supports (west) ↕", - "Water edge (west) ↕", - "Water edge (east) ↕", - "Supports (east) ↕", - "Somaria path ↕", - "Pipe path ↕", - "Nothing", - "Wall torches (west) ↕", - "Wall torches (east) ↕", - "Wall decors tight A (west) ↕", - "Wall decors tight A (east) ↕", - "Wall decors tight B (west) ↕", - "Wall decors tight B (east) ↕", - "Cannon hole (west) ↕", - "Cannon hole (east) ↕", - "Tall torches ↕", - "Thick rail ↕", - "Blocks ↕", - "Long rail ↕", - "Jump ledge (west) ↕", - "Jump ledge (east) ↕", - "Rug trim (west) ↕", - "Rug trim (east) ↕", - "Bar ↕", - "Wall flair (west) ↕", - "Wall flair (east) ↕", - "Blue pegs ↕", - "Orange pegs ↕", - "Invisible floor ↕", - "Fake pots ↕", - "Hammer pegs ↕", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Diagonal ceiling A ◤", - "Diagonal ceiling A ◣", - "Diagonal ceiling A ◥", - "Diagonal ceiling A ◢", - "Pit ⇲", - "Diagonal layer 2 mask A ◤", - "Diagonal layer 2 mask A ◣", - "Diagonal layer 2 mask A ◥", - "Diagonal layer 2 mask A ◢", - "Diagonal layer 2 mask B ◤", // TODO: VERIFY - "Diagonal layer 2 mask B ◣", // TODO: VERIFY - "Diagonal layer 2 mask B ◥", // TODO: VERIFY - "Diagonal layer 2 mask B ◢", // TODO: VERIFY - "Nothing", - "Nothing", - "Nothing", - "Jump ledge (north) ↔", - "Jump ledge (south) ↔", - "Rug ↔", - "Rug trim (north) ↔", - "Rug trim (south) ↔", - "Archery game curtains ↔", - "Wall flair (north) ↔", - "Wall flair (south) ↔", - "Blue pegs ↔", - "Orange pegs ↔", - "Invisible floor ↔", - "Fake pressure plates ↔", - "Fake pots ↔", - "Hammer pegs ↔", - "Nothing", - "Nothing", - "Ceiling (large) ⇲", - "Chest platform (tall) ⇲", - "Layer 2 pit mask (large) ⇲", - "Layer 2 pit mask (medium) ⇲", - "Floor 1 ⇲", - "Floor 3 ⇲", - "Layer 2 mask (large) ⇲", - "Floor 4 ⇲", - "Water floor ⇲ ", - "Flood water (medium) ⇲ ", - "Conveyor floor ⇲ ", - "Nothing", - "Nothing", - "Moving wall (west) ⇲", - "Moving wall (east) ⇲", - "Nothing", - "Nothing", - "Icy floor A ⇲", - "Icy floor B ⇲", - "Moving wall flag", // TODO: WTF IS THIS? - "Moving wall flag", // TODO: WTF IS THIS? - "Moving wall flag", // TODO: WTF IS THIS? - "Moving wall flag", // TODO: WTF IS THIS? - "Layer 2 mask (medium) ⇲", - "Flood water (large) ⇲", - "Layer 2 swim mask ⇲", - "Flood water B (large) ⇲", - "Floor 2 ⇲", - "Chest platform (short) ⇲", - "Table / rock ⇲", - "Spike blocks ⇲", - "Spiked floor ⇲", - "Floor 7 ⇲", - "Tiled floor ⇲", - "Rupee floor ⇲", - "Conveyor upwards ⇲", - "Conveyor downwards ⇲", - "Conveyor leftwards ⇲", - "Conveyor rightwards ⇲", - "Heavy current water ⇲", - "Floor 10 ⇲", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", - "Nothing", -}; - -static const absl::string_view Type2RoomObjectNames[] = { - "Corner (top, concave) ▛", - "Corner (top, concave) ▙", - "Corner (top, concave) ▜", - "Corner (top, concave) ▟", - "Corner (top, convex) ▟", - "Corner (top, convex) ▜", - "Corner (top, convex) ▙", - "Corner (top, convex) ▛", - "Corner (bottom, concave) ▛", - "Corner (bottom, concave) ▙", - "Corner (bottom, concave) ▜", - "Corner (bottom, concave) ▟", - "Corner (bottom, convex) ▟", - "Corner (bottom, convex) ▜", - "Corner (bottom, convex) ▙", - "Corner (bottom, convex) ▛", - "Kinked corner north (bottom) ▜", - "Kinked corner south (bottom) ▟", - "Kinked corner north (bottom) ▛", - "Kinked corner south (bottom) ▙", - "Kinked corner west (bottom) ▙", - "Kinked corner west (bottom) ▛", - "Kinked corner east (bottom) ▟", - "Kinked corner east (bottom) ▜", - "Deep corner (concave) ▛", - "Deep corner (concave) ▙", - "Deep corner (concave) ▜", - "Deep corner (concave) ▟", - "Large brazier", - "Statue", - "Star tile (disabled)", - "Star tile (enabled)", - "Small torch (lit)", - "Barrel", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Table", - "Fairy statue", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Chair", - "Bed", - "Fireplace", - "Mario portrait", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Interroom stairs (up)", - "Interroom stairs (down)", - "Interroom stairs B (down)", - "Intraroom stairs north B", // TODO: VERIFY LAYER HANDLING - "Intraroom stairs north (separate layers)", - "Intraroom stairs north (merged layers)", - "Intraroom stairs north (swim layer)", - "Block", - "Water ladder (north)", - "Water ladder (south)", // TODO: NEEDS IN GAME VERIFICATION - "Dam floodgate", - "Interroom spiral stairs up (top)", - "Interroom spiral stairs down (top)", - "Interroom spiral stairs up (bottom)", - "Interroom spiral stairs down (bottom)", - "Sanctuary wall (north)", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Pew", - "Magic bat altar", -}; - -static const absl::string_view Type3RoomObjectNames[] = { - "Waterfall face (empty)", - "Waterfall face (short)", - "Waterfall face (long)", - "Somaria path endpoint", - "Somaria path intersection ╋", - "Somaria path corner ┏", - "Somaria path corner ┗", - "Somaria path corner ┓", - "Somaria path corner ┛", - "Somaria path intersection ┳", - "Somaria path intersection ┻", - "Somaria path intersection ┣", - "Somaria path intersection ┫", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Somaria path 2-way endpoint", - "Somaria path crossover", - "Babasu hole (north)", - "Babasu hole (south)", - "9 blue rupees", - "Telepathy tile", - "Warp door", // TODO: NEEDS IN GAME VERIFICATION THAT THIS IS USELESS - "Kholdstare's shell", - "Hammer peg", - "Prison cell", - "Big key lock", - "Chest", - "Chest (open)", - "Intraroom stairs south", // TODO: VERIFY LAYER HANDLING - "Intraroom stairs south (separate layers)", - "Intraroom stairs south (merged layers)", - "Interroom straight stairs up (north, top)", - "Interroom straight stairs down (north, top)", - "Interroom straight stairs up (south, top)", - "Interroom straight stairs down (south, top)", - "Deep corner (convex) ▟", - "Deep corner (convex) ▜", - "Deep corner (convex) ▙", - "Deep corner (convex) ▛", - "Interroom straight stairs up (north, bottom)", - "Interroom straight stairs down (north, bottom)", - "Interroom straight stairs up (south, bottom)", - "Interroom straight stairs down (south, bottom)", - "Lamp cones", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Liftable large block", - "Agahnim's altar", - "Agahnim's boss room", - "Pot", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Big chest", - "Big chest (open)", - "Intraroom stairs south (swim layer)", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Pipe end (south)", - "Pipe end (north)", - "Pipe end (east)", - "Pipe end (west)", - "Pipe corner ▛", - "Pipe corner ▙", - "Pipe corner ▜", - "Pipe corner ▟", - "Pipe-rock intersection ⯊", - "Pipe-rock intersection ⯋", - "Pipe-rock intersection ◖", - "Pipe-rock intersection ◗", - "Pipe crossover", - "Bombable floor", - "Fake bombable floor", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Warp tile", - "Tool rack", - "Furnace", - "Tub (wide)", - "Anvil", - "Warp tile (disabled)", - "Pressure plate", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Blue peg", - "Orange peg", - "Fortune teller room", - "Unknown", // TODO: NEEDS IN GAME CHECKING - "Bar corner ▛", - "Bar corner ▙", - "Bar corner ▜", - "Bar corner ▟", - "Decorative bowl", - "Tub (tall)", - "Bookcase", - "Range", - "Suitcase", - "Bar bottles", - "Arrow game hole (west)", - "Arrow game hole (east)", - "Vitreous goo gfx", - "Fake pressure plate", - "Medusa head", - "4-way shooter block", - "Pit", - "Wall crack (north)", - "Wall crack (south)", - "Wall crack (west)", - "Wall crack (east)", - "Large decor", - "Water grate (north)", - "Water grate (south)", - "Water grate (west)", - "Water grate (east)", - "Window sunlight", - "Floor sunlight", - "Trinexx's shell", - "Layer 2 mask (full)", - "Boss entrance", - "Minigame chest", - "Ganon door", - "Triforce wall ornament", - "Triforce floor tiles", - "Freezor hole", - "Pile of bones", - "Vitreous goo damage", - "Arrow tile ↑", - "Arrow tile ↓", - "Arrow tile →", - "Nothing", -}; - static const absl::string_view TileTypeNames[] = { "$00 Nothing (standard floor)", "$01 Collision", diff --git a/src/app/core/controller.cc b/src/app/core/controller.cc index 65b66100..dd6177d4 100644 --- a/src/app/core/controller.cc +++ b/src/app/core/controller.cc @@ -2,8 +2,8 @@ #include #include -#include -#include +#include +#include #include #include @@ -11,9 +11,10 @@ #include "absl/status/status.h" #include "absl/strings/str_format.h" +#include "app/core/platform/font_loader.h" #include "app/editor/master_editor.h" -#include "gui/icons.h" -#include "gui/style.h" +#include "app/gui/icons.h" +#include "app/gui/style.h" namespace yaze { namespace app { @@ -32,6 +33,21 @@ void InitializeKeymap() { io.KeyMap[ImGuiKey_LeftCtrl] = SDL_GetScancodeFromKey(SDLK_LCTRL); } +void ImGui_ImplSDL2_SetClipboardText(void *user_data, const char *text) { + SDL_SetClipboardText(text); +} + +const char *ImGui_ImplSDL2_GetClipboardText(void *user_data) { + return SDL_GetClipboardText(); +} + +void InitializeClipboard() { + ImGuiIO &io = ImGui::GetIO(); + io.SetClipboardTextFn = ImGui_ImplSDL2_SetClipboardText; + io.GetClipboardTextFn = ImGui_ImplSDL2_GetClipboardText; + io.ClipboardUserData = nullptr; +} + void HandleKeyDown(SDL_Event &event) { ImGuiIO &io = ImGui::GetIO(); switch (event.key.keysym.sym) { @@ -81,10 +97,8 @@ void HandleMouseMovement(int &wheel) { } // namespace -bool Controller::isActive() const { return active_; } - -absl::Status Controller::onEntry() { - RETURN_IF_ERROR(CreateWindow()) +absl::Status Controller::OnEntry() { + RETURN_IF_ERROR(CreateSDL_Window()) RETURN_IF_ERROR(CreateRenderer()) RETURN_IF_ERROR(CreateGuiContext()) InitializeKeymap(); @@ -93,7 +107,7 @@ absl::Status Controller::onEntry() { return absl::OkStatus(); } -void Controller::onInput() { +void Controller::OnInput() { int wheel = 0; SDL_Event event; ImGuiIO &io = ImGui::GetIO(); @@ -124,7 +138,6 @@ void Controller::onInput() { break; } break; - default: break; } @@ -133,33 +146,40 @@ void Controller::onInput() { HandleMouseMovement(wheel); } -void Controller::onLoad() { master_editor_.UpdateScreen(); } +void Controller::OnLoad() { PRINT_IF_ERROR(master_editor_.Update()); } -void Controller::doRender() const { - SDL_RenderClear(renderer_.get()); +void Controller::DoRender() const { ImGui::Render(); - ImGui_ImplSDLRenderer_RenderDrawData(ImGui::GetDrawData()); + SDL_RenderClear(renderer_.get()); + ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData()); SDL_RenderPresent(renderer_.get()); } -void Controller::onExit() const { - ImGui_ImplSDLRenderer_Shutdown(); +void Controller::OnExit() { + master_editor_.Shutdown(); + Mix_CloseAudio(); + ImGui_ImplSDLRenderer2_Shutdown(); ImGui_ImplSDL2_Shutdown(); ImGui::DestroyContext(); SDL_Quit(); } -absl::Status Controller::CreateWindow() { +absl::Status Controller::CreateSDL_Window() { if (SDL_Init(SDL_INIT_EVERYTHING) != 0) { return absl::InternalError( absl::StrFormat("SDL_Init: %s\n", SDL_GetError())); } else { + SDL_DisplayMode displayMode; + SDL_GetCurrentDisplayMode(0, &displayMode); + int screenWidth = displayMode.w * 0.8; + int screenHeight = displayMode.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 - kScreenWidth, // width, in pixels - kScreenHeight, // height, in pixels + screenWidth, // width, in pixels + screenHeight, // height, in pixels SDL_WINDOW_RESIZABLE), sdl_deleter()); if (window_ == nullptr) { @@ -167,7 +187,7 @@ absl::Status Controller::CreateWindow() { absl::StrFormat("SDL_CreateWindow: %s\n", SDL_GetError())); } // Initialize SDL_mixer - if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) { + if (Mix_OpenAudio(32000, MIX_DEFAULT_FORMAT, 2, 1024) < 0) { printf("SDL_mixer could not initialize! SDL_mixer Error: %s\n", Mix_GetError()); } @@ -191,35 +211,86 @@ absl::Status Controller::CreateRenderer() { } absl::Status Controller::CreateGuiContext() { + IMGUI_CHECKVERSION(); ImGui::CreateContext(); + ImGuiIO &io = ImGui::GetIO(); + if (flags()->kUseNewImGuiInput) { + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + } + // Initialize ImGui for SDL ImGui_ImplSDL2_InitForSDLRenderer(window_.get(), renderer_.get()); - ImGui_ImplSDLRenderer_Init(renderer_.get()); + ImGui_ImplSDLRenderer2_Init(renderer_.get()); - // Load available fonts - const ImGuiIO &io = ImGui::GetIO(); - io.Fonts->AddFontFromFileTTF("assets/font/Karla-Regular.ttf", 14.0f); + if (flags()->kLoadSystemFonts) { + LoadSystemFonts(); + } else { + RETURN_IF_ERROR(LoadFontFamilies()); + } - // merge in icons from Google Material Design + // Set the default style + gui::ColorsYaze(); + + // Build a new ImGui frame + ImGui_ImplSDLRenderer2_NewFrame(); + ImGui_ImplSDL2_NewFrame(window_.get()); + + return absl::OkStatus(); +} + +absl::Status Controller::LoadFontFamilies() const { + ImGuiIO &io = ImGui::GetIO(); + + // Define constants + static const char *KARLA_REGULAR = "assets/font/Karla-Regular.ttf"; + static const char *ROBOTO_MEDIUM = "assets/font/Roboto-Medium.ttf"; + static const char *COUSINE_REGULAR = "assets/font/Cousine-Regular.ttf"; + static const char *DROID_SANS = "assets/font/DroidSans.ttf"; + static const char *NOTO_SANS_JP = "assets/font/NotoSansJP.ttf"; + static const char *IBM_PLEX_JP = "assets/font/IBMPlexSansJP-Bold.ttf"; + static const float FONT_SIZE_DEFAULT = 14.0f; + static const float FONT_SIZE_DROID_SANS = 16.0f; + static const float ICON_FONT_SIZE = 18.0f; + + // Icon configuration static const ImWchar icons_ranges[] = {ICON_MIN_MD, 0xf900, 0}; ImFontConfig icons_config; icons_config.MergeMode = true; icons_config.GlyphOffset.y = 5.0f; icons_config.GlyphMinAdvanceX = 13.0f; icons_config.PixelSnapH = true; - io.Fonts->AddFontFromFileTTF(FONT_ICON_FILE_NAME_MD, 18.0f, &icons_config, - icons_ranges); - io.Fonts->AddFontFromFileTTF("assets/font/Roboto-Medium.ttf", 14.0f); - io.Fonts->AddFontFromFileTTF("assets/font/Cousine-Regular.ttf", 14.0f); - io.Fonts->AddFontFromFileTTF("assets/font/DroidSans.ttf", 16.0f); - // Set the default style - gui::ColorsYaze(); + // Japanese font configuration + ImFontConfig japanese_font_config; + japanese_font_config.MergeMode = true; + icons_config.GlyphOffset.y = 5.0f; + icons_config.GlyphMinAdvanceX = 13.0f; + icons_config.PixelSnapH = true; - // Build a new ImGui frame - ImGui_ImplSDLRenderer_NewFrame(); - ImGui_ImplSDL2_NewFrame(window_.get()); + // List of fonts to be loaded + std::vector font_paths = {KARLA_REGULAR, ROBOTO_MEDIUM, + COUSINE_REGULAR, IBM_PLEX_JP}; + + // Load fonts with associated icon and Japanese merges + for (const auto &font_path : font_paths) { + float font_size = + (font_path == DROID_SANS) ? FONT_SIZE_DROID_SANS : FONT_SIZE_DEFAULT; + + if (!io.Fonts->AddFontFromFileTTF(font_path, font_size)) { + return absl::InternalError( + absl::StrFormat("Failed to load font from %s", font_path)); + } + + // Merge icon set + io.Fonts->AddFontFromFileTTF(FONT_ICON_FILE_NAME_MD, ICON_FONT_SIZE, + &icons_config, icons_ranges); + + // Merge Japanese font + io.Fonts->AddFontFromFileTTF(NOTO_SANS_JP, 18.0f, &japanese_font_config, + io.Fonts->GetGlyphRangesJapanese()); + } return absl::OkStatus(); } diff --git a/src/app/core/controller.h b/src/app/core/controller.h index ba5af3e4..3a6b9f86 100644 --- a/src/app/core/controller.h +++ b/src/app/core/controller.h @@ -2,17 +2,19 @@ #define YAZE_APP_CORE_CONTROLLER_H #include -#include -#include +#include +#include #include #include #include #include "absl/status/status.h" +#include "app/core/common.h" +#include "app/core/editor.h" #include "app/editor/master_editor.h" -#include "gui/icons.h" -#include "gui/style.h" +#include "app/gui/icons.h" +#include "app/gui/style.h" int main(int argc, char **argv); @@ -20,26 +22,34 @@ namespace yaze { namespace app { namespace core { -class Controller { +class Controller : public ExperimentFlags { public: - bool isActive() const; - absl::Status onEntry(); - void onInput(); - void onLoad(); - void onLoadDelta(); - void doRender() const; - void onExit() const; + bool IsActive() const { return active_; } + absl::Status OnEntry(); + void OnInput(); + void OnLoad(); + void DoRender() const; + void OnExit(); private: struct sdl_deleter { - void operator()(SDL_Window *p) const { SDL_DestroyWindow(p); } - void operator()(SDL_Renderer *p) const { SDL_DestroyRenderer(p); } + void operator()(SDL_Window *p) const { + if (p) { + SDL_DestroyWindow(p); + } + } + void operator()(SDL_Renderer *p) const { + if (p) { + SDL_DestroyRenderer(p); + } + } void operator()(SDL_Texture *p) const { SDL_DestroyTexture(p); } }; - absl::Status CreateWindow(); + absl::Status CreateSDL_Window(); absl::Status CreateRenderer(); absl::Status CreateGuiContext(); + absl::Status LoadFontFamilies() const; void CloseWindow() { active_ = false; } friend int ::main(int argc, char **argv); diff --git a/src/app/core/editor.h b/src/app/core/editor.h new file mode 100644 index 00000000..2aa5b88d --- /dev/null +++ b/src/app/core/editor.h @@ -0,0 +1,21 @@ +#ifndef YAZE_APP_CORE_EDITOR_H +#define YAZE_APP_CORE_EDITOR_H + +#include "absl/status/status.h" + +class Editor { + public: + Editor() = default; + virtual ~Editor() = default; + + virtual absl::Status Cut() = 0; + virtual absl::Status Copy() = 0; + virtual absl::Status Paste() = 0; + + virtual absl::Status Undo() = 0; + virtual absl::Status Redo() = 0; + + virtual absl::Status Update() = 0; +}; + +#endif // YAZE_APP_CORE_EDITOR_H \ No newline at end of file diff --git a/src/app/core/platform/app_delegate.h b/src/app/core/platform/app_delegate.h new file mode 100644 index 00000000..10d60f86 --- /dev/null +++ b/src/app/core/platform/app_delegate.h @@ -0,0 +1,14 @@ +#ifndef YAZE_APP_CORE_PLATFORM_APP_DELEGATE_H +#define YAZE_APP_CORE_PLATFORM_APP_DELEGATE_H + +#ifdef __cplusplus +extern "C" { +#endif + +void InitializeCocoa(); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // YAZE_APP_CORE_PLATFORM_APP_DELEGATE_H diff --git a/src/app/core/platform/app_delegate.mm b/src/app/core/platform/app_delegate.mm new file mode 100644 index 00000000..1bb56c21 --- /dev/null +++ b/src/app/core/platform/app_delegate.mm @@ -0,0 +1,218 @@ +// AppDelegate.mm +#import + +#import "app/core/controller.h" +#import "app/core/editor.h" +#import "app/core/platform/app_delegate.h" +#import "app/core/platform/file_dialog.h" +#import "app/rom.h" + +@interface AppDelegate : NSObject +- (void)setupMenus; +// - (void)changeApplicationIcon; +@end + +@implementation AppDelegate + +// - (void)changeApplicationIcon { +// NSImage *newIcon = [NSImage imageNamed:@"newIcon"]; +// [NSApp setApplicationIconImage:newIcon]; +// } + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + [self setupMenus]; +} + +- (void)setupMenus { + NSMenu *mainMenu = [NSApp mainMenu]; + + NSMenuItem *fileMenuItem = [mainMenu itemWithTitle:@"File"]; + if (!fileMenuItem) { + NSMenu *fileMenu = [[NSMenu alloc] initWithTitle:@"File"]; + fileMenuItem = [[NSMenuItem alloc] initWithTitle:@"File" action:nil keyEquivalent:@""]; + [fileMenuItem setSubmenu:fileMenu]; + + NSMenuItem *openItem = [[NSMenuItem alloc] initWithTitle:@"Open" + action:@selector(openFileAction:) + keyEquivalent:@"o"]; + [fileMenu addItem:openItem]; + + // Open Recent + NSMenu *openRecentMenu = [[NSMenu alloc] initWithTitle:@"Open Recent"]; + NSMenuItem *openRecentMenuItem = [[NSMenuItem alloc] initWithTitle:@"Open Recent" + action:nil + keyEquivalent:@""]; + [openRecentMenuItem setSubmenu:openRecentMenu]; + [fileMenu addItem:openRecentMenuItem]; + + // Add a separator + [fileMenu addItem:[NSMenuItem separatorItem]]; + + // Save + NSMenuItem *saveItem = [[NSMenuItem alloc] initWithTitle:@"Save" action:nil keyEquivalent:@"s"]; + [fileMenu addItem:saveItem]; + + // Separator + [fileMenu addItem:[NSMenuItem separatorItem]]; + + // Options submenu + NSMenu *optionsMenu = [[NSMenu alloc] initWithTitle:@"Options"]; + NSMenuItem *optionsMenuItem = [[NSMenuItem alloc] initWithTitle:@"Options" + action:nil + keyEquivalent:@""]; + [optionsMenuItem setSubmenu:optionsMenu]; + + // Flag checkmark field + NSMenuItem *flagItem = [[NSMenuItem alloc] initWithTitle:@"Flag" + action:@selector(toggleFlagAction:) + keyEquivalent:@""]; + [flagItem setTarget:self]; + [flagItem setState:NSControlStateValueOff]; + [optionsMenu addItem:flagItem]; + [fileMenu addItem:optionsMenuItem]; + + [mainMenu insertItem:fileMenuItem atIndex:1]; + } + + NSMenuItem *editMenuItem = [mainMenu itemWithTitle:@"Edit"]; + if (!editMenuItem) { + NSMenu *editMenu = [[NSMenu alloc] initWithTitle:@"Edit"]; + editMenuItem = [[NSMenuItem alloc] initWithTitle:@"Edit" action:nil keyEquivalent:@""]; + [editMenuItem setSubmenu:editMenu]; + + NSMenuItem *undoItem = [[NSMenuItem alloc] initWithTitle:@"Undo" action:nil keyEquivalent:@"z"]; + + [editMenu addItem:undoItem]; + + NSMenuItem *redoItem = [[NSMenuItem alloc] initWithTitle:@"Redo" action:nil keyEquivalent:@"Z"]; + + [editMenu addItem:redoItem]; + + // Add a separator + [editMenu addItem:[NSMenuItem separatorItem]]; + + NSMenuItem *cutItem = [[NSMenuItem alloc] initWithTitle:@"Cut" + action:@selector(cutAction:) + keyEquivalent:@"x"]; + [editMenu addItem:cutItem]; + + NSMenuItem *copyItem = [[NSMenuItem alloc] initWithTitle:@"Copy" action:nil keyEquivalent:@"c"]; + [editMenu addItem:copyItem]; + + NSMenuItem *pasteItem = [[NSMenuItem alloc] initWithTitle:@"Paste" + action:nil + keyEquivalent:@"v"]; + + [editMenu addItem:pasteItem]; + + // Add a separator + [editMenu addItem:[NSMenuItem separatorItem]]; + + NSMenuItem *selectAllItem = [[NSMenuItem alloc] initWithTitle:@"Select All" + action:nil + keyEquivalent:@"a"]; + + [editMenu addItem:selectAllItem]; + + [mainMenu insertItem:editMenuItem atIndex:2]; + } + + NSMenuItem *viewMenuItem = [mainMenu itemWithTitle:@"View"]; + if (!viewMenuItem) { + NSMenu *viewMenu = [[NSMenu alloc] initWithTitle:@"View"]; + viewMenuItem = [[NSMenuItem alloc] initWithTitle:@"View" action:nil keyEquivalent:@""]; + [viewMenuItem setSubmenu:viewMenu]; + + // Emulator view button + NSMenuItem *emulatorViewItem = [[NSMenuItem alloc] initWithTitle:@"Emulator View" + action:nil + keyEquivalent:@"1"]; + + [viewMenu addItem:emulatorViewItem]; + + // Hex Editor View + NSMenuItem *hexEditorViewItem = [[NSMenuItem alloc] initWithTitle:@"Hex Editor View" + action:nil + keyEquivalent:@"2"]; + + [viewMenu addItem:hexEditorViewItem]; + + // Disassembly view button + NSMenuItem *disassemblyViewItem = [[NSMenuItem alloc] initWithTitle:@"Disassembly View" + action:nil + keyEquivalent:@"3"]; + + [viewMenu addItem:disassemblyViewItem]; + + // Memory view button + NSMenuItem *memoryViewItem = [[NSMenuItem alloc] initWithTitle:@"Memory View" + action:nil + keyEquivalent:@"4"]; + + [viewMenu addItem:memoryViewItem]; + + // Add a separator + [viewMenu addItem:[NSMenuItem separatorItem]]; + + // Toggle fullscreen button + NSMenuItem *toggleFullscreenItem = [[NSMenuItem alloc] initWithTitle:@"Toggle Fullscreen" + action:nil + keyEquivalent:@"f"]; + + [viewMenu addItem:toggleFullscreenItem]; + + [mainMenu insertItem:viewMenuItem atIndex:3]; + } + + NSMenuItem *helpMenuItem = [mainMenu itemWithTitle:@"Help"]; + if (!helpMenuItem) { + NSMenu *helpMenu = [[NSMenu alloc] initWithTitle:@"Help"]; + helpMenuItem = [[NSMenuItem alloc] initWithTitle:@"Help" action:nil keyEquivalent:@""]; + [helpMenuItem setSubmenu:helpMenu]; + + // URL to online documentation + NSMenuItem *documentationItem = [[NSMenuItem alloc] initWithTitle:@"Documentation" + action:nil + keyEquivalent:@"?"]; + [helpMenu addItem:documentationItem]; + + [mainMenu insertItem:helpMenuItem atIndex:4]; + } +} + +// Action method for the New menu item +- (void)newFileAction:(id)sender { + NSLog(@"New File action triggered"); +} + +- (void)toggleFlagAction:(id)sender { + NSMenuItem *flagItem = (NSMenuItem *)sender; + if ([flagItem state] == NSControlStateValueOff) { + [flagItem setState:NSControlStateValueOn]; + } else { + [flagItem setState:NSControlStateValueOff]; + } +} + +- (void)openFileAction:(id)sender { + yaze::app::SharedROM::shared_rom_->LoadFromFile(FileDialogWrapper::ShowOpenFileDialog()); +} + +- (void)cutAction:(id)sender { + // TODO: Implement +} + +- (void)openRecentFileAction:(id)sender { + NSLog(@"Open Recent File action triggered"); +} + +extern "C" void InitializeCocoa() { + @autoreleasepool { + AppDelegate *delegate = [[AppDelegate alloc] init]; + [NSApplication sharedApplication]; + [NSApp setDelegate:delegate]; + [NSApp finishLaunching]; + } +} + +@end \ No newline at end of file diff --git a/src/app/core/platform/clipboard.h b/src/app/core/platform/clipboard.h new file mode 100644 index 00000000..a50a01c3 --- /dev/null +++ b/src/app/core/platform/clipboard.h @@ -0,0 +1,32 @@ +#ifndef YAZE_APP_CORE_PLATFORM_CLIPBOARD_H +#define YAZE_APP_CORE_PLATFORM_CLIPBOARD_H + +#ifdef _WIN32 + +void CopyImageToClipboard(const std::vector& data); +void GetImageFromClipboard(std::vector& data, int& width, int& height); + +#elif defined(__APPLE__) + +#include + +void CopyImageToClipboard(const std::vector& data); +void GetImageFromClipboard(std::vector& data, int& width, int& height); + +#elif defined(__linux__) + +#include + +void CopyImageToClipboard(const std::vector& data) { + std::cout << "CopyImageToClipboard() is not implemented on Linux." + << std::endl; +} +void GetImageFromClipboard(std::vector& data, int& width, + int& height) { + std::cout << "GetImageFromClipboard() is not implemented on Linux." + << std::endl; +} + +#endif + +#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 new file mode 100644 index 00000000..b3dc6f9c --- /dev/null +++ b/src/app/core/platform/clipboard.mm @@ -0,0 +1,42 @@ +#include "clipboard.h" + +#include +#include + +#import + +void CopyImageToClipboard(const std::vector& pngData) { + NSData* data = [NSData dataWithBytes:pngData.data() length:pngData.size()]; + NSImage* image = [[NSImage alloc] initWithData:data]; + + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + [pasteboard writeObjects:@[ image ]]; +} + +void GetImageFromClipboard(std::vector& pixel_data, int& width, int& height) { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + NSArray* classArray = [NSArray arrayWithObject:[NSImage class]]; + NSDictionary* options = [NSDictionary dictionary]; + + NSImage* image = [pasteboard readObjectsForClasses:classArray options:options].firstObject; + if (!image) { + width = height = 0; + return; + } + + // Assuming the image is in an RGBA format + CGImageRef cgImage = [image CGImageForProposedRect:nil context:nil hints:nil]; + width = (int)CGImageGetWidth(cgImage); + height = (int)CGImageGetHeight(cgImage); + + size_t bytesPerRow = 4 * width; + size_t totalBytes = bytesPerRow * height; + pixel_data.resize(totalBytes); + + CGContextRef context = CGBitmapContextCreate( + pixel_data.data(), width, height, 8, bytesPerRow, CGColorSpaceCreateDeviceRGB(), + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); + CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); + CGContextRelease(context); +} \ No newline at end of file diff --git a/src/app/core/platform/file_dialog.h b/src/app/core/platform/file_dialog.h new file mode 100644 index 00000000..45725bc9 --- /dev/null +++ b/src/app/core/platform/file_dialog.h @@ -0,0 +1,65 @@ +#include + +#ifdef _WIN32 +// Include Windows-specific headers +#include +#include + +class FileDialogWrapper { + public: + static std::string ShowOpenFileDialog() { + CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + + IFileDialog *pfd = NULL; + HRESULT hr = + CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL, + IID_IFileDialog, reinterpret_cast(&pfd)); + std::string file_path_windows; + if (SUCCEEDED(hr)) { + // Show the dialog + hr = pfd->Show(NULL); + if (SUCCEEDED(hr)) { + IShellItem *psiResult; + hr = pfd->GetResult(&psiResult); + if (SUCCEEDED(hr)) { + // Get the file path + PWSTR pszFilePath; + psiResult->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); + char str[128]; + wcstombs(str, pszFilePath, 128); + file_path_windows = str; + psiResult->Release(); + CoTaskMemFree(pszFilePath); + } + } + pfd->Release(); + } + + CoUninitialize(); + return file_path_windows; + } +}; + +#elif defined(__APPLE__) + +#include + +class FileDialogWrapper { + public: + static std::string ShowOpenFileDialog(); +}; + +#elif defined(__linux__) + +class FileDialogWrapper { + public: + static std::string ShowOpenFileDialog() { + // Linux-specific file dialog implementation using GTK + // ... + return "file_path_linux"; + } +}; + +#else +#error "Unsupported platform." +#endif \ No newline at end of file diff --git a/src/app/core/platform/file_dialog.mm b/src/app/core/platform/file_dialog.mm new file mode 100644 index 00000000..91c3581d --- /dev/null +++ b/src/app/core/platform/file_dialog.mm @@ -0,0 +1,17 @@ +#import +#include "app/core/platform/file_dialog.h" + +std::string FileDialogWrapper::ShowOpenFileDialog() { + NSOpenPanel* openPanel = [NSOpenPanel openPanel]; + [openPanel setCanChooseFiles:YES]; + [openPanel setCanChooseDirectories:NO]; + [openPanel setAllowsMultipleSelection:NO]; + + if ([openPanel runModal] == NSModalResponseOK) { + NSURL* url = [[openPanel URLs] objectAtIndex:0]; + NSString* path = [url path]; + return std::string([path UTF8String]); + } + + return ""; +} diff --git a/src/app/core/platform/font_loader.cc b/src/app/core/platform/font_loader.cc new file mode 100644 index 00000000..a166aa55 --- /dev/null +++ b/src/app/core/platform/font_loader.cc @@ -0,0 +1,81 @@ +#include "app/core/platform/font_loader.h" + +#include + +#include +#include + +#ifdef _WIN32 +#include + +int CALLBACK EnumFontFamExProc(const LOGFONT* lpelfe, const TEXTMETRIC* lpntme, + DWORD FontType, LPARAM lParam) { + // Step 3: Load the font into ImGui + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->AddFontFromFileTTF(lpelfe->lfFaceName, 16.0f); + + return 1; +} + +void LoadSystemFonts() { + HKEY hKey; + std::vector fontPaths; + + // Open the registry key where fonts are listed + if (RegOpenKeyEx( + HKEY_LOCAL_MACHINE, + TEXT("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts"), 0, + KEY_READ, &hKey) == ERROR_SUCCESS) { + DWORD valueCount; + DWORD maxValueNameSize; + DWORD maxValueDataSize; + + // Query the number of entries and the maximum size of the names and values + RegQueryInfoKey(hKey, NULL, NULL, NULL, NULL, NULL, NULL, &valueCount, + &maxValueNameSize, &maxValueDataSize, NULL, NULL); + + char* valueName = new char[maxValueNameSize + 1]; // +1 for null terminator + BYTE* valueData = new BYTE[maxValueDataSize + 1]; // +1 for null terminator + + // Enumerate all font entries + for (DWORD i = 0; i < valueCount; i++) { + DWORD valueNameSize = maxValueNameSize + 1; // +1 for null terminator + DWORD valueDataSize = maxValueDataSize + 1; // +1 for null terminator + DWORD valueType; + + // Clear buffers + memset(valueName, 0, valueNameSize); + memset(valueData, 0, valueDataSize); + + // Get the font name and file path + if (RegEnumValue(hKey, i, valueName, &valueNameSize, NULL, &valueType, + valueData, &valueDataSize) == ERROR_SUCCESS) { + if (valueType == REG_SZ) { + // Add the font file path to the vector + std::string fontPath((char*)valueData); + fontPaths.push_back(fontPath); + } + } + } + + delete[] valueName; + delete[] valueData; + + RegCloseKey(hKey); + } + + ImGuiIO& io = ImGui::GetIO(); + + for (const auto& fontPath : fontPaths) { + io.Fonts->AddFontFromFileTTF(fontPath.c_str(), 16.0f); + } +} + +#elif defined(__linux__) + +void LoadSystemFonts() { + // Load Linux System Fonts into ImGui + // ... +} + +#endif \ No newline at end of file diff --git a/src/app/core/platform/font_loader.h b/src/app/core/platform/font_loader.h new file mode 100644 index 00000000..c4686055 --- /dev/null +++ b/src/app/core/platform/font_loader.h @@ -0,0 +1,15 @@ +// FontLoader.h +#ifndef FONTLOADER_H +#define FONTLOADER_H + +// Function declaration for loading system fonts into ImGui +void LoadSystemFonts(); + +#ifdef _WIN32 +#include +// Windows specific function declaration for loading system fonts into ImGui +int CALLBACK EnumFontFamExProc(const LOGFONT* lpelfe, const TEXTMETRIC* lpntme, + DWORD FontType, LPARAM lParam); +#endif + +#endif // FONTLOADER_H diff --git a/src/app/core/platform/font_loader.mm b/src/app/core/platform/font_loader.mm new file mode 100644 index 00000000..9dede77b --- /dev/null +++ b/src/app/core/platform/font_loader.mm @@ -0,0 +1,50 @@ +// FontLoader.mm +#include "app/core/platform/font_loader.h" +#import +#import +#include + +#include "app/gui/icons.h" + +void LoadSystemFonts() { + // List of common macOS system fonts + NSArray *fontNames = @[ @"Helvetica", @"Times New Roman", @"Courier", @"Arial", @"Verdana" ]; + + for (NSString *fontName in fontNames) { + NSFont *font = [NSFont fontWithName:fontName size:14.0]; + if (!font) { + NSLog(@"Font not found: %@", fontName); + continue; + } + + CTFontDescriptorRef fontDescriptor = + CTFontDescriptorCreateWithNameAndSize((CFStringRef)font.fontName, font.pointSize); + CFURLRef fontURL = (CFURLRef)CTFontDescriptorCopyAttribute(fontDescriptor, kCTFontURLAttribute); + NSString *fontPath = [(NSURL *)fontURL path]; + CFRelease(fontDescriptor); + + if (fontPath != nil && [[NSFileManager defaultManager] isReadableFileAtPath:fontPath]) { + // Load the font into ImGui + ImGuiIO &io = ImGui::GetIO(); + ImFontConfig icons_config; + icons_config.MergeMode = true; + icons_config.GlyphOffset.y = 5.0f; + icons_config.GlyphMinAdvanceX = 13.0f; + icons_config.PixelSnapH = true; + static const ImWchar icons_ranges[] = {ICON_MIN_MD, 0xf900, 0}; + static const float ICON_FONT_SIZE = 18.0f; + ImFont *imFont = io.Fonts->AddFontFromFileTTF([fontPath UTF8String], 14.0f); + if (!imFont) { + NSLog(@"Failed to load font: %@", fontPath); + } + io.Fonts->AddFontFromFileTTF(FONT_ICON_FILE_NAME_MD, ICON_FONT_SIZE, &icons_config, + icons_ranges); + } else { + NSLog(@"Font file not accessible: %@", fontPath); + } + + if (fontURL) { + CFRelease(fontURL); + } + } +} \ No newline at end of file diff --git a/src/app/delta/CMakeLists.txt b/src/app/delta/CMakeLists.txt deleted file mode 100644 index 087dd9f7..00000000 --- a/src/app/delta/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ - -add_library(delta-service delta.proto) -target_link_libraries(delta-service - PUBLIC - protobuf::libprotobuf - gRPC::grpc - gRPC::grpc++ -) - -target_include_directories(delta-service - PUBLIC - ${CMAKE_CURRENT_BINARY_DIR} - ${PROTOBUF_INCLUDE_PATH} -) - -get_target_property(grpc_cpp_plugin_location gRPC::grpc_cpp_plugin LOCATION) - -# compile the message types -protobuf_generate(TARGET delta-service LANGUAGE cpp) - -# compile the GRPC services -protobuf_generate( - TARGET - delta-service - LANGUAGE - grpc - GENERATE_EXTENSIONS - .grpc.pb.h - .grpc.pb.cc - PLUGIN - "protoc-gen-grpc=${grpc_cpp_plugin_location}" -) \ No newline at end of file diff --git a/src/app/delta/client.cc b/src/app/delta/client.cc deleted file mode 100644 index 4a24a78d..00000000 --- a/src/app/delta/client.cc +++ /dev/null @@ -1,54 +0,0 @@ -#include "client.h" - -#include -#include -#include -#include -#include - -#include "absl/status/status.h" -#include "src/app/delta/delta.grpc.pb.h" -#include "src/app/delta/delta.pb.h" - -namespace yaze { -namespace app { -namespace delta { - -using grpc::Channel; -using grpc::ClientAsyncResponseReader; -using grpc::ClientContext; -using grpc::CompletionQueue; -using grpc::Server; -using grpc::ServerBuilder; -using grpc::Status; - -void Client::CreateChannel() { - auto channel = grpc::CreateChannel("localhost:50051", - grpc::InsecureChannelCredentials()); - stub_ = ::YazeDelta::NewStub(channel); -} - -absl::Status Client::InitRepo(std::string author_name, - std::string project_name) { - Repository new_repo; - new_repo.set_author_name(author_name); - new_repo.set_project_name(project_name); - new_repo.mutable_tree()->Add(); - - InitRequest request; - request.set_allocated_repo(&new_repo); - - InitResponse response; - Status status = stub_->Init(&rpc_context, request, &response); - - if (!status.ok()) { - std::cerr << status.error_code() << ": " << status.error_message() - << std::endl; - return absl::InternalError(status.error_message()); - } - return absl::OkStatus(); -} - -} // namespace delta -} // namespace app -} // namespace yaze diff --git a/src/app/delta/client.h b/src/app/delta/client.h deleted file mode 100644 index 6c3d7f74..00000000 --- a/src/app/delta/client.h +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef YAZE_APP_DELTA_CLIENT_H -#define YAZE_APP_DELTA_CLIENT_H - -#include -#include -#include -#include -#include - -#include - -#include "absl/status/status.h" -#include "src/app/delta/delta.grpc.pb.h" -#include "src/app/delta/delta.pb.h" - -namespace yaze { -namespace app { -namespace delta { - -using grpc::Channel; -using grpc::ClientAsyncResponseReader; -using grpc::ClientContext; -using grpc::CompletionQueue; -using grpc::Server; -using grpc::ServerBuilder; -using grpc::Status; - -class Client { - public: - Client() = default; - void CreateChannel(); - absl::Status InitRepo(std::string author_name, std::string project_name); - - private: - ClientContext rpc_context; - std::vector repos_; - std::unique_ptr stub_; -}; - -} // namespace delta -} // namespace app -} // namespace yaze - -#endif \ No newline at end of file diff --git a/src/app/delta/delta.cc b/src/app/delta/delta.cc deleted file mode 100644 index 4a413d67..00000000 --- a/src/app/delta/delta.cc +++ /dev/null @@ -1,32 +0,0 @@ -#if defined(_WIN32) -#define main SDL_main -#endif - -#include "absl/debugging/failure_signal_handler.h" -#include "absl/debugging/symbolize.h" -#include "app/core/controller.h" -#include "app/delta/viewer.h" - -int main(int argc, char** argv) { - absl::InitializeSymbolizer(argv[0]); - - absl::FailureSignalHandlerOptions options; - absl::InstallFailureSignalHandler(options); - - yaze::app::core::Controller controller; - yaze::app::delta::Viewer viewer; - - auto entry_status = controller.onEntry(); - if (!entry_status.ok()) { - return EXIT_FAILURE; - } - - while (controller.isActive()) { - controller.onInput(); - viewer.Update(); - controller.doRender(); - } - controller.onExit(); - - return EXIT_SUCCESS; -} \ No newline at end of file diff --git a/src/app/delta/delta.proto b/src/app/delta/delta.proto deleted file mode 100644 index c15090b3..00000000 --- a/src/app/delta/delta.proto +++ /dev/null @@ -1,105 +0,0 @@ -syntax = "proto3"; -option cc_enable_arenas = true; - -service YazeDelta { - rpc Init(InitRequest) returns (InitResponse) {} - - rpc Clone(CloneRequest) returns (CloneResponse) {} - - rpc Push(PushRequest) returns (PushResponse) {} - rpc Pull(PullRequest) returns (PullResponse) {} - - rpc Sync(stream SyncRequest) returns (stream SyncResponse) {} - - rpc CreateBranch(CreateBranchRequest) returns (CreateBranchResponse) {} - rpc DeleteBranch(DeleteBranchRequest) returns (DeleteBranchResponse) {} - - rpc Merge(MergeRequest) returns (MergeResponse) {} - rpc UndoMerge(UndoMergeRequest) returns (UndoMergeResponse) {} -} - -enum ChangeType { - OVERWORLD_MAP = 0; - DUNGEON_MAP = 1; - MONOLOGUE = 2; - PALETTE = 3; - OBJECT = 4; - ASSEMBLY = 5; - MISC = 6; -} - -message Delta { - int64 offset = 1; - int64 length = 2; - bytes data = 3; - ChangeType type = 4; -} - -message Commit { - int64 commit_id = 1; - int64 parent_commit_id = 2; - string author_name = 3; - string message_header = 4; - optional string message_body = 5; - repeated Delta delta = 6; - int64 signature = 7; -} - -message Branch { - string branch_name = 1; - optional string parent_name = 2; - repeated Commit commits = 3; -} - -message Repository { - string project_name = 1; - string author_name = 2; - int64 signature = 3; - optional bool locked = 4; - optional string password = 5; - repeated Branch tree = 6; -} - -message InitRequest { - Repository repo = 1; -} - -message InitResponse { - int32 response = 1; -} - -message CloneRequest {} - -message CloneResponse {} - -message PushRequest { - string author_name = 1; - string repository_name= 2; - string branch_name = 3; - repeated Commit commits = 4; -} -message PushResponse {} - -message PullRequest { - string repository_name = 1; - string branch_name = 2; - repeated Commit commits = 3; -} - -message PullResponse {} - -message SyncRequest {} - -message SyncResponse {} - -message CreateBranchRequest {} -message CreateBranchResponse {} - -message DeleteBranchRequest {} -message DeleteBranchResponse {} - -message MergeRequest {} -message MergeResponse {} - -message UndoMergeRequest {} -message UndoMergeResponse {} \ No newline at end of file diff --git a/src/app/delta/service.cc b/src/app/delta/service.cc deleted file mode 100644 index cdfb5504..00000000 --- a/src/app/delta/service.cc +++ /dev/null @@ -1,87 +0,0 @@ -#include "service.h" - -#include -#include -#include -#include -#include - -#include - -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "src/app/delta/delta.grpc.pb.h" -#include "src/app/delta/delta.pb.h" - -namespace yaze { -namespace app { -namespace delta { - -using grpc::Channel; -using grpc::ClientAsyncResponseReader; -using grpc::ClientContext; -using grpc::CompletionQueue; -using grpc::Server; -using grpc::ServerBuilder; -using grpc::Status; - -namespace { -auto FindRepository(std::vector& repos, const std::string& name) { - for (auto& repo : repos) { - if (repo.project_name() == name) { - return repo.mutable_tree(); - } - } -} - -auto FindBranch(google::protobuf::RepeatedPtrField* repo, - const std::string& branch_name) { - for (auto it = repo->begin(); it != repo->end(); ++it) { - if (it->branch_name() == branch_name) { - return it->mutable_commits(); - } - } -} -} // namespace - -Status DeltaService::Init(grpc::ServerContext* context, - const InitRequest* request, InitResponse* reply) { - std::filesystem::create_directories("./.yaze"); - repos_.push_back(request->repo()); - // std::ofstream commit_stream("./.yaze/commits"); - // for (const auto& repo : repos_) { - // for (const auto& branch : repo.tree()) { - // for (const auto& commit : branch.commits()) { - // commit_stream << commit.DebugString(); - // } - // } - // } - return Status::OK; -} - -Status DeltaService::Push(grpc::ServerContext* context, - const PushRequest* request, PushResponse* reply) { - const auto& repository_name = request->repository_name(); - const auto& branch_name = request->branch_name(); - auto repo = FindRepository(repos_, repository_name); - auto mutable_commits = FindBranch(repo, branch_name); - auto size = request->commits().size(); - for (int i = 1; i < size; ++i) { - *mutable_commits->Add() = request->commits().at(i); - } - return Status::OK; -} - -Status DeltaService::Pull(grpc::ServerContext* context, - const PullRequest* request, PullResponse* reply) { - return Status::OK; -} - -Status DeltaService::Clone(grpc::ServerContext* context, - const CloneRequest* request, CloneResponse* reply) { - return Status::OK; -} - -} // namespace delta -} // namespace app -} // namespace yaze diff --git a/src/app/delta/service.h b/src/app/delta/service.h deleted file mode 100644 index d986def7..00000000 --- a/src/app/delta/service.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef YAZE_APP_DELTA_SERVICE_H -#define YAZE_APP_DELTA_SERVICE_H - -#include - -#include -#include -#include -#include -#include - -#include - -#include "absl/status/status.h" -#include "src/app/delta/delta.grpc.pb.h" -#include "src/app/delta/delta.pb.h" - -namespace yaze { -namespace app { -namespace delta { - -using grpc::Status; - -class DeltaService final : public ::YazeDelta::Service { - public: - Status Init(grpc::ServerContext* context, const InitRequest* request, - InitResponse* reply) override; - - Status Push(grpc::ServerContext* context, const PushRequest* request, - PushResponse* reply) override; - - Status Pull(grpc::ServerContext* context, const PullRequest* request, - PullResponse* reply) override; - - Status Clone(grpc::ServerContext* context, const CloneRequest* request, - CloneResponse* reply) override; - - auto Repos() const { return repos_; } - - private: - std::vector repos_; -}; - -} // namespace delta -} // namespace app -} // namespace yaze - -#endif \ No newline at end of file diff --git a/src/app/delta/viewer.cc b/src/app/delta/viewer.cc deleted file mode 100644 index c3848ebe..00000000 --- a/src/app/delta/viewer.cc +++ /dev/null @@ -1,229 +0,0 @@ -#include "viewer.h" - -#include -#include -#include -#include -#include - -#include "absl/status/status.h" -#include "app/core/constants.h" -#include "app/gfx/snes_palette.h" -#include "app/gfx/snes_tile.h" -#include "app/rom.h" -#include "gui/canvas.h" -#include "gui/icons.h" -#include "gui/input.h" - -namespace yaze { -namespace app { -namespace delta { -namespace { - -constexpr ImGuiWindowFlags kMainEditorFlags = - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_MenuBar | - ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoTitleBar; - -void NewMasterFrame() { - const ImGuiIO& io = ImGui::GetIO(); - ImGui::NewFrame(); - ImGui::SetNextWindowPos(ImVec2(0, 0)); - ImVec2 dimensions(io.DisplaySize.x, io.DisplaySize.y); - ImGui::SetNextWindowSize(dimensions, ImGuiCond_Always); - - if (!ImGui::Begin("##YazeMain", nullptr, kMainEditorFlags)) { - ImGui::End(); - return; - } -} - -} // namespace - -void Viewer::Update() { - NewMasterFrame(); - DrawYazeMenu(); - DrawFileDialog(); - - ImGui::Text(ICON_MD_CHANGE_HISTORY); - ImGui::SameLine(); - ImGui::Text("%s", rom_.GetTitle()); - - ImGui::Separator(); - - ImGui::Button(ICON_MD_SYNC); - ImGui::SameLine(); - ImGui::Button(ICON_MD_ARROW_UPWARD); - ImGui::SameLine(); - ImGui::Button(ICON_MD_ARROW_DOWNWARD); - ImGui::SameLine(); - ImGui::Button(ICON_MD_MERGE); - ImGui::SameLine(); - - ImGui::Button(ICON_MD_MANAGE_HISTORY); - ImGui::SameLine(); - ImGui::Button(ICON_MD_LAN); - ImGui::SameLine(); - ImGui::Button(ICON_MD_COMMIT); - ImGui::SameLine(); - ImGui::Button(ICON_MD_DIFFERENCE); - - ImGui::Separator(); - - ImGui::SetNextItemWidth(75.f); - ImGui::Button(ICON_MD_SEND); - ImGui::SameLine(); - ImGui::InputText("Server Address", &client_address_); - - ImGui::SetNextItemWidth(75.f); - ImGui::Button(ICON_MD_DOWNLOAD); - ImGui::SameLine(); - ImGui::InputText("Repository Source", &client_address_); - - ImGui::Separator(); - DrawBranchTree(); - ImGui::End(); -} - -void Viewer::DrawFileDialog() { - if (ImGuiFileDialog::Instance()->Display("ChooseFileDlgKey")) { - if (ImGuiFileDialog::Instance()->IsOk()) { - std::string filePathName = ImGuiFileDialog::Instance()->GetFilePathName(); - rom_.LoadFromFile(filePathName); - } - ImGuiFileDialog::Instance()->Close(); - } -} - -void Viewer::DrawYazeMenu() { - MENU_BAR() - DrawFileMenu(); - DrawViewMenu(); - END_MENU_BAR() -} - -void Viewer::DrawFileMenu() const { - if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("Open", "Ctrl+O")) { - ImGuiFileDialog::Instance()->OpenDialog("ChooseFileDlgKey", "Open ROM", - ".sfc,.smc", "."); - } - - MENU_ITEM2("Save", "Ctrl+S") {} - - ImGui::EndMenu(); - } -} - -void Viewer::DrawViewMenu() { - static bool show_imgui_metrics = false; - static bool show_imgui_style_editor = false; - static bool show_memory_editor = false; - static bool show_imgui_demo = false; - - if (show_imgui_metrics) { - ImGui::ShowMetricsWindow(&show_imgui_metrics); - } - - if (show_memory_editor) { - static MemoryEditor mem_edit; - mem_edit.DrawWindow("Memory Editor", (void*)&rom_, rom_.size()); - } - - if (show_imgui_demo) { - ImGui::ShowDemoWindow(); - } - - if (show_imgui_style_editor) { - ImGui::Begin("Style Editor (ImGui)", &show_imgui_style_editor); - ImGui::ShowStyleEditor(); - ImGui::End(); - } - - if (ImGui::BeginMenu("View")) { - ImGui::MenuItem("HEX Editor", nullptr, &show_memory_editor); - ImGui::MenuItem("ImGui Demo", nullptr, &show_imgui_demo); - ImGui::Separator(); - if (ImGui::BeginMenu("GUI Tools")) { - ImGui::MenuItem("Metrics (ImGui)", nullptr, &show_imgui_metrics); - ImGui::MenuItem("Style Editor (ImGui)", nullptr, - &show_imgui_style_editor); - ImGui::EndMenu(); - } - ImGui::EndMenu(); - } -} - -void Viewer::DrawBranchTree() { - static ImGuiTableFlags flags = - ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | - ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | - ImGuiTableFlags_NoBordersInBody; - - if (ImGui::BeginTable("3ways", 3, flags)) { - // The first column will use the default _WidthStretch when ScrollX is Off - // and _WidthFixed when ScrollX is On - ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_NoHide); - ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, - 10 * 12.0f); - ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, - 10 * 18.0f); - ImGui::TableHeadersRow(); - - // Simple storage to output a dummy file-system. - struct MyTreeNode { - const char* Name; - const char* Type; - int Size; - int ChildIdx; - int ChildCount; - static void DisplayNode(const MyTreeNode* node, - const MyTreeNode* all_nodes) { - ImGui::TableNextRow(); - ImGui::TableNextColumn(); - const bool is_folder = (node->ChildCount > 0); - if (is_folder) { - bool open = - ImGui::TreeNodeEx(node->Name, ImGuiTreeNodeFlags_SpanFullWidth); - ImGui::TableNextColumn(); - ImGui::TextDisabled("--"); - ImGui::TableNextColumn(); - ImGui::TextUnformatted(node->Type); - if (open) { - for (int child_n = 0; child_n < node->ChildCount; child_n++) - DisplayNode(&all_nodes[node->ChildIdx + child_n], all_nodes); - ImGui::TreePop(); - } - } else { - ImGui::TreeNodeEx( - node->Name, ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet | - ImGuiTreeNodeFlags_NoTreePushOnOpen | - ImGuiTreeNodeFlags_SpanFullWidth); - ImGui::TableNextColumn(); - ImGui::Text("%d", node->Size); - ImGui::TableNextColumn(); - ImGui::TextUnformatted(node->Type); - } - } - }; - static const MyTreeNode nodes[] = { - {"lttp-redux", "Repository", -1, 1, 3}, - {"main", "Branch", -1, 4, 2}, - {"hyrule-castle", "Branch", -1, 4, 2}, - {"lost-woods", "Branch", -1, 6, 3}, - {"Added some bushes", "Commit", 1024, -1, -1}, - {"Constructed a new house", "Commit", 123000, -1, -1}, - {"File1_b.wav", "Commit", 456000, -1, -1}, - {"Image001.png", "Commit", 203128, -1, -1}, - {"Copy of Image001.png", "Commit", 203256, -1, -1}, - {"Copy of Image001 (Final2).png", "Commit", 203512, -1, -1}, - }; - - MyTreeNode::DisplayNode(&nodes[0], nodes); - - ImGui::EndTable(); - } -} -} // namespace delta -} // namespace app -} // namespace yaze \ No newline at end of file diff --git a/src/app/delta/viewer.h b/src/app/delta/viewer.h deleted file mode 100644 index edcc8d48..00000000 --- a/src/app/delta/viewer.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef YAZE_APP_DELTA_VIEWER_H -#define YAZE_APP_DELTA_VIEWER_H - -#include -#include -#include -#include -#include - -#include "absl/status/status.h" -#include "app/core/constants.h" -#include "app/delta/client.h" -#include "app/gfx/snes_palette.h" -#include "app/gfx/snes_tile.h" -#include "app/rom.h" -#include "gui/canvas.h" -#include "gui/icons.h" -#include "gui/input.h" - -namespace yaze { -namespace app { -namespace delta { -class Viewer { - public: - void Update(); - - private: - void DrawFileDialog(); - - void DrawYazeMenu(); - void DrawFileMenu() const; - void DrawViewMenu(); - - void DrawBranchTree(); - - std::string client_address_; - - ROM rom_; - Client client_; -}; -} // namespace delta -} // namespace app -} // namespace yaze - -#endif \ No newline at end of file diff --git a/src/app/editor/context/gfx_context.cc b/src/app/editor/context/gfx_context.cc new file mode 100644 index 00000000..a239ba92 --- /dev/null +++ b/src/app/editor/context/gfx_context.cc @@ -0,0 +1,35 @@ +#include "app/editor/context/gfx_context.h" + +#include + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "app/core/editor.h" +#include "app/gui/pipeline.h" +#include "app/editor/modules/palette_editor.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/rom.h" +#include "app/zelda3/overworld.h" + +namespace yaze { +namespace app { +namespace editor { + +absl::Status GfxContext::Update() { return absl::OkStatus(); } + +gfx::Bitmap GfxContext::current_ow_gfx_bmp_; +gfx::SNESPalette GfxContext::current_ow_palette_; +gfx::Bitmap GfxContext::tile16_blockset_bmp_; +gfx::Bitmap GfxContext::tile8_blockset_bmp_; +std::vector GfxContext::tile16_individual_bmp_; +std::vector GfxContext::tile8_individual_bmp_; + +} // namespace editor +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/editor/context/gfx_context.h b/src/app/editor/context/gfx_context.h new file mode 100644 index 00000000..18cd687b --- /dev/null +++ b/src/app/editor/context/gfx_context.h @@ -0,0 +1,56 @@ +#ifndef YAZE_APP_EDITOR_VRAM_CONTEXT_H +#define YAZE_APP_EDITOR_VRAM_CONTEXT_H + +#include + +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "app/core/editor.h" +#include "app/gui/pipeline.h" +#include "app/editor/modules/palette_editor.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/rom.h" +#include "app/zelda3/overworld.h" + +// Create a class which manages the current VRAM state of Link to the Past, +// including static members for the Bitmaps and Palettes as well as the current +// blockset as to update all of the overworld maps with the new blockset when it +// is changed. This class will also manage the current tile16 and tile8 +// selection, as well as the current palette selection. + +namespace yaze { +namespace app { +namespace editor { + +class GfxContext { + public: + absl::Status Update(); + + protected: + static gfx::Bitmap current_ow_gfx_bmp_; + + static gfx::SNESPalette current_ow_palette_; + + static gfx::Bitmap tile16_blockset_bmp_; + + static gfx::Bitmap tile8_blockset_bmp_; + + // Bitmaps for the tile16 individual tiles + static std::vector tile16_individual_bmp_; + + // Bitmaps for the tile8 individual tiles + static std::vector tile8_individual_bmp_; +}; + +} // namespace editor +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_EDITOR_VRAM_CONTEXT_H \ No newline at end of file diff --git a/src/app/editor/dungeon_editor.cc b/src/app/editor/dungeon_editor.cc index 151c2736..09ae5c65 100644 --- a/src/app/editor/dungeon_editor.cc +++ b/src/app/editor/dungeon_editor.cc @@ -1,59 +1,378 @@ #include "dungeon_editor.h" -#include "gui/icons.h" +#include + +#include "app/core/common.h" +#include "app/gfx/snes_palette.h" +#include "app/gui/canvas.h" +#include "app/gui/icons.h" +#include "app/gui/input.h" +#include "app/gui/pipeline.h" +#include "app/rom.h" +#include "app/zelda3/dungeon/object_names.h" +#include "app/zelda3/dungeon/room_names.h" +#include "zelda3/dungeon/room.h" namespace yaze { namespace app { namespace editor { -void DungeonEditor::Update() { +constexpr ImGuiTableFlags kDungeonObjectTableFlags = + ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | + ImGuiTableFlags_Hideable | ImGuiTableFlags_BordersOuter | + ImGuiTableFlags_BordersV; + +using ImGui::TableHeadersRow; +using ImGui::TableNextColumn; +using ImGui::TableNextRow; +using ImGui::TableSetupColumn; + +absl::Status DungeonEditor::Update() { + if (!is_loaded_ && rom()->isLoaded()) { + for (int i = 0; i < 0x100; i++) { + rooms_.emplace_back(zelda3::dungeon::Room(i)); + rooms_[i].LoadHeader(); + rooms_[i].LoadRoomFromROM(); + if (flags()->kDrawDungeonRoomGraphics) { + rooms_[i].LoadRoomGraphics(); + } + } + graphics_bin_ = rom()->graphics_bin(); + full_palette_ = + rom()->palette_group("dungeon_main")[current_palette_group_id_]; + current_palette_group_ = + gfx::CreatePaletteGroupFromLargePalette(full_palette_); + + // Create a vector of pointers to the current block bitmaps + for (int block : rooms_[current_room_id_].blocks()) { + room_gfx_sheets_.emplace_back(&graphics_bin_[block]); + } + + is_loaded_ = true; + } + + if (refresh_graphics_) { + for (int block : rooms_[current_room_id_].blocks()) { + graphics_bin_[block].ApplyPalette( + current_palette_group_[current_palette_id_]); + rom()->UpdateBitmap(&graphics_bin_[block]); + } + refresh_graphics_ = false; + } + DrawToolset(); + + if (palette_showing_) { + ImGui::Begin("Palette Editor", &palette_showing_, 0); + current_palette_ = + rom()->palette_group("dungeon_main")[current_palette_group_id_]; + gui::SelectablePalettePipeline(current_palette_id_, refresh_graphics_, + current_palette_); + ImGui::End(); + } + + if (ImGui::BeginTable("#DungeonEditTable", 3, kDungeonTableFlags, + ImVec2(0, 0))) { + TableSetupColumn("Room Selector"); + TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch, + ImGui::GetContentRegionAvail().x); + TableSetupColumn("Object Selector"); + TableHeadersRow(); + TableNextRow(); + + TableNextColumn(); + DrawRoomSelector(); + + TableNextColumn(); + DrawDungeonTabView(); + + TableNextColumn(); + DrawTileSelector(); + ImGui::EndTable(); + } + return absl::OkStatus(); +} + +void DungeonEditor::DrawToolset() { + if (ImGui::BeginTable("DWToolset", 13, ImGuiTableFlags_SizingFixedFit, + ImVec2(0, 0))) { + TableSetupColumn("#undoTool"); + TableSetupColumn("#redoTool"); + TableSetupColumn("#separator"); + TableSetupColumn("#anyTool"); + + TableSetupColumn("#bg1Tool"); + TableSetupColumn("#bg2Tool"); + TableSetupColumn("#bg3Tool"); + TableSetupColumn("#separator"); + TableSetupColumn("#spriteTool"); + TableSetupColumn("#itemTool"); + TableSetupColumn("#doorTool"); + TableSetupColumn("#blockTool"); + + ImGui::TableNextColumn(); + if (ImGui::Button(ICON_MD_UNDO)) { + PRINT_IF_ERROR(Undo()); + } + + ImGui::TableNextColumn(); + if (ImGui::Button(ICON_MD_REDO)) { + PRINT_IF_ERROR(Redo()); + } + + ImGui::TableNextColumn(); + ImGui::Text(ICON_MD_MORE_VERT); + + ImGui::TableNextColumn(); + if (ImGui::RadioButton(ICON_MD_FILTER_NONE, + background_type_ == kBackgroundAny)) { + background_type_ = kBackgroundAny; + } + + ImGui::TableNextColumn(); + if (ImGui::RadioButton(ICON_MD_FILTER_1, + background_type_ == kBackground1)) { + background_type_ = kBackground1; + } + + ImGui::TableNextColumn(); + if (ImGui::RadioButton(ICON_MD_FILTER_2, + background_type_ == kBackground2)) { + background_type_ = kBackground2; + } + + ImGui::TableNextColumn(); + if (ImGui::RadioButton(ICON_MD_FILTER_3, + background_type_ == kBackground3)) { + background_type_ = kBackground3; + } + + ImGui::TableNextColumn(); + ImGui::Text(ICON_MD_MORE_VERT); + + ImGui::TableNextColumn(); + if (ImGui::RadioButton(ICON_MD_PEST_CONTROL, placement_type_ == kSprite)) { + placement_type_ = kSprite; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Sprites"); + } + + ImGui::TableNextColumn(); + if (ImGui::RadioButton(ICON_MD_GRASS, placement_type_ == kItem)) { + placement_type_ = kItem; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Items"); + } + + ImGui::TableNextColumn(); + if (ImGui::RadioButton(ICON_MD_SENSOR_DOOR, placement_type_ == kDoor)) { + placement_type_ = kDoor; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Doors"); + } + + ImGui::TableNextColumn(); + if (ImGui::RadioButton(ICON_MD_SQUARE, placement_type_ == kBlock)) { + placement_type_ = kBlock; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Blocks"); + } + + ImGui::TableNextColumn(); + if (ImGui::Button(ICON_MD_PALETTE)) { + palette_showing_ = !palette_showing_; + } + + ImGui::EndTable(); + } +} + +void DungeonEditor::DrawRoomSelector() { + if (rom()->isLoaded()) { + gui::InputHexWord("Room ID", ¤t_room_id_); + gui::InputHex("Palette ID", ¤t_palette_id_); + + if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)9); + ImGui::BeginChild(child_id, ImGui::GetContentRegionAvail(), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + int i = 0; + for (const auto each_room_name : zelda3::dungeon::kRoomNames) { + ImGui::Selectable(each_room_name.data(), current_room_id_ == i, + ImGuiSelectableFlags_AllowDoubleClick); + if (ImGui::IsItemClicked()) { + active_rooms_.push_back(i); + } + i += 1; + } + } + ImGui::EndChild(); + } +} + +void DungeonEditor::DrawDungeonTabView() { + static int next_tab_id = 0; + + if (ImGui::BeginTabBar("MyTabBar", kDungeonTabBarFlags)) { + // TODO: Manage the room that is being added to the tab bar. + if (ImGui::TabItemButton("+", kDungeonTabFlags)) { + active_rooms_.push_back(next_tab_id++); // Add new tab + } + + // Submit our regular tabs + for (int n = 0; n < active_rooms_.Size;) { + bool open = true; + + if (ImGui::BeginTabItem( + zelda3::dungeon::kRoomNames[active_rooms_[n]].data(), &open, + ImGuiTabItemFlags_None)) { + DrawDungeonCanvas(active_rooms_[n]); + ImGui::EndTabItem(); + } + + if (!open) + active_rooms_.erase(active_rooms_.Data + n); + else + n++; + } + + ImGui::EndTabBar(); + } + ImGui::Separator(); +} + +void DungeonEditor::DrawDungeonCanvas(int room_id) { + ImGui::BeginGroup(); + + gui::InputHexByte("Layout", &rooms_[room_id].layout); + ImGui::SameLine(); + + gui::InputHexByte("Blockset", &rooms_[room_id].blockset); + ImGui::SameLine(); + + gui::InputHexByte("Spriteset", &rooms_[room_id].spriteset); + ImGui::SameLine(); + + gui::InputHexByte("Palette", &rooms_[room_id].palette); + + gui::InputHexByte("Floor1", &rooms_[room_id].floor1); + ImGui::SameLine(); + + gui::InputHexByte("Floor2", &rooms_[room_id].floor2); + ImGui::SameLine(); + + gui::InputHexWord("Message ID", &rooms_[room_id].message_id_); + ImGui::SameLine(); + + ImGui::EndGroup(); + canvas_.DrawBackground(); canvas_.DrawContextMenu(); canvas_.DrawGrid(); canvas_.DrawOverlay(); } -void DungeonEditor::DrawToolset() { - if (ImGui::BeginTable("DWToolset", 9, toolset_table_flags_, ImVec2(0, 0))) { - ImGui::TableSetupColumn("#undoTool"); - ImGui::TableSetupColumn("#redoTool"); - ImGui::TableSetupColumn("#history"); - ImGui::TableSetupColumn("#separator"); - ImGui::TableSetupColumn("#bg1Tool"); - ImGui::TableSetupColumn("#bg2Tool"); - ImGui::TableSetupColumn("#bg3Tool"); - ImGui::TableSetupColumn("#itemTool"); - ImGui::TableSetupColumn("#spriteTool"); +void DungeonEditor::DrawRoomGraphics() { + const auto height = 0x40; + room_gfx_canvas_.DrawBackground(ImVec2(256 + 1, 0x10 * 0x40 + 1)); + room_gfx_canvas_.DrawContextMenu(); + room_gfx_canvas_.DrawTileSelector(32); + if (is_loaded_) { + auto blocks = rooms_[current_room_id_].blocks(); + int current_block = 0; + for (int block : blocks) { + int offset = height * (current_block + 1); + int top_left_y = room_gfx_canvas_.zero_point().y + 2; + if (current_block >= 1) { + top_left_y = room_gfx_canvas_.zero_point().y + height * current_block; + } + room_gfx_canvas_.GetDrawList()->AddImage( + (void*)graphics_bin_[block].texture(), + ImVec2(room_gfx_canvas_.zero_point().x + 2, top_left_y), + ImVec2(room_gfx_canvas_.zero_point().x + 0x100, + room_gfx_canvas_.zero_point().y + offset)); + current_block += 1; + } + } + room_gfx_canvas_.DrawGrid(32.0f); + room_gfx_canvas_.DrawOverlay(); +} + +void DungeonEditor::DrawTileSelector() { + if (ImGui::BeginTabBar("##TabBar", ImGuiTabBarFlags_FittingPolicyScroll)) { + if (ImGui::BeginTabItem("Room Graphics")) { + if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)3); + ImGui::BeginChild(child_id, ImGui::GetContentRegionAvail(), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + DrawRoomGraphics(); + } + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Object Renderer")) { + DrawObjectRenderer(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } +} + +void DungeonEditor::DrawObjectRenderer() { + if (ImGui::BeginTable("DungeonObjectEditorTable", 2, kDungeonObjectTableFlags, + ImVec2(0, 0))) { + TableSetupColumn("Dungeon Objects", ImGuiTableColumnFlags_WidthStretch, + ImGui::GetContentRegionAvail().x); + TableSetupColumn("Canvas"); ImGui::TableNextColumn(); - ImGui::Button(ICON_MD_UNDO); + ImGui::BeginChild("DungeonObjectButtons", ImVec2(250, 0), true); - ImGui::TableNextColumn(); - ImGui::Button(ICON_MD_REDO); + int selected_object = 0; + int i = 0; + for (const auto object_name : zelda3::dungeon::Type1RoomObjectNames) { + if (ImGui::Selectable(object_name.data(), selected_object == i)) { + selected_object = i; + current_object_ = i; + object_renderer_.LoadObject(i, + rooms_[current_room_id_].mutable_blocks()); + rom()->RenderBitmap(object_renderer_.bitmap()); + object_loaded_ = true; + } + i += 1; + } - ImGui::TableNextColumn(); - ImGui::Button(ICON_MD_MANAGE_HISTORY); + ImGui::EndChild(); + // Right side of the table - Canvas ImGui::TableNextColumn(); - ImGui::Text(ICON_MD_MORE_VERT); + ImGui::BeginChild("DungeonObjectCanvas", ImVec2(276, 0x10 * 0x40 + 1), + true); - ImGui::TableNextColumn(); - ImGui::Button(ICON_MD_FILTER_1); + object_canvas_.DrawBackground(ImVec2(256 + 1, 0x10 * 0x40 + 1)); + object_canvas_.DrawContextMenu(); + object_canvas_.DrawTileSelector(32); + if (object_loaded_) { + object_canvas_.DrawBitmap(*object_renderer_.bitmap(), 0, 0); + } + object_canvas_.DrawGrid(32.0f); + object_canvas_.DrawOverlay(); - ImGui::TableNextColumn(); - ImGui::Button(ICON_MD_FILTER_2); - - ImGui::TableNextColumn(); - ImGui::Button(ICON_MD_FILTER_3); - - ImGui::TableNextColumn(); - ImGui::Button(ICON_MD_GRASS); - - ImGui::TableNextColumn(); - ImGui::Button(ICON_MD_PEST_CONTROL_RODENT); + ImGui::EndChild(); ImGui::EndTable(); } + + // if (object_loaded_) { + // ImGui::Begin("Memory Viewer", &object_loaded_, 0); + // auto memory = object_renderer_.memory(); + // static MemoryEditor mem_edit; + // mem_edit.DrawContents((void*)object_renderer_.memory_ptr(), + // memory.size()); + // ImGui::End(); + // } } } // namespace editor diff --git a/src/app/editor/dungeon_editor.h b/src/app/editor/dungeon_editor.h index 5c000d2d..cb158303 100644 --- a/src/app/editor/dungeon_editor.h +++ b/src/app/editor/dungeon_editor.h @@ -3,22 +3,97 @@ #include -#include "gui/canvas.h" -#include "gui/icons.h" +#include "app/core/common.h" +#include "app/core/editor.h" +#include "app/editor/modules/palette_editor.h" +#include "app/gui/canvas.h" +#include "app/gui/icons.h" +#include "app/rom.h" +#include "zelda3/dungeon/room.h" +#include "zelda3/dungeon/room_object.h" namespace yaze { namespace app { namespace editor { -class DungeonEditor { + +constexpr ImGuiTabItemFlags kDungeonTabFlags = + ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_NoTooltip; + +constexpr ImGuiTabBarFlags kDungeonTabBarFlags = + ImGuiTabBarFlags_AutoSelectNewTabs | ImGuiTabBarFlags_Reorderable | + ImGuiTabBarFlags_FittingPolicyResizeDown | + ImGuiTabBarFlags_TabListPopupButton; + +constexpr ImGuiTableFlags kDungeonTableFlags = + ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | + ImGuiTableFlags_Hideable | ImGuiTableFlags_BordersOuter | + ImGuiTableFlags_BordersV; + +class DungeonEditor : public Editor, + public SharedROM, + public core::ExperimentFlags { public: - void Update(); + absl::Status Update() override; + absl::Status Cut() override { return absl::OkStatus(); } + absl::Status Copy() override { return absl::OkStatus(); } + absl::Status Paste() override { return absl::OkStatus(); } + absl::Status Undo() override { return absl::OkStatus(); } + absl::Status Redo() override { return absl::OkStatus(); } private: void DrawToolset(); + void DrawRoomSelector(); + + void DrawDungeonTabView(); + void DrawDungeonCanvas(int room_id); + + void DrawRoomGraphics(); + void DrawTileSelector(); + void DrawObjectRenderer(); + + enum BackgroundType { + kNoBackground, + kBackground1, + kBackground2, + kBackground3, + kBackgroundAny, + }; + enum PlacementType { kNoType, kSprite, kItem, kDoor, kBlock }; + + int background_type_ = kNoBackground; + int placement_type_ = kNoType; + int current_object_ = 0; + + bool is_loaded_ = false; + bool object_loaded_ = false; + bool palette_showing_ = false; + bool refresh_graphics_ = false; + bool show_object_render_ = false; + + uint16_t current_room_id_ = 0; + uint64_t current_palette_id_ = 0; + uint64_t current_palette_group_id_ = 0; + + ImVector active_rooms_; + + PaletteEditor palette_editor_; + gfx::SNESPalette current_palette_; + gfx::SNESPalette full_palette_; + gfx::PaletteGroup current_palette_group_; gui::Canvas canvas_; - ImGuiTableFlags toolset_table_flags_ = ImGuiTableFlags_SizingFixedFit; + gui::Canvas room_gfx_canvas_; + gui::Canvas object_canvas_; + + gfx::Bitmap room_gfx_bmp_; + gfx::BitmapTable graphics_bin_; + + std::vector room_gfx_sheets_; + std::vector rooms_; + std::vector room_graphics_; + zelda3::dungeon::DungeonObjectRenderer object_renderer_; }; + } // namespace editor } // namespace app } // namespace yaze diff --git a/src/app/editor/graphics_editor.cc b/src/app/editor/graphics_editor.cc new file mode 100644 index 00000000..c4dcf494 --- /dev/null +++ b/src/app/editor/graphics_editor.cc @@ -0,0 +1,768 @@ +#include "app/editor/graphics_editor.h" + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "app/core/platform/clipboard.h" +#include "app/editor/modules/palette_editor.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/compression.h" +#include "app/gfx/scad_format.h" +#include "app/gfx/snes_palette.h" +#include "app/gfx/snes_tile.h" +#include "app/gui/canvas.h" +#include "app/gui/input.h" +#include "app/gui/pipeline.h" +#include "app/gui/style.h" +#include "app/rom.h" + +namespace yaze { +namespace app { +namespace editor { + +using ImGui::Button; +using ImGui::InputInt; +using ImGui::InputText; +using ImGui::SameLine; + +constexpr ImGuiTableFlags kGfxEditTableFlags = + ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | + ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | + ImGuiTableFlags_SizingFixedFit; + +constexpr ImGuiTabBarFlags kGfxEditTabBarFlags = + ImGuiTabBarFlags_AutoSelectNewTabs | ImGuiTabBarFlags_Reorderable | + ImGuiTabBarFlags_FittingPolicyResizeDown | + ImGuiTabBarFlags_TabListPopupButton; + +absl::Status GraphicsEditor::Update() { + TAB_BAR("##TabBar") + status_ = UpdateGfxEdit(); + status_ = UpdateScadView(); + status_ = UpdateLinkGfxView(); + END_TAB_BAR() + CLEAR_AND_RETURN_STATUS(status_) + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::UpdateGfxEdit() { + TAB_ITEM("Graphics Editor") + + if (ImGui::BeginTable("##GfxEditTable", 3, kGfxEditTableFlags, + ImVec2(0, 0))) { + for (const auto& name : kGfxEditColumnNames) + ImGui::TableSetupColumn(name.data()); + + ImGui::TableHeadersRow(); + + NEXT_COLUMN(); + status_ = UpdateGfxSheetList(); + + NEXT_COLUMN(); + if (rom()->isLoaded()) { + DrawGfxEditToolset(); + status_ = UpdateGfxTabView(); + } + + NEXT_COLUMN(); + if (rom()->isLoaded()) { + status_ = UpdatePaletteColumn(); + } + } + ImGui::EndTable(); + + END_TAB_ITEM() + return absl::OkStatus(); +} + +void GraphicsEditor::DrawGfxEditToolset() { + if (ImGui::BeginTable("##GfxEditToolset", 9, ImGuiTableFlags_SizingFixedFit, + ImVec2(0, 0))) { + for (const auto& name : + {"Select", "Pencil", "Fill", "Copy Sheet", "Paste Sheet", "Zoom Out", + "Zoom In", "Current Color", "Tile Size"}) + ImGui::TableSetupColumn(name); + + ImGui::TableNextColumn(); + if (Button(ICON_MD_SELECT_ALL)) { + gfx_edit_mode_ = GfxEditMode::kSelect; + } + + ImGui::TableNextColumn(); + if (Button(ICON_MD_DRAW)) { + gfx_edit_mode_ = GfxEditMode::kPencil; + } + + ImGui::TableNextColumn(); + if (Button(ICON_MD_FORMAT_COLOR_FILL)) { + gfx_edit_mode_ = GfxEditMode::kFill; + } + + ImGui::TableNextColumn(); + if (Button(ICON_MD_CONTENT_COPY)) { + std::vector png_data = + rom()->bitmap_manager().GetBitmap(current_sheet_)->GetPngData(); + CopyImageToClipboard(png_data); + } + + ImGui::TableNextColumn(); + if (Button(ICON_MD_CONTENT_PASTE)) { + std::vector png_data; + int width, height; + GetImageFromClipboard(png_data, width, height); + if (png_data.size() > 0) { + rom() + ->bitmap_manager() + .GetBitmap(current_sheet_) + ->LoadFromPngData(png_data, width, height); + rom()->UpdateBitmap(rom() + ->mutable_bitmap_manager() + ->mutable_bitmap(current_sheet_) + .get()); + } + } + HOVER_HINT("Paste from Clipboard"); + + ImGui::TableNextColumn(); + if (Button(ICON_MD_ZOOM_OUT)) { + if (current_scale_ >= 0.0f) { + current_scale_ -= 1.0f; + } + } + + ImGui::TableNextColumn(); + if (Button(ICON_MD_ZOOM_IN)) { + if (current_scale_ <= 16.0f) { + current_scale_ += 1.0f; + } + } + + ImGui::TableNextColumn(); + auto bitmap = rom()->bitmap_manager()[current_sheet_]; + auto palette = bitmap->palette(); + for (int i = 0; i < 8; i++) { + ImGui::SameLine(); + auto color = + ImVec4(palette[i].GetRGB().x / 255.0f, palette[i].GetRGB().y / 255.0f, + palette[i].GetRGB().z / 255.0f, 255.0f); + if (ImGui::ColorButton(absl::StrFormat("Palette Color %d", i).c_str(), + color)) { + current_color_ = color; + } + } + + ImGui::TableNextColumn(); + gui::InputHexByte("Tile Size", &tile_size_, 0x01); + + ImGui::EndTable(); + } +} + +absl::Status GraphicsEditor::UpdateGfxSheetList() { + ImGui::BeginChild( + "##GfxEditChild", ImVec2(0, 0), true, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysVerticalScrollbar); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + for (auto& [key, value] : rom()->bitmap_manager()) { + ImGui::BeginChild(absl::StrFormat("##GfxSheet%02X", key).c_str(), + ImVec2(0x100 + 1, 0x40 + 1), true, + ImGuiWindowFlags_NoDecoration); + ImGui::PopStyleVar(); + gui::Canvas graphics_bin_canvas_; + auto select_tile_event = [&]() { + if (value.get()->IsActive()) { + auto texture = value.get()->texture(); + graphics_bin_canvas_.GetDrawList()->AddImage( + (void*)texture, + ImVec2(graphics_bin_canvas_.zero_point().x + 2, + graphics_bin_canvas_.zero_point().y + 2), + ImVec2(graphics_bin_canvas_.zero_point().x + + value.get()->width() * sheet_scale_, + graphics_bin_canvas_.zero_point().y + + value.get()->height() * sheet_scale_)); + + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + current_sheet_ = key; + open_sheets_.insert(key); + } + + // Add a slightly transparent rectangle behind the text + ImVec2 text_pos(graphics_bin_canvas_.zero_point().x + 2, + graphics_bin_canvas_.zero_point().y + 2); + ImVec2 text_size = + ImGui::CalcTextSize(absl::StrFormat("%02X", key).c_str()); + ImVec2 rent_min(text_pos.x, text_pos.y); + ImVec2 rent_max(text_pos.x + text_size.x, text_pos.y + text_size.y); + + graphics_bin_canvas_.GetDrawList()->AddRectFilled( + rent_min, rent_max, IM_COL32(0, 125, 0, 128)); + + graphics_bin_canvas_.GetDrawList()->AddText( + text_pos, IM_COL32(125, 255, 125, 255), + absl::StrFormat("%02X", key).c_str()); + } + }; + + graphics_bin_canvas_.UpdateEvent( + select_tile_event, ImVec2(0x100 + 1, 0x40 + 1), 0x20, sheet_scale_, + /*grid_size=*/16.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::EndChild(); + } + ImGui::PopStyleVar(); + ImGui::EndChild(); + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::UpdateGfxTabView() { + static int next_tab_id = 0; + + if (ImGui::BeginTabBar("##GfxEditTabBar", kGfxEditTabBarFlags)) { + if (ImGui::TabItemButton( + "+", ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_NoTooltip)) { + open_sheets_.insert(next_tab_id++); + } + + for (auto& sheet_id : open_sheets_) { + bool open = true; + if (ImGui::BeginTabItem(absl::StrFormat("%d", sheet_id).c_str(), &open, + ImGuiTabItemFlags_None)) { + current_sheet_ = sheet_id; + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + release_queue_.push(sheet_id); + } + if (ImGui::IsItemHovered()) { + if (ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { + release_queue_.push(sheet_id); + child_window_sheets_.insert(sheet_id); + } + } + + const auto child_id = + absl::StrFormat("##GfxEditPaletteChildWindow%d", sheet_id); + ImGui::BeginChild(child_id.c_str(), ImVec2(0, 0), true, + ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_AlwaysVerticalScrollbar | + ImGuiWindowFlags_AlwaysHorizontalScrollbar); + + auto draw_tile_event = [&]() { + gfx::Bitmap& current_bitmap = + *rom()->mutable_bitmap_manager()->mutable_bitmap(sheet_id); + current_sheet_canvas_.DrawTileOnBitmap(tile_size_, current_bitmap, + current_color_); + rom()->UpdateBitmap(¤t_bitmap); + }; + + auto size = ImVec2(0x80, 0x20); + current_sheet_canvas_.UpdateColorPainter( + *rom()->bitmap_manager()[sheet_id], current_color_, draw_tile_event, + size, tile_size_, current_scale_, 8.0f); + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + if (!open) release_queue_.push(sheet_id); + } + + ImGui::EndTabBar(); + } + + // Release any tabs that were closed + while (!release_queue_.empty()) { + auto sheet_id = release_queue_.top(); + open_sheets_.erase(sheet_id); + release_queue_.pop(); + } + + // Draw any child windows that were created + if (!child_window_sheets_.empty()) { + int id_to_release = -1; + for (const auto& id : child_window_sheets_) { + bool active = true; + ImGui::SetNextWindowPos(ImGui::GetIO().MousePos, ImGuiCond_Once); + ImGui::SetNextWindowSize(ImVec2(0x100 + 1 * 16, 0x40 + 1 * 16), + ImGuiCond_Once); + ImGui::Begin(absl::StrFormat("##GfxEditPaletteChildWindow%d", id).c_str(), + &active, ImGuiWindowFlags_AlwaysUseWindowPadding); + current_sheet_ = id; + current_sheet_canvas_.UpdateColorPainter( + *rom()->bitmap_manager()[id], current_color_, + [&]() { + + }, + ImVec2(0x100, 0x40), tile_size_, current_scale_, 8.0f); + ImGui::End(); + + if (active == false) { + id_to_release = id; + } + } + if (id_to_release != -1) { + child_window_sheets_.erase(id_to_release); + } + } + + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::UpdatePaletteColumn() { + auto palette_group = rom()->palette_group( + kPaletteGroupAddressesKeys[edit_palette_group_name_index_]); + + auto palette = palette_group[edit_palette_index_]; + + if (rom()->isLoaded()) { + gui::TextWithSeparators("ROM Palette"); + ImGui::SetNextItemWidth(100.f); + ImGui::Combo("Palette Group", (int*)&edit_palette_group_name_index_, + kPaletteGroupAddressesKeys, + IM_ARRAYSIZE(kPaletteGroupAddressesKeys)); + ImGui::SetNextItemWidth(100.f); + gui::InputHex("Palette Group Index", &edit_palette_index_); + } + + gui::SelectablePalettePipeline(edit_palette_sub_index_, refresh_graphics_, + palette); + + if (refresh_graphics_) { + rom()->bitmap_manager()[current_sheet_]->ApplyPaletteWithTransparent( + palette, edit_palette_sub_index_); + rom()->UpdateBitmap( + rom()->mutable_bitmap_manager()->mutable_bitmap(current_sheet_).get()); + refresh_graphics_ = false; + } + + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::UpdateLinkGfxView() { + TAB_ITEM("Player Animations") + + const auto link_gfx_offset = 0x80000; + const auto link_gfx_length = 0x7000; + + // Load Links graphics from the ROM + RETURN_IF_ERROR(rom()->LoadLinkGraphics()); + + // Split it into the pose data frames + // Create an animation step display for the poses + // Allow the user to modify the frames used in an anim step + // LinkOAM_AnimationSteps: + // #_0D85FB + + END_TAB_ITEM() + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::UpdateScadView() { + TAB_ITEM("Prototype") + + RETURN_IF_ERROR(DrawToolset()) + + if (open_memory_editor_) { + ImGui::Begin("Memory Editor", &open_memory_editor_); + RETURN_IF_ERROR(DrawMemoryEditor()) + ImGui::End(); + } + + BEGIN_TABLE("#gfxEditTable", 4, kGfxEditFlags) + SETUP_COLUMN("File Import (BIN, CGX, ROM)") + SETUP_COLUMN("Palette (COL)") + ImGui::TableSetupColumn("Tilemaps and Objects (SCR, PNL, OBJ)", + ImGuiTableColumnFlags_WidthFixed); + SETUP_COLUMN("Graphics Preview") + TABLE_HEADERS() + NEXT_COLUMN() { + status_ = DrawCgxImport(); + status_ = DrawClipboardImport(); + status_ = DrawFileImport(); + status_ = DrawExperimentalFeatures(); + } + + NEXT_COLUMN() { status_ = DrawPaletteControls(); } + + NEXT_COLUMN() + gui::BitmapCanvasPipeline(scr_canvas_, scr_bitmap_, 0x200, 0x200, 0x20, + scr_loaded_, false, 0); + status_ = DrawScrImport(); + + NEXT_COLUMN() + if (super_donkey_) { + if (refresh_graphics_) { + for (int i = 0; i < graphics_bin_.size(); i++) { + graphics_bin_[i].ApplyPalette( + col_file_palette_group_[current_palette_index_]); + rom()->UpdateBitmap(&graphics_bin_[i]); + } + refresh_graphics_ = false; + } + // Load the full graphics space from `super_donkey_1.bin` + gui::GraphicsBinCanvasPipeline(0x100, 0x40, 0x20, num_sheets_to_load_, 3, + super_donkey_, graphics_bin_); + } else if (cgx_loaded_ && col_file_) { + // Load the CGX graphics + gui::BitmapCanvasPipeline(import_canvas_, cgx_bitmap_, 0x100, 16384, 0x20, + cgx_loaded_, true, 5); + } else { + // Load the BIN/Clipboard Graphics + gui::BitmapCanvasPipeline(import_canvas_, bin_bitmap_, 0x100, 16384, 0x20, + gfx_loaded_, true, 2); + } + END_TABLE() + + END_TAB_ITEM() + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::DrawToolset() { + if (ImGui::BeginTable("GraphicsToolset", 2, ImGuiTableFlags_SizingFixedFit, + ImVec2(0, 0))) { + for (const auto& name : kGfxToolsetColumnNames) + ImGui::TableSetupColumn(name.data()); + + ImGui::TableNextColumn(); + if (Button(ICON_MD_MEMORY)) { + if (!open_memory_editor_) { + open_memory_editor_ = true; + } else { + open_memory_editor_ = false; + } + } + + TEXT_COLUMN("Open Memory Editor") // Separator + + ImGui::EndTable(); + } + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::DrawCgxImport() { + gui::TextWithSeparators("Cgx Import"); + InputInt("BPP", ¤t_bpp_); + + InputText("##CGXFile", cgx_file_name_, sizeof(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_)); + is_open_ = true; + cgx_loaded_ = true; + }); + gui::ButtonPipe("Copy CGX Path", + [this]() { ImGui::SetClipboardText(cgx_file_path_); }); + + gui::ButtonPipe("Load CGX Data", [this]() { + status_ = gfx::LoadCgx(current_bpp_, cgx_file_path_, cgx_data_, + decoded_cgx_, extra_cgx_data_); + + cgx_bitmap_.InitializeFromData(0x80, 0x200, 8, decoded_cgx_); + if (col_file_) { + cgx_bitmap_.ApplyPalette(decoded_col_); + rom()->RenderBitmap(&cgx_bitmap_); + } + }); + + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::DrawScrImport() { + InputText("##ScrFile", scr_file_name_, sizeof(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; + }); + + InputInt("SCR Mod", &scr_mod_value_); + + gui::ButtonPipe("Load Scr Data", [this]() { + status_ = gfx::LoadScr(scr_file_path_, scr_mod_value_, scr_data_); + + decoded_scr_data_.resize(0x100 * 0x100); + status_ = gfx::DrawScrWithCgx(current_bpp_, scr_data_, decoded_scr_data_, + decoded_cgx_); + + scr_bitmap_.InitializeFromData(0x100, 0x100, 8, decoded_scr_data_); + if (scr_loaded_) { + scr_bitmap_.ApplyPalette(decoded_col_); + rom()->RenderBitmap(&scr_bitmap_); + } + }); + + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::DrawPaletteControls() { + gui::TextWithSeparators("COL Import"); + InputText("##ColFile", col_file_name_, sizeof(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(); + } + col_file_palette_group_ = gfx::CreatePaletteGroupFromColFile(col_data_); + col_file_palette_ = gfx::SNESPalette(col_data_); + + // gigaleak dev format based code + decoded_col_ = gfx::DecodeColFile(col_file_path_); + col_file_ = true; + is_open_ = true; + }); + + gui::ButtonPipe("Copy COL Path", + [this]() { ImGui::SetClipboardText(col_file_path_); }); + + if (rom()->isLoaded()) { + gui::TextWithSeparators("ROM Palette"); + gui::InputHex("Palette Index", ¤t_palette_index_); + ImGui::Combo("Palette", ¤t_palette_, kPaletteGroupAddressesKeys, + IM_ARRAYSIZE(kPaletteGroupAddressesKeys)); + } + + if (col_file_palette_.size() != 0) { + gui::SelectablePalettePipeline(current_palette_index_, refresh_graphics_, + col_file_palette_); + } + + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::DrawObjImport() { + gui::TextWithSeparators("OBJ Import"); + + InputText("##ObjFile", obj_file_path_, sizeof(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; + }); + + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::DrawTilemapImport() { + gui::TextWithSeparators("Tilemap Import"); + + InputText("##TMapFile", tilemap_file_path_, sizeof(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_); + + // 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; + }); + + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::DrawFileImport() { + gui::TextWithSeparators("BIN Import"); + + InputText("##ROMFile", file_path_, sizeof(file_path_)); + SameLine(); + + gui::FileDialogPipeline("ImportDlgKey", ".bin,.hex\0", "Open BIN", [this]() { + strncpy(file_path_, ImGuiFileDialog::Instance()->GetFilePathName().c_str(), + sizeof(file_path_)); + status_ = temp_rom_.LoadFromFile(file_path_); + is_open_ = true; + }); + + gui::ButtonPipe("Copy File Path", + [this]() { ImGui::SetClipboardText(file_path_); }); + + 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 { + return absl::InvalidArgumentError( + "Please select a file before importing."); + } + } + + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::DrawClipboardImport() { + gui::TextWithSeparators("Clipboard Import"); + gui::ButtonPipe("Paste from Clipboard", [this]() { + const char* text = ImGui::GetClipboardText(); + if (text) { + const auto clipboard_data = Bytes(text, text + strlen(text)); + ImGui::MemFree((void*)text); + status_ = temp_rom_.LoadFromBytes(clipboard_data); + is_open_ = true; + open_memory_editor_ = true; + } + }); + gui::InputHex("Offset", &clipboard_offset_); + gui::InputHex("Size", &clipboard_size_); + gui::InputHex("Num Sheets", &num_sheets_to_load_); + + gui::ButtonPipe("Decompress Clipboard Data", [this]() { + if (temp_rom_.isLoaded()) { + status_ = DecompressImportData(0x40000); + } else { + status_ = absl::InvalidArgumentError( + "Please paste data into the clipboard before " + "decompressing."); + } + }); + + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::DrawExperimentalFeatures() { + gui::TextWithSeparators("Experimental"); + if (Button("Decompress Super Donkey Full")) { + if (strlen(file_path_) > 0) { + RETURN_IF_ERROR(DecompressSuperDonkey()) + } else { + return absl::InvalidArgumentError( + "Please select `super_donkey_1.bin` before " + "importing."); + } + } + ImGui::SetItemTooltip( + "Requires `super_donkey_1.bin` to be imported under the " + "BIN import section."); + return absl::OkStatus(); +} + +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()); + } + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::DecompressImportData(int size) { + ASSIGN_OR_RETURN(import_data_, gfx::lc_lz2::DecompressV2( + temp_rom_.data(), current_offset_, size)) + + auto converted_sheet = gfx::SnesTo8bppSheet(import_data_, 3); + bin_bitmap_.Create(core::kTilesheetWidth, 0x2000, core::kTilesheetDepth, + converted_sheet); + + if (rom()->isLoaded()) { + auto palette_group = rom()->palette_group("ow_main"); + z3_rom_palette_ = palette_group[current_palette_]; + if (col_file_) { + bin_bitmap_.ApplyPalette(col_file_palette_); + } else { + bin_bitmap_.ApplyPalette(z3_rom_palette_); + } + } + + rom()->RenderBitmap(&bin_bitmap_); + gfx_loaded_ = true; + + return absl::OkStatus(); +} + +absl::Status GraphicsEditor::DecompressSuperDonkey() { + int i = 0; + for (const auto& offset : kSuperDonkeyTiles) { + int offset_value = + std::stoi(offset, nullptr, 16); // convert hex string to int + ASSIGN_OR_RETURN( + auto decompressed_data, + gfx::lc_lz2::DecompressV2(temp_rom_.data(), offset_value, 0x1000)) + auto converted_sheet = gfx::SnesTo8bppSheet(decompressed_data, 3); + graphics_bin_[i] = + gfx::Bitmap(core::kTilesheetWidth, core::kTilesheetHeight, + core::kTilesheetDepth, converted_sheet); + if (col_file_) { + graphics_bin_[i].ApplyPalette( + col_file_palette_group_[current_palette_index_]); + } else { + // ROM palette + auto palette_group = + rom()->palette_group(kPaletteGroupAddressesKeys[current_palette_]); + z3_rom_palette_ = palette_group[current_palette_index_]; + graphics_bin_[i].ApplyPalette(z3_rom_palette_); + } + + rom()->RenderBitmap(&graphics_bin_[i]); + i++; + } + + for (const auto& offset : kSuperDonkeySprites) { + int offset_value = + std::stoi(offset, nullptr, 16); // convert hex string to int + ASSIGN_OR_RETURN( + auto decompressed_data, + gfx::lc_lz2::DecompressV2(temp_rom_.data(), offset_value, 0x1000)) + auto converted_sheet = gfx::SnesTo8bppSheet(decompressed_data, 3); + graphics_bin_[i] = + gfx::Bitmap(core::kTilesheetWidth, core::kTilesheetHeight, + core::kTilesheetDepth, converted_sheet); + if (col_file_) { + graphics_bin_[i].ApplyPalette( + col_file_palette_group_[current_palette_index_]); + } else { + // ROM palette + auto palette_group = + rom()->palette_group(kPaletteGroupAddressesKeys[current_palette_]); + z3_rom_palette_ = palette_group[current_palette_index_]; + graphics_bin_[i].ApplyPalette(z3_rom_palette_); + } + + rom()->RenderBitmap(&graphics_bin_[i]); + i++; + } + super_donkey_ = true; + num_sheets_to_load_ = i; + + return absl::OkStatus(); +} + +} // namespace editor +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/editor/graphics_editor.h b/src/app/editor/graphics_editor.h new file mode 100644 index 00000000..448cb300 --- /dev/null +++ b/src/app/editor/graphics_editor.h @@ -0,0 +1,193 @@ +#ifndef YAZE_APP_EDITOR_GRAPHICS_EDITOR_H +#define YAZE_APP_EDITOR_GRAPHICS_EDITOR_H + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "app/editor/modules/palette_editor.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_tile.h" +#include "app/gui/canvas.h" +#include "app/gui/input.h" +#include "app/gui/pipeline.h" +#include "app/rom.h" +#include "app/zelda3/overworld.h" + +namespace yaze { +namespace app { +namespace editor { + +// "99973","A3D80", + +const std::string kSuperDonkeyTiles[] = { + "97C05", "98219", "9871E", "98C00", "99084", "995AF", "99DE0", "9A27E", + "9A741", "9AC31", "9B07E", "9B55C", "9B963", "9BB99", "9C009", "9C4B4", + "9C92B", "9CDD6", "9D2C2", "9E037", "9E527", "9EA56", "9EF65", "9FCD1", + "A0193", "A059E", "A0B17", "A0FB6", "A14A5", "A1988", "A1E66", "A232B", + "A27F0", "A2B6E", "A302C", "A3453", "A38CA", "A42BB", "A470C", "A4BA9", + "A5089", "A5385", "A5742", "A5BCC", "A6017", "A6361", "A66F8"}; + +const std::string kSuperDonkeySprites[] = { + "A8E5D", "A9435", "A9934", "A9D83", "AA2F1", "AA6D4", "AABE4", "AB127", + "AB65A", "ABBDD", "AC38D", "AC797", "ACCC8", "AD0AE", "AD245", "AD554", + "ADAAC", "ADECC", "AE453", "AE9D2", "AEF40", "AF3C9", "AF92E", "AFE9D", + "B03D2", "B09AC", "B0F0C", "B1430", "B1859", "B1E01", "B229A", "B2854", + "B2D27", "B31D7", "B3B58", "B40B5", "B45A5", "B4D64", "B5031", "B555F", + "B5F30", "B6858", "B70DD", "B7526", "B79EC", "B7C83", "B80F7", "B85CC", + "B8A3F", "B8F97", "B94F2", "B9A20", "B9E9A", "BA3A2", "BA8F6", "BACDC", + "BB1F9", "BB781", "BBCCA", "BC26D", "BC7D4", "BCBB0", "BD082", "BD5FC", + "BE115", "BE5C2", "BEB63", "BF0CB", "BF607", "BFA55", "BFD71", "C017D", + "C0567", "C0981", "C0BA7", "C116D", "C166A", "C1FE0", "C24CE", "C2B19"}; + +constexpr const char* kPaletteGroupAddressesKeys[] = { + "ow_main", "ow_aux", "ow_animated", "hud", + "global_sprites", "armors", "swords", "shields", + "sprites_aux1", "sprites_aux2", "sprites_aux3", "dungeon_main", + "grass", "3d_object", "ow_mini_map", +}; + +static constexpr std::string_view kGfxEditColumnNames[] = { + "Tilesheets", "Current Graphics", "Palette Controls"}; + +static constexpr absl::string_view kGfxToolsetColumnNames[] = { + "#memoryEditor", + "##separator_gfx1", +}; + +constexpr ImGuiTableFlags kGfxEditFlags = ImGuiTableFlags_Reorderable | + ImGuiTableFlags_Resizable | + ImGuiTableFlags_SizingStretchSame; + +class GraphicsEditor : public SharedROM { + public: + absl::Status Update(); + + private: + enum class GfxEditMode { + kSelect, + kPencil, + kFill, + }; + + // Graphics Editor Tab + absl::Status UpdateGfxEdit(); + absl::Status UpdateGfxSheetList(); + absl::Status UpdateGfxTabView(); + absl::Status UpdatePaletteColumn(); + void DrawGfxEditToolset(); + + // Link Graphics Edit Tab + absl::Status UpdateLinkGfxView(); + + // Prototype Graphics Viewer + absl::Status UpdateScadView(); + + // Import Functions + absl::Status DrawCgxImport(); + absl::Status DrawScrImport(); + absl::Status DrawFileImport(); + absl::Status DrawObjImport(); + absl::Status DrawTilemapImport(); + + // Other Functions + absl::Status DrawToolset(); + absl::Status DrawPaletteControls(); + absl::Status DrawClipboardImport(); + absl::Status DrawExperimentalFeatures(); + absl::Status DrawMemoryEditor(); + + absl::Status DecompressImportData(int size); + absl::Status DecompressSuperDonkey(); + + // Member Variables + ImVec4 current_color_; + uint16_t current_sheet_ = 0; + uint8_t tile_size_ = 0x01; + std::set open_sheets_; + std::set child_window_sheets_; + std::stack release_queue_; + uint64_t edit_palette_group_name_index_ = 0; + uint64_t edit_palette_group_index_ = 0; + uint64_t edit_palette_index_ = 0; + uint64_t edit_palette_sub_index_ = 0; + float sheet_scale_ = 2.0f; + float current_scale_ = 4.0f; + + // Prototype Graphics Viewer + int current_palette_ = 0; + uint64_t current_offset_ = 0; + uint64_t current_size_ = 0; + uint64_t current_palette_index_ = 0; + int current_bpp_ = 0; + int scr_mod_value_ = 0; + + uint64_t num_sheets_to_load_ = 1; + uint64_t bin_size_ = 0; + uint64_t clipboard_offset_ = 0; + uint64_t clipboard_size_ = 0; + + bool refresh_graphics_ = false; + bool open_memory_editor_ = false; + bool gfx_loaded_ = false; + bool is_open_ = false; + bool super_donkey_ = false; + bool col_file_ = false; + bool cgx_loaded_ = false; + bool scr_loaded_ = false; + 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] = ""; + + GfxEditMode gfx_edit_mode_ = GfxEditMode::kSelect; + + ROM temp_rom_; + ROM tilemap_rom_; + zelda3::Overworld overworld_; + MemoryEditor cgx_memory_editor_; + MemoryEditor col_memory_editor_; + PaletteEditor palette_editor_; + Bytes import_data_; + Bytes graphics_buffer_; + std::vector decoded_cgx_; + std::vector cgx_data_; + std::vector extra_cgx_data_; + std::vector decoded_col_; + std::vector scr_data_; + std::vector decoded_scr_data_; + gfx::Bitmap cgx_bitmap_; + gfx::Bitmap scr_bitmap_; + gfx::Bitmap bin_bitmap_; + gfx::Bitmap link_full_sheet_; + gfx::BitmapTable graphics_bin_; + gfx::BitmapTable clipboard_graphics_bin_; + gfx::BitmapTable link_graphics_; + gfx::PaletteGroup col_file_palette_group_; + gfx::SNESPalette z3_rom_palette_; + gfx::SNESPalette col_file_palette_; + gfx::SNESPalette link_palette_; + gui::Canvas import_canvas_; + gui::Canvas scr_canvas_; + gui::Canvas super_donkey_canvas_; + gui::Canvas current_sheet_canvas_; + absl::Status status_; +}; + +} // namespace editor +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_EDITOR_GRAPHICS_EDITOR_H \ No newline at end of file diff --git a/src/app/editor/master_editor.cc b/src/app/editor/master_editor.cc index 16ae7243..14ad3473 100644 --- a/src/app/editor/master_editor.cc +++ b/src/app/editor/master_editor.cc @@ -7,18 +7,27 @@ #include #include "absl/status/status.h" +#include "app/core/common.h" #include "app/core/constants.h" -#include "app/editor/assembly_editor.h" +#include "app/core/platform/file_dialog.h" #include "app/editor/dungeon_editor.h" -#include "app/editor/music_editor.h" +#include "app/editor/graphics_editor.h" +#include "app/editor/modules/assembly_editor.h" +#include "app/editor/modules/music_editor.h" +#include "app/editor/modules/palette_editor.h" #include "app/editor/overworld_editor.h" +#include "app/editor/screen_editor.h" +#include "app/editor/sprite_editor.h" +#include "app/emu/emulator.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/pipeline.h" +#include "app/gui/style.h" +#include "app/gui/widgets.h" #include "app/rom.h" -#include "gui/canvas.h" -#include "gui/icons.h" -#include "gui/input.h" -#include "gui/widgets.h" namespace yaze { namespace app { @@ -32,9 +41,9 @@ constexpr ImGuiWindowFlags kMainEditorFlags = ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoTitleBar; void NewMasterFrame() { - const ImGuiIO &io = ImGui::GetIO(); + const ImGuiIO& io = ImGui::GetIO(); ImGui::NewFrame(); - ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowPos(gui::kZeroPos); ImVec2 dimensions(io.DisplaySize.x, io.DisplaySize.y); ImGui::SetNextWindowSize(dimensions, ImGuiCond_Always); @@ -44,8 +53,8 @@ void NewMasterFrame() { } } -bool BeginCentered(const char *name) { - ImGuiIO const &io = ImGui::GetIO(); +bool BeginCentered(const char* name) { + ImGuiIO const& io = ImGui::GetIO(); ImVec2 pos(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGuiWindowFlags flags = @@ -54,30 +63,65 @@ bool BeginCentered(const char *name) { return ImGui::Begin(name, nullptr, flags); } -void DisplayStatus(absl::Status &status) { - if (BeginCentered("StatusWindow")) { - ImGui::Text("%s", status.ToString().c_str()); - ImGui::Spacing(); - ImGui::NextColumn(); - ImGui::Columns(1); - ImGui::Separator(); - ImGui::NewLine(); - ImGui::SameLine(270); - if (ImGui::Button("OK", ImVec2(200, 0))) { - status = absl::OkStatus(); +class RecentFilesManager { + public: + RecentFilesManager(const std::string& filename) : filename_(filename) {} + + void AddFile(const std::string& filePath) { + // Add a file to the list, avoiding duplicates + auto it = std::find(recentFiles_.begin(), recentFiles_.end(), filePath); + if (it == recentFiles_.end()) { + recentFiles_.push_back(filePath); } - ImGui::End(); } -} + + void Save() { + std::ofstream file(filename_); + if (!file.is_open()) { + return; // Handle the error appropriately + } + + for (const auto& filePath : recentFiles_) { + file << filePath << std::endl; + } + } + + void Load() { + std::ifstream file(filename_); + if (!file.is_open()) { + return; // Handle the error appropriately + } + + recentFiles_.clear(); + std::string line; + while (std::getline(file, line)) { + if (!line.empty()) { + recentFiles_.push_back(line); + } + } + } + + const std::vector& GetRecentFiles() const { + return recentFiles_; + } + + private: + std::string filename_; + std::vector recentFiles_; +}; } // namespace +using ImGui::BeginMenu; +using ImGui::MenuItem; +using ImGui::Text; + void MasterEditor::SetupScreen(std::shared_ptr renderer) { sdl_renderer_ = renderer; - rom_.SetupRenderer(renderer); + rom()->SetupRenderer(renderer); } -void MasterEditor::UpdateScreen() { +absl::Status MasterEditor::Update() { NewMasterFrame(); DrawYazeMenu(); @@ -86,34 +130,72 @@ void MasterEditor::UpdateScreen() { DrawAboutPopup(); DrawInfoPopup(); + if (rom()->isLoaded() && !rom_assets_loaded_) { + // Initialize overworld graphics, maps, and palettes + RETURN_IF_ERROR(overworld_editor_.LoadGraphics()); + rom_assets_loaded_ = true; + } + TAB_BAR("##TabBar") - DrawOverworldEditor(); - DrawDungeonEditor(); - DrawMusicEditor(); - DrawSpriteEditor(); - DrawScreenEditor(); - DrawPaletteEditor(); + + gui::RenderTabItem("Overworld", [&]() { + current_editor_ = &overworld_editor_; + status_ = overworld_editor_.Update(); + }); + + gui::RenderTabItem("Dungeon", [&]() { + current_editor_ = &dungeon_editor_; + status_ = dungeon_editor_.Update(); + }); + + gui::RenderTabItem("Graphics", + [&]() { status_ = graphics_editor_.Update(); }); + gui::RenderTabItem("Sprites", [&]() { status_ = sprite_editor_.Update(); }); + gui::RenderTabItem("Palettes", [&]() { status_ = palette_editor_.Update(); }); + gui::RenderTabItem("Screens", [&]() { screen_editor_.Update(); }); + gui::RenderTabItem("Music", [&]() { music_editor_.Update(); }); END_TAB_BAR() ImGui::End(); + + return absl::OkStatus(); } void MasterEditor::DrawFileDialog() { - if (ImGuiFileDialog::Instance()->Display("ChooseFileDlgKey")) { - if (ImGuiFileDialog::Instance()->IsOk()) { - std::string filePathName = ImGuiFileDialog::Instance()->GetFilePathName(); - status_ = rom_.LoadFromFile(filePathName); - overworld_editor_.SetupROM(rom_); - screen_editor_.SetupROM(rom_); - palette_editor_.SetupROM(rom_); - } - ImGuiFileDialog::Instance()->Close(); - } + gui::FileDialogPipeline("ChooseFileDlgKey", ".sfc,.smc", std::nullopt, [&]() { + std::string filePathName = ImGuiFileDialog::Instance()->GetFilePathName(); + status_ = rom()->LoadFromFile(filePathName); + static RecentFilesManager manager("recent_files.txt"); + + // Load existing recent files + manager.Load(); + + // Add a new file + manager.AddFile(filePathName); + + // Save the updated list + manager.Save(); + }); } void MasterEditor::DrawStatusPopup() { if (!status_.ok()) { - DisplayStatus(status_); + show_status_ = true; + prev_status_ = status_; + } + + if (show_status_ && (BeginCentered("StatusWindow"))) { + Text("%s", prev_status_.ToString().c_str()); + ImGui::Spacing(); + ImGui::NextColumn(); + ImGui::Columns(1); + ImGui::Separator(); + ImGui::NewLine(); + ImGui::SameLine(270); + if (ImGui::Button("OK", gui::kDefaultModalSize)) { + show_status_ = false; + } + ImGui::End(); } } @@ -121,13 +203,13 @@ void MasterEditor::DrawAboutPopup() { if (about_) ImGui::OpenPopup("About"); if (ImGui::BeginPopupModal("About", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("Yet Another Zelda3 Editor - v0.02"); - ImGui::Text("Written by: scawful"); + Text("Yet Another Zelda3 Editor - v0.05"); + Text("Written by: scawful"); ImGui::Spacing(); - ImGui::Text("Special Thanks: Zarby89, JaredBrian"); + Text("Special Thanks: Zarby89, JaredBrian"); ImGui::Separator(); - if (ImGui::Button("Close", ImVec2(200, 0))) { + if (ImGui::Button("Close", gui::kDefaultModalSize)) { about_ = false; ImGui::CloseCurrentPopup(); } @@ -139,10 +221,10 @@ void MasterEditor::DrawInfoPopup() { if (rom_info_) ImGui::OpenPopup("ROM Information"); if (ImGui::BeginPopupModal("ROM Information", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("Title: %s", rom_.GetTitle()); - ImGui::Text("ROM Size: %ld", rom_.size()); + Text("Title: %s", rom()->title()); + Text("ROM Size: %ld", rom()->size()); - if (ImGui::Button("Close", ImVec2(200, 0))) { + if (ImGui::Button("Close", gui::kDefaultModalSize)) { rom_info_ = false; ImGui::CloseCurrentPopup(); } @@ -151,52 +233,145 @@ void MasterEditor::DrawInfoPopup() { } void MasterEditor::DrawYazeMenu() { + static bool show_display_settings = false; + static bool show_command_line_interface = false; + MENU_BAR() DrawFileMenu(); DrawEditMenu(); DrawViewMenu(); DrawHelpMenu(); + + ImGui::SameLine(ImGui::GetWindowWidth() - ImGui::GetStyle().ItemSpacing.x - + ImGui::CalcTextSize(ICON_MD_DISPLAY_SETTINGS).x - 150); + // Modify the style of the button to have no background color + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + if (ImGui::Button(ICON_MD_DISPLAY_SETTINGS)) { + show_display_settings = !show_display_settings; + } + + if (ImGui::Button(ICON_MD_TERMINAL)) { + show_command_line_interface = !show_command_line_interface; + } + ImGui::PopStyleColor(); + + Text("%s", absl::StrCat("yaze v", core::kYazeVersion).c_str()); + END_MENU_BAR() + + if (show_display_settings) { + ImGui::Begin("Display Settings", &show_display_settings, + ImGuiWindowFlags_None); + gui::DrawDisplaySettings(); + ImGui::End(); + } + + if (show_command_line_interface) { + ImGui::Begin("Command Line Interface", &show_command_line_interface, + ImGuiWindowFlags_None); + Text("Enter a command:"); + ImGui::End(); + } } -void MasterEditor::DrawFileMenu() const { - if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("Open", "Ctrl+O")) { - ImGuiFileDialog::Instance()->OpenDialog("ChooseFileDlgKey", "Open ROM", - ".sfc,.smc", "."); +void MasterEditor::DrawFileMenu() { + static bool save_as_menu = false; + + if (BeginMenu("File")) { + if (MenuItem("Open", "Ctrl+O")) { + if (flags()->kNewFileDialogWrapper) { + auto file_name = FileDialogWrapper::ShowOpenFileDialog(); + PRINT_IF_ERROR(rom()->LoadFromFile(file_name)); + static RecentFilesManager manager("recent_files.txt"); + manager.Load(); + manager.AddFile(file_name); + manager.Save(); + } else { + ImGuiFileDialog::Instance()->OpenDialog("ChooseFileDlgKey", "Open ROM", + ".sfc,.smc", "."); + } } - MENU_ITEM2("Save", "Ctrl+S") {} - MENU_ITEM("Save As..") {} + if (BeginMenu("Open Recent")) { + static RecentFilesManager manager("recent_files.txt"); + manager.Load(); + if (manager.GetRecentFiles().empty()) { + MenuItem("No Recent Files", nullptr, false, false); + } else { + for (const auto& filePath : manager.GetRecentFiles()) { + if (MenuItem(filePath.c_str())) { + status_ = rom()->LoadFromFile(filePath); + } + } + } + ImGui::EndMenu(); + } + + MENU_ITEM2("Save", "Ctrl+S") { status_ = rom()->SaveToFile(backup_rom_); } + MENU_ITEM("Save As..") { save_as_menu = true; } + + if (rom()->isLoaded()) { + MENU_ITEM("Reload") { status_ = rom()->Reload(); } + MENU_ITEM("Close") { status_ = rom()->Close(); } + } ImGui::Separator(); - // TODO: Make these options matter - if (ImGui::BeginMenu("Options")) { - static bool enabled = true; - ImGui::MenuItem("Enabled", "", &enabled); - ImGui::BeginChild("child", ImVec2(0, 60), true); - for (int i = 0; i < 10; i++) ImGui::Text("Scrolling Text %d", i); - ImGui::EndChild(); - static float f = 0.5f; - static int n = 0; - ImGui::SliderFloat("Value", &f, 0.0f, 1.0f); - ImGui::InputFloat("Input", &f, 0.1f); - ImGui::Combo("Combo", &n, "Yes\0No\0Maybe\0\0"); + if (BeginMenu("Options")) { + MenuItem("Backup ROM", "", &backup_rom_); + ImGui::Separator(); + Text("Experiment Flags"); + ImGui::Checkbox("Enable Texture Streaming", + &mutable_flags()->kLoadTexturesAsStreaming); + ImGui::Checkbox("Enable Overworld Sprites", + &mutable_flags()->kDrawOverworldSprites); + ImGui::Checkbox("Use Bitmap Manager", + &mutable_flags()->kUseBitmapManager); + ImGui::Checkbox("Log Instructions to Debugger", + &mutable_flags()->kLogInstructions); + ImGui::Checkbox("Use New ImGui Input", + &mutable_flags()->kUseNewImGuiInput); + ImGui::Checkbox("Save All Palettes", &mutable_flags()->kSaveAllPalettes); + ImGui::Checkbox("Save With Change Queue", + &mutable_flags()->kSaveWithChangeQueue); + ImGui::Checkbox("Draw Dungeon Room Graphics", + &mutable_flags()->kDrawDungeonRoomGraphics); ImGui::EndMenu(); } + + ImGui::Separator(); + + if (MenuItem("Quit", "Ctrl+Q")) { + // TODO: Implement quit confirmation dialog. + } + ImGui::EndMenu(); } + + if (save_as_menu) { + static std::string save_as_filename = ""; + ImGui::Begin("Save As..", &save_as_menu, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::InputText("Filename", &save_as_filename); + if (ImGui::Button("Save", gui::kDefaultModalSize)) { + status_ = rom()->SaveToFile(backup_rom_, save_as_filename); + save_as_menu = false; + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", gui::kDefaultModalSize)) { + save_as_menu = false; + } + ImGui::End(); + } } void MasterEditor::DrawEditMenu() { - if (ImGui::BeginMenu("Edit")) { - MENU_ITEM2("Undo", "Ctrl+Z") { status_ = overworld_editor_.Undo(); } - MENU_ITEM2("Redo", "Ctrl+Y") { status_ = overworld_editor_.Redo(); } + if (BeginMenu("Edit")) { + MENU_ITEM2("Undo", "Ctrl+Z") { status_ = current_editor_->Undo(); } + MENU_ITEM2("Redo", "Ctrl+Y") { status_ = current_editor_->Redo(); } ImGui::Separator(); - MENU_ITEM2("Cut", "Ctrl+X") { status_ = overworld_editor_.Cut(); } - MENU_ITEM2("Copy", "Ctrl+C") { status_ = overworld_editor_.Copy(); } - MENU_ITEM2("Paste", "Ctrl+V") { status_ = overworld_editor_.Paste(); } + MENU_ITEM2("Cut", "Ctrl+X") { status_ = current_editor_->Cut(); } + MENU_ITEM2("Copy", "Ctrl+C") { status_ = current_editor_->Copy(); } + MENU_ITEM2("Paste", "Ctrl+V") { status_ = current_editor_->Paste(); } ImGui::Separator(); MENU_ITEM2("Find", "Ctrl+F") {} ImGui::Separator(); @@ -207,11 +382,18 @@ void MasterEditor::DrawEditMenu() { void MasterEditor::DrawViewMenu() { static bool show_imgui_metrics = false; - static bool show_imgui_style_editor = false; static bool show_memory_editor = false; static bool show_asm_editor = false; static bool show_imgui_demo = false; static bool show_memory_viewer = false; + static bool show_palette_editor = false; + static bool show_emulator = false; + + if (show_emulator) { + ImGui::Begin("Emulator", &show_emulator, ImGuiWindowFlags_MenuBar); + emulator_.Run(); + ImGui::End(); + } if (show_imgui_metrics) { ImGui::ShowMetricsWindow(&show_imgui_metrics); @@ -219,7 +401,7 @@ void MasterEditor::DrawViewMenu() { if (show_memory_editor) { static MemoryEditor mem_edit; - mem_edit.DrawWindow("Memory Editor", (void *)&rom_, rom_.size()); + mem_edit.DrawWindow("Memory Editor", (void*)&(*rom()), rom()->size()); } if (show_imgui_demo) { @@ -227,12 +409,12 @@ void MasterEditor::DrawViewMenu() { } if (show_asm_editor) { - assembly_editor_.Update(); + assembly_editor_.Update(show_asm_editor); } - if (show_imgui_style_editor) { - ImGui::Begin("Style Editor (ImGui)", &show_imgui_style_editor); - ImGui::ShowStyleEditor(); + if (show_palette_editor) { + ImGui::Begin("Palette Editor", &show_palette_editor); + status_ = palette_editor_.Update(); ImGui::End(); } @@ -250,7 +432,7 @@ void MasterEditor::DrawViewMenu() { ImGui::TableNextRow(); for (int column = 0; column < 3; column++) { ImGui::TableNextColumn(); - ImGui::Text("Cell %d,%d", column, row); + Text("Cell %d,%d", column, row); } } ImGui::EndTable(); @@ -259,62 +441,42 @@ void MasterEditor::DrawViewMenu() { ImGui::End(); } - if (ImGui::BeginMenu("View")) { - ImGui::MenuItem("HEX Editor", nullptr, &show_memory_editor); - ImGui::MenuItem("ASM Editor", nullptr, &show_asm_editor); - ImGui::MenuItem("Memory Viewer", nullptr, &show_memory_viewer); - ImGui::MenuItem("ImGui Demo", nullptr, &show_imgui_demo); - ImGui::Separator(); - if (ImGui::BeginMenu("GUI Tools")) { - ImGui::MenuItem("Metrics (ImGui)", nullptr, &show_imgui_metrics); - ImGui::MenuItem("Style Editor (ImGui)", nullptr, - &show_imgui_style_editor); - ImGui::EndMenu(); - } + if (BeginMenu("View")) { + MenuItem("Emulator", nullptr, &show_emulator); + MenuItem("HEX Editor", nullptr, &show_memory_editor); + MenuItem("ASM Editor", nullptr, &show_asm_editor); + MenuItem("Palette Editor", nullptr, &show_palette_editor); + MenuItem("Memory Viewer", nullptr, &show_memory_viewer); + MenuItem("ImGui Demo", nullptr, &show_imgui_demo); + MenuItem("ImGui Metrics", nullptr, &show_imgui_metrics); ImGui::EndMenu(); } } void MasterEditor::DrawHelpMenu() { - if (ImGui::BeginMenu("Help")) { - if (ImGui::MenuItem("About")) about_ = true; + static bool open_rom_help = false; + if (BeginMenu("Help")) { + if (MenuItem("How to open a ROM")) open_rom_help = true; + if (MenuItem("About")) about_ = true; ImGui::EndMenu(); } -} -void MasterEditor::DrawOverworldEditor() { - TAB_ITEM("Overworld") - status_ = overworld_editor_.Update(); - END_TAB_ITEM() -} + if (open_rom_help) ImGui::OpenPopup("Open a ROM"); + if (ImGui::BeginPopupModal("Open a ROM", nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + Text("File -> Open"); + Text("Select a ROM file to open"); + Text("Supported ROMs (headered or unheadered):"); + Text("The Legend of Zelda: A Link to the Past"); + Text("US Version 1.0"); + Text("JP Version 1.0"); -void MasterEditor::DrawDungeonEditor() { - TAB_ITEM("Dungeon") - dungeon_editor_.Update(); - END_TAB_ITEM() -} - -void MasterEditor::DrawPaletteEditor() { - TAB_ITEM("Palettes") - status_ = palette_editor_.Update(); - END_TAB_ITEM() -} - -void MasterEditor::DrawScreenEditor() { - TAB_ITEM("Screens") - screen_editor_.Update(); - END_TAB_ITEM() -} - -void MasterEditor::DrawMusicEditor() { - TAB_ITEM("Music") - music_editor_.Update(); - END_TAB_ITEM() -} - -void MasterEditor::DrawSpriteEditor() { - TAB_ITEM("Sprites") - END_TAB_ITEM() + if (ImGui::Button("Close", gui::kDefaultModalSize)) { + open_rom_help = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } } } // namespace editor diff --git a/src/app/editor/master_editor.h b/src/app/editor/master_editor.h index 66712aee..725b1a3a 100644 --- a/src/app/editor/master_editor.h +++ b/src/app/editor/master_editor.h @@ -8,28 +8,40 @@ #include #include "absl/status/status.h" +#include "app/core/common.h" #include "app/core/constants.h" -#include "app/editor/assembly_editor.h" +#include "app/gui/pipeline.h" +#include "app/editor/context/gfx_context.h" #include "app/editor/dungeon_editor.h" -#include "app/editor/music_editor.h" +#include "app/editor/graphics_editor.h" +#include "app/editor/modules/assembly_editor.h" +#include "app/editor/modules/music_editor.h" +#include "app/editor/modules/palette_editor.h" #include "app/editor/overworld_editor.h" -#include "app/editor/palette_editor.h" #include "app/editor/screen_editor.h" +#include "app/editor/sprite_editor.h" +#include "app/emu/emulator.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/rom.h" -#include "gui/canvas.h" -#include "gui/icons.h" -#include "gui/input.h" namespace yaze { namespace app { namespace editor { -class MasterEditor { +class MasterEditor : public SharedROM, + public GfxContext, + public core::ExperimentFlags { public: + MasterEditor() { current_editor_ = &overworld_editor_; } + void SetupScreen(std::shared_ptr renderer); - void UpdateScreen(); + absl::Status Update(); + + void Shutdown() { overworld_editor_.Shutdown(); } private: void DrawFileDialog(); @@ -38,31 +50,34 @@ class MasterEditor { void DrawInfoPopup(); void DrawYazeMenu(); - void DrawFileMenu() const; + void DrawFileMenu(); void DrawEditMenu(); void DrawViewMenu(); void DrawHelpMenu(); - void DrawOverworldEditor(); - void DrawDungeonEditor(); - void DrawPaletteEditor(); - void DrawMusicEditor(); - void DrawScreenEditor(); - void DrawSpriteEditor(); - bool about_ = false; bool rom_info_ = false; + bool backup_rom_ = true; + bool show_status_ = false; + bool rom_assets_loaded_ = false; + + absl::Status status_; + absl::Status prev_status_; std::shared_ptr sdl_renderer_; - absl::Status status_; + + emu::Emulator emulator_; AssemblyEditor assembly_editor_; DungeonEditor dungeon_editor_; + GraphicsEditor graphics_editor_; + MusicEditor music_editor_; OverworldEditor overworld_editor_; PaletteEditor palette_editor_; ScreenEditor screen_editor_; - MusicEditor music_editor_; - ROM rom_; + SpriteEditor sprite_editor_; + + Editor *current_editor_ = nullptr; }; } // namespace editor diff --git a/src/app/editor/assembly_editor.cc b/src/app/editor/modules/assembly_editor.cc similarity index 66% rename from src/app/editor/assembly_editor.cc rename to src/app/editor/modules/assembly_editor.cc index aacdcaaa..b1a55fd5 100644 --- a/src/app/editor/assembly_editor.cc +++ b/src/app/editor/modules/assembly_editor.cc @@ -1,19 +1,19 @@ #include "assembly_editor.h" +#include "app/gui/widgets.h" #include "core/constants.h" -#include "gui/widgets.h" namespace yaze { namespace app { namespace editor { AssemblyEditor::AssemblyEditor() { - text_editor_.SetLanguageDefinition(gui::widgets::GetAssemblyLanguageDef()); + text_editor_.SetLanguageDefinition(gui::GetAssemblyLanguageDef()); text_editor_.SetPalette(TextEditor::GetDarkPalette()); } -void AssemblyEditor::Update() { - ImGui::Begin("Assembly Editor", &file_is_loaded_); +void AssemblyEditor::Update(bool& is_loaded) { + ImGui::Begin("Assembly Editor", &is_loaded); MENU_BAR() DrawFileMenu(); DrawEditMenu(); @@ -32,8 +32,6 @@ void AssemblyEditor::Update() { ImGui::End(); } - - void AssemblyEditor::InlineUpdate() { ChangeActiveFile("assets/asm/template_song.asm"); auto cpos = text_editor_.GetCursorPosition(); @@ -48,10 +46,49 @@ void AssemblyEditor::InlineUpdate() { text_editor_.Render("##asm_editor", ImVec2(0, 0)); } -void AssemblyEditor::ChangeActiveFile(const std::string& filename) { +void AssemblyEditor::ChangeActiveFile(const std::string_view& filename) { current_file_ = filename; } +void AssemblyEditor::DrawFileView() { + ImGui::BeginTable("##table_view", 4, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg); + + // Table headers + ImGui::TableSetupColumn("Files", ImGuiTableColumnFlags_WidthFixed, 150.0f); + ImGui::TableSetupColumn("Line", ImGuiTableColumnFlags_WidthFixed, 60.0f); + ImGui::TableSetupColumn("Address", ImGuiTableColumnFlags_WidthFixed, 100.0f); + ImGui::TableSetupColumn("Editor", ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableHeadersRow(); + + // Table data + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + // TODO: Add tree view of files + + ImGui::TableNextColumn(); + // TODO: Add line number + + ImGui::TableNextColumn(); + // TODO: Add address per line + + ImGui::TableNextColumn(); + + auto cpos = text_editor_.GetCursorPosition(); + SetEditorText(); + ImGui::Text("%6d/%-6d %6d lines | %s | %s | %s | %s", cpos.mLine + 1, + cpos.mColumn + 1, text_editor_.GetTotalLines(), + text_editor_.IsOverwrite() ? "Ovr" : "Ins", + text_editor_.CanUndo() ? "*" : " ", + text_editor_.GetLanguageDefinition().mName.c_str(), + current_file_.c_str()); + + text_editor_.Render("##asm_editor"); + + ImGui::EndTable(); +} + void AssemblyEditor::DrawFileMenu() { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Open", "Ctrl+O")) { diff --git a/src/app/editor/assembly_editor.h b/src/app/editor/modules/assembly_editor.h similarity index 84% rename from src/app/editor/assembly_editor.h rename to src/app/editor/modules/assembly_editor.h index 63195aa8..aefdb111 100644 --- a/src/app/editor/assembly_editor.h +++ b/src/app/editor/modules/assembly_editor.h @@ -16,13 +16,16 @@ class AssemblyEditor { public: AssemblyEditor(); - void Update(); + void Update(bool &is_loaded); void InlineUpdate(); - void ChangeActiveFile(const std::string &); + void ChangeActiveFile(const std::string_view &); private: void DrawFileMenu(); void DrawEditMenu(); + + void DrawFileView(); + void SetEditorText(); bool file_is_loaded_ = false; diff --git a/src/app/editor/modules/gfx_group_editor.cc b/src/app/editor/modules/gfx_group_editor.cc new file mode 100644 index 00000000..789b2605 --- /dev/null +++ b/src/app/editor/modules/gfx_group_editor.cc @@ -0,0 +1,182 @@ +#include "gfx_group_editor.h" + +#include + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "app/core/editor.h" +#include "app/editor/modules/palette_editor.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/pipeline.h" +#include "app/gui/widgets.h" +#include "app/rom.h" +#include "app/zelda3/overworld.h" + +namespace yaze { +namespace app { +namespace editor { + +using ImGui::SameLine; +using ImGui::TableHeadersRow; +using ImGui::TableNextColumn; +using ImGui::TableNextRow; +using ImGui::TableSetupColumn; + +absl::Status GfxGroupEditor::Update() { + if (ImGui::BeginTabBar("GfxGroupEditor")) { + if (ImGui::BeginTabItem("Main")) { + gui::InputHexByte("Selected Blockset", &selected_blockset_); + ImGui::Text("Values"); + if (ImGui::BeginTable("##BlocksetTable", 2, ImGuiTableFlags_Borders, + ImVec2(0, 0))) { + TableSetupColumn("Inputs", ImGuiTableColumnFlags_WidthStretch, + ImGui::GetContentRegionAvail().x); + TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed, 256); + TableHeadersRow(); + TableNextRow(); + TableNextColumn(); + { + ImGui::BeginGroup(); + for (int i = 0; i < 8; i++) { + ImGui::SetNextItemWidth(100.f); + gui::InputHexByte(("##blockset0" + std::to_string(i)).c_str(), + &rom()->main_blockset_ids[selected_blockset_][i]); + if (i != 3 && i != 7) { + SameLine(); + } + } + ImGui::EndGroup(); + } + TableNextColumn(); + { + ImGui::BeginGroup(); + for (int i = 0; i < 8; i++) { + int sheet_id = rom()->main_blockset_ids[selected_blockset_][i]; + auto &sheet = *rom()->bitmap_manager()[sheet_id]; + if (sheet_id != last_sheet_id_) { + last_sheet_id_ = sheet_id; + auto palette_group = rom()->palette_group("ow_main"); + auto palette = palette_group[preview_palette_id_]; + sheet.ApplyPalette(palette); + rom()->UpdateBitmap(&sheet); + } + gui::BitmapCanvasPipeline(blockset_canvas_, sheet, 256, 0x10 * 0x04, + 0x20, true, false, 22); + } + ImGui::EndGroup(); + } + ImGui::EndTable(); + } + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Rooms")) { + gui::InputHexByte("Selected Blockset", &selected_roomset_); + + ImGui::Text("Values - Overwrites 4 of main blockset"); + if (ImGui::BeginTable("##Roomstable", 2, ImGuiTableFlags_Borders, + ImVec2(0, 0))) { + TableSetupColumn("Inputs", ImGuiTableColumnFlags_WidthStretch, + ImGui::GetContentRegionAvail().x); + TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed, 256); + TableHeadersRow(); + TableNextRow(); + TableNextColumn(); + { + ImGui::BeginGroup(); + for (int i = 0; i < 4; i++) { + ImGui::SetNextItemWidth(100.f); + gui::InputHexByte(("##roomset0" + std::to_string(i)).c_str(), + &rom()->room_blockset_ids[selected_roomset_][i]); + if (i != 3 && i != 7) { + SameLine(); + } + } + ImGui::EndGroup(); + } + TableNextColumn(); + { + ImGui::BeginGroup(); + for (int i = 0; i < 4; i++) { + int sheet_id = rom()->room_blockset_ids[selected_roomset_][i]; + auto &sheet = *rom()->bitmap_manager()[sheet_id]; + gui::BitmapCanvasPipeline(roomset_canvas_, sheet, 256, 0x10 * 0x04, + 0x20, true, false, 23); + } + ImGui::EndGroup(); + } + ImGui::EndTable(); + } + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Sprites")) { + gui::InputHexByte("Selected Spriteset", &selected_spriteset_); + + ImGui::Text("Values"); + if (ImGui::BeginTable("##SpritesTable", 2, ImGuiTableFlags_Borders, + ImVec2(0, 0))) { + TableSetupColumn("Inputs", ImGuiTableColumnFlags_WidthStretch, + ImGui::GetContentRegionAvail().x); + TableSetupColumn("Sheets", ImGuiTableColumnFlags_WidthFixed, 256); + TableHeadersRow(); + TableNextRow(); + TableNextColumn(); + { + ImGui::BeginGroup(); + for (int i = 0; i < 4; i++) { + ImGui::SetNextItemWidth(100.f); + gui::InputHexByte(("##spriteset0" + std::to_string(i)).c_str(), + &rom()->spriteset_ids[selected_spriteset_][i]); + if (i != 3 && i != 7) { + SameLine(); + } + } + ImGui::EndGroup(); + } + TableNextColumn(); + { + ImGui::BeginGroup(); + for (int i = 0; i < 4; i++) { + int sheet_id = rom()->spriteset_ids[selected_spriteset_][i]; + auto sheet = *rom()->bitmap_manager()[sheet_id]; + gui::BitmapCanvasPipeline(spriteset_canvas_, sheet, 256, + 0x10 * 0x04, 0x20, true, false, 24); + } + ImGui::EndGroup(); + } + ImGui::EndTable(); + } + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Palettes")) { + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + ImGui::Separator(); + ImGui::Text("Palette: "); + ImGui::InputInt("##PreviewPaletteID", &preview_palette_id_); + + return absl::OkStatus(); +} + +void GfxGroupEditor::InitBlockset(gfx::Bitmap tile16_blockset) { + tile16_blockset_bmp_ = tile16_blockset; +} + +} // namespace editor +} // namespace app +} // namespace yaze diff --git a/src/app/editor/modules/gfx_group_editor.h b/src/app/editor/modules/gfx_group_editor.h new file mode 100644 index 00000000..939ab8eb --- /dev/null +++ b/src/app/editor/modules/gfx_group_editor.h @@ -0,0 +1,57 @@ +#ifndef YAZE_APP_EDITOR_GFX_GROUP_EDITOR_H +#define YAZE_APP_EDITOR_GFX_GROUP_EDITOR_H + +#include + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "app/core/editor.h" +#include "app/gui/pipeline.h" +#include "app/editor/modules/palette_editor.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/widgets.h" +#include "app/rom.h" +#include "app/zelda3/overworld.h" + +namespace yaze { +namespace app { +namespace editor { + +class GfxGroupEditor : public SharedROM { + public: + absl::Status Update(); + + void InitBlockset(gfx::Bitmap tile16_blockset); + + private: + int preview_palette_id_ = 0; + int last_sheet_id_ = 0; + uint8_t selected_blockset_ = 0; + uint8_t selected_roomset_ = 0; + uint8_t selected_spriteset_ = 0; + + gui::Canvas blockset_canvas_; + gui::Canvas roomset_canvas_; + gui::Canvas spriteset_canvas_; + + gfx::SNESPalette palette_; + gfx::PaletteGroup palette_group_; + gfx::Bitmap tile16_blockset_bmp_; + + std::vector tile16_individual_data_; + std::vector tile16_individual_; + + gui::BitmapViewer gfx_group_viewer_; + zelda3::Overworld overworld_; +}; + +} // namespace editor +} // namespace app +} // namespace yaze +#endif // YAZE_APP_EDITOR_GFX_GROUP_EDITOR_H \ No newline at end of file diff --git a/src/app/editor/music_editor.cc b/src/app/editor/modules/music_editor.cc similarity index 81% rename from src/app/editor/music_editor.cc rename to src/app/editor/modules/music_editor.cc index a635f081..e29f2c7f 100644 --- a/src/app/editor/music_editor.cc +++ b/src/app/editor/modules/music_editor.cc @@ -4,13 +4,13 @@ #include #include "absl/strings/str_format.h" -#include "app/editor/assembly_editor.h" -#include "gui/canvas.h" -#include "gui/icons.h" -#include "gui/input.h" -#include "snes_spc/demo/demo_util.h" -#include "snes_spc/demo/wave_writer.h" -#include "snes_spc/snes_spc/spc.h" +#include "app/editor/modules/assembly_editor.h" +#include "app/gui/canvas.h" +#include "app/gui/icons.h" +#include "app/gui/input.h" +// #include "snes_spc/demo/demo_util.h" +// #include "snes_spc/demo/wave_writer.h" +// #include "snes_spc/snes_spc/spc.h" namespace yaze { namespace app { @@ -21,46 +21,46 @@ namespace { #define BUF_SIZE 2048 void PlaySPC() { - /* Create emulator and filter */ - SNES_SPC* snes_spc = spc_new(); - SPC_Filter* filter = spc_filter_new(); - if (!snes_spc || !filter) error("Out of memory"); + // /* Create emulator and filter */ + // SNES_SPC* snes_spc = spc_new(); + // SPC_Filter* filter = spc_filter_new(); + // if (!snes_spc || !filter) error("Out of memory"); - /* Load SPC */ - { - /* Load file into memory */ - long spc_size; - void* spc = load_file("assets/music/hyrule_field.spc", &spc_size); + // /* Load SPC */ + // { + // /* Load file into memory */ + // long spc_size; + // void* spc = load_file("assets/music/hyrule_field.spc", &spc_size); - /* Load SPC data into emulator */ - error(spc_load_spc(snes_spc, spc, spc_size)); - free(spc); /* emulator makes copy of data */ + // /* Load SPC data into emulator */ + // error(spc_load_spc(snes_spc, spc, spc_size)); + // free(spc); /* emulator makes copy of data */ - /* Most SPC files have garbage data in the echo buffer, so clear that */ - spc_clear_echo(snes_spc); + // /* Most SPC files have garbage data in the echo buffer, so clear that */ + // spc_clear_echo(snes_spc); - /* Clear filter before playing */ - spc_filter_clear(filter); - } + // /* Clear filter before playing */ + // spc_filter_clear(filter); + // } - /* Record 20 seconds to wave file */ - wave_open(spc_sample_rate, "out.wav"); - wave_enable_stereo(); - while (wave_sample_count() < 30 * spc_sample_rate * 2) { - /* Play into buffer */ - short buf[BUF_SIZE]; - error(spc_play(snes_spc, BUF_SIZE, buf)); + // /* Record 20 seconds to wave file */ + // wave_open(spc_sample_rate, "out.wav"); + // wave_enable_stereo(); + // while (wave_sample_count() < 30 * spc_sample_rate * 2) { + // /* Play into buffer */ + // short buf[BUF_SIZE]; + // error(spc_play(snes_spc, BUF_SIZE, buf)); - /* Filter samples */ - spc_filter_run(filter, buf, BUF_SIZE); + // /* Filter samples */ + // spc_filter_run(filter, buf, BUF_SIZE); - wave_write(buf, BUF_SIZE); - } + // wave_write(buf, BUF_SIZE); + // } - /* Cleanup */ - spc_filter_delete(filter); - spc_delete(snes_spc); - wave_close(); + // /* Cleanup */ + // spc_filter_delete(filter); + // spc_delete(snes_spc); + // wave_close(); } } // namespace @@ -223,10 +223,10 @@ void MusicEditor::DrawToolset() { if (is_playing) { if (!has_loaded_song) { - PlaySPC(); - current_song_ = Mix_LoadMUS("out.wav"); - Mix_PlayMusic(current_song_, -1); - has_loaded_song = true; + // PlaySPC(); + // current_song_ = Mix_LoadMUS("out.wav"); + // Mix_PlayMusic(current_song_, -1); + // has_loaded_song = true; } // // If there is no music playing @@ -252,11 +252,13 @@ void MusicEditor::DrawToolset() { ImGui::Combo("#songs_in_game", &selected_option, kGameSongs, 30); gui::ItemLabel("Controls: ", gui::ItemLabelFlags::Left); - if (ImGui::BeginTable("SongToolset", 5, toolset_table_flags_, ImVec2(0, 0))) { + if (ImGui::BeginTable("SongToolset", 6, toolset_table_flags_, ImVec2(0, 0))) { ImGui::TableSetupColumn("#play"); ImGui::TableSetupColumn("#rewind"); ImGui::TableSetupColumn("#fastforward"); ImGui::TableSetupColumn("#volume"); + ImGui::TableSetupColumn("#debug"); + ImGui::TableSetupColumn("#slider"); ImGui::TableNextColumn(); @@ -271,6 +273,9 @@ void MusicEditor::DrawToolset() { BUTTON_COLUMN(ICON_MD_FAST_REWIND) BUTTON_COLUMN(ICON_MD_FAST_FORWARD) BUTTON_COLUMN(ICON_MD_VOLUME_UP) + if (ImGui::Button(ICON_MD_ACCESS_TIME)) { + music_tracker_.LoadSongs(*rom()); + } ImGui::TableNextColumn(); ImGui::SliderInt("Volume", ¤t_volume, 0, 100); ImGui::EndTable(); diff --git a/src/app/editor/music_editor.h b/src/app/editor/modules/music_editor.h similarity index 87% rename from src/app/editor/music_editor.h rename to src/app/editor/modules/music_editor.h index 929d36ae..f860bfda 100644 --- a/src/app/editor/music_editor.h +++ b/src/app/editor/modules/music_editor.h @@ -5,13 +5,15 @@ #include #include "absl/strings/str_format.h" -#include "app/editor/assembly_editor.h" -#include "gui/canvas.h" -#include "gui/icons.h" -#include "gui/input.h" -#include "snes_spc/demo/demo_util.h" -#include "snes_spc/demo/wave_writer.h" -#include "snes_spc/snes_spc/spc.h" +#include "app/editor/modules/assembly_editor.h" +#include "app/gui/canvas.h" +#include "app/gui/icons.h" +#include "app/gui/input.h" +#include "app/rom.h" +#include "app/zelda3/music/tracker.h" +// #include "snes_spc/demo/demo_util.h" +// #include "snes_spc/demo/wave_writer.h" +// #include "snes_spc/snes_spc/spc.h" namespace yaze { namespace app { @@ -52,7 +54,7 @@ static const char* kGameSongs[] = {"Title", static constexpr absl::string_view kSongNotes[] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", "C"}; -class MusicEditor { +class MusicEditor : public SharedROM { public: void Update(); @@ -63,7 +65,9 @@ class MusicEditor { void DrawSongToolset(); void DrawToolset(); - Mix_Music* current_song_ = NULL; + zelda3::Tracker music_tracker_; + + // Mix_Music* current_song_ = NULL; AssemblyEditor assembly_editor_; ImGuiTableFlags toolset_table_flags_ = ImGuiTableFlags_SizingFixedFit; diff --git a/src/app/editor/modules/palette_editor.cc b/src/app/editor/modules/palette_editor.cc new file mode 100644 index 00000000..4f020668 --- /dev/null +++ b/src/app/editor/modules/palette_editor.cc @@ -0,0 +1,288 @@ +#include "palette_editor.h" + +#include + +#include "absl/status/status.h" +#include "app/gfx/snes_palette.h" +#include "app/gui/canvas.h" +#include "app/gui/color.h" +#include "app/gui/icons.h" + +static inline float ImSaturate(float f) { + return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : f; +} + +#define IM_F32_TO_INT8_SAT(_VAL) \ + ((int)(ImSaturate(_VAL) * 255.0f + 0.5f)) // Saturated, always output 0..255 + +int CustomFormatString(char* buf, size_t buf_size, const char* fmt, ...) { + va_list args; + va_start(args, fmt); +#ifdef IMGUI_USE_STB_SPRINTF + int w = stbsp_vsnprintf(buf, (int)buf_size, fmt, args); +#else + int w = vsnprintf(buf, buf_size, fmt, args); +#endif + va_end(args); + if (buf == nullptr) return w; + if (w == -1 || w >= (int)buf_size) w = (int)buf_size - 1; + buf[w] = 0; + return w; +} + +namespace yaze { +namespace app { +namespace editor { + +absl::Status PaletteEditor::Update() { + if (ImGui::BeginTable("paletteEditorTable", 2, + ImGuiTableFlags_Reorderable | + ImGuiTableFlags_Resizable | + ImGuiTableFlags_SizingStretchSame, + ImVec2(0, 0))) { + ImGui::TableSetupColumn("Palette Groups", + ImGuiTableColumnFlags_WidthStretch, + ImGui::GetContentRegionAvail().x); + ImGui::TableSetupColumn("Editor", ImGuiTableColumnFlags_WidthStretch, + ImGui::GetContentRegionAvail().x); + ImGui::TableHeadersRow(); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + for (int category = 0; category < kNumPalettes; ++category) { + if (ImGui::TreeNode(kPaletteCategoryNames[category].data())) { + status_ = DrawPaletteGroup(category); + ImGui::TreePop(); + } + } + ImGui::TableNextColumn(); + ImGui::Text("Test Column"); + ImGui::EndTable(); + } + + CLEAR_AND_RETURN_STATUS(status_) + + return absl::OkStatus(); +} + +void PaletteEditor::EditColorInPalette(gfx::SNESPalette& palette, int index) { + if (index >= palette.size()) { + // Handle error: the index is out of bounds + return; + } + + // Get the current color + auto currentColor = palette.GetColor(index).GetRGB(); + if (ImGui::ColorPicker4("Color Picker", (float*)&palette[index])) { + // The color was modified, update it in the palette + palette(index, currentColor); + } +} + +void PaletteEditor::ResetColorToOriginal( + gfx::SNESPalette& palette, int index, + const gfx::SNESPalette& originalPalette) { + if (index >= palette.size() || index >= originalPalette.size()) { + // Handle error: the index is out of bounds + return; + } + + auto originalColor = originalPalette.GetColor(index).GetRGB(); + palette(index, originalColor); +} + +absl::Status PaletteEditor::DrawPaletteGroup(int category) { + if (!rom()->isLoaded()) { + return absl::NotFoundError("ROM not open, no palettes to display"); + } + + const auto size = + rom()->palette_group(kPaletteGroupNames[category].data()).size(); + auto palettes = rom()->palette_group(kPaletteGroupNames[category].data()); + static bool edit_color = false; + for (int j = 0; j < size; j++) { + ImGui::Text("%d", j); + + auto palette = palettes[j]; + auto pal_size = palette.size(); + + for (int n = 0; n < pal_size; n++) { + ImGui::PushID(n); + if ((n % 8) != 0) ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y); + + auto popup_id = + absl::StrCat(kPaletteCategoryNames[category].data(), j, "_", n); + + // Small icon of the color in the palette + if (gui::SNESColorButton(popup_id, palette[n], palette_button_flags)) { + edit_color = true; + } + + if (ImGui::BeginPopupContextItem(popup_id.c_str())) { + RETURN_IF_ERROR(HandleColorPopup(palette, category, j, n)) + } + + if (edit_color) { + // The color button was clicked, open the popup + if (ImGui::ColorEdit4(popup_id.c_str(), + gfx::ToFloatArray(palette[n]).data(), + palette_button_flags)) { + EditColorInPalette(palette, n); + } + } + + ImGui::PopID(); + } + } + return absl::OkStatus(); +} + +absl::Status PaletteEditor::HandleColorPopup(gfx::SNESPalette& palette, int i, + int j, int n) { + auto col = gfx::ToFloatArray(palette[n]); + if (ImGui::ColorEdit4("Edit Color", col.data(), color_popup_flags)) { + RETURN_IF_ERROR(rom()->UpdatePaletteColor(kPaletteGroupNames[i].data(), j, + n, palette[n])) + } + + if (ImGui::Button("Copy as..", ImVec2(-1, 0))) ImGui::OpenPopup("Copy"); + + if (ImGui::BeginPopup("Copy")) { + int cr = IM_F32_TO_INT8_SAT(col[0]); + int cg = IM_F32_TO_INT8_SAT(col[1]); + int cb = IM_F32_TO_INT8_SAT(col[2]); + char buf[64]; + + CustomFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff)", col[0], + col[1], col[2]); + + if (ImGui::Selectable(buf)) ImGui::SetClipboardText(buf); + CustomFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d)", cr, cg, cb); + if (ImGui::Selectable(buf)) ImGui::SetClipboardText(buf); + CustomFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", cr, cg, cb); + if (ImGui::Selectable(buf)) ImGui::SetClipboardText(buf); + ImGui::EndPopup(); + } + + ImGui::EndPopup(); + return absl::OkStatus(); +} + +void PaletteEditor::DisplayPalette(gfx::SNESPalette& palette, bool loaded) { + static ImVec4 color = ImVec4(0, 0, 0, 255.f); + ImGuiColorEditFlags misc_flags = ImGuiColorEditFlags_AlphaPreview | + ImGuiColorEditFlags_NoDragDrop | + ImGuiColorEditFlags_NoOptions; + + // Generate a default palette. The palette will persist and can be edited. + static bool init = false; + if (loaded && !init) { + InitializeSavedPalette(palette); + init = true; + } + + static ImVec4 backup_color; + bool open_popup = ImGui::ColorButton("MyColor##3b", color, misc_flags); + ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); + open_popup |= ImGui::Button("Palette"); + if (open_popup) { + ImGui::OpenPopup("mypicker"); + backup_color = color; + } + + if (ImGui::BeginPopup("mypicker")) { + TEXT_WITH_SEPARATOR("Current Overworld Palette"); + ImGui::ColorPicker4("##picker", (float*)&color, + misc_flags | ImGuiColorEditFlags_NoSidePreview | + ImGuiColorEditFlags_NoSmallPreview); + ImGui::SameLine(); + + ImGui::BeginGroup(); // Lock X position + ImGui::Text("Current ==>"); + ImGui::SameLine(); + ImGui::Text("Previous"); + + if (ImGui::Button("Update Map Palette")) { + } + + ImGui::ColorButton( + "##current", color, + ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_AlphaPreviewHalf, + ImVec2(60, 40)); + ImGui::SameLine(); + + if (ImGui::ColorButton( + "##previous", backup_color, + ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_AlphaPreviewHalf, + ImVec2(60, 40))) + color = backup_color; + + // List of Colors in Overworld Palette + ImGui::Separator(); + ImGui::Text("Palette"); + for (int n = 0; n < IM_ARRAYSIZE(saved_palette_); n++) { + ImGui::PushID(n); + if ((n % 8) != 0) ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y); + + if (ImGui::ColorButton("##palette", saved_palette_[n], + palette_button_flags_2, ImVec2(20, 20))) + color = ImVec4(saved_palette_[n].x, saved_palette_[n].y, + saved_palette_[n].z, color.w); // Preserve alpha! + + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = + ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) + memcpy((float*)&saved_palette_[n], payload->Data, sizeof(float) * 3); + if (const ImGuiPayload* payload = + ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) + memcpy((float*)&saved_palette_[n], payload->Data, sizeof(float) * 4); + ImGui::EndDragDropTarget(); + } + + ImGui::PopID(); + } + ImGui::EndGroup(); + ImGui::EndPopup(); + } +} + +void PaletteEditor::DrawPortablePalette(gfx::SNESPalette& palette) { + static bool init = false; + if (!init) { + InitializeSavedPalette(palette); + init = true; + } + + if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)100); + ImGui::BeginChild(child_id, ImGui::GetContentRegionAvail(), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + ImGui::BeginGroup(); // Lock X position + ImGui::Text("Palette"); + for (int n = 0; n < IM_ARRAYSIZE(saved_palette_); n++) { + ImGui::PushID(n); + if ((n % 8) != 0) ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y); + + if (ImGui::ColorButton("##palette", saved_palette_[n], + palette_button_flags_2, ImVec2(20, 20))) + ImVec4(saved_palette_[n].x, saved_palette_[n].y, saved_palette_[n].z, + 1.0f); // Preserve alpha! + + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = + ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) + memcpy((float*)&saved_palette_[n], payload->Data, sizeof(float) * 3); + if (const ImGuiPayload* payload = + ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) + memcpy((float*)&saved_palette_[n], payload->Data, sizeof(float) * 4); + ImGui::EndDragDropTarget(); + } + + ImGui::PopID(); + } + ImGui::EndGroup(); + } + ImGui::EndChild(); +} + +} // namespace editor +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/editor/modules/palette_editor.h b/src/app/editor/modules/palette_editor.h new file mode 100644 index 00000000..dcd263ad --- /dev/null +++ b/src/app/editor/modules/palette_editor.h @@ -0,0 +1,122 @@ +#ifndef YAZE_APP_EDITOR_PALETTE_EDITOR_H +#define YAZE_APP_EDITOR_PALETTE_EDITOR_H + +#include + +#include "absl/status/status.h" +#include "app/gfx/snes_palette.h" +#include "app/gui/canvas.h" +#include "app/gui/icons.h" +#include "app/rom.h" + +namespace yaze { +namespace app { +namespace editor { + +constexpr int kNumPalettes = 11; + +static constexpr absl::string_view kPaletteCategoryNames[] = { + "Sword", "Shield", "Clothes", "World Colors", + "Area Colors", "Enemies", "Dungeons", "World Map", + "Dungeon Map", "Triforce", "Crystal"}; + +static constexpr absl::string_view kPaletteGroupNames[] = { + "swords", "shields", "armors", "ow_main", + "ow_aux", "global_sprites", "dungeon_main", "ow_mini_map", + "ow_mini_map", "3d_object", "3d_object"}; + +struct PaletteChange { + std::string groupName; + size_t paletteIndex; + size_t colorIndex; + gfx::SNESColor originalColor; + gfx::SNESColor newColor; +}; + +class PaletteEditorHistory { + public: + // Record a change in the palette editor + void RecordChange(const std::string& groupName, size_t paletteIndex, + size_t colorIndex, const gfx::SNESColor& originalColor, + const gfx::SNESColor& newColor) { + // Check size and remove the oldest if necessary + if (recentChanges.size() >= maxHistorySize) { + recentChanges.pop_front(); + } + + // Push the new change + recentChanges.push_back( + {groupName, paletteIndex, colorIndex, originalColor, newColor}); + } + + // Get recent changes for display in the palette editor + const std::deque& GetRecentChanges() const { + return recentChanges; + } + + // Restore the original color + gfx::SNESColor GetOriginalColor(const std::string& groupName, + size_t paletteIndex, + size_t colorIndex) const { + for (const auto& change : recentChanges) { + if (change.groupName == groupName && + change.paletteIndex == paletteIndex && + change.colorIndex == colorIndex) { + return change.originalColor; + } + } + // Handle error or return default (this is just an example, + // handle as appropriate for your application) + return gfx::SNESColor(); + } + + private: + std::deque recentChanges; + static const size_t maxHistorySize = 50; // or any other number you deem fit +}; + +class PaletteEditor : public SharedROM { + public: + absl::Status Update(); + absl::Status DrawPaletteGroups(); + + void EditColorInPalette(gfx::SNESPalette& palette, int index); + void ResetColorToOriginal(gfx::SNESPalette& palette, int index, + const gfx::SNESPalette& originalPalette); + void DisplayPalette(gfx::SNESPalette& palette, bool loaded); + void DrawPortablePalette(gfx::SNESPalette& palette); + + private: + absl::Status DrawPaletteGroup(int category); + absl::Status HandleColorPopup(gfx::SNESPalette& palette, int i, int j, int n); + + void InitializeSavedPalette(const gfx::SNESPalette& palette) { + for (int n = 0; n < palette.size(); n++) { + saved_palette_[n].x = palette.GetColor(n).GetRGB().x / 255; + saved_palette_[n].y = palette.GetColor(n).GetRGB().y / 255; + saved_palette_[n].z = palette.GetColor(n).GetRGB().z / 255; + saved_palette_[n].w = 255; // Alpha + } + } + + absl::Status status_; + + PaletteEditorHistory history_; + + ImVec4 saved_palette_[256] = {}; + ImVec4 current_color_; + + ImGuiColorEditFlags color_popup_flags = + ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoAlpha; + ImGuiColorEditFlags palette_button_flags = + ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoTooltip; + ImGuiColorEditFlags palette_button_flags_2 = ImGuiColorEditFlags_NoAlpha | + ImGuiColorEditFlags_NoPicker | + ImGuiColorEditFlags_NoTooltip; +}; + +} // namespace editor +} // namespace app +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/app/editor/modules/tile16_editor.cc b/src/app/editor/modules/tile16_editor.cc new file mode 100644 index 00000000..08ff7dbb --- /dev/null +++ b/src/app/editor/modules/tile16_editor.cc @@ -0,0 +1,298 @@ +#include "tile16_editor.h" + +#include + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "app/core/editor.h" +#include "app/editor/modules/palette_editor.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/pipeline.h" +#include "app/rom.h" +#include "app/zelda3/overworld.h" + +namespace yaze { +namespace app { +namespace editor { + +using ImGui::BeginChild; +using ImGui::BeginTabBar; +using ImGui::BeginTabItem; +using ImGui::BeginTable; +using ImGui::Combo; +using ImGui::EndChild; +using ImGui::EndTabBar; +using ImGui::EndTabItem; +using ImGui::TableHeadersRow; +using ImGui::TableNextColumn; +using ImGui::TableNextRow; +using ImGui::TableSetupColumn; + +absl::Status Tile16Editor::Update() { + // Create a tab for Tile16 Editing + static bool start_task = false; + if (ImGui::Button("Test")) { + start_task = true; + } + + if (start_task && !map_blockset_loaded_) { + LoadTile8(); + } + + // Create a tab bar for Tile16 Editing and Tile16 Transfer + if (BeginTabBar("Tile16 Editor Tabs")) { + if (BeginTabItem("Tile16 Editing")) { + if (BeginTable("#Tile16EditorTable", 2, TABLE_BORDERS_RESIZABLE, + ImVec2(0, 0))) { + TableSetupColumn("Tiles", ImGuiTableColumnFlags_WidthFixed, + ImGui::GetContentRegionAvail().x); + TableSetupColumn("Properties", ImGuiTableColumnFlags_WidthStretch, + ImGui::GetContentRegionAvail().x); + TableHeadersRow(); + TableNextRow(); + TableNextColumn(); + RETURN_IF_ERROR(UpdateBlockset()); + + TableNextColumn(); + RETURN_IF_ERROR(UpdateTile16Edit()); + + ImGui::EndTable(); + } + + ImGui::EndTabItem(); + } + + // Create a tab for Tile16 Transfer + if (BeginTabItem("Tile16 Transfer")) { + if (BeginTable("#Tile16TransferTable", 2, + ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable, + ImVec2(0, 0))) { + TableSetupColumn("Current ROM Tiles", ImGuiTableColumnFlags_WidthFixed, + ImGui::GetContentRegionAvail().x / 2); + TableSetupColumn("Transfer ROM Tiles", ImGuiTableColumnFlags_WidthFixed, + ImGui::GetContentRegionAvail().x / 2); + TableHeadersRow(); + TableNextRow(); + + TableNextColumn(); + RETURN_IF_ERROR(UpdateBlockset()); + + TableNextColumn(); + RETURN_IF_ERROR(UpdateTransferTileCanvas()); + + ImGui::EndTable(); + } + + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + return absl::OkStatus(); +} + +absl::Status Tile16Editor::UpdateBlockset() { + gui::BitmapCanvasPipeline(blockset_canvas_, tile16_blockset_bmp_, 0x100, + (8192 * 2), 0x20, map_blockset_loaded_, true, 55); + + if (!blockset_canvas_.Points().empty()) { + uint16_t x = blockset_canvas_.Points().front().x / 32; + uint16_t y = blockset_canvas_.Points().front().y / 32; + + notify_tile16.mutable_get() = x + (y * 8); + notify_tile16.apply_changes(); + if (notify_tile16.modified()) { + current_tile16_bmp_ = tile16_individual_[notify_tile16]; + current_tile16_bmp_.ApplyPalette( + rom()->palette_group("ow_main")[current_palette_]); + rom()->RenderBitmap(¤t_tile16_bmp_); + } + } + + return absl::OkStatus(); +} + +absl::Status Tile16Editor::UpdateTile16Edit() { + if (ImGui::BeginChild("Tile8 Selector", + ImVec2(ImGui::GetContentRegionAvail().x, 0x100), + true)) { + tile8_source_canvas_.DrawBackground( + ImVec2(core::kTilesheetWidth * 2, core::kTilesheetHeight * 0x10 * 2)); + tile8_source_canvas_.DrawContextMenu(); + tile8_source_canvas_.DrawTileSelector(16); + tile8_source_canvas_.DrawBitmap(current_gfx_bmp_, 0, 0, 2.0f); + tile8_source_canvas_.DrawGrid(16.0f); + tile8_source_canvas_.DrawOverlay(); + } + ImGui::EndChild(); + + if (ImGui::BeginChild("Tile16 Editor Options", + ImVec2(ImGui::GetContentRegionAvail().x, 0x50), true)) { + tile16_edit_canvas_.DrawBackground(ImVec2(0x40, 0x40)); + tile16_edit_canvas_.DrawContextMenu(); + // if (current_tile8_bmp_.modified()) { + // rom()->UpdateBitmap(¤t_tile8_bmp_); + // current_tile8_bmp_.set_modified(false); + // } + tile16_edit_canvas_.DrawBitmap(current_tile16_bmp_, 0, 0, 4.0f); + tile16_edit_canvas_.HandleTileEdits( + tile8_source_canvas_, current_gfx_individual_, current_tile8_bmp_, + current_tile8_, 2.0f); + + tile16_edit_canvas_.DrawGrid(128.0f); + tile16_edit_canvas_.DrawOverlay(); + } + ImGui::EndChild(); + DrawTileEditControls(); + + return absl::OkStatus(); +} + +void Tile16Editor::DrawTileEditControls() { + ImGui::Separator(); + ImGui::Text("Options:"); + gui::InputHexByte("Palette", ¬ify_palette.mutable_get()); + notify_palette.apply_changes(); + if (notify_palette.modified()) { + current_gfx_bmp_.ApplyPalette( + rom()->palette_group("ow_main")[notify_palette.get()]); + current_tile16_bmp_.ApplyPalette( + rom()->palette_group("ow_main")[notify_palette.get()]); + rom()->UpdateBitmap(¤t_gfx_bmp_); + } + + ImGui::Checkbox("X Flip", &x_flip); + ImGui::Checkbox("Y Flip", &y_flip); + ImGui::Checkbox("Priority Tile", &priority_tile); +} + +absl::Status Tile16Editor::UpdateTransferTileCanvas() { + // Create a button for loading another ROM + if (ImGui::Button("Load ROM")) { + ImGuiFileDialog::Instance()->OpenDialog( + "ChooseTransferFileDlgKey", "Open Transfer ROM", ".sfc,.smc", "."); + } + gui::FileDialogPipeline( + "ChooseTransferFileDlgKey", ".sfc,.smc", std::nullopt, [&]() { + std::string filePathName = + ImGuiFileDialog::Instance()->GetFilePathName(); + transfer_status_ = transfer_rom_.LoadFromFile(filePathName); + transfer_started_ = true; + }); + + if (transfer_started_ && !transfer_blockset_loaded_) { + PRINT_IF_ERROR(transfer_rom_.LoadAllGraphicsData()) + graphics_bin_ = transfer_rom_.graphics_bin(); + + // Load the Link to the Past overworld. + PRINT_IF_ERROR(transfer_overworld_.Load(transfer_rom_)) + transfer_overworld_.SetCurrentMap(0); + palette_ = transfer_overworld_.AreaPalette(); + + // Create the tile16 blockset image + gui::BuildAndRenderBitmapPipeline(0x80, 0x2000, 0x80, + transfer_overworld_.Tile16Blockset(), + *rom(), transfer_blockset_bmp_, palette_); + transfer_blockset_loaded_ = true; + } + + // Create a canvas for holding the tiles which will be exported + gui::BitmapCanvasPipeline(transfer_canvas_, transfer_blockset_bmp_, 0x100, + (8192 * 2), 0x20, transfer_blockset_loaded_, true, + 3); + + return absl::OkStatus(); +} + +using core::TaskManager; + +absl::Status Tile16Editor::InitBlockset( + const gfx::Bitmap& tile16_blockset_bmp, gfx::Bitmap current_gfx_bmp, + const std::vector& tile16_individual) { + tile16_blockset_bmp_ = tile16_blockset_bmp; + tile16_individual_ = tile16_individual; + current_gfx_bmp_ = current_gfx_bmp; + + return absl::OkStatus(); +} + +absl::Status Tile16Editor::LoadTile8() { + current_gfx_individual_.reserve(128); + + // Define the task function + std::function taskFunc = [&](int index) { + auto current_gfx_data = current_gfx_bmp_.mutable_data(); + std::vector tile_data; + tile_data.reserve(0x40); + for (int i = 0; i < 0x40; i++) { + tile_data.emplace_back(0x00); + } + + // Copy the pixel data for the current tile into the vector + for (int ty = 0; ty < 8; ty++) { + for (int tx = 0; tx < 8; tx++) { + int position = tx + (ty * 0x08); + uint8_t value = + current_gfx_data[(index % 16 * 32) + (index / 16 * 32 * 0x80) + + (ty * 0x80) + tx]; + tile_data[position] = value; + } + } + + current_gfx_individual_.emplace_back(); + current_gfx_individual_[index].Create(0x08, 0x08, 0x80, tile_data); + current_gfx_individual_[index].ApplyPalette( + rom()->palette_group("ow_main")[current_palette_]); + rom()->RenderBitmap(¤t_gfx_individual_[index]); + }; + + // Create the task manager + static bool started = false; + if (!started) { + task_manager_ = TaskManager>(127, 1); + started = true; + } + task_manager_.ExecuteTasks(taskFunc); + + if (task_manager_.IsTaskComplete()) { + // All tasks are complete + current_tile8_bmp_ = current_gfx_individual_[0]; + map_blockset_loaded_ = true; + } + + // auto current_gfx_data = current_gfx_bmp_.mutable_data(); + // for (int i = 0; i < 128; i++) { + // std::vector tile_data(0x40, 0x00); + + // Copy the pixel data for the current tile into the vector + // for (int ty = 0; ty < 8; ty++) { + // for (int tx = 0; tx < 8; tx++) { + // int position = tx + (ty * 0x10); + // uint8_t value = current_gfx_data[(i % 16 * 32) + (i / 16 * 32 * 0x80) + // + + // (ty * 0x80) + tx]; + // tile_data[position] = value; + // } + // } + + // current_gfx_individual_data_.emplace_back(tile_data); + // current_gfx_individual_.emplace_back(); + // current_gfx_individual_[i].Create(0x08, 0x08, 0x80, tile_data); + // current_gfx_individual_[i].ApplyPalette( + // rom()->palette_group("ow_main")[current_palette_]); + // rom()->RenderBitmap(¤t_gfx_individual_[i]); + // } + return absl::OkStatus(); +} + +} // namespace editor +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/editor/modules/tile16_editor.h b/src/app/editor/modules/tile16_editor.h new file mode 100644 index 00000000..81beb8da --- /dev/null +++ b/src/app/editor/modules/tile16_editor.h @@ -0,0 +1,107 @@ +#ifndef YAZE_APP_EDITOR_TILE16EDITOR_H +#define YAZE_APP_EDITOR_TILE16EDITOR_H + +#include + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "app/core/editor.h" +#include "app/gui/pipeline.h" +#include "app/editor/modules/palette_editor.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/rom.h" +#include "app/zelda3/overworld.h" + +namespace yaze { +namespace app { +namespace editor { + +class Tile16Editor : public SharedROM { + public: + absl::Status Update(); + + absl::Status UpdateBlockset(); + absl::Status UpdateTile16Edit(); + + void DrawTileEditControls(); + + absl::Status UpdateTransferTileCanvas(); + + absl::Status InitBlockset(const gfx::Bitmap& tile16_blockset_bmp, + gfx::Bitmap current_gfx_bmp, + const std::vector& tile16_individual); + + absl::Status LoadTile8(); + + private: + bool map_blockset_loaded_ = false; + bool transfer_started_ = false; + bool transfer_blockset_loaded_ = false; + + int current_tile16_ = 0; + int current_tile8_ = 0; + uint8_t current_palette_ = 0; + + core::NotifyValue notify_tile16; + core::NotifyValue notify_palette; + + // Canvas dimensions + int canvas_width; + int canvas_height; + + // Texture ID for the canvas + int texture_id; + + // Various options for the Tile16 Editor + bool x_flip; + bool y_flip; + bool priority_tile; + int tile_size; + + // Tile16 blockset for selecting the tile to edit + gui::Canvas blockset_canvas_; + gfx::Bitmap tile16_blockset_bmp_; + + // Canvas for editing the selected tile + gui::Canvas tile16_edit_canvas_; + gfx::Bitmap current_tile16_bmp_; + gfx::Bitmap current_tile8_bmp_; + + // Tile8 canvas to get the tile to drawing in the tile16_edit_canvas_ + gui::Canvas tile8_source_canvas_; + gfx::Bitmap current_gfx_bmp_; + + gui::Canvas transfer_canvas_; + gfx::Bitmap transfer_blockset_bmp_; + gfx::Bitmap transfer_current_bmp_; + + std::vector tile16_individual_data_; + std::vector tile16_individual_; + + std::vector current_gfx_individual_; + + std::vector current_tile16_data_; + + PaletteEditor palette_editor_; + + gfx::SNESPalette palette_; + zelda3::Overworld transfer_overworld_; + + gfx::BitmapTable graphics_bin_; + + ROM transfer_rom_; + absl::Status transfer_status_; + + core::TaskManager> task_manager_; +}; + +} // namespace editor +} // namespace app +} // namespace yaze +#endif // YAZE_APP_EDITOR_TILE16EDITOR_H \ No newline at end of file diff --git a/src/app/editor/overworld_editor.cc b/src/app/editor/overworld_editor.cc index f37c78f7..f11c115e 100644 --- a/src/app/editor/overworld_editor.cc +++ b/src/app/editor/overworld_editor.cc @@ -9,143 +9,312 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" -#include "app/editor/palette_editor.h" +#include "app/core/common.h" +#include "app/editor/modules/palette_editor.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/pipeline.h" +#include "app/gui/style.h" +#include "app/gui/widgets.h" #include "app/rom.h" #include "app/zelda3/overworld.h" -#include "gui/canvas.h" -#include "gui/icons.h" namespace yaze { namespace app { namespace editor { -// ---------------------------------------------------------------------------- +using ImGui::Button; +using ImGui::Separator; +using ImGui::TableHeadersRow; +using ImGui::TableNextColumn; +using ImGui::TableNextRow; +using ImGui::TableSetupColumn; +using ImGui::Text; absl::Status OverworldEditor::Update() { - // Initialize overworld graphics, maps, and palettes - if (rom_.isLoaded() && !all_gfx_loaded_) { - RETURN_IF_ERROR(LoadGraphics()) + if (rom()->isLoaded() && !all_gfx_loaded_) { + RETURN_IF_ERROR(tile16_editor_.InitBlockset( + tile16_blockset_bmp_, current_gfx_bmp_, tile16_individual_)); + gfx_group_editor_.InitBlockset(tile16_blockset_bmp_); all_gfx_loaded_ = true; + } else if (!rom()->isLoaded() && all_gfx_loaded_) { + // TODO: Destroy the overworld graphics canvas. + // Reset the editor if the ROM is unloaded + Shutdown(); + all_gfx_loaded_ = false; + map_blockset_loaded_ = false; } // Draws the toolset for editing the Overworld. RETURN_IF_ERROR(DrawToolset()) - ImGui::Separator(); - if (ImGui::BeginTable("#owEditTable", 2, ow_edit_flags, ImVec2(0, 0))) { - ImGui::TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch, - ImGui::GetContentRegionAvail().x); - ImGui::TableSetupColumn("Tile Selector"); - ImGui::TableHeadersRow(); - ImGui::TableNextRow(); - ImGui::TableNextColumn(); + if (ImGui::BeginTable(kOWEditTable.data(), 2, kOWEditFlags, ImVec2(0, 0))) { + TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch, + ImGui::GetContentRegionAvail().x); + TableSetupColumn("Tile Selector", ImGuiTableColumnFlags_WidthFixed, 256); + TableHeadersRow(); + TableNextRow(); + TableNextColumn(); DrawOverworldCanvas(); - ImGui::TableNextColumn(); + TableNextColumn(); DrawTileSelector(); ImGui::EndTable(); } + if (!status_.ok()) { + auto temp = status_; + status_ = absl::OkStatus(); + return temp; + } + return absl::OkStatus(); } // ---------------------------------------------------------------------------- absl::Status OverworldEditor::DrawToolset() { - if (ImGui::BeginTable("OWToolset", 17, toolset_table_flags, ImVec2(0, 0))) { + static bool show_gfx_group = false; + static bool show_tile16 = false; + + if (ImGui::BeginTable("OWToolset", 19, kToolsetTableFlags, ImVec2(0, 0))) { for (const auto &name : kToolsetColumnNames) ImGui::TableSetupColumn(name.data()); - BUTTON_COLUMN(ICON_MD_UNDO) // Undo - BUTTON_COLUMN(ICON_MD_REDO) // Redo - TEXT_COLUMN(ICON_MD_MORE_VERT) // Separator - BUTTON_COLUMN(ICON_MD_ZOOM_OUT) // Zoom Out - BUTTON_COLUMN(ICON_MD_ZOOM_IN) // Zoom In - TEXT_COLUMN(ICON_MD_MORE_VERT) // Separator - BUTTON_COLUMN(ICON_MD_DRAW) // Draw Tile - BUTTON_COLUMN(ICON_MD_DOOR_FRONT) // Entrances - BUTTON_COLUMN(ICON_MD_DOOR_BACK) // Exits - BUTTON_COLUMN(ICON_MD_GRASS) // Items - BUTTON_COLUMN(ICON_MD_PEST_CONTROL_RODENT) // Sprites - BUTTON_COLUMN(ICON_MD_ADD_LOCATION) // Transports - BUTTON_COLUMN(ICON_MD_MUSIC_NOTE) // Music - TEXT_COLUMN(ICON_MD_MORE_VERT) // Separator - ImGui::TableNextColumn(); // Palette + NEXT_COLUMN() + if (ImGui::Button(ICON_MD_UNDO)) { + status_ = Undo(); + } + + NEXT_COLUMN() + if (ImGui::Button(ICON_MD_REDO)) { + status_ = Redo(); + } + + TEXT_COLUMN(ICON_MD_MORE_VERT) // Separator + + NEXT_COLUMN() + if (ImGui::Button(ICON_MD_ZOOM_OUT)) { + ow_map_canvas_.ZoomOut(); + } + + NEXT_COLUMN() + if (ImGui::Button(ICON_MD_ZOOM_IN)) { + ow_map_canvas_.ZoomIn(); + } + + TEXT_COLUMN(ICON_MD_MORE_VERT) // Separator + + NEXT_COLUMN() + if (ImGui::Button(ICON_MD_DRAW)) { + current_mode = EditingMode::DRAW_TILE; + } + HOVER_HINT("Draw Tile") + + NEXT_COLUMN() + if (ImGui::Button(ICON_MD_DOOR_FRONT)) { + current_mode = EditingMode::ENTRANCES; + } + HOVER_HINT("Entrances") + + NEXT_COLUMN() + if (ImGui::Button(ICON_MD_DOOR_BACK)) { + current_mode = EditingMode::EXITS; + } + HOVER_HINT("Exits") + + NEXT_COLUMN() + if (ImGui::Button(ICON_MD_GRASS)) { + current_mode = EditingMode::ITEMS; + } + HOVER_HINT("Items") + + NEXT_COLUMN() + if (ImGui::Button(ICON_MD_PEST_CONTROL_RODENT)) { + current_mode = EditingMode::SPRITES; + } + HOVER_HINT("Sprites") + + NEXT_COLUMN() + if (ImGui::Button(ICON_MD_ADD_LOCATION)) { + current_mode = EditingMode::TRANSPORTS; + } + HOVER_HINT("Transports") + + NEXT_COLUMN() + if (ImGui::Button(ICON_MD_MUSIC_NOTE)) { + current_mode = EditingMode::MUSIC; + } + HOVER_HINT("Music") + + TableNextColumn(); + if (ImGui::Button(ICON_MD_GRID_VIEW)) { + show_tile16 = !show_tile16; + } + + TableNextColumn(); + if (ImGui::Button(ICON_MD_TABLE_CHART)) { + show_gfx_group = !show_gfx_group; + } + + TEXT_COLUMN(ICON_MD_MORE_VERT) // Separator + + TableNextColumn(); // Palette palette_editor_.DisplayPalette(palette_, overworld_.isLoaded()); + TEXT_COLUMN(ICON_MD_MORE_VERT) // Separator + + TableNextColumn(); // Experimental + ImGui::Checkbox("Experimental", &show_experimental); + ImGui::EndTable(); } + + if (show_experimental) { + RETURN_IF_ERROR(DrawExperimentalModal()) + } + + if (show_tile16) { + // Create a table in ImGui for the Tile16 Editor + ImGui::Begin("Tile16 Editor", &show_tile16); + RETURN_IF_ERROR(tile16_editor_.Update()) + ImGui::End(); + } + + if (show_gfx_group) { + ImGui::Begin("Gfx Group Editor", &show_gfx_group); + RETURN_IF_ERROR(gfx_group_editor_.Update()) + ImGui::End(); + } return absl::OkStatus(); } // ---------------------------------------------------------------------------- void OverworldEditor::DrawOverworldMapSettings() { - if (ImGui::BeginTable("#mapSettings", 8, ow_map_flags, ImVec2(0, 0), -1)) { - for (const auto &name : kOverworldSettingsColumnNames) - ImGui::TableSetupColumn(name.data()); + if (ImGui::BeginTable(kOWMapTable.data(), 7, kOWMapFlags, ImVec2(0, 0), -1)) { + for (const auto &name : {"##1stCol", "##gfxCol", "##palCol", "##sprgfxCol", + "##sprpalCol", "##msgidCol", "##2ndCol"}) + ImGui::TableSetupColumn(name); - ImGui::TableNextColumn(); - ImGui::SetNextItemWidth(50.f); - ImGui::InputInt("Current Map", ¤t_map_); + TableNextColumn(); + ImGui::SetNextItemWidth(120.f); + ImGui::Combo("##world", ¤t_world_, kWorldList.data(), 3); - ImGui::TableNextColumn(); + TableNextColumn(); + gui::InputHexByte( + "Gfx", + overworld_.mutable_overworld_map(current_map_)->mutable_area_graphics(), + kInputFieldSize); + + TableNextColumn(); + gui::InputHexByte( + "Palette", + overworld_.mutable_overworld_map(current_map_)->mutable_area_palette(), + kInputFieldSize); + + TableNextColumn(); + gui::InputHexByte("Spr Gfx", + overworld_.mutable_overworld_map(current_map_) + ->mutable_sprite_graphics(game_state_), + kInputFieldSize); + + TableNextColumn(); + gui::InputHexByte("Spr Palette", + overworld_.mutable_overworld_map(current_map_) + ->mutable_sprite_palette(game_state_), + kInputFieldSize); + + TableNextColumn(); + gui::InputHexByte( + "Msg Id", + overworld_.mutable_overworld_map(current_map_)->mutable_message_id(), + kInputFieldSize); + + TableNextColumn(); ImGui::SetNextItemWidth(100.f); - ImGui::Combo("##world", ¤t_world_, - "Light World\0Dark World\0Extra World\0"); + ImGui::Combo("##World", &game_state_, kGamePartComboString.data(), 3); - ImGui::TableNextColumn(); - ImGui::Text("GFX"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(kInputFieldSize); - ImGui::InputText("##mapGFX", map_gfx_, kByteSize); - - ImGui::TableNextColumn(); - ImGui::Text("Palette"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(kInputFieldSize); - ImGui::InputText("##mapPal", map_palette_, kByteSize); - - ImGui::TableNextColumn(); - ImGui::Text("Spr GFX"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(kInputFieldSize); - ImGui::InputText("##sprGFX", spr_gfx_, kByteSize); - - ImGui::TableNextColumn(); - ImGui::Text("Spr Palette"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(kInputFieldSize); - ImGui::InputText("##sprPal", spr_palette_, kByteSize); - - ImGui::TableNextColumn(); - ImGui::Text("Msg ID"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(50.f); - ImGui::InputText("##msgid", spr_palette_, kMessageIdSize); - - ImGui::TableNextColumn(); - ImGui::Checkbox("Show grid", &opt_enable_grid); // TODO ImGui::EndTable(); } } // ---------------------------------------------------------------------------- -void OverworldEditor::DrawOverworldEntrances() { - for (const auto &each : overworld_.Entrances()) { +void OverworldEditor::DrawOverworldEntrances(ImVec2 canvas_p0, + ImVec2 scrolling) { + for (auto &each : overworld_.Entrances()) { if (each.map_id_ < 0x40 + (current_world_ * 0x40) && each.map_id_ >= (current_world_ * 0x40)) { ow_map_canvas_.DrawRect(each.x_, each.y_, 16, 16, ImVec4(210, 24, 210, 150)); std::string str = absl::StrFormat("%#x", each.entrance_id_); ow_map_canvas_.DrawText(str, each.x_ - 4, each.y_ - 2); + + // Check if this entrance is being clicked and dragged + if (IsMouseHoveringOverEntrance(each, canvas_p0, scrolling) && + ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { + dragged_entrance_ = &each; + is_dragging_entrance_ = true; + if (ImGui::BeginDragDropSource()) { + ImGui::SetDragDropPayload("ENTRANCE_PAYLOAD", &each, + sizeof(zelda3::OverworldEntrance)); + Text("Moving Entrance ID: %s", str.c_str()); + ImGui::EndDragDropSource(); + } + } else if (is_dragging_entrance_ && dragged_entrance_ == &each && + ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + // Adjust the X and Y position of the entrance rectangle based on the + // mouse position when it is released after being dragged + const ImGuiIO &io = ImGui::GetIO(); + const ImVec2 origin(canvas_p0.x + scrolling.x, + canvas_p0.y + scrolling.y); + dragged_entrance_->x_ = io.MousePos.x - origin.x - 8; + dragged_entrance_->y_ = io.MousePos.y - origin.y - 8; + is_dragging_entrance_ = false; + } } } } +bool OverworldEditor::IsMouseHoveringOverEntrance( + const zelda3::OverworldEntrance &entrance, ImVec2 canvas_p0, + ImVec2 scrolling) { + // Get the mouse position relative to the canvas + const ImGuiIO &io = ImGui::GetIO(); + const ImVec2 origin(canvas_p0.x + scrolling.x, canvas_p0.y + scrolling.y); + const ImVec2 mouse_pos(io.MousePos.x - origin.x, io.MousePos.y - origin.y); + + // Check if the mouse is hovering over the entrance + if (mouse_pos.x >= entrance.x_ && mouse_pos.x <= entrance.x_ + 16 && + mouse_pos.y >= entrance.y_ && mouse_pos.y <= entrance.y_ + 16) { + return true; + } + return false; +} + +// ---------------------------------------------------------------------------- + +void OverworldEditor::DrawOverworldSprites() { + for (const auto &sprite : overworld_.Sprites(game_state_)) { + // Get the sprite's bitmap and real X and Y positions + auto id = sprite.id(); + const gfx::Bitmap &sprite_bitmap = sprite_previews_[id]; + int realX = sprite.GetRealX(); + int realY = sprite.GetRealY(); + + // Draw the sprite's bitmap onto the canvas at its real X and Y positions + ow_map_canvas_.DrawBitmap(sprite_bitmap, realX, realY); + ow_map_canvas_.DrawRect(realX, realY, sprite.Width(), sprite.Height(), + ImVec4(255, 0, 0, 150)); + std::string str = absl::StrFormat("%s", sprite.Name()); + ow_map_canvas_.DrawText(str, realX - 4, realY - 2); + } +} + // ---------------------------------------------------------------------------- void OverworldEditor::DrawOverworldMaps() { @@ -153,42 +322,169 @@ void OverworldEditor::DrawOverworldMaps() { int yy = 0; for (int i = 0; i < 0x40; i++) { int world_index = i + (current_world_ * 0x40); - int map_x = (xx * 0x200); - int map_y = (yy * 0x200); - ow_map_canvas_.DrawBitmap(maps_bmp_[world_index], map_x, map_y); + int map_x = (xx * 0x200 * ow_map_canvas_.global_scale()); + int map_y = (yy * 0x200 * ow_map_canvas_.global_scale()); + ow_map_canvas_.DrawBitmap(maps_bmp_[world_index], map_x, map_y, + ow_map_canvas_.global_scale()); xx++; if (xx >= 8) { yy++; xx = 0; } - DrawOverworldEntrances(); } } -// ---------------------------------------------------------------------------- +void OverworldEditor::DrawOverworldEdits() { + auto mouse_position = ow_map_canvas_.drawn_tile_position(); + auto canvas_size = ow_map_canvas_.canvas_size(); + int x = mouse_position.x / canvas_size.x; + int y = mouse_position.y / canvas_size.y; + + // Determine which overworld map the user is currently editing. + DetermineActiveMap(mouse_position); + + // Render the updated map bitmap. + RenderUpdatedMapBitmap(mouse_position, + tile16_individual_data_[current_tile16_]); +} + +void OverworldEditor::DetermineActiveMap(const ImVec2 &mouse_position) { + // Assuming each small map is 256x256 pixels (adjust as needed) + constexpr int small_map_size = 512; + + // Calculate which small map the mouse is currently over + int map_x = mouse_position.x / small_map_size; + int map_y = mouse_position.y / small_map_size; + + // Calculate the index of the map in the `maps_bmp_` vector + current_map_ = map_x + map_y * 8; +} + +void OverworldEditor::RenderUpdatedMapBitmap(const ImVec2 &click_position, + const Bytes &tile_data) { + // Calculate the tile position relative to the current active map + constexpr int tile_size = 16; // Tile size is 16x16 pixels + + // Calculate the tile index for x and y based on the click_position + int tile_index_x = (static_cast(click_position.x) % 512) / tile_size; + int tile_index_y = (static_cast(click_position.y) % 512) / tile_size; + + // Calculate the pixel start position based on tile index and tile size + ImVec2 start_position; + start_position.x = tile_index_x * tile_size; + start_position.y = tile_index_y * tile_size; + + // Get the current map's bitmap from the BitmapTable + gfx::Bitmap ¤t_bitmap = maps_bmp_[current_map_]; + + // Update the bitmap's pixel data based on the start_position and tile_data + for (int y = 0; y < tile_size; ++y) { + for (int x = 0; x < tile_size; ++x) { + int pixel_index = (start_position.y + y) * current_bitmap.width() + + (start_position.x + x); + current_bitmap.WriteToPixel(pixel_index, tile_data[y * tile_size + x]); + } + } + + // Render the updated bitmap to the canvas + rom()->UpdateBitmap(¤t_bitmap); +} + +void OverworldEditor::SaveOverworldChanges() { + // Store the changes made by the user to the ROM (or project file) + rom()->QueueChanges([&]() { + PRINT_IF_ERROR(overworld_.SaveOverworldMaps()); + if (!overworld_.CreateTile32Tilemap()) { + // overworld_.SaveMap16Tiles(); + PRINT_IF_ERROR(overworld_.SaveMap32Tiles()); + } else { + std::cout << "Failed to create tile32 tilemap" << std::endl; + } + }); +} + +void OverworldEditor::CheckForOverworldEdits() { + if (!blockset_canvas_.Points().empty()) { + // User has selected a tile they want to draw from the blockset. + int x = blockset_canvas_.Points().front().x / 32; + int y = blockset_canvas_.Points().front().y / 32; + current_tile16_ = x + (y * 8); + if (ow_map_canvas_.DrawTilePainter(tile16_individual_[current_tile16_], + 16)) { + // Update the overworld map. + DrawOverworldEdits(); + } + } +} + +void OverworldEditor::CheckForCurrentMap() { + // DetermineActiveMap(ImGui::GetIO().MousePos); + // 4096x4096, 512x512 maps and some are larges maps 1024x1024 + auto current_map_x = current_map_ % 8; + auto current_map_y = current_map_ / 8; + auto large_map_size = 1024; + auto map_size = 512; + + auto mouse_position = ImGui::GetIO().MousePos; + + // Assuming each small map is 256x256 pixels (adjust as needed) + constexpr int small_map_size = 512; + + // Calculate which small map the mouse is currently over + int map_x = mouse_position.x / small_map_size; + int map_y = mouse_position.y / small_map_size; + + // Calculate the index of the map in the `maps_bmp_` vector + current_map_ = map_x + map_y * 8; + + if (overworld_.overworld_map(current_map_).IsLargeMap()) { + // Draw an outline around the current map + ow_map_canvas_.DrawOutline(current_map_x * large_map_size, + current_map_y * large_map_size, large_map_size, + large_map_size); + } else { + // Draw an outline around the current map + ow_map_canvas_.DrawOutline(current_map_x * map_size, + current_map_y * map_size, map_size, map_size); + } + + static int prev_map_; + + if (current_map_ != prev_map_) { + // Update the current map's tile16 blockset + // core::BuildAndRenderBitmapPipeline( + // 0x80, 0x2000, 0x80, maps_bmp_[current_map_].mutable_data(), *rom(), + // maps_bmp_[current_map_], palette_); + + prev_map_ = current_map_; + } +} // Overworld Editor canvas // Allows the user to make changes to the overworld map. void OverworldEditor::DrawOverworldCanvas() { - DrawOverworldMapSettings(); - ImGui::Separator(); + if (all_gfx_loaded_) { + DrawOverworldMapSettings(); + Separator(); + } + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); if (ImGuiID child_id = ImGui::GetID((void *)(intptr_t)7); ImGui::BeginChild(child_id, ImGui::GetContentRegionAvail(), true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar)) { ow_map_canvas_.DrawBackground(ImVec2(0x200 * 8, 0x200 * 8)); + ImGui::PopStyleVar(2); ow_map_canvas_.DrawContextMenu(); if (overworld_.isLoaded()) { DrawOverworldMaps(); - // User has selected a tile they want to draw from the blockset. - if (!blockset_canvas_.Points().empty()) { - int x = blockset_canvas_.Points().front().x / 32; - int y = blockset_canvas_.Points().front().y / 32; - std::cout << x << " " << y << std::endl; - current_tile16_ = x + (y * 8); - std::cout << current_tile16_ << std::endl; - ow_map_canvas_.DrawTilePainter(tile16_individual_[current_tile16_], 16); + DrawOverworldEntrances(ow_map_canvas_.zero_point(), + ow_map_canvas_.Scrolling()); + if (flags()->kDrawOverworldSprites) { + DrawOverworldSprites(); } + CheckForCurrentMap(); + CheckForOverworldEdits(); } ow_map_canvas_.DrawGrid(64.0f); ow_map_canvas_.DrawOverlay(); @@ -196,21 +492,6 @@ void OverworldEditor::DrawOverworldCanvas() { ImGui::EndChild(); } -// ---------------------------------------------------------------------------- - -// Tile 16 Selector -// Displays all the tiles in the game. -void OverworldEditor::DrawTile16Selector() { - blockset_canvas_.DrawBackground(ImVec2(0x100 + 1, (8192 * 2) + 1)); - blockset_canvas_.DrawContextMenu(); - blockset_canvas_.DrawBitmap(tile16_blockset_bmp_, 2, map_blockset_loaded_); - blockset_canvas_.DrawTileSelector(32); - blockset_canvas_.DrawGrid(32.0f); - blockset_canvas_.DrawOverlay(); -} - -// ---------------------------------------------------------------------------- - // Tile 8 Selector // Displays all the individual tiles that make up a tile16. void OverworldEditor::DrawTile8Selector() { @@ -218,65 +499,46 @@ void OverworldEditor::DrawTile8Selector() { ImVec2(0x100 + 1, kNumSheetsToLoad * 0x40 + 1)); graphics_bin_canvas_.DrawContextMenu(); if (all_gfx_loaded_) { - for (const auto &[key, value] : graphics_bin_) { + // for (const auto &[key, value] : graphics_bin_) { + for (auto &[key, value] : rom()->bitmap_manager()) { int offset = 0x40 * (key + 1); - int top_left_y = graphics_bin_canvas_.GetZeroPoint().y + 2; + int top_left_y = graphics_bin_canvas_.zero_point().y + 2; if (key >= 1) { - top_left_y = graphics_bin_canvas_.GetZeroPoint().y + 0x40 * key; + top_left_y = graphics_bin_canvas_.zero_point().y + 0x40 * key; } + auto texture = value.get()->texture(); graphics_bin_canvas_.GetDrawList()->AddImage( - (void *)value.GetTexture(), - ImVec2(graphics_bin_canvas_.GetZeroPoint().x + 2, top_left_y), - ImVec2(graphics_bin_canvas_.GetZeroPoint().x + 0x100, - graphics_bin_canvas_.GetZeroPoint().y + offset)); + (void *)texture, + ImVec2(graphics_bin_canvas_.zero_point().x + 2, top_left_y), + ImVec2(graphics_bin_canvas_.zero_point().x + 0x100, + graphics_bin_canvas_.zero_point().y + offset)); } } graphics_bin_canvas_.DrawGrid(16.0f); graphics_bin_canvas_.DrawOverlay(); } -// ---------------------------------------------------------------------------- - -// Displays the graphics tilesheets that are available on the current selected -// overworld map. -void OverworldEditor::DrawAreaGraphics() { - current_gfx_canvas_.DrawBackground(ImVec2(256 + 1, 0x10 * 0x40 + 1)); - current_gfx_canvas_.DrawContextMenu(); - current_gfx_canvas_.DrawTileSelector(32); - current_gfx_canvas_.DrawBitmap(current_gfx_bmp_, 2, overworld_.isLoaded()); - current_gfx_canvas_.DrawGrid(32.0f); - current_gfx_canvas_.DrawOverlay(); -} - -// ---------------------------------------------------------------------------- - void OverworldEditor::DrawTileSelector() { - if (ImGui::BeginTabBar("##TabBar", ImGuiTabBarFlags_FittingPolicyScroll)) { + if (ImGui::BeginTabBar(kTileSelectorTab.data(), + ImGuiTabBarFlags_FittingPolicyScroll)) { if (ImGui::BeginTabItem("Tile16")) { - if (ImGuiID child_id = ImGui::GetID((void *)(intptr_t)2); - ImGui::BeginChild(child_id, ImGui::GetContentRegionAvail(), true, - ImGuiWindowFlags_AlwaysVerticalScrollbar)) { - DrawTile16Selector(); - } - ImGui::EndChild(); + gui::BitmapCanvasPipeline(blockset_canvas_, tile16_blockset_bmp_, 0x100, + (8192 * 2), 0x20, map_blockset_loaded_, true, + 1); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Tile8")) { - if (ImGuiID child_id = ImGui::GetID((void *)(intptr_t)1); - ImGui::BeginChild(child_id, ImGui::GetContentRegionAvail(), true, - ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + if (ImGui::BeginChild("##tile8viewer", ImGui::GetContentRegionAvail(), + true, ImGuiWindowFlags_AlwaysVerticalScrollbar)) { DrawTile8Selector(); } ImGui::EndChild(); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Area Graphics")) { - if (ImGuiID child_id = ImGui::GetID((void *)(intptr_t)3); - ImGui::BeginChild(child_id, ImGui::GetContentRegionAvail(), true, - ImGuiWindowFlags_AlwaysVerticalScrollbar)) { - DrawAreaGraphics(); - } - ImGui::EndChild(); + gui::BitmapCanvasPipeline(current_gfx_canvas_, current_gfx_bmp_, 256, + 0x10 * 0x40, 0x20, overworld_.isLoaded(), true, + 3); ImGui::EndTabItem(); } ImGui::EndTabBar(); @@ -287,36 +549,40 @@ void OverworldEditor::DrawTileSelector() { absl::Status OverworldEditor::LoadGraphics() { // Load all of the graphics data from the game. - PRINT_IF_ERROR(rom_.LoadAllGraphicsData()) - graphics_bin_ = rom_.GetGraphicsBin(); + PRINT_IF_ERROR(rom()->LoadAllGraphicsData()) + graphics_bin_ = rom()->graphics_bin(); // Load the Link to the Past overworld. - RETURN_IF_ERROR(overworld_.Load(rom_)) + RETURN_IF_ERROR(overworld_.Load(*rom())) palette_ = overworld_.AreaPalette(); - current_gfx_bmp_.Create(0x80, 0x200, 0x40, overworld_.AreaGraphics()); - current_gfx_bmp_.ApplyPalette(palette_); - rom_.RenderBitmap(¤t_gfx_bmp_); + + // Create the area graphics image + gui::BuildAndRenderBitmapPipeline(0x80, 0x200, 0x40, + overworld_.AreaGraphics(), *rom(), + current_gfx_bmp_, palette_); // Create the tile16 blockset image - tile16_blockset_bmp_.Create(0x80, 8192, 0x80, overworld_.Tile16Blockset()); - tile16_blockset_bmp_.ApplyPalette(palette_); - rom_.RenderBitmap(&tile16_blockset_bmp_); + gui::BuildAndRenderBitmapPipeline(0x80, 0x2000, 0x80, + overworld_.Tile16Blockset(), *rom(), + tile16_blockset_bmp_, palette_); map_blockset_loaded_ = true; // Copy the tile16 data into individual tiles. auto tile16_data = overworld_.Tile16Blockset(); + std::cout << tile16_data.size() << std::endl; + // Loop through the tiles and copy their pixel data into separate vectors for (int i = 0; i < 4096; i++) { // Create a new vector for the pixel data of the current tile - Bytes tile_data; - for (int j = 0; j < 32 * 32; j++) tile_data.push_back(0x00); + Bytes tile_data(16 * 16, 0x00); // More efficient initialization // Copy the pixel data for the current tile into the vector - for (int ty = 0; ty < 32; ty++) { - for (int tx = 0; tx < 32; tx++) { - int position = (tx + (ty * 0x20)); - uchar value = tile16_data[i + tx + (ty * 0x80)]; + for (int ty = 0; ty < 16; ty++) { + for (int tx = 0; tx < 16; tx++) { + int position = tx + (ty * 0x10); + uchar value = + tile16_data[(i % 8 * 16) + (i / 8 * 16 * 0x80) + (ty * 0x80) + tx]; tile_data[position] = value; } } @@ -327,23 +593,87 @@ absl::Status OverworldEditor::LoadGraphics() { // Render the bitmaps of each tile. for (int id = 0; id < 4096; id++) { - gfx::Bitmap new_tile16; - tile16_individual_.emplace_back(new_tile16); - tile16_individual_[id].Create(0x10, 0x10, 0x80, - tile16_individual_data_[id]); - tile16_individual_[id].ApplyPalette(palette_); - rom_.RenderBitmap(&tile16_individual_[id]); + tile16_individual_.emplace_back(); + gui::BuildAndRenderBitmapPipeline(0x10, 0x10, 0x80, + tile16_individual_data_[id], *rom(), + tile16_individual_[id], palette_); } // Render the overworld maps loaded from the ROM. - for (int i = 0; i < core::kNumOverworldMaps; ++i) { + for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) { overworld_.SetCurrentMap(i); auto palette = overworld_.AreaPalette(); - maps_bmp_[i].Create(0x200, 0x200, 0x200, overworld_.BitmapData()); - maps_bmp_[i].ApplyPalette(palette); - rom_.RenderBitmap(&(maps_bmp_[i])); + gui::BuildAndRenderBitmapPipeline(0x200, 0x200, 0x200, + overworld_.BitmapData(), *rom(), + maps_bmp_[i], palette); } + if (flags()->kDrawOverworldSprites) { + RETURN_IF_ERROR(LoadSpriteGraphics()); + } + + return absl::OkStatus(); +} + +absl::Status OverworldEditor::LoadSpriteGraphics() { + // Render the sprites for each Overworld map + for (int i = 0; i < 3; i++) + for (auto const &sprite : overworld_.Sprites(i)) { + int width = sprite.Width(); + int height = sprite.Height(); + int depth = 0x40; + auto spr_gfx = sprite.PreviewGraphics(); + sprite_previews_[sprite.id()].Create(width, height, depth, spr_gfx); + sprite_previews_[sprite.id()].ApplyPalette(palette_); + rom()->RenderBitmap(&(sprite_previews_[sprite.id()])); + } + return absl::OkStatus(); +} + +absl::Status OverworldEditor::DrawExperimentalModal() { + ImGui::Begin("Experimental", &show_experimental); + + gui::TextWithSeparators("PROTOTYPE OVERWORLD TILEMAP LOADER"); + Text("Please provide two files:"); + Text("One based on MAPn.DAT, which represents the overworld tilemap"); + Text("One based on MAPDATn.DAT, which is the tile32 configurations."); + Text("Currently, loading CGX for this component is NOT supported. "); + Text("Please load a US ROM of LTTP (JP ROM support coming soon)."); + Text( + "Once you've loaded the files, you can click the button below to load " + "the tilemap into the editor"); + + ImGui::InputText("##TilemapFile", &ow_tilemap_filename_); + ImGui::SameLine(); + gui::FileDialogPipeline( + "ImportTilemapsKey", ".DAT,.dat\0", "Tilemap Hex File", [this]() { + ow_tilemap_filename_ = ImGuiFileDialog::Instance()->GetFilePathName(); + }); + + ImGui::InputText("##Tile32ConfigurationFile", + &tile32_configuration_filename_); + ImGui::SameLine(); + gui::FileDialogPipeline("ImportTile32Key", ".DAT,.dat\0", "Tile32 Hex File", + [this]() { + tile32_configuration_filename_ = + ImGuiFileDialog::Instance()->GetFilePathName(); + }); + + if (ImGui::Button("Load Prototype Overworld with ROM graphics")) { + RETURN_IF_ERROR(LoadGraphics()) + all_gfx_loaded_ = true; + } + + gui::TextWithSeparators("Configuration"); + + gui::InputHexShort("Tilemap File Offset (High)", &tilemap_file_offset_high_); + gui::InputHexShort("Tilemap File Offset (Low)", &tilemap_file_offset_low_); + + gui::InputHexShort("LW Maps to Load", &light_maps_to_load_); + gui::InputHexShort("DW Maps to Load", &dark_maps_to_load_); + gui::InputHexShort("SP Maps to Load", &sp_maps_to_load_); + + ImGui::End(); return absl::OkStatus(); } diff --git a/src/app/editor/overworld_editor.h b/src/app/editor/overworld_editor.h index 4c664843..1665e941 100644 --- a/src/app/editor/overworld_editor.h +++ b/src/app/editor/overworld_editor.h @@ -10,14 +10,19 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_format.h" -#include "app/editor/palette_editor.h" +#include "app/core/common.h" +#include "app/core/editor.h" +#include "app/editor/modules/gfx_group_editor.h" +#include "app/editor/modules/palette_editor.h" +#include "app/editor/modules/tile16_editor.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/pipeline.h" #include "app/rom.h" #include "app/zelda3/overworld.h" -#include "gui/canvas.h" -#include "gui/icons.h" namespace yaze { namespace app { @@ -31,43 +36,98 @@ static constexpr uint kTile8DisplayHeight = 64; static constexpr float kInputFieldSize = 30.f; static constexpr absl::string_view kToolsetColumnNames[] = { - "#undoTool", "#redoTool", "#drawTool", "#separator2", - "#zoomOutTool", "#zoomInTool", "#separator", "#history", - "#entranceTool", "#exitTool", "#itemTool", "#spriteTool", - "#transportTool", "#musicTool"}; + "#undoTool", "#redoTool", "#drawTool", "#separator2", + "#zoomOutTool", "#zoomInTool", "#separator", "#history", + "#entranceTool", "#exitTool", "#itemTool", "#spriteTool", + "#transportTool", "#musicTool", "#separator3", "#tilemapTool"}; -static constexpr absl::string_view kOverworldSettingsColumnNames[] = { - "##1stCol", "##gfxCol", "##palCol", "##sprgfxCol", - "##sprpalCol", "##msgidCol", "##2ndCol"}; +constexpr ImGuiTableFlags kOWMapFlags = ImGuiTableFlags_Borders; +constexpr ImGuiTableFlags kToolsetTableFlags = ImGuiTableFlags_SizingFixedFit; +constexpr ImGuiTableFlags kOWEditFlags = + ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | + ImGuiTableFlags_Hideable | ImGuiTableFlags_BordersOuter | + ImGuiTableFlags_BordersV; -class OverworldEditor { +constexpr absl::string_view kWorldList = + "Light World\0Dark World\0Extra World\0"; + +constexpr absl::string_view kGamePartComboString = "Part 0\0Part 1\0Part 2\0"; + +constexpr absl::string_view kTileSelectorTab = "##TileSelectorTabBar"; +constexpr absl::string_view kOWEditTable = "##OWEditTable"; +constexpr absl::string_view kOWMapTable = "#MapSettingsTable"; + +class OverworldEditor : public Editor, + public SharedROM, + public core::ExperimentFlags { public: - absl::Status Update(); - absl::Status Undo() const { return absl::UnimplementedError("Undo"); } - absl::Status Redo() const { return absl::UnimplementedError("Redo"); } - absl::Status Cut() const { return absl::UnimplementedError("Cut"); } - absl::Status Copy() const { return absl::UnimplementedError("Copy"); } - absl::Status Paste() const { return absl::UnimplementedError("Paste"); } - void SetupROM(ROM &rom) { rom_ = rom; } + absl::Status Update() final; + absl::Status Undo() { return absl::UnimplementedError("Undo"); } + absl::Status Redo() { return absl::UnimplementedError("Redo"); } + absl::Status Cut() { return absl::UnimplementedError("Cut"); } + absl::Status Copy() { return absl::UnimplementedError("Copy"); } + absl::Status Paste() { return absl::UnimplementedError("Paste"); } + + auto overworld() { return &overworld_; } + + void Shutdown() { + for (auto &bmp : tile16_individual_) { + bmp.Cleanup(); + } + for (auto &[i, bmp] : maps_bmp_) { + bmp.Cleanup(); + } + for (auto &[i, bmp] : graphics_bin_) { + bmp.Cleanup(); + } + for (auto &[i, bmp] : current_graphics_set_) { + bmp.Cleanup(); + } + } + + absl::Status LoadGraphics(); private: absl::Status DrawToolset(); void DrawOverworldMapSettings(); - void DrawOverworldEntrances(); + void DrawOverworldEntrances(ImVec2 canvas_p, ImVec2 scrolling); void DrawOverworldMaps(); + void DrawOverworldSprites(); + + void DrawOverworldEdits(); + void RenderUpdatedMapBitmap(const ImVec2 &click_position, + const Bytes &tile_data); + void SaveOverworldChanges(); + void DetermineActiveMap(const ImVec2 &mouse_position); + + void CheckForOverworldEdits(); + void CheckForCurrentMap(); void DrawOverworldCanvas(); - void DrawTile16Selector(); void DrawTile8Selector(); - void DrawAreaGraphics(); void DrawTileSelector(); - absl::Status LoadGraphics(); + + absl::Status LoadSpriteGraphics(); + absl::Status DrawExperimentalModal(); + + enum class EditingMode { + DRAW_TILE, + ENTRANCES, + EXITS, + ITEMS, + SPRITES, + TRANSPORTS, + MUSIC + }; + + EditingMode current_mode = EditingMode::DRAW_TILE; int current_world_ = 0; int current_map_ = 0; int current_tile16_ = 0; int selected_tile_ = 0; + int game_state_ = 0; char map_gfx_[3] = ""; char map_palette_[3] = ""; char spr_gfx_[3] = ""; @@ -75,41 +135,58 @@ class OverworldEditor { char message_id_[5] = ""; char staticgfx[16]; + uint32_t tilemap_file_offset_high_ = 0; + uint32_t tilemap_file_offset_low_ = 0; + uint32_t light_maps_to_load_ = 0x51; + uint32_t dark_maps_to_load_ = 0x2A; + uint32_t sp_maps_to_load_ = 0x07; + bool opt_enable_grid = true; bool all_gfx_loaded_ = false; bool map_blockset_loaded_ = false; bool selected_tile_loaded_ = false; bool update_selected_tile_ = true; + bool is_dragging_entrance_ = false; + bool show_tile16_editor_ = false; + bool show_gfx_group_editor_ = false; - ImGuiTableFlags toolset_table_flags = ImGuiTableFlags_SizingFixedFit; - ImGuiTableFlags ow_map_flags = ImGuiTableFlags_Borders; - ImGuiTableFlags ow_edit_flags = ImGuiTableFlags_Reorderable | - ImGuiTableFlags_Resizable | - ImGuiTableFlags_SizingStretchSame; + bool IsMouseHoveringOverEntrance(const zelda3::OverworldEntrance &entrance, + ImVec2 canvas_p, ImVec2 scrolling); + zelda3::OverworldEntrance *dragged_entrance_; + + bool show_experimental = false; + std::string ow_tilemap_filename_ = ""; + std::string tile32_configuration_filename_ = ""; Bytes selected_tile_data_; - std::unordered_map graphics_bin_; - std::unordered_map current_graphics_set_; - std::unordered_map maps_bmp_; - std::unordered_map sprite_previews_; - std::vector tile16_individual_data_; std::vector tile16_individual_; - ROM rom_; + std::vector tile8_individual_data_; + std::vector tile8_individual_; + + Tile16Editor tile16_editor_; + GfxGroupEditor gfx_group_editor_; PaletteEditor palette_editor_; zelda3::Overworld overworld_; + gui::Canvas ow_map_canvas_; + gui::Canvas current_gfx_canvas_; + gui::Canvas blockset_canvas_; + gui::Canvas graphics_bin_canvas_; + gfx::SNESPalette palette_; gfx::Bitmap selected_tile_bmp_; gfx::Bitmap tile16_blockset_bmp_; gfx::Bitmap current_gfx_bmp_; gfx::Bitmap all_gfx_bmp; - gui::Canvas ow_map_canvas_; - gui::Canvas current_gfx_canvas_; - gui::Canvas blockset_canvas_; - gui::Canvas graphics_bin_canvas_; + gfx::BitmapTable maps_bmp_; + gfx::BitmapTable graphics_bin_; + gfx::BitmapTable current_graphics_set_; + gfx::BitmapTable sprite_previews_; + + absl::Status status_; }; } // namespace editor } // namespace app diff --git a/src/app/editor/palette_editor.cc b/src/app/editor/palette_editor.cc deleted file mode 100644 index 00e6c381..00000000 --- a/src/app/editor/palette_editor.cc +++ /dev/null @@ -1,128 +0,0 @@ -#include "palette_editor.h" - -#include - -#include "absl/status/status.h" -#include "app/gfx/snes_palette.h" -#include "gui/canvas.h" -#include "gui/icons.h" - -namespace yaze { -namespace app { -namespace editor { - -absl::Status PaletteEditor::Update() { - for (int i = 0; i < 11; ++i) { - if (ImGui::TreeNode(kPaletteCategoryNames[i].data())) { - auto size = rom_.GetPaletteGroup(kPaletteGroupNames[i].data()).size; - auto palettes = rom_.GetPaletteGroup(kPaletteGroupNames[i].data()); - for (int j = 0; j < size; j++) { - ImGui::Text("%d", j); - auto palette = palettes[j]; - for (int n = 0; n < size; n++) { - ImGui::PushID(n); - if ((n % 8) != 0) - ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y); - - ImGuiColorEditFlags palette_button_flags = - ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker; - if (ImGui::ColorButton("##palette", palette[n].RGB(), - palette_button_flags, ImVec2(20, 20))) - current_color_ = - ImVec4(palette[n].rgb.x, palette[n].rgb.y, palette[n].rgb.z, - current_color_.w); // Preserve alpha! - - ImGui::PopID(); - } - } - ImGui::TreePop(); - } - } - return absl::OkStatus(); -} - -void PaletteEditor::DisplayPalette(gfx::SNESPalette& palette, bool loaded) { - static ImVec4 color = ImVec4(0, 0, 0, 255.f); - ImGuiColorEditFlags misc_flags = ImGuiColorEditFlags_AlphaPreview | - ImGuiColorEditFlags_NoDragDrop | - ImGuiColorEditFlags_NoOptions; - - // Generate a default palette. The palette will persist and can be edited. - static bool init = false; - static ImVec4 saved_palette[256] = {}; - if (loaded && !init) { - for (int n = 0; n < palette.size_; n++) { - saved_palette[n].x = palette.GetColor(n).rgb.x / 255; - saved_palette[n].y = palette.GetColor(n).rgb.y / 255; - saved_palette[n].z = palette.GetColor(n).rgb.z / 255; - saved_palette[n].w = 255; // Alpha - } - init = true; - } - - static ImVec4 backup_color; - bool open_popup = ImGui::ColorButton("MyColor##3b", color, misc_flags); - ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); - open_popup |= ImGui::Button("Palette"); - if (open_popup) { - ImGui::OpenPopup("mypicker"); - backup_color = color; - } - if (ImGui::BeginPopup("mypicker")) { - ImGui::Text("Current Overworld Palette"); - ImGui::Separator(); - ImGui::ColorPicker4("##picker", (float*)&color, - misc_flags | ImGuiColorEditFlags_NoSidePreview | - ImGuiColorEditFlags_NoSmallPreview); - ImGui::SameLine(); - - ImGui::BeginGroup(); // Lock X position - ImGui::Text("Current ==>"); - ImGui::SameLine(); - ImGui::Text("Previous"); - - ImGui::ColorButton( - "##current", color, - ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_AlphaPreviewHalf, - ImVec2(60, 40)); - ImGui::SameLine(); - - if (ImGui::ColorButton( - "##previous", backup_color, - ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_AlphaPreviewHalf, - ImVec2(60, 40))) - color = backup_color; - ImGui::Separator(); - ImGui::Text("Palette"); - for (int n = 0; n < IM_ARRAYSIZE(saved_palette); n++) { - ImGui::PushID(n); - if ((n % 8) != 0) ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y); - - ImGuiColorEditFlags palette_button_flags = ImGuiColorEditFlags_NoAlpha | - ImGuiColorEditFlags_NoPicker | - ImGuiColorEditFlags_NoTooltip; - if (ImGui::ColorButton("##palette", saved_palette[n], - palette_button_flags, ImVec2(20, 20))) - color = ImVec4(saved_palette[n].x, saved_palette[n].y, - saved_palette[n].z, color.w); // Preserve alpha! - - if (ImGui::BeginDragDropTarget()) { - if (const ImGuiPayload* payload = - ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) - memcpy((float*)&saved_palette[n], payload->Data, sizeof(float) * 3); - if (const ImGuiPayload* payload = - ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) - memcpy((float*)&saved_palette[n], payload->Data, sizeof(float) * 4); - ImGui::EndDragDropTarget(); - } - - ImGui::PopID(); - } - ImGui::EndGroup(); - ImGui::EndPopup(); - } -} - -} // namespace editor -} // namespace app -} // namespace yaze \ No newline at end of file diff --git a/src/app/editor/palette_editor.h b/src/app/editor/palette_editor.h deleted file mode 100644 index 80ab485f..00000000 --- a/src/app/editor/palette_editor.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef YAZE_APP_EDITOR_PALETTE_EDITOR_H -#define YAZE_APP_EDITOR_PALETTE_EDITOR_H - -#include - -#include "absl/status/status.h" -#include "app/gfx/snes_palette.h" -#include "app/rom.h" -#include "gui/canvas.h" -#include "gui/icons.h" - -namespace yaze { -namespace app { -namespace editor { - -static constexpr absl::string_view kPaletteCategoryNames[] = { - "Sword", "Shield", "Clothes", "World Colors", - "Area Colors", "Enemies", "Dungeons", "World Map", - "Dungeon Map", "Triforce", "Crystal"}; - -static constexpr absl::string_view kPaletteGroupNames[] = { - "swords", "shields", "armors", "ow_main", - "ow_aux", "global_sprites", "dungeon_main", "ow_mini_map", - "ow_mini_map", "3d_object", "3d_object"}; - -class PaletteEditor { - public: - absl::Status Update(); - void DisplayPalette(gfx::SNESPalette& palette, bool loaded); - - auto SetupROM(ROM& rom) { rom_ = rom; } - - private: - ImVec4 current_color_; - ROM rom_; -}; - -} // namespace editor -} // namespace app -} // namespace yaze - -#endif \ No newline at end of file diff --git a/src/app/editor/screen_editor.cc b/src/app/editor/screen_editor.cc index bcf19352..a261ba8f 100644 --- a/src/app/editor/screen_editor.cc +++ b/src/app/editor/screen_editor.cc @@ -15,9 +15,9 @@ #include "app/core/constants.h" #include "app/gfx/bitmap.h" #include "app/gfx/snes_tile.h" -#include "gui/canvas.h" -#include "gui/icons.h" -#include "gui/input.h" +#include "app/gui/canvas.h" +#include "app/gui/icons.h" +#include "app/gui/input.h" namespace yaze { namespace app { @@ -32,38 +32,14 @@ void ScreenEditor::Update() { DrawNamingScreenEditor(); DrawOverworldMapEditor(); DrawDungeonMapsEditor(); - DrawMosaicEditor(); END_TAB_BAR() } -void ScreenEditor::DrawWorldGrid(int world, int h, int w) { - const float time = (float)ImGui::GetTime(); - - int i = 0; - if (world == 1) { - i = 64; - } else if (world == 2) { - i = 128; - } - for (int y = 0; y < h; y++) - for (int x = 0; x < w; x++) { - if (x > 0) ImGui::SameLine(); - ImGui::PushID(y * 4 + x); - std::string label = absl::StrCat(" #", absl::StrFormat("%x", i)); - if (ImGui::Selectable(label.c_str(), mosaic_tiles_[i] != 0, 0, - ImVec2(35, 25))) { - mosaic_tiles_[i] ^= 1; - } - ImGui::PopID(); - i++; - } -} - void ScreenEditor::DrawInventoryMenuEditor() { TAB_ITEM("Inventory Menu") static bool create = false; - if (!create && rom_.isLoaded()) { + if (!create && rom()->isLoaded()) { inventory_.Create(); palette_ = inventory_.Palette(); create = true; @@ -117,40 +93,6 @@ void ScreenEditor::DrawDungeonMapsEditor() { END_TAB_ITEM() } -void ScreenEditor::DrawMosaicEditor() { - TAB_ITEM("Mosaic Transitions") - - if (ImGui::BeginTable("Worlds", 3, ImGuiTableFlags_Borders)) { - ImGui::TableSetupColumn("Light World"); - ImGui::TableSetupColumn("Dark World"); - ImGui::TableSetupColumn("Special World"); - ImGui::TableHeadersRow(); - - ImGui::TableNextColumn(); - DrawWorldGrid(0); - - ImGui::TableNextColumn(); - DrawWorldGrid(1); - - ImGui::TableNextColumn(); - DrawWorldGrid(2, 4); - - ImGui::EndTable(); - } - - gui::InputHex("Routine Location", &overworldCustomMosaicASM); - - if (ImGui::Button("Generate Mosaic Assembly")) { - auto mosaic = - rom_.PatchOverworldMosaic(mosaic_tiles_, overworldCustomMosaicASM); - if (!mosaic.ok()) { - std::cout << mosaic; - } - } - - END_TAB_ITEM() -} - void ScreenEditor::DrawToolset() { static bool show_bg1 = true; static bool show_bg2 = true; diff --git a/src/app/editor/screen_editor.h b/src/app/editor/screen_editor.h index 6de72848..a70c566c 100644 --- a/src/app/editor/screen_editor.h +++ b/src/app/editor/screen_editor.h @@ -9,30 +9,22 @@ #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 "app/gui/icons.h" #include "app/rom.h" -#include "app/zelda3/inventory.h" -#include "gui/canvas.h" -#include "gui/color.h" -#include "gui/icons.h" +#include "app/zelda3/screen/inventory.h" namespace yaze { namespace app { namespace editor { -using MosaicArray = std::array; -static int overworldCustomMosaicASM = 0x1301D0; - -class ScreenEditor { +class ScreenEditor : public SharedROM { public: ScreenEditor(); - void SetupROM(ROM &rom) { - rom_ = rom; - inventory_.SetupROM(rom_); - } void Update(); private: - void DrawMosaicEditor(); void DrawTitleScreenEditor(); void DrawNamingScreenEditor(); void DrawOverworldMapEditor(); @@ -41,11 +33,7 @@ class ScreenEditor { void DrawToolset(); void DrawInventoryToolset(); - void DrawWorldGrid(int world, int h = 8, int w = 8); - char mosaic_tiles_[core::kNumOverworldMaps]; - - ROM rom_; Bytes all_gfx_; zelda3::Inventory inventory_; gfx::SNESPalette palette_; diff --git a/src/app/editor/sprite_editor.cc b/src/app/editor/sprite_editor.cc new file mode 100644 index 00000000..ba3d2a53 --- /dev/null +++ b/src/app/editor/sprite_editor.cc @@ -0,0 +1,11 @@ +#include "sprite_editor.h" + +namespace yaze { +namespace app { +namespace editor { + +absl::Status SpriteEditor::Update() { return absl::OkStatus(); } + +} // namespace editor +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/editor/sprite_editor.h b/src/app/editor/sprite_editor.h new file mode 100644 index 00000000..4bee1ae2 --- /dev/null +++ b/src/app/editor/sprite_editor.h @@ -0,0 +1,19 @@ +#ifndef YAZE_APP_EDITOR_SPRITE_EDITOR_H +#define YAZE_APP_EDITOR_SPRITE_EDITOR_H + +#include "absl/status/status.h" + +namespace yaze { +namespace app { +namespace editor { + +class SpriteEditor { + public: + absl::Status Update(); +}; + +} // namespace editor +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_EDITOR_SPRITE_EDITOR_H \ No newline at end of file diff --git a/src/app/emu/audio/apu.cc b/src/app/emu/audio/apu.cc new file mode 100644 index 00000000..ededc355 --- /dev/null +++ b/src/app/emu/audio/apu.cc @@ -0,0 +1,139 @@ +#include "app/emu/audio/apu.h" + +#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 { + +void APU::Init() { + // Set the clock frequency + clock_.SetFrequency(kApuClockSpeed); + + // Initialize Digital Signal Processor Callbacks + dsp_.SetSampleFetcher([this](uint16_t address) -> uint8_t { + return this->FetchSampleFromRam(address); + }); + + dsp_.SetSamplePusher( + [this](int16_t sample) { this->PushToAudioBuffer(sample); }); +} + +void APU::Reset() { + clock_.ResetAccumulatedTime(); + spc700_.Reset(); + dsp_.Reset(); +} + +void APU::Update() { + auto cycles_to_run = clock_.GetCycleCount(); + + for (auto i = 0; i < cycles_to_run; ++i) { + // Update the APU + UpdateChannelSettings(); + + // Update the SPC700 + uint8_t opcode = spc700_.read(spc700_.PC); + spc700_.ExecuteInstructions(opcode); + spc700_.PC++; + } + + ProcessSamples(); +} + +void APU::Notify(uint32_t address, uint8_t data) { + if (address < 0x2140 || address > 0x2143) { + return; + } + auto offset = address - 0x2140; + spc700_.write(offset, data); + + // HACK - This is a temporary solution to get the APU to play audio + ports_[address - 0x2140] = data; + switch (address) { + case 0x2140: + if (data == BEGIN_SIGNAL) { + SignalReady(); + } + break; + case 0x2141: + // TODO: Handle data byte transfer here + break; + case 0x2142: + // TODO: Handle the setup of destination address + break; + case 0x2143: + // TODO: Handle additional communication/commands + break; + } +} + +void APU::ProcessSamples() { + // Fetch sample data from AudioRam + // Iterate over all voices + for (uint8_t voice_num = 0; voice_num < 8; voice_num++) { + // Fetch the sample data for the current voice from AudioRam + uint8_t sample = FetchSampleForVoice(voice_num); + + // Process the sample through DSP + int16_t processed_sample = dsp_.ProcessSample(voice_num, sample); + + // Add the processed sample to the audio buffer + audio_samples_.push_back(processed_sample); + } +} + +uint8_t APU::FetchSampleForVoice(uint8_t voice_num) { + uint16_t address = CalculateAddressForVoice(voice_num); + return aram_.read(address); +} + +uint16_t APU::CalculateAddressForVoice(uint8_t voice_num) { + // TODO: Calculate the address for the specified voice + return voice_num; +} + +int16_t APU::GetNextSample() { + if (!audio_samples_.empty()) { + int16_t sample = audio_samples_.front(); + audio_samples_.erase(audio_samples_.begin()); + return sample; + } + return 0; // TODO: Return the last sample instead of 0. +} + +const std::vector& APU::GetAudioSamples() const { + return audio_samples_; +} + +void APU::UpdateChannelSettings() { + // TODO: Implement this method to update the channel settings. +} + +int16_t APU::GenerateSample(int channel) { + // TODO: Implement this method to generate a sample for the specified channel. +} + +void APU::ApplyEnvelope(int channel) { + // TODO: Implement this method to apply an envelope to the specified channel. +} + +uint8_t APU::ReadDspMemory(uint16_t address) { + return dsp_.ReadGlobalReg(address); +} + +void APU::WriteDspMemory(uint16_t address, uint8_t value) { + dsp_.WriteGlobalReg(address, value); +} + +} // namespace emu +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/emu/audio/apu.h b/src/app/emu/audio/apu.h new file mode 100644 index 00000000..fb4fe7af --- /dev/null +++ b/src/app/emu/audio/apu.h @@ -0,0 +1,137 @@ +#ifndef YAZE_APP_EMU_APU_H_ +#define YAZE_APP_EMU_APU_H_ + +#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 { + +/** + * + * 64 kilobytes of RAM are mapped across the 16-bit memory space of the SPC-700. + * Some regions of this space are overlaid with special hardware functions. + * + * Range Note + * $0000-00EF Zero Page RAM + * $00F0-00FF Sound CPU Registers + * $0100-01FF Stack Page RAM + * $0200-FFBF RAM + * $FFC0-FFFF IPL ROM or RAM + * + * The region at $FFC0-FFFF will normally read from the 64-byte IPL ROM, but the + * underlying RAM can always be written to, and the high bit of the Control + * register $F1 can be cleared to unmap the IPL ROM and allow read access to + * this RAM. + * + */ + +const int kApuClockSpeed = 1024000; // 1.024 MHz +const int apuSampleRate = 32000; // 32 KHz +const int apuClocksPerSample = 64; // 64 clocks per sample + +class APU : public Observer { + public: + APU(MemoryImpl &memory, AudioRam &aram, Clock &clock) + : aram_(aram), clock_(clock), memory_(memory) {} + + void Init(); + void Reset(); + void Update(); + void Notify(uint32_t address, uint8_t data) override; + + void ProcessSamples(); + uint8_t FetchSampleForVoice(uint8_t voice_num); + uint16_t CalculateAddressForVoice(uint8_t voice_num); + int16_t GetNextSample(); + + // Called upon a reset + void Initialize() { + spc700_.Reset(); + dsp_.Reset(); + SignalReady(); + } + + // Set Port 0 = $AA and Port 1 = $BB + void SignalReady() { + memory_.WriteByte(0x2140, READY_SIGNAL_0); + memory_.WriteByte(0x2141, READY_SIGNAL_1); + } + + void WriteToPort(uint8_t portNum, uint8_t value) { + ports_[portNum] = value; + switch (portNum) { + case 0: + memory_.WriteByte(0x2140, value); + break; + case 1: + memory_.WriteByte(0x2141, value); + break; + case 2: + memory_.WriteByte(0x2142, value); + break; + case 3: + memory_.WriteByte(0x2143, value); + break; + } + } + + void UpdateClock(int delta_time) { clock_.UpdateClock(delta_time); } + + // Method to fetch a sample from AudioRam + uint8_t FetchSampleFromRam(uint16_t address) const { + return aram_.read(address); + } + + // Method to push a processed sample to the audio buffer + void PushToAudioBuffer(int16_t sample) { audio_samples_.push_back(sample); } + + // Returns the audio samples for the current frame + const std::vector &GetAudioSamples() const; + + private: + // Constants for communication + static const uint8_t READY_SIGNAL_0 = 0xAA; + static const uint8_t READY_SIGNAL_1 = 0xBB; + static const uint8_t BEGIN_SIGNAL = 0xCC; + + // Port buffers (equivalent to $2140 to $2143 for the main CPU) + uint8_t ports_[4] = {0}; + + // Updates internal state based on APU register settings + void UpdateChannelSettings(); + + // Generates a sample for an audio channel + int16_t GenerateSample(int channel); + + // Applies an envelope to an audio channel + void ApplyEnvelope(int channel); + + // Handles DSP (Digital Signal Processor) memory reads and writes + uint8_t ReadDspMemory(uint16_t address); + void WriteDspMemory(uint16_t address, uint8_t value); + + // Member variables to store internal APU state and resources + AudioRam &aram_; + Clock &clock_; + MemoryImpl &memory_; + + DigitalSignalProcessor dsp_; + Spc700 spc700_{aram_}; + std::vector audio_samples_; + + std::function ready_callback_; +}; + +} // namespace emu +} // namespace app +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/app/emu/audio/dsp.cc b/src/app/emu/audio/dsp.cc new file mode 100644 index 00000000..ccc5b2c6 --- /dev/null +++ b/src/app/emu/audio/dsp.cc @@ -0,0 +1,281 @@ +#include "app/emu/audio/dsp.h" + +#include "app/emu/memory/memory.h" + +namespace yaze { +namespace app { +namespace emu { + +void DigitalSignalProcessor::Reset() {} + +uint8_t DigitalSignalProcessor::ReadVoiceReg(uint8_t voice, uint8_t reg) const { + voice %= kNumVoices; + switch (reg % kNumVoiceRegs) { + case 0: + return voices_[voice].vol_left; + case 1: + return voices_[voice].vol_right; + case 2: + return voices_[voice].pitch_low; + case 3: + return voices_[voice].pitch_high; + case 4: + return voices_[voice].source_number; + case 5: + return voices_[voice].adsr1; + case 6: + return voices_[voice].adsr2; + case 7: + return voices_[voice].gain; + case 8: + return voices_[voice].envx; + case 9: + return voices_[voice].outx; + default: + return 0; // This shouldn't happen, but it's good to have a default + // case + } +} + +void DigitalSignalProcessor::WriteVoiceReg(uint8_t voice, uint8_t reg, uint8_t value) { + voice %= kNumVoices; + switch (reg % kNumVoiceRegs) { + case 0: + voices_[voice].vol_left = static_cast(value); + break; + case 1: + voices_[voice].vol_right = static_cast(value); + break; + case 2: + voices_[voice].pitch_low = value; + break; + case 3: + voices_[voice].pitch_high = value; + break; + case 4: + voices_[voice].source_number = value; + break; + case 5: + voices_[voice].adsr1 = value; + break; + case 6: + voices_[voice].adsr2 = value; + break; + case 7: + voices_[voice].gain = value; + break; + // Note: envx and outx are read-only, so they don't have cases here + } +} + +// Set the callbacks +void DigitalSignalProcessor::SetSampleFetcher(SampleFetcher fetcher) { sample_fetcher_ = fetcher; } + +void DigitalSignalProcessor::SetSamplePusher(SamplePusher pusher) { sample_pusher_ = pusher; } + +int16_t DigitalSignalProcessor::DecodeSample(uint8_t voice_num) { + Voice const& voice = voices_[voice_num]; + uint16_t sample_address = voice.source_number; + + // Use the callback to fetch the sample + int16_t sample = static_cast(sample_fetcher_(sample_address) << 8); + return sample; +} + +int16_t DigitalSignalProcessor::ProcessSample(uint8_t voice_num, int16_t sample) { + Voice const& voice = voices_[voice_num]; + + // Adjust the pitch (for simplicity, we're just adjusting the sample value) + sample += voice.pitch_low + (voice.pitch_high << 8); + + // Apply volume (separate for left and right for stereo sound) + int16_t left_sample = (sample * voice.vol_left) / 255; + int16_t right_sample = (sample * voice.vol_right) / 255; + + // Combine stereo samples into a single 16-bit value + return (left_sample + right_sample) / 2; +} + +void DigitalSignalProcessor::MixSamples() { + int16_t mixed_sample = 0; + + for (uint8_t i = 0; i < kNumVoices; i++) { + int16_t decoded_sample = DecodeSample(i); + int16_t processed_sample = ProcessSample(i, decoded_sample); + mixed_sample += processed_sample; + } + + // Clamp the mixed sample to 16-bit range + if (mixed_sample > 32767) { + mixed_sample = 32767; + } else if (mixed_sample < -32768) { + mixed_sample = -32768; + } + + // Use the callback to push the mixed sample + sample_pusher_(mixed_sample); +} + +void DigitalSignalProcessor::UpdateEnvelope(uint8_t voice) { + uint8_t adsr1 = ReadVoiceReg(voice, 0x05); + uint8_t adsr2 = ReadVoiceReg(voice, 0x06); + uint8_t gain = ReadVoiceReg(voice, 0x07); + + uint8_t enableADSR = (adsr1 & 0x80) >> 7; + + if (enableADSR) { + // Handle ADSR envelope + Voice& voice_obj = voices_[voice]; + switch (voice_obj.state) { + case VoiceState::ATTACK: + // Update amplitude based on attack rate + voice_obj.current_amplitude += AttackRate(adsr1); + if (voice_obj.current_amplitude >= ENVELOPE_MAX) { + voice_obj.current_amplitude = ENVELOPE_MAX; + voice_obj.state = VoiceState::DECAY; + } + break; + case VoiceState::DECAY: + // Update amplitude based on decay rate + voice_obj.current_amplitude -= DecayRate(adsr2); + if (voice_obj.current_amplitude <= voice_obj.decay_level) { + voice_obj.current_amplitude = voice_obj.decay_level; + voice_obj.state = VoiceState::SUSTAIN; + } + break; + case VoiceState::SUSTAIN: + // Keep amplitude at the calculated decay level + voice_obj.current_amplitude = voice_obj.decay_level; + break; + case VoiceState::RELEASE: + // Update amplitude based on release rate + voice_obj.current_amplitude -= ReleaseRate(adsr2); + if (voice_obj.current_amplitude <= 0) { + voice_obj.current_amplitude = 0; + voice_obj.state = VoiceState::OFF; + } + break; + default: + break; + } + } else { + // Handle Gain envelope + // Extract mode from the gain byte + uint8_t mode = (gain & 0xE0) >> 5; + uint8_t rate = gain & 0x1F; + + Voice& voice_obj = voices_[voice]; + + switch (mode) { + case 0: // Direct Designation + case 1: + case 2: + case 3: + voice_obj.current_amplitude = + rate << 3; // Multiplying by 8 to scale to 0-255 + break; + + case 6: // Increase Mode (Linear) + voice_obj.current_amplitude += gainTimings[0][rate]; + if (voice_obj.current_amplitude > ENVELOPE_MAX) { + voice_obj.current_amplitude = ENVELOPE_MAX; + } + break; + + case 7: // Increase Mode (Bent Line) + // Hypothetical behavior: Increase linearly at first, then increase + // more slowly You'll likely need to adjust this based on your + // specific requirements + if (voice_obj.current_amplitude < (ENVELOPE_MAX / 2)) { + voice_obj.current_amplitude += gainTimings[1][rate]; + } else { + voice_obj.current_amplitude += gainTimings[1][rate] / 2; + } + if (voice_obj.current_amplitude > ENVELOPE_MAX) { + voice_obj.current_amplitude = ENVELOPE_MAX; + } + break; + + case 4: // Decrease Mode (Linear) + if (voice_obj.current_amplitude < gainTimings[2][rate]) { + voice_obj.current_amplitude = 0; + } else { + voice_obj.current_amplitude -= gainTimings[2][rate]; + } + break; + + case 5: // Decrease Mode (Exponential) + voice_obj.current_amplitude -= + (voice_obj.current_amplitude * gainTimings[3][rate]) / ENVELOPE_MAX; + break; + + default: + // Default behavior can be handled here if necessary + break; + } + } +} + +void DigitalSignalProcessor::update_voice_state(uint8_t voice_num) { + if (voice_num >= kNumVoices) return; + + Voice& voice = voices_[voice_num]; + switch (voice.state) { + case VoiceState::OFF: + // Reset current amplitude + voice.current_amplitude = 0; + break; + + case VoiceState::ATTACK: + // Increase the current amplitude at a rate defined by the ATTACK + // setting + voice.current_amplitude += AttackRate(voice.adsr1); + if (voice.current_amplitude >= ENVELOPE_MAX) { + voice.current_amplitude = ENVELOPE_MAX; + voice.state = VoiceState::DECAY; + voice.decay_level = CalculateDecayLevel(voice.adsr2); + } + break; + + case VoiceState::DECAY: + // Decrease the current amplitude at a rate defined by the DECAY setting + voice.current_amplitude -= DecayRate(voice.adsr2); + if (voice.current_amplitude <= voice.decay_level) { + voice.current_amplitude = voice.decay_level; + voice.state = VoiceState::SUSTAIN; + } + break; + + case VoiceState::SUSTAIN: + // Keep the current amplitude at the decay level + break; + + case VoiceState::RELEASE: + // Decrease the current amplitude at a rate defined by the RELEASE + // setting + voice.current_amplitude -= ReleaseRate(voice.adsr2); + if (voice.current_amplitude == 0) { + voice.state = VoiceState::OFF; + } + break; + } +} + +void DigitalSignalProcessor::process_envelope(uint8_t voice_num) { + if (voice_num >= kNumVoices) return; + + Voice& voice = voices_[voice_num]; + + // Update the voice state first (based on keys, etc.) + update_voice_state(voice_num); + + // Calculate the envelope value based on the current amplitude + voice.envx = calculate_envelope_value(voice.current_amplitude); + + // Apply the envelope value to the audio output + apply_envelope_to_output(voice_num); +} + +} // namespace emu +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/emu/audio/dsp.h b/src/app/emu/audio/dsp.h new file mode 100644 index 00000000..15dc161e --- /dev/null +++ b/src/app/emu/audio/dsp.h @@ -0,0 +1,315 @@ +#ifndef YAZE_APP_EMU_AUDIO_S_DSP_H +#define YAZE_APP_EMU_AUDIO_S_DSP_H + +#include +#include +#include + +#include "app/emu/memory/memory.h" + +namespace yaze { +namespace app { +namespace emu { + +using SampleFetcher = std::function; +using SamplePusher = std::function; + +/** + * + * The S-DSP is a digital signal processor generating the sound data. + * + * A DSP register can be selected with $F2, after which it can be read or + * written at $F3. Often it is useful to load the register address into A, and + * the value to send in Y, so that MOV $F2, YA can be used to do both in one + * 16-bit instruction. + * + * The DSP register address space only has 7 bits. The high bit of $F2, if set, + * will make the selected register read-only via $F3. + * + * When initializing the DSP registers for the first time, take care not to + * accidentally enable echo writeback via FLG, because it will immediately begin + * overwriting values in RAM. + * + * Voices + * There are 8 voices, numbered 0 to 7. + * Each voice X has 10 registers in the range $X0-$X9. + * + * Name Address Bits Notes + * VOL (L) $X0 SVVV VVVV Left channel volume, signed. + * VOL (R) $X1 SVVV VVVV Right channel volume, signed. + * P (L) $X2 LLLL LLLL Low 8 bits of sample pitch. + * P (H) $X3 --HH HHHH High 6 bits of sample pitch. + * SCRN $X4 SSSS SSSS Selects a sample source entry from the + * directory ADSR (1) $X5 EDDD AAAA ADSR enable (E), decay rate (D), + * attack rate (A). + * ADSR (2) $X6 SSSR RRRR Sustain level (S), release rate (R). + * GAIN $X7 0VVV VVVV 1MMV VVVV Mode (M), value (V). + * ENVX $X8 0VVV VVVV Reads current 7-bit value of ADSR/GAIN + * envelope. + * OUTX $X9 SVVV VVVV Reads signed 8-bit value of current + * sample wave multiplied by ENVX, before applying VOL. + * + */ + +class DigitalSignalProcessor { + private: + static const size_t kNumVoices = 8; + static const size_t kNumVoiceRegs = 10; + static const size_t kNumGlobalRegs = 15; + + enum class VoiceState { OFF, ATTACK, DECAY, SUSTAIN, RELEASE }; + + struct Voice { + int8_t vol_left; // x0 + int8_t vol_right; // x1 + uint8_t pitch_low; // x2 + uint8_t pitch_high; // x3 + uint8_t source_number; // x4 + uint8_t adsr1; // x5 + uint8_t adsr2; // x6 + uint8_t gain; // x7 + uint8_t envx; // x8 (read-only) + int8_t outx; // x9 (read-only) + + VoiceState state = VoiceState::OFF; + uint16_t current_amplitude = 0; // Current amplitude value used for ADSR + uint16_t decay_level; // Calculated decay level based on ADSR settings + }; + Voice voices_[8]; + + // Global DSP registers + uint8_t mvol_left; // 0C + uint8_t mvol_right; // 0D + uint8_t evol_left; // 0E + uint8_t evol_right; // 0F + uint8_t kon; // 10 + uint8_t koff; // 11 + uint8_t flags; // 12 + uint8_t endx; // 13 (read-only) + + // Global registers + std::vector globalRegs = std::vector(kNumGlobalRegs, 0x00); + + static const uint16_t ENVELOPE_MAX = 2047; // $7FF + + // Attack times in ms + const std::vector attackTimes = { + 4100, 2600, 1500, 1000, 640, 380, 260, 160, 96, 64, 40, 24, 16, 10, 6, 0}; + + // Decay times in ms + const std::vector decayTimes = {1200, 740, 440, 290, + 180, 110, 74, 37}; + + // Release times in ms + const std::vector releaseTimes = { + // "Infinite" is represented by a large value, e.g., UINT32_MAX + UINT32_MAX, 38000, 28000, 24000, 19000, 14000, 12000, 9400, + 7100, 5900, 4700, 3500, 2900, 2400, 1800, 1500, + 1200, 880, 740, 590, 440, 370, 290, 220, + 180, 150, 110, 92, 74, 55, 37, 18}; + + // Gain timings for decrease linear, decrease exponential, etc. + // Organized by mode: [Linear Increase, Bentline Increase, Linear Decrease, + // Exponential Decrease] + const std::vector> gainTimings = { + {UINT32_MAX, 3100, 2600, 2000, 1500, 1300, 1000, 770, 640, 510, 380, + 320, 260, 190, 160, 130, 96, 80, 64, 48, 40, 32, + 24, 20, 16, 12, 10, 8, 6, 4, 2}, + {UINT32_MAX, 5400, 4600, 3500, 2600, 2300, 1800, 1300, 1100, 900, + 670, 560, 450, 340, 280, 220, 170, 140, 110, 84, + 70, 56, 42, 35, 28, 21, 18, 14, 11, 7, + /*3.5=*/3}, + // Repeating the Linear Increase timings for Linear Decrease, since they + // are the same. + {UINT32_MAX, 3100, 2600, 2000, 1500, 1300, 1000, 770, 640, 510, 380, + 320, 260, 190, 160, 130, 96, 80, 64, 48, 40, 32, + 24, 20, 16, 12, 10, 8, 6, 4, 2}, + {UINT32_MAX, 38000, 28000, 24000, 19000, 14000, 12000, 9400, + 7100, 5900, 4700, 3500, 2900, 2400, 1800, 1500, + 1200, 880, 740, 590, 440, 370, 290, 220, + 180, 150, 110, 92, 55, 37, 18}}; + + // DSP Period Table + const std::vector> DigitalSignalProcessorPeriodTable = { + // ... Your DSP period table here ... + }; + + // DSP Period Offset + const std::vector DigitalSignalProcessorPeriodOffset = { + // ... Your DSP period offsets here ... + }; + + uint8_t calculate_envelope_value(uint16_t amplitude) const { + // Convert the 16-bit amplitude to an 8-bit envelope value + return amplitude >> 8; + } + + void apply_envelope_to_output(uint8_t voice_num) { + Voice& voice = voices_[voice_num]; + + // Scale the OUTX by the envelope value + // This might be a linear scaling, or more complex operations can be used + voice.outx = (voice.outx * voice.envx) / 255; + } + + SampleFetcher sample_fetcher_; + SamplePusher sample_pusher_; + + public: + DigitalSignalProcessor() = default; + + void Reset(); + + void SetSampleFetcher(std::function fetcher); + void SetSamplePusher(std::function pusher); + + // Read a byte from a voice register + uint8_t ReadVoiceReg(uint8_t voice, uint8_t reg) const; + + // Write a byte to a voice register + void WriteVoiceReg(uint8_t voice, uint8_t reg, uint8_t value); + + // Read a byte from a global register + uint8_t ReadGlobalReg(uint8_t reg) const { + return globalRegs[reg % kNumGlobalRegs]; + } + + // Write a byte to a global register + void WriteGlobalReg(uint8_t reg, uint8_t value) { + globalRegs[reg % kNumGlobalRegs] = value; + } + + int16_t DecodeSample(uint8_t voice_num); + int16_t ProcessSample(uint8_t voice_num, int16_t sample); + void MixSamples(); + + // Trigger a voice to start playing + void trigger_voice(uint8_t voice_num) { + if (voice_num >= kNumVoices) return; + + Voice& voice = voices_[voice_num]; + voice.state = VoiceState::ATTACK; + // Initialize other state management variables if needed + } + + // Release a voice (e.g., note release in ADSR) + void release_voice(uint8_t voice_num) { + if (voice_num >= kNumVoices) return; + + Voice& voice = voices_[voice_num]; + if (voice.state != VoiceState::OFF) { + voice.state = VoiceState::RELEASE; + } + // Update other state management variables if needed + } + + // Calculate envelope for a given voice + void UpdateEnvelope(uint8_t voice); + + // Voice-related functions (implementations) + void set_voice_volume(int voice_num, int8_t left, int8_t right) { + voices_[voice_num].vol_left = left; + voices_[voice_num].vol_right = right; + } + + void set_voice_pitch(int voice_num, uint16_t pitch) { + voices_[voice_num].pitch_low = pitch & 0xFF; + voices_[voice_num].pitch_high = (pitch >> 8) & 0xFF; + } + + void set_voice_source_number(int voice_num, uint8_t srcn) { + voices_[voice_num].source_number = srcn; + } + + void set_voice_adsr(int voice_num, uint8_t adsr1, uint8_t adsr2) { + voices_[voice_num].adsr1 = adsr1; + voices_[voice_num].adsr2 = adsr2; + } + + void set_voice_gain(int voice_num, uint8_t gain) { + voices_[voice_num].gain = gain; + } + + uint8_t read_voice_envx(int voice_num) { return voices_[voice_num].envx; } + + int8_t read_voice_outx(int voice_num) { return voices_[voice_num].outx; } + + // Global DSP functions + void set_master_volume(int8_t left, int8_t right) { + mvol_left = left; + mvol_right = right; + } + + void set_echo_volume(int8_t left, int8_t right) { + evol_left = left; + evol_right = right; + } + + void update_voice_state(uint8_t voice_num); + + // Override the key_on and key_off methods to utilize the new state management + void key_on(uint8_t value) { + for (uint8_t i = 0; i < kNumVoices; i++) { + if (value & (1 << i)) { + trigger_voice(i); + } + } + } + + void key_off(uint8_t value) { + for (uint8_t i = 0; i < kNumVoices; i++) { + if (value & (1 << i)) { + release_voice(i); + } + } + } + + void set_flags(uint8_t value) { + flags = value; + // More logic may be needed here depending on flag behaviors + } + + uint8_t read_endx() { return endx; } + + uint16_t AttackRate(uint8_t adsr1) { + // Convert the ATTACK portion of adsr1 into a rate of amplitude change + // You might need to adjust this logic based on the exact ADSR + // implementation details + return (adsr1 & 0x0F) * 16; // Just a hypothetical conversion + } + + uint16_t DecayRate(uint8_t adsr2) { + // Convert the DECAY portion of adsr2 into a rate of amplitude change + return ((adsr2 >> 4) & 0x07) * 8; // Hypothetical conversion + } + + uint16_t ReleaseRate(uint8_t adsr2) { + // Convert the RELEASE portion of adsr2 into a rate of amplitude change + return (adsr2 & 0x0F) * 16; // Hypothetical conversion + } + + uint16_t CalculateDecayLevel(uint8_t adsr2) { + // Calculate the decay level based on the SUSTAIN portion of adsr2 + // This is the level the amplitude will decay to before entering the SUSTAIN + // phase Again, adjust based on your implementation details + return ((adsr2 >> 4) & 0x07) * 256; // Hypothetical conversion + } + + // Envelope processing for all voices + // Goes through each voice and processes its envelope. + void process_envelopes() { + for (size_t i = 0; i < kNumVoices; ++i) { + process_envelope(i); + } + } + + // Envelope processing for a specific voice + // For a given voice, update its state (ADSR), calculate the envelope value, + // and apply the envelope to the audio output. + void process_envelope(uint8_t voice_num); +}; +} // namespace emu +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_EMU_AUDIO_S_DSP_H \ No newline at end of file diff --git a/src/app/emu/audio/internal/addressing.cc b/src/app/emu/audio/internal/addressing.cc new file mode 100644 index 00000000..069eb607 --- /dev/null +++ b/src/app/emu/audio/internal/addressing.cc @@ -0,0 +1,94 @@ +#include "app/emu/audio/spc700.h" + +namespace yaze { +namespace app { +namespace emu { + +// Immediate +uint8_t Spc700::imm() { + PC++; + return read(PC); +} + +// Direct page +uint8_t Spc700::dp() { + PC++; + uint8_t offset = read(PC); + return read((PSW.P << 8) + offset); +} + +uint8_t& Spc700::mutable_dp() { + PC++; + uint8_t offset = read(PC); + return mutable_read((PSW.P << 8) + offset); +} + +uint8_t Spc700::get_dp_addr() { + PC++; + uint8_t offset = read(PC); + return (PSW.P << 8) + offset; +} + +// Direct page indexed by X +uint8_t Spc700::dp_plus_x() { + PC++; + uint8_t offset = read(PC); + return read((PSW.P << 8) + offset + X); +} + +// Direct page indexed by Y +uint8_t Spc700::dp_plus_y() { + PC++; + uint8_t offset = read(PC); + return read((PSW.P << 8) + offset + Y); +} + +// Indexed indirect (add index before 16-bit lookup). +uint16_t Spc700::dp_plus_x_indirect() { + PC++; + uint16_t addr = read_16(PC + X); + return addr; +} + +// Indirect indexed (add index after 16-bit lookup). +uint16_t Spc700::dp_indirect_plus_y() { + PC++; + uint16_t offset = read_16(PC); + return offset + Y; +} + +uint16_t Spc700::abs() { + PC++; + uint16_t addr = read(PC) | (read(PC) << 8); + return addr; +} + +int8_t Spc700::rel() { + PC++; + return static_cast(read(PC)); +} + +uint8_t Spc700::i() { return read((PSW.P << 8) + X); } + +uint8_t Spc700::i_postinc() { + uint8_t value = read((PSW.P << 8) + X); + X++; + return value; +} + +uint16_t Spc700::addr_plus_i() { + PC++; + uint16_t addr = read(PC) | (read(PC) << 8); + return read(addr) + X; +} + +uint16_t Spc700::addr_plus_i_indexed() { + PC++; + uint16_t addr = read(PC) | (read(PC) << 8); + addr += X; + return read(addr) | (read(addr + 1) << 8); +} + +} // namespace emu +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/emu/audio/internal/instructions.cc b/src/app/emu/audio/internal/instructions.cc new file mode 100644 index 00000000..f8f973b1 --- /dev/null +++ b/src/app/emu/audio/internal/instructions.cc @@ -0,0 +1,361 @@ +#include "app/emu/audio/spc700.h" + +namespace yaze { +namespace app { +namespace emu { + +void Spc700::MOV(uint8_t& dest, uint8_t operand) { + dest = operand; + PSW.Z = (operand == 0); + PSW.N = (operand & 0x80); +} + +void Spc700::MOV_ADDR(uint16_t address, uint8_t operand) { + write(address, operand); + PSW.Z = (operand == 0); + PSW.N = (operand & 0x80); +} + +void Spc700::ADC(uint8_t& dest, uint8_t operand) { + uint16_t result = dest + operand + PSW.C; + PSW.V = ((A ^ result) & (operand ^ result) & 0x80); + PSW.C = (result > 0xFF); + PSW.Z = ((result & 0xFF) == 0); + PSW.N = (result & 0x80); + PSW.H = ((A ^ operand ^ result) & 0x10); + dest = result & 0xFF; +} + +void Spc700::SBC(uint8_t& dest, uint8_t operand) { + uint16_t result = dest - operand - (1 - PSW.C); + PSW.V = ((dest ^ result) & (dest ^ operand) & 0x80); + PSW.C = (result < 0x100); + PSW.Z = ((result & 0xFF) == 0); + PSW.N = (result & 0x80); + PSW.H = ((dest ^ operand ^ result) & 0x10); + dest = result & 0xFF; +} + +void Spc700::CMP(uint8_t& dest, uint8_t operand) { + uint16_t result = dest - operand; + PSW.C = (result < 0x100); + PSW.Z = ((result & 0xFF) == 0); + PSW.N = (result & 0x80); +} + +void Spc700::AND(uint8_t& dest, uint8_t operand) { + dest &= operand; + PSW.Z = (dest == 0); + PSW.N = (dest & 0x80); +} + +void Spc700::OR(uint8_t& dest, uint8_t operand) { + dest |= operand; + PSW.Z = (dest == 0); + PSW.N = (dest & 0x80); +} + +void Spc700::EOR(uint8_t& dest, uint8_t operand) { + dest ^= operand; + PSW.Z = (dest == 0); + PSW.N = (dest & 0x80); +} + +void Spc700::ASL(uint8_t operand) { + PSW.C = (operand & 0x80); + operand <<= 1; + PSW.Z = (operand == 0); + PSW.N = (operand & 0x80); + // A = value; +} + +void Spc700::LSR(uint8_t& operand) { + PSW.C = (operand & 0x01); + operand >>= 1; + PSW.Z = (operand == 0); + PSW.N = (operand & 0x80); +} + +void Spc700::ROL(uint8_t operand, bool isImmediate) { + uint8_t value = isImmediate ? imm() : operand; + uint8_t carry = PSW.C; + PSW.C = (value & 0x80); + value <<= 1; + value |= carry; + PSW.Z = (value == 0); + PSW.N = (value & 0x80); + // operand = value; +} + +void Spc700::XCN(uint8_t operand, bool isImmediate) { + uint8_t value = isImmediate ? imm() : operand; + value = ((value & 0xF0) >> 4) | ((value & 0x0F) << 4); + PSW.Z = (value == 0); + PSW.N = (value & 0x80); + // operand = value; +} + +void Spc700::INC(uint8_t& operand) { + operand++; + PSW.Z = (operand == 0); + PSW.N = (operand & 0x80); +} + +void Spc700::DEC(uint8_t& operand) { + operand--; + PSW.Z = (operand == 0); + PSW.N = (operand & 0x80); +} + +void Spc700::MOVW(uint16_t& dest, uint16_t operand) { + dest = operand; + PSW.Z = (operand == 0); + PSW.N = (operand & 0x8000); +} + +void Spc700::INCW(uint16_t& operand) { + operand++; + PSW.Z = (operand == 0); + PSW.N = (operand & 0x8000); +} + +void Spc700::DECW(uint16_t& operand) { + operand--; + PSW.Z = (operand == 0); + PSW.N = (operand & 0x8000); +} + +void Spc700::ADDW(uint16_t& dest, uint16_t operand) { + uint32_t result = dest + operand; + PSW.C = (result > 0xFFFF); + PSW.Z = ((result & 0xFFFF) == 0); + PSW.N = (result & 0x8000); + PSW.V = ((dest ^ result) & (operand ^ result) & 0x8000); + dest = result & 0xFFFF; +} + +void Spc700::SUBW(uint16_t& dest, uint16_t operand) { + uint32_t result = dest - operand; + PSW.C = (result < 0x10000); + PSW.Z = ((result & 0xFFFF) == 0); + PSW.N = (result & 0x8000); + PSW.V = ((dest ^ result) & (dest ^ operand) & 0x8000); + dest = result & 0xFFFF; +} + +void Spc700::CMPW(uint16_t operand) { + uint32_t result = YA - operand; + PSW.C = (result < 0x10000); + PSW.Z = ((result & 0xFFFF) == 0); + PSW.N = (result & 0x8000); +} + +void Spc700::MUL(uint8_t operand) { + uint16_t result = A * operand; + YA = result; + PSW.Z = (result == 0); + PSW.N = (result & 0x8000); +} + +void Spc700::DIV(uint8_t operand) { + if (operand == 0) { + // Handle divide by zero error + return; + } + uint8_t quotient = A / operand; + uint8_t remainder = A % operand; + A = quotient; + Y = remainder; + PSW.Z = (quotient == 0); + PSW.N = (quotient & 0x80); +} + +void Spc700::BRA(int8_t offset) { PC += offset; } + +void Spc700::BEQ(int8_t offset) { + if (PSW.Z) { + PC += offset; + } +} + +void Spc700::BNE(int8_t offset) { + if (!PSW.Z) { + PC += offset; + } +} + +void Spc700::BCS(int8_t offset) { + if (PSW.C) { + PC += offset; + } +} + +void Spc700::BCC(int8_t offset) { + if (!PSW.C) { + PC += offset; + } +} + +void Spc700::BVS(int8_t offset) { + if (PSW.V) { + PC += offset; + } +} + +void Spc700::BVC(int8_t offset) { + if (!PSW.V) { + PC += offset; + } +} + +void Spc700::BMI(int8_t offset) { + if (PSW.N) { + PC += offset; + } +} + +void Spc700::BPL(int8_t offset) { + if (!PSW.N) { + PC += offset; + } +} + +void Spc700::BBS(uint8_t bit, uint8_t operand) { + if (operand & (1 << bit)) { + PC += rel(); + } +} + +void Spc700::BBC(uint8_t bit, uint8_t operand) { + if (!(operand & (1 << bit))) { + PC += rel(); + } +} + +// CBNE DBNZ +// JMP +void Spc700::JMP(uint16_t address) { PC = address; } + +void Spc700::CALL(uint16_t address) { + uint16_t return_address = PC + 2; + write(SP, return_address & 0xFF); + write(SP - 1, (return_address >> 8) & 0xFF); + SP -= 2; + PC = address; +} + +void Spc700::PCALL(uint8_t offset) { + uint16_t return_address = PC + 2; + write(SP, return_address & 0xFF); + write(SP - 1, (return_address >> 8) & 0xFF); + SP -= 2; + PC += offset; +} + +void Spc700::TCALL(uint8_t offset) { + uint16_t return_address = PC + 2; + write(SP, return_address & 0xFF); + write(SP - 1, (return_address >> 8) & 0xFF); + SP -= 2; + PC = 0xFFDE + offset; +} + +void Spc700::BRK() { + uint16_t return_address = PC + 2; + write(SP, return_address & 0xFF); + write(SP - 1, (return_address >> 8) & 0xFF); + SP -= 2; + PC = 0xFFDE; +} + +void Spc700::RET() { + uint16_t return_address = read(SP) | (read(SP + 1) << 8); + SP += 2; + PC = return_address; +} + +void Spc700::RETI() { + uint16_t return_address = read(SP) | (read(SP + 1) << 8); + SP += 2; + PC = return_address; + PSW.I = 1; +} + +void Spc700::PUSH(uint8_t operand) { + write(SP, operand); + SP--; +} + +void Spc700::POP(uint8_t& operand) { + SP++; + operand = read(SP); +} + +void Spc700::SET1(uint8_t bit, uint8_t& operand) { operand |= (1 << bit); } + +void Spc700::CLR1(uint8_t bit, uint8_t& operand) { operand &= ~(1 << bit); } + +void Spc700::TSET1(uint8_t bit, uint8_t& operand) { + PSW.C = (operand & (1 << bit)); + operand |= (1 << bit); +} + +void Spc700::TCLR1(uint8_t bit, uint8_t& operand) { + PSW.C = (operand & (1 << bit)); + operand &= ~(1 << bit); +} + +void Spc700::AND1(uint8_t bit, uint8_t& operand) { + operand &= (1 << bit); + PSW.Z = (operand == 0); + PSW.N = (operand & 0x80); +} + +void Spc700::OR1(uint8_t bit, uint8_t& operand) { + operand |= (1 << bit); + PSW.Z = (operand == 0); + PSW.N = (operand & 0x80); +} + +void Spc700::EOR1(uint8_t bit, uint8_t& operand) { + operand ^= (1 << bit); + PSW.Z = (operand == 0); + PSW.N = (operand & 0x80); +} + +void Spc700::NOT1(uint8_t bit, uint8_t& operand) { + operand ^= (1 << bit); + PSW.Z = (operand == 0); + PSW.N = (operand & 0x80); +} + +void Spc700::MOV1(uint8_t bit, uint8_t& operand) { + PSW.C = (operand & (1 << bit)); + operand |= (1 << bit); +} + +void Spc700::CLRC() { PSW.C = 0; } + +void Spc700::SETC() { PSW.C = 1; } + +void Spc700::NOTC() { PSW.C = !PSW.C; } + +void Spc700::CLRV() { PSW.V = 0; } + +void Spc700::CLRP() { PSW.P = 0; } + +void Spc700::SETP() { PSW.P = 1; } + +void Spc700::EI() { PSW.I = 1; } + +void Spc700::DI() { PSW.I = 0; } + +void Spc700::NOP() { PC++; } + +void Spc700::SLEEP() {} + +void Spc700::STOP() {} + +} // namespace emu +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/emu/audio/internal/opcodes.h b/src/app/emu/audio/internal/opcodes.h new file mode 100644 index 00000000..15a48cc7 --- /dev/null +++ b/src/app/emu/audio/internal/opcodes.h @@ -0,0 +1,263 @@ +#pragma once + +#include +#include +#include + +const std::unordered_map spc_opcode_map = { + {0x00, "NOP"}, + {0x01, "TCALL0"}, + {0x02, "SET1 direct.0"}, + {0x03, "BBS direct.0,rel"}, + {0x04, "OR A,direct"}, + {0x05, "OR A,abs"}, + {0x06, "OR A,(X)"}, + {0x07, "OR A,(direct+X)"}, + {0x08, "OR A,#imm"}, + {0x09, "OR direct,imm"}, + {0x0A, "OR1 C,membit"}, + {0x0B, "ASL direct"}, + {0x0C, "ASL abs"}, + {0x0D, "PUSH PSW"}, + {0x0E, "TSET1 abs"}, + {0x0F, "BRK"}, + {0x10, "BPL rel"}, + {0x11, "TCALL1"}, + {0x12, "CLR1 direct.0"}, + {0x13, "BBC direct.0,rel"}, + {0x14, "OR A,direct+X"}, + {0x15, "OR A,abs+X"}, + {0x16, "OR A,abs+Y"}, + {0x17, "OR A,(direct)+Y"}, + {0x18, "OR direct,direct"}, + {0x19, "OR (X),(Y)"}, + {0x1A, "DECW direct"}, + {0x1B, "ASL direct+X"}, + {0x1C, "ASL A"}, + {0x1D, "DEC X"}, + {0x1E, "CMP X,abs"}, + {0x1F, "JMP (abs+X)"}, + {0x20, "CLRP"}, + {0x21, "TCALL2"}, + {0x22, "SET1 direct.1"}, + {0x23, "BBS direct.1,rel"}, + {0x24, "AND A,direct"}, + {0x25, "AND A,abs"}, + {0x26, "AND A,(X)"}, + {0x27, "AND A,(direct+X)"}, + {0x28, "AND A,#imm"}, + {0x29, "AND direct,imm"}, + {0x2A, "OR1 C,/membit"}, + {0x2B, "ROL direct"}, + {0x2C, "ROL abs"}, + {0x2D, "PUSH A"}, + {0x2E, "CBNE direct,rel"}, + {0x2F, "BRA rel"}, + {0x30, "BMI rel"}, + {0x31, "TCALL3"}, + {0x32, "CLR1 direct.1"}, + {0x33, "BBC direct.1,rel"}, + {0x34, "AND A,direct+X"}, + {0x35, "AND A,abs+X"}, + {0x36, "AND A,abs+Y"}, + {0x37, "AND A,(direct)+Y"}, + {0x38, "AND direct,direct"}, + {0x39, "AND (X),(Y)"}, + {0x3A, "INCW direct"}, + {0x3B, "ROL direct+X"}, + {0x3C, "ROL A"}, + {0x3D, "INC X"}, + {0x3E, "CMP X,direct"}, + {0x3F, "CALL abs"}, + {0x40, "SETP"}, + {0x41, "TCALL4"}, + {0x42, "SET1 direct.2"}, + {0x43, "BBS direct.2,rel"}, + {0x44, "EOR A,direct"}, + {0x45, "EOR A,abs"}, + {0x46, "EOR A,(X)"}, + {0x47, "EOR A,(direct+X)"}, + {0x48, "EOR A,#imm"}, + {0x49, "EOR direct,imm"}, + {0x4A, "AND1 C,membit"}, + {0x4B, "LSR direct"}, + {0x4C, "LSR abs"}, + {0x4D, "PUSH X"}, + {0x4E, "TCLR1 abs"}, + {0x4F, "PCALL addr"}, + {0x50, "BVC rel"}, + {0x51, "TCALL5"}, + {0x52, "CLR1 direct.2"}, + {0x53, "BBC direct.2,rel"}, + {0x54, "EOR A,direct+X"}, + {0x55, "EOR A,abs+X"}, + {0x56, "EOR A,abs+Y"}, + {0x57, "EOR A,(direct)+Y"}, + {0x58, "EOR direct,direct"}, + {0x59, "EOR (X),(Y)"}, + {0x5A, "CMPW YA,direct"}, + {0x5B, "LSR direct+X"}, + {0x5C, "LSR A"}, + {0x5D, "MOV X,A"}, + {0x5E, "CMP Y,abs"}, + {0x5F, "JMP abs"}, + {0x60, "CLRC"}, + {0x61, "TCALL6"}, + {0x62, "SET1 direct.3"}, + {0x63, "BBS direct.3,rel"}, + {0x64, "CMP A,direct"}, + {0x65, "CMP A,abs"}, + {0x66, "CMP A,(X)"}, + {0x67, "CMP A,(direct+X)"}, + {0x68, "CMP A,#imm"}, + {0x69, "CMP direct,imm"}, + {0x6A, "AND1 C,/membit"}, + {0x6B, "ROR direct"}, + {0x6C, "ROR abs"}, + {0x6D, "PUSH Y"}, + {0x6E, "DBNZ direct,rel"}, + {0x6F, "RET"}, + {0x70, "BVS rel"}, + {0x71, "TCALL7"}, + {0x72, "CLR1 direct.3"}, + {0x73, "BBC direct.3,rel"}, + {0x74, "CMP A,direct+X"}, + {0x75, "CMP A,abs+X"}, + {0x76, "CMP A,abs+Y"}, + {0x77, "CMP A,(direct)+Y"}, + {0x78, "CMP direct,direct"}, + {0x79, "CMP (X),(Y)"}, + {0x7A, "ADDW YA,direct"}, + {0x7B, "ROR direct+X"}, + {0x7C, "ROR A"}, + {0x7D, "MOV A,X"}, + {0x7E, "CMP Y,direct"}, + {0x7F, "RETI"}, + {0x80, "SETC"}, + {0x81, "TCALL8"}, + {0x82, "SET1 direct.4"}, + {0x83, "BBS direct.4,rel"}, + {0x84, "ADC A,direct"}, + {0x85, "ADC A,abs"}, + {0x86, "ADC A,(X)"}, + {0x87, "ADC A,(direct+X)"}, + {0x88, "ADC A,#imm"}, + {0x89, "ADC direct,imm"}, + {0x8A, "EOR1 C,membit"}, + {0x8B, "DEC direct"}, + {0x8C, "DEC abs"}, + {0x8D, "MOV Y,#imm"}, + {0x8E, "POP PSW"}, + {0x8F, "MOV direct,#imm"}, + {0x90, "BCC rel"}, + {0x91, "TCALL9"}, + {0x92, "CLR1 direct.4"}, + {0x93, "BBC direct.4,rel"}, + {0x94, "ADC A,direct+X"}, + {0x95, "ADC A,abs+X"}, + {0x96, "ADC A,abs+Y"}, + {0x97, "ADC A,(direct)+Y"}, + {0x98, "ADC direct,direct"}, + {0x99, "ADC (X),(Y)"}, + {0x9A, "SUBW YA,direct"}, + {0x9B, "DEC direct+X"}, + {0x9C, "DEC A"}, + {0x9D, "MOV X,SP"}, + {0x9E, "DIV YA,X"}, + {0x9F, "XCN A"}, + {0xA0, "EI"}, + {0xA1, "TCALL10"}, + {0xA2, "SET1 direct.5"}, + {0xA3, "BBS direct.5,rel"}, + {0xA4, "SBC A,direct"}, + {0xA5, "SBC A,abs"}, + {0xA6, "SBC A,(X)"}, + {0xA7, "SBC A,(direct+X)"}, + {0xA8, "SBC A,#imm"}, + {0xA9, "SBC direct,imm"}, + {0xAA, "MOV1 C,membit"}, + {0xAB, "INC direct"}, + {0xAC, "INC abs"}, + {0xAD, "CMP Y,#imm"}, + {0xAE, "POP A"}, + {0xAF, "MOV (X)+,A"}, + {0xB0, "BCS rel"}, + {0xB1, "TCALL11"}, + {0xB2, "CLR1 direct.5"}, + {0xB3, "BBC direct.5,rel"}, + {0xB4, "SBC A,direct+X"}, + {0xB5, "SBC A,abs+X"}, + {0xB6, "SBC A,abs+Y"}, + {0xB7, "SBC A,(direct)+Y"}, + {0xB8, "SBC direct,direct"}, + {0xB9, "SBC (X),(Y)"}, + {0xBA, "MOVW YA,direct"}, + {0xBB, "INC direct+X"}, + {0xBC, "INC A"}, + {0xBD, "MOV SP,X"}, + {0xBE, "DAS"}, + {0xBF, "MOV A,(X)+"}, + {0xC0, "DI"}, + {0xC1, "TCALL12"}, + {0xC2, "SET1 direct.6"}, + {0xC3, "BBS direct.6,rel"}, + {0xC4, "MOV direct,A"}, + {0xC5, "MOV abs,A"}, + {0xC6, "MOV (X),A"}, + {0xC7, "MOV (direct+X),A"}, + {0xC8, "CMP X,#imm"}, + {0xC9, "MOV abs,X"}, + {0xCA, "MOV1 membit,C"}, + {0xCB, "MOV direct,Y"}, + {0xCC, "MOV abs,Y"}, + {0xCD, "MOV X,#imm"}, + {0xCE, "POP X"}, + {0xCF, "MUL YA"}, + {0xD0, "BNE rel"}, + {0xD1, "TCALL13"}, + {0xD2, "CLR1 direct.6"}, + {0xD3, "BBC direct.6,rel"}, + {0xD4, "MOV direct+X,A"}, + {0xD5, "MOV abs+X,A"}, + {0xD6, "MOV abs+Y,A"}, + {0xD7, "MOV (direct)+Y,A"}, + {0xD8, "MOV direct,X"}, + {0xD9, "MOV direct+Y,X"}, + {0xDA, "MOVW direct,YA"}, + {0xDB, "MOV direct+X,Y"}, + {0xDC, "DEC Y"}, + {0xDD, "MOV A,Y"}, + {0xDE, "CBNE direct+X,rel"}, + {0xDF, "DAA"}, + {0xE0, "CLRV"}, + {0xE1, "TCALL14"}, + {0xE2, "SET1 direct.7"}, + {0xE3, "BBS direct.7,rel"}, + {0xE4, "MOV A,direct"}, + {0xE5, "MOV A,abs"}, + {0xE6, "MOV A,(X)"}, + {0xE7, "MOV A,(direct+X)"}, + {0xE8, "MOV A,#imm"}, + {0xE9, "MOV X,abs"}, + {0xEA, "NOT1 membit"}, + {0xEB, "MOV Y,direct"}, + {0xEC, "MOV Y,abs"}, + {0xED, "NOTC"}, + {0xEE, "POP Y"}, + {0xEF, "SLEEP"}, + {0xF0, "BEQ rel"}, + {0xF1, "TCALL15"}, + {0xF2, "CLR1 direct.7"}, + {0xF3, "BBC direct.7,rel"}, + {0xF4, "MOV A,direct+X"}, + {0xF5, "MOV A,abs+X"}, + {0xF6, "MOV A,abs+Y"}, + {0xF7, "MOV A,(direct)+Y"}, + {0xF8, "MOV X,direct"}, + {0xF9, "MOV X,direct+Y"}, + {0xFA, "MOV direct,S"}, + {0xFB, "MOV Y,direct+X"}, + {0xFC, "INC Y"}, + {0xFD, "MOV Y,A"}, + {0xFE, "DBNZ Y,rel"}, + {0xFF, "STOP"}}; diff --git a/src/app/emu/audio/spc700.cc b/src/app/emu/audio/spc700.cc new file mode 100644 index 00000000..5fb8e162 --- /dev/null +++ b/src/app/emu/audio/spc700.cc @@ -0,0 +1,957 @@ +#include "app/emu/audio/spc700.h" + +#include +#include +#include +#include +#include + +#include "app/emu/audio/internal/opcodes.h" + +namespace yaze { +namespace app { +namespace emu { + +void Spc700::Reset() { + PC = 0; + A = 0; + X = 0; + Y = 0; + SP = 0xFF; + PSW = ByteToFlags(0x00); + aram_.reset(); +} + +void Spc700::BootIplRom() { + PC = 0xFFC0; + A = 0; + X = 0; + Y = 0; + int i = 0; + while (PC != 0xFFC0 + 0x3F) { + uint8_t opcode = read(PC); + ExecuteInstructions(opcode); + PC++; + i++; + + if (i > 1000) { + break; + } + } +} + +void Spc700::ExecuteInstructions(uint8_t opcode) { + uint16_t initialPC = PC; + switch (opcode) { + // 8-bit Move Memory to Register + case 0xE8: // MOV A, #imm + { + MOV(A, imm()); + break; + } + case 0xE6: // MOV A, (X) + { + MOV(A, X); + break; + } + case 0xBF: // MOV A, (X)+ + { + MOV(A, X); + X++; + break; + } + case 0xE4: // MOV A, dp + { + MOV(A, dp()); + break; + } + case 0xF4: // MOV A, dp+X + { + MOV(A, dp_plus_x()); + break; + } + case 0xE5: // MOV A, !abs + { + MOV(A, read(abs())); + break; + } + case 0xF5: // MOV A, !abs+X + { + MOV(A, abs() + X); + break; + } + case 0xF6: // MOV A, !abs+Y + { + MOV(A, abs() + Y); + break; + } + case 0xE7: // MOV A, [dp+X] + { + MOV(A, read(dp_plus_x_indirect())); + break; + } + case 0xF7: // MOV A, [dp]+Y + { + MOV(A, read(dp_indirect_plus_y())); + break; + } + case 0xCD: // MOV X, #imm + { + MOV(X, imm()); + break; + } + case 0xF8: // MOV X, dp + { + MOV(X, dp()); + break; + } + case 0xF9: // MOV X, dp+Y + { + MOV(X, dp_plus_y()); + break; + } + case 0xE9: // MOV X, !abs + { + MOV(X, abs()); + break; + } + case 0x8D: // MOV Y, #imm + { + MOV(Y, imm()); + break; + } + case 0xEB: // MOV Y, dp + { + MOV(Y, dp()); + break; + } + case 0xFB: // MOV Y, dp+X + { + MOV(Y, dp_plus_x()); + break; + } + case 0xEC: // MOV Y, !abs + { + MOV(Y, abs()); + break; + } + + // 8-bit move register to memory + case 0xC6: // MOV (X), A + { + MOV_ADDR(X, A); + break; + } + case 0xAF: // MOV (X)+, A + { + MOV_ADDR(X, A); + break; + } + case 0xC4: // MOV dp, A + { + MOV_ADDR(get_dp_addr(), A); + break; + } + case 0xD4: // MOV dp+X, A + { + MOV_ADDR(get_dp_addr() + X, A); + break; + } + case 0xC5: // MOV !abs, A + { + MOV_ADDR(abs(), A); + break; + } + case 0xD5: // MOV !abs+X, A + { + MOV_ADDR(abs() + X, A); + break; + } + case 0xD6: // MOV !abs+Y, A + { + MOV_ADDR(abs() + Y, A); + break; + } + case 0xC7: // MOV [dp+X], A + { + MOV_ADDR(dp_plus_x_indirect(), A); + break; + } + case 0xD7: // MOV [dp]+Y, A + { + MOV_ADDR(dp_indirect_plus_y(), A); + break; + } + case 0xD8: // MOV dp, X + { + MOV_ADDR(get_dp_addr(), X); + break; + } + case 0xD9: // MOV dp+Y, X + { + MOV_ADDR(get_dp_addr() + Y, X); + break; + } + case 0xC9: // MOV !abs, X + { + MOV_ADDR(abs(), X); + break; + } + case 0xCB: // MOV dp, Y + { + MOV_ADDR(get_dp_addr(), Y); + break; + } + case 0xDB: // MOV dp+X, Y + { + MOV_ADDR(get_dp_addr() + X, Y); + break; + } + case 0xCC: // MOV !abs, Y + { + MOV_ADDR(abs(), Y); + break; + } + + // . 8-bit move register to register / special direct page moves + case 0x7D: // MOV A, X + { + MOV(A, X); + break; + } + case 0xDD: // MOV A, Y + { + MOV(A, Y); + break; + } + case 0x5D: // MOV X, A + { + MOV(X, A); + break; + } + case 0xFD: // MOV Y, A + { + MOV(Y, A); + break; + } + case 0x9D: // MOV X, SP + { + MOV(X, SP); + break; + } + case 0xBD: // MOV SP, X + { + MOV(SP, X); + break; + } + case 0xFA: // MOV dp, dp + { + MOV_ADDR(get_dp_addr(), dp()); + break; + } + case 0x8F: // MOV dp, #imm + { + MOV_ADDR(get_dp_addr(), imm()); + break; + } + + // . 8-bit arithmetic + case 0x88: // ADC A, #imm + { + ADC(A, imm()); + break; + } + case 0x86: // ADC A, (X) + { + ADC(A, X); + break; + } + case 0x84: // ADC A, dp + { + ADC(A, dp()); + break; + } + case 0x94: // ADC A, dp+X + { + ADC(A, dp_plus_x()); + break; + } + case 0x85: // ADC A, !abs + { + ADC(A, abs()); + break; + } + case 0x95: // ADC A, !abs+X + { + ADC(A, abs() + X); + break; + } + case 0x96: // ADC A, !abs+Y + { + ADC(A, abs() + Y); + break; + } + case 0x87: // ADC A, [dp+X] + { + ADC(A, dp_plus_x_indirect()); + break; + } + case 0x97: // ADC A, [dp]+Y + { + ADC(A, dp_indirect_plus_y()); + break; + } + case 0x99: // ADC (X), (Y) + break; + case 0x89: // ADC dp, dp + { + ADC(mutable_dp(), dp()); + break; + } + case 0x98: // ADC dp, #imm + { + ADC(mutable_dp(), imm()); + break; + } + + case 0xA8: // SBC A, #imm + { + SBC(A, imm()); + break; + } + case 0xA6: // SBC A, (X) + { + SBC(A, mutable_read(X)); + break; + } + case 0xA4: // SBC A, dp + { + SBC(A, dp()); + break; + } + case 0xB4: // SBC A, dp+X + { + SBC(A, dp_plus_x()); + break; + } + case 0xA5: // SBC A, !abs + { + SBC(A, abs()); + break; + } + case 0xB5: // SBC A, !abs+X + { + SBC(A, abs() + X); + break; + } + case 0xB6: // SBC A, !abs+Y + { + SBC(A, abs() + Y); + break; + } + case 0xA7: // SBC A, [dp+X] + { + SBC(A, dp_plus_x_indirect()); + break; + } + case 0xB7: // SBC A, [dp]+Y + { + SBC(A, dp_indirect_plus_y()); + break; + } + case 0xB9: // SBC (X), (Y) + { + SBC(mutable_read(X), mutable_read(Y)); + break; + } + case 0xA9: // SBC dp, dp + { + SBC(mutable_dp(), dp()); + break; + } + case 0xB8: // SBC dp, #imm + { + SBC(mutable_dp(), imm()); + break; + } + + case 0x68: // CMP A, #imm + { + CMP(A, imm()); + break; + } + case 0x66: // CMP A, (X) + { + CMP(A, read(X)); + break; + } + case 0x64: // CMP A, dp + { + CMP(A, dp()); + break; + } + case 0x74: // CMP A, dp+X + { + CMP(A, dp_plus_x()); + break; + } + case 0x65: // CMP A, !abs + { + CMP(A, abs()); + break; + } + case 0x75: // CMP A, !abs+X + { + CMP(A, abs() + X); + break; + } + case 0x76: // CMP A, !abs+Y + { + CMP(A, abs() + Y); + break; + } + case 0x67: // CMP A, [dp+X] + break; + case 0x77: // CMP A, [dp]+Y + break; + case 0x79: // CMP (X), (Y) + break; + case 0x69: // CMP dp, dp + { + CMP(mutable_dp(), dp()); + break; + } + case 0x78: // CMP dp, #imm + { + CMP(mutable_dp(), imm()); + break; + } + case 0xC8: // CMP X, #imm + { + CMP(X, imm()); + break; + } + case 0x3E: // CMP X, dp + { + CMP(X, dp()); + break; + } + case 0x1E: // CMP X, !abs + { + CMP(X, abs()); + break; + } + case 0xAD: // CMP Y, #imm + { + CMP(Y, imm()); + break; + } + case 0x7E: // CMP Y, dp + { + CMP(Y, dp()); + break; + } + case 0x5E: // CMP Y, !abs + { + CMP(Y, abs()); + break; + } + + // 8-bit boolean logic + case 0x28: // AND A, #imm + { + AND(A, imm()); + break; + } + case 0x26: // AND A, (X) + { + AND(A, mutable_read(X)); + break; + } + case 0x24: // AND A, dp + { + AND(A, dp()); + break; + } + case 0x34: // AND A, dp+X + { + AND(A, dp_plus_x()); + break; + } + case 0x25: // AND A, !abs + { + AND(A, abs()); + break; + } + case 0x35: // AND A, !abs+X + { + AND(A, abs() + X); + break; + } + case 0x36: // AND A, !abs+Y + { + AND(A, abs() + Y); + break; + } + case 0x27: // AND A, [dp+X] + { + AND(A, dp_plus_x_indirect()); + break; + } + case 0x37: // AND A, [dp]+Y + { + AND(A, dp_indirect_plus_y()); + break; + } + case 0x39: // AND (X), (Y) + { + AND(mutable_read(X), mutable_read(Y)); + break; + } + case 0x29: // AND dp, dp + { + AND(mutable_dp(), dp()); + break; + } + case 0x38: // AND dp, #imm + { + AND(mutable_dp(), imm()); + break; + } + + case 0x08: // OR A, #imm + { + OR(A, imm()); + break; + } + case 0x06: // OR A, (X) + { + OR(A, mutable_read(X)); + break; + } + case 0x04: // OR A, dp + { + OR(A, dp()); + break; + } + case 0x14: // OR A, dp+X + { + OR(A, dp_plus_x()); + break; + } + case 0x05: // OR A, !abs + { + OR(A, abs()); + break; + } + case 0x15: // OR A, !abs+X + { + OR(A, abs() + X); + break; + } + case 0x16: // OR A, !abs+Y + { + OR(A, abs() + Y); + break; + } + case 0x07: // OR A, [dp+X] + { + OR(A, dp_plus_x_indirect()); + break; + } + case 0x17: // OR A, [dp]+Y + { + OR(A, dp_indirect_plus_y()); + break; + } + case 0x19: // OR (X), (Y) + OR(mutable_read(X), mutable_read(Y)); + break; + case 0x09: // OR dp, dp + OR(mutable_dp(), dp()); + break; + case 0x18: // OR dp, #imm + OR(mutable_dp(), imm()); + break; + case 0x48: // EOR A, #imm + EOR(A, imm()); + break; + case 0x46: // EOR A, (X) + EOR(A, mutable_read(X)); + break; + case 0x44: // EOR A, dp + EOR(A, dp()); + break; + case 0x54: // EOR A, dp+X + EOR(A, dp_plus_x()); + break; + case 0x45: // EOR A, !abs + EOR(A, abs()); + break; + case 0x55: // EOR A, !abs+X + EOR(A, abs() + X); + break; + case 0x56: // EOR A, !abs+Y + EOR(A, abs() + Y); + break; + case 0x47: // EOR A, [dp+X] + EOR(A, dp_plus_x_indirect()); + break; + case 0x57: // EOR A, [dp]+Y + EOR(A, dp_indirect_plus_y()); + break; + case 0x59: // EOR (X), (Y) + EOR(mutable_read(X), mutable_read(Y)); + break; + case 0x49: // EOR dp, dp + EOR(mutable_dp(), dp()); + break; + case 0x58: // EOR dp, #imm + EOR(mutable_dp(), imm()); + break; + + // . 8-bit increment / decrement + + case 0xBC: // INC A + INC(A); + break; + case 0xAB: // INC dp + INC(mutable_dp()); + break; + case 0xBB: // INC dp+X + INC(mutable_read((PSW.P << 8) + dp_plus_x())); + break; + case 0xAC: // INC !abs + INC(mutable_read(abs())); + break; + case 0x3D: // INC X + INC(X); + break; + case 0xFC: // INC Y + INC(Y); + break; + case 0x9C: // DEC A + DEC(A); + break; + case 0x8B: // DEC dp + DEC(mutable_dp()); + break; + case 0x9B: // DEC dp+X + DEC(mutable_read((PSW.P << 8) + dp_plus_x())); + break; + case 0x8C: // DEC !abs + DEC(mutable_read(abs())); + break; + case 0x1D: // DEC X + DEC(X); + break; + case 0xDC: // DEC Y + DEC(Y); + break; + + // 8-bit shift / rotation + + case 0x1C: // ASL A + ASL(A); + break; + case 0x0B: // ASL dp + ASL(dp()); + break; + case 0x1B: // ASL dp+X + ASL(dp_plus_x()); + break; + case 0x0C: // ASL !abs + ASL(abs()); + break; + case 0x5C: // LSR A + LSR(A); + break; + case 0x4B: // LSR dp + LSR(mutable_dp()); + break; + case 0x5B: // LSR dp+X + LSR(mutable_read((PSW.P << 8) + dp_plus_x())); + break; + case 0x4C: // LSR !abs + LSR(mutable_read(abs())); + break; + + case 0x3C: // ROL A + ROL(A); + break; + case 0x2B: // ROL dp + ROL(dp()); + break; + case 0x3B: // ROL dp+X + ROL(dp_plus_x()); + break; + case 0x2C: // ROL !abs + ROL(abs()); + break; + case 0x7C: // ROR A + // ROR(A); + break; + case 0x6B: // ROR dp + // ROR(dp()); + break; + case 0x7B: // ROR dp+X + // ROR(dp_plus_x()); + break; + case 0x6C: // ROR !abs + // ROR(abs()); + break; + case 0x9F: // XCN A Exchange nibbles of A + XCN(A); + break; + + // . 16-bit operations + + case 0xBA: // MOVW YA, dp + MOVW(YA, dp()); + break; + case 0xDA: // MOVW dp, YA + MOVW(mutable_read_16(dp()), YA); + break; + case 0x3A: // INCW dp + INCW(mutable_read_16(dp())); + break; + case 0x1A: // DECW dp + DECW(mutable_read_16(dp())); + break; + case 0x7A: // ADDW YA, dp + ADDW(YA, dp()); + break; + case 0x9A: // SUBW YA, dp + SUBW(YA, dp()); + break; + case 0x5A: // CMPW YA, dp + // CMPW(YA, dp()); + break; + case 0xCF: // MUL YA + MUL(YA); + break; + case 0x9E: // DIV YA, X + // DIV(YA, X); + break; + + // . decimal adjust + + case 0xDF: // DAA A + break; + case 0xBE: // DAS A + break; + + // . branching + + case 0x2F: // BRA rel + BRA(rel()); + break; + case 0xF0: // BEQ rel + BEQ(rel()); + break; + case 0xD0: // BNE rel + BNE(rel()); + break; + case 0xB0: // BCS rel + BCS(rel()); + break; + case 0x90: // BCC rel + BCC(rel()); + break; + case 0x70: // BVS rel + BVS(rel()); + break; + case 0x50: // BVC rel + BVC(rel()); + break; + case 0x30: // BMI rel + BMI(rel()); + break; + case 0x10: // BPL rel + BPL(rel()); + break; + case 0x2E: // CBNE dp, rel + break; + case 0xDE: // CBNE dp+X, rel + break; + case 0x6E: // DBNZ dp, rel + break; + case 0xFE: // DBNZ Y, rel + break; + case 0x5F: // JMP !abs + JMP(abs()); + break; + case 0x1F: // JMP [!abs+X] + // JMP_INDIRECT(abs() + X); + break; + + // . subroutines + case 0x3F: // CALL !abs + { + CALL(abs()); + break; + } + case 0x4F: // PCALL up + { + PCALL(imm()); + break; + } + case 0x6F: // RET + { + RET(); + break; + } + case 0x7F: // RETI + { + RETI(); + break; + } + + // . stack + case 0x2D: // PUSH A + { + PUSH(A); + break; + } + case 0x4D: // PUSH X + { + PUSH(X); + break; + } + case 0x6D: // PUSH Y + { + PUSH(Y); + break; + } + case 0x0D: // PUSH PSW + { + PUSH(FlagsToByte(PSW)); + break; + } + + case 0xAE: // POP A + { + POP(A); + break; + } + case 0xCE: // POP X + { + POP(X); + break; + } + case 0xEE: // POP Y + { + POP(Y); + break; + } + case 0x8E: // POP PSW + { + uint8_t flags_byte; + POP(flags_byte); + PSW = ByteToFlags(flags_byte); + break; + } + + // . memory bit operations + + case 0xEA: // NOT1 abs, bit + // NOT1(abs(), bit()); + break; + case 0xAA: // MOV1 C, abs, bit + break; + case 0xCA: // MOV1 abs, bit, C + break; + case 0x4A: // AND1 C, abs, bit + break; + case 0x6A: // AND1 C, /abs, bit + break; + case 0x0A: // OR1 C, abs, bit + break; + case 0x2A: // OR1 C, /abs, bit + break; + case 0x8A: // EOR1 C, abs, bit + break; + + // . status flags + + case 0x60: // CLRC + CLRC(); + break; + case 0x80: // SETC + SETC(); + break; + case 0xED: // NOTC + NOTC(); + break; + case 0xE0: // CLRV + CLRV(); + break; + case 0x20: // CLRP + CLRP(); + break; + case 0x40: // SETP + SETP(); + break; + case 0xA0: // EI + EI(); + break; + case 0xC0: // DI + DI(); + break; + + // .no-operation and haltF + case 0x00: // NOP + { + NOP(); + break; + } + case 0xEF: // SLEEP + { + SLEEP(); + break; + } + case 0x0F: // STOP + { + STOP(); + break; + } + + default: + std::cout << "Unknown opcode: " << std::hex << opcode << std::endl; + break; + } + + LogInstruction(initialPC, opcode); +} + +void Spc700::LogInstruction(uint16_t initial_pc, uint8_t opcode) { + std::string mnemonic = spc_opcode_map.at(opcode); + + std::stringstream log_entry_stream; + log_entry_stream << "\033[1;36m$" << std::hex << std::setw(4) + << std::setfill('0') << initial_pc << "\033[0m"; + log_entry_stream << " \033[1;32m" << std::hex << std::setw(2) + << std::setfill('0') << static_cast(opcode) << "\033[0m" + << " \033[1;35m" << std::setw(18) << std::left + << std::setfill(' ') << mnemonic << "\033[0m"; + + log_entry_stream << " \033[1;33mA: " << std::hex << std::setw(2) + << std::setfill('0') << std::right << static_cast(A) + << "\033[0m"; + log_entry_stream << " \033[1;33mX: " << std::hex << std::setw(2) + << std::setfill('0') << std::right << static_cast(X) + << "\033[0m"; + log_entry_stream << " \033[1;33mY: " << std::hex << std::setw(2) + << std::setfill('0') << std::right << static_cast(Y) + << "\033[0m"; + std::string log_entry = log_entry_stream.str(); + + std::cerr << log_entry << std::endl; + + // Append the log entry to the log + log_.push_back(log_entry); +} + +} // namespace emu +} // namespace app +} // namespace yaze diff --git a/src/app/emu/audio/spc700.h b/src/app/emu/audio/spc700.h new file mode 100644 index 00000000..f76d6170 --- /dev/null +++ b/src/app/emu/audio/spc700.h @@ -0,0 +1,267 @@ +#ifndef YAZE_APP_EMU_SPC700_H +#define YAZE_APP_EMU_SPC700_H + +#include +#include +#include +#include + +namespace yaze { +namespace app { +namespace emu { + +class AudioRam { + public: + virtual ~AudioRam() = default; + virtual void reset() = 0; + virtual uint8_t read(uint16_t address) const = 0; + virtual uint8_t& mutable_read(uint16_t address) = 0; + virtual void write(uint16_t address, uint8_t value) = 0; +}; + +class AudioRamImpl : public AudioRam { + static const int ARAM_SIZE = 0x10000; + std::vector ram = std::vector(ARAM_SIZE, 0); + + public: + AudioRamImpl() = default; + void reset() override { ram = std::vector(ARAM_SIZE, 0); } + + uint8_t read(uint16_t address) const override { + return ram[address % ARAM_SIZE]; + } + + uint8_t& mutable_read(uint16_t address) override { + return ram.at(address % ARAM_SIZE); + } + + void write(uint16_t address, uint8_t value) override { + ram[address % ARAM_SIZE] = value; + } +}; + +class Spc700 { + private: + AudioRam& aram_; + std::vector log_; + + const uint8_t ipl_rom_[64]{ + 0xCD, 0xEF, 0xBD, 0xE8, 0x00, 0xC6, 0x1D, 0xD0, 0xFC, 0x8F, 0xAA, + 0xF4, 0x8F, 0xBB, 0xF5, 0x78, 0xCC, 0xF4, 0xD0, 0xFB, 0x2F, 0x19, + 0xEB, 0xF4, 0xD0, 0xFC, 0x7E, 0xF4, 0xD0, 0x0B, 0xE4, 0xF5, 0xCB, + 0xF4, 0xD7, 0x00, 0xFC, 0xD0, 0xF3, 0xAB, 0x01, 0x10, 0xEF, 0x7E, + 0xF4, 0x10, 0xEB, 0xBA, 0xF6, 0xDA, 0x00, 0xBA, 0xF4, 0xC4, 0xF4, + 0xDD, 0x5D, 0xD0, 0xDB, 0x1F, 0x00, 0x00, 0xC0, 0xFF}; + + public: + explicit Spc700(AudioRam& aram) : aram_(aram) {} + + // Registers + uint8_t A = 0x00; // 8-bit accumulator + uint8_t X = 0x00; // 8-bit index + uint8_t Y = 0x00; // 8-bit index + uint16_t YA = 0x00; // 16-bit pair of A (lsb) and Y (msb) + uint16_t PC = 0xFFC0; // program counter + uint8_t SP = 0x00; // stack pointer + + struct Flags { + uint8_t N : 1; // Negative flag + uint8_t V : 1; // Overflow flag + uint8_t P : 1; // Direct page flag + uint8_t B : 1; // Break flag + uint8_t H : 1; // Half-carry flag + uint8_t I : 1; // Interrupt enable + uint8_t Z : 1; // Zero flag + uint8_t C : 1; // Carry flag + }; + Flags PSW; // Processor status word + + uint8_t FlagsToByte(Flags flags) { + return (flags.N << 7) | (flags.V << 6) | (flags.P << 5) | (flags.B << 4) | + (flags.H << 3) | (flags.I << 2) | (flags.Z << 1) | (flags.C); + } + + Flags ByteToFlags(uint8_t byte) { + Flags flags; + flags.N = (byte & 0x80) >> 7; + flags.V = (byte & 0x40) >> 6; + flags.P = (byte & 0x20) >> 5; + flags.B = (byte & 0x10) >> 4; + flags.H = (byte & 0x08) >> 3; + flags.I = (byte & 0x04) >> 2; + flags.Z = (byte & 0x02) >> 1; + flags.C = (byte & 0x01); + return flags; + } + + void Reset(); + + void BootIplRom(); + + void ExecuteInstructions(uint8_t opcode); + void LogInstruction(uint16_t initial_pc, uint8_t opcode); + + // Read a byte from the memory-mapped registers + uint8_t read(uint16_t address) { + if (address < 0xFFC0) { + return aram_.read(address); + } else { + // Check if register is set to unmap the IPL ROM + if (read(0xF1) & 0x80) { + return aram_.read(address); + } + return ipl_rom_[address - 0xFFC0]; + } + } + + uint8_t& mutable_read(uint16_t address) { + if (address < 0xFFC0) { + return aram_.mutable_read(address); + } else { + // NOTE: Mutable access to IPL ROM is not allowed + return aram_.mutable_read(address); + } + } + + uint16_t& mutable_read_16(uint16_t address) { + if (address < 0xFFC0) { + return *reinterpret_cast(&aram_.mutable_read(address)); + } else { + // NOTE: Mutable access to IPL ROM is not allowed + return *reinterpret_cast(&aram_.mutable_read(address)); + } + } + + uint16_t read_16(uint16_t address) { + if (address < 0xFFC0) { + return (aram_.read(address) | (aram_.read(address + 1) << 8)); + } else { + // Check if register is set to unmap the IPL ROM + if (read(0xF1) & 0x80) { + return aram_.read(address); + } + return ipl_rom_[address - 0xFFC0]; + } + } + + // Write a byte to the memory-mapped registers + void write(uint16_t address, uint8_t value) { + if (address < 0xFFC0) { + aram_.write(address, value); + } else { + // Check if register is set to unmap the IPL ROM + if (read(0xF1) & 0x80) { + aram_.write(address, value); + } + } + } + + // ====================================================== + // Addressing modes + + // Immediate + uint8_t imm(); + + // Direct page + uint8_t dp(); + uint8_t& mutable_dp(); + + uint8_t get_dp_addr(); + + // Direct page indexed by X + uint8_t dp_plus_x(); + + // Direct page indexed by Y + uint8_t dp_plus_y(); + + // Indexed indirect (add index before 16-bit lookup). + uint16_t dp_plus_x_indirect(); + + // Indirect indexed (add index after 16-bit lookup). + uint16_t dp_indirect_plus_y(); + + uint16_t abs(); + + int8_t rel(); + + uint8_t i(); + + uint8_t i_postinc(); + + uint16_t addr_plus_i(); + + uint16_t addr_plus_i_indexed(); + + // ========================================================================== + // Instructions + + void MOV(uint8_t& dest, uint8_t operand); + void MOV_ADDR(uint16_t address, uint8_t operand); + void ADC(uint8_t& dest, uint8_t operand); + void SBC(uint8_t& dest, uint8_t operand); + void CMP(uint8_t& dest, uint8_t operand); + void AND(uint8_t& dest, uint8_t operand); + void OR(uint8_t& dest, uint8_t operand); + void EOR(uint8_t& dest, uint8_t operand); + void ASL(uint8_t operand); + void LSR(uint8_t& operand); + void ROL(uint8_t operand, bool isImmediate = false); + void XCN(uint8_t operand, bool isImmediate = false); + void INC(uint8_t& operand); + void DEC(uint8_t& operand); + void MOVW(uint16_t& dest, uint16_t operand); + void INCW(uint16_t& operand); + void DECW(uint16_t& operand); + void ADDW(uint16_t& dest, uint16_t operand); + void SUBW(uint16_t& dest, uint16_t operand); + void CMPW(uint16_t operand); + void MUL(uint8_t operand); + void DIV(uint8_t operand); + void BRA(int8_t offset); + void BEQ(int8_t offset); + void BNE(int8_t offset); + void BCS(int8_t offset); + void BCC(int8_t offset); + void BVS(int8_t offset); + void BVC(int8_t offset); + void BMI(int8_t offset); + void BPL(int8_t offset); + void BBS(uint8_t bit, uint8_t operand); + void BBC(uint8_t bit, uint8_t operand); + void JMP(uint16_t address); + void CALL(uint16_t address); + void PCALL(uint8_t offset); + void TCALL(uint8_t offset); + void BRK(); + void RET(); + void RETI(); + void PUSH(uint8_t operand); + void POP(uint8_t& operand); + void SET1(uint8_t bit, uint8_t& operand); + void CLR1(uint8_t bit, uint8_t& operand); + void TSET1(uint8_t bit, uint8_t& operand); + void TCLR1(uint8_t bit, uint8_t& operand); + void AND1(uint8_t bit, uint8_t& operand); + void OR1(uint8_t bit, uint8_t& operand); + void EOR1(uint8_t bit, uint8_t& operand); + void NOT1(uint8_t bit, uint8_t& operand); + void MOV1(uint8_t bit, uint8_t& operand); + void CLRC(); + void SETC(); + void NOTC(); + void CLRV(); + void CLRP(); + void SETP(); + void EI(); + void DI(); + void NOP(); + void SLEEP(); + void STOP(); + + // CBNE DBNZ +}; + +} // namespace emu +} // namespace app +} // namespace yaze +#endif // YAZE_APP_EMU_SPC700_H \ No newline at end of file diff --git a/src/app/emu/cpu/clock.h b/src/app/emu/cpu/clock.h new file mode 100644 index 00000000..17a7a42c --- /dev/null +++ b/src/app/emu/cpu/clock.h @@ -0,0 +1,63 @@ +#ifndef YAZE_APP_EMU_CLOCK_H_ +#define YAZE_APP_EMU_CLOCK_H_ + +#include + +namespace yaze { +namespace app { +namespace emu { + +class Clock { + public: + virtual ~Clock() = default; + virtual void UpdateClock(double delta) = 0; + virtual unsigned long long GetCycleCount() const = 0; + virtual void ResetAccumulatedTime() = 0; + virtual void SetFrequency(float new_frequency) = 0; + virtual float GetFrequency() const = 0; +}; + +class ClockImpl : public Clock { + public: + ClockImpl() = default; + virtual ~ClockImpl() = default; + + void UpdateCycleCount(double deltaTime) { + accumulatedTime += deltaTime; + double cycleTime = 1.0 / frequency; + + while (accumulatedTime >= cycleTime) { + Cycle(); + accumulatedTime -= cycleTime; + } + } + + void Cycle() { + cycle++; + cycleCount++; + } + + void UpdateClock(double delta) override { + UpdateCycleCount(delta); + ResetAccumulatedTime(); + } + + void ResetAccumulatedTime() override { accumulatedTime = 0.0; } + unsigned long long GetCycleCount() const override { return cycleCount; } + float GetFrequency() const override { return frequency; } + void SetFrequency(float new_frequency) override { + this->frequency = new_frequency; + } + + 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 +}; + +} // namespace emu +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_EMU_CLOCK_H_ \ No newline at end of file diff --git a/src/app/emu/cpu/cpu.cc b/src/app/emu/cpu/cpu.cc new file mode 100644 index 00000000..78ea134e --- /dev/null +++ b/src/app/emu/cpu/cpu.cc @@ -0,0 +1,1995 @@ +#include "cpu.h" + +#include +#include +#include +#include +#include + +namespace yaze { +namespace app { +namespace emu { + +void CPU::Update(UpdateMode mode, int stepCount) { + int cycles = (mode == UpdateMode::Run) ? clock.GetCycleCount() : stepCount; + + // Execute the calculated number of cycles + for (int i = 0; i < cycles; i++) { + if (IsBreakpoint(PC)) { + break; + } + + // Fetch and execute an instruction + ExecuteInstruction(ReadByte((PB << 16) + PC)); + + // Handle any interrupts, if necessary + HandleInterrupts(); + + if (mode == UpdateMode::Step) { + break; + } + } +} + +void CPU::ExecuteInstruction(uint8_t opcode) { + uint8_t cycles = 0; + uint8_t instruction_length = 0; + uint32_t operand = 0; + bool immediate = false; + bool accumulator_mode = GetAccumulatorSize(); + + switch (opcode) { + case 0x61: // ADC DP Indexed Indirect, X + { + cycles = 6; + if (!m()) cycles++; + instruction_length = 2; + operand = ReadByte(DirectPageIndexedIndirectX()); + ADC(operand); + break; + } + case 0x63: // ADC Stack Relative + { + cycles = 4; + if (!m()) cycles++; + instruction_length = 2; + operand = ReadByte(StackRelative()); + ADC(operand); + break; + } + case 0x65: // ADC Direct Page + { + cycles = 3; + if (!m()) cycles++; + instruction_length = 2; + operand = ReadByte(DirectPage()); + ADC(operand); + break; + } + case 0x67: // ADC DP Indirect Long + { + cycles = 6; + if (!m()) cycles++; + instruction_length = 2; + operand = ReadWord(DirectPageIndirectLong()); + ADC(operand); + break; + } + case 0x69: // ADC Immediate + { + cycles = 2; + if (!m()) cycles++; + if (GetAccumulatorSize()) { + instruction_length = 2; + } else { + instruction_length = 3; + } + operand = Immediate(); + immediate = true; + ADC(operand); + break; + } + case 0x6D: // ADC Absolute + { + cycles = 4; + if (!m()) cycles++; + instruction_length = 3; + operand = ReadWord(Absolute()); + ADC(operand); + break; + } + case 0x6F: // ADC Absolute Long + { + cycles = 5; + if (!m()) cycles++; + instruction_length = 4; + operand = ReadWord(AbsoluteLong()); + ADC(operand); + break; + } + case 0x71: // ADC DP Indirect Indexed, Y + { + cycles = 5; + if (!m()) cycles++; + instruction_length = 2; + operand = ReadByteOrWord(DirectPageIndirectIndexedY()); + ADC(operand); + break; + } + case 0x72: // ADC DP Indirect + { + cycles = 5; + if (!m()) cycles++; + instruction_length = 2; + operand = ReadByte(DirectPageIndirect()); + ADC(operand); + break; + } + case 0x73: // ADC SR Indirect Indexed, Y + { + cycles = 7; + if (!m()) cycles++; + instruction_length = 2; + operand = ReadByte(StackRelativeIndirectIndexedY()); + ADC(operand); + break; + } + case 0x75: // ADC DP Indexed, X + { + cycles = 4; + if (!m()) cycles++; + instruction_length = 2; + operand = ReadByteOrWord(DirectPageIndexedX()); + ADC(operand); + break; + } + case 0x77: // ADC DP Indirect Long Indexed, Y + { + cycles = 6; + if (!m()) cycles++; + instruction_length = 2; + operand = ReadByteOrWord(DirectPageIndirectLongIndexedY()); + ADC(operand); + break; + } + case 0x79: // ADC Absolute Indexed, Y + { + cycles = 4; + if (!m()) cycles++; + instruction_length = 3; + operand = ReadWord(AbsoluteIndexedY()); + ADC(operand); + break; + } + case 0x7D: // ADC Absolute Indexed, X + { + cycles = 4; + if (!m()) cycles++; + instruction_length = 3; + operand = ReadWord(AbsoluteIndexedX()); + ADC(operand); + break; + } + case 0x7F: // ADC Absolute Long Indexed, X + { + cycles = 5; + if (!m()) cycles++; + instruction_length = 4; + operand = ReadByteOrWord(AbsoluteLongIndexedX()); + ADC(operand); + break; + } + + case 0x21: // AND DP Indexed Indirect, X + { + operand = ReadByteOrWord(DirectPageIndexedIndirectX()); + AND(operand, true); // Not immediate, but value has been retrieved + break; + } + case 0x23: // AND Stack Relative + { + operand = StackRelative(); + AND(operand); + break; + } + case 0x25: // AND Direct Page + { + operand = DirectPage(); + AND(operand); + break; + } + case 0x27: // AND DP Indirect Long + { + operand = DirectPageIndirectLong(); + AND(operand); + break; + } + case 0x29: // AND Immediate + { + operand = Immediate(); + immediate = true; + AND(operand, true); + break; + } + case 0x2D: // AND Absolute + { + operand = Absolute(); + AND(operand); + break; + } + case 0x2F: // AND Absolute Long + { + operand = AbsoluteLong(); + ANDAbsoluteLong(operand); + break; + } + case 0x31: // AND DP Indirect Indexed, Y + { + operand = DirectPageIndirectIndexedY(); + AND(operand); + break; + } + case 0x32: // AND DP Indirect + { + operand = DirectPageIndirect(); + AND(operand); + break; + } + case 0x33: // AND SR Indirect Indexed, Y + { + operand = StackRelativeIndirectIndexedY(); + AND(operand); + break; + } + case 0x35: // AND DP Indexed, X + { + operand = DirectPageIndexedX(); + AND(operand); + break; + } + case 0x37: // AND DP Indirect Long Indexed, Y + { + operand = DirectPageIndirectLongIndexedY(); + AND(operand); + break; + } + case 0x39: // AND Absolute Indexed, Y + { + operand = AbsoluteIndexedY(); + AND(operand); + break; + } + case 0x3D: // AND Absolute Indexed, X + { + operand = AbsoluteIndexedX(); + AND(operand); + break; + } + case 0x3F: // AND Absolute Long Indexed, X + { + operand = AbsoluteLongIndexedX(); + AND(operand); + break; + } + + case 0x06: // ASL Direct Page + { + operand = DirectPage(); + ASL(operand); + break; + } + case 0x0A: // ASL Accumulator + { + A <<= 1; + A &= 0xFE; + SetCarryFlag(A & 0x80); + SetNegativeFlag(A); + SetZeroFlag(!A); + break; + } + case 0x0E: // ASL Absolute + { + operand = Absolute(); + ASL(operand); + break; + } + case 0x16: // ASL DP Indexed, X + { + operand = ReadByteOrWord((0x00 << 16) + DirectPageIndexedX()); + ASL(operand); + break; + } + case 0x1E: // ASL Absolute Indexed, X + { + operand = AbsoluteIndexedX(); + ASL(operand); + break; + } + + case 0x90: // BCC Branch if carry clear + { + operand = FetchByte(); + BCC(operand); + break; + } + + case 0xB0: // BCS Branch if carry set + { + operand = FetchByte(); + BCS(operand); + break; + } + + case 0xF0: // BEQ Branch if equal (zero set) + { + operand = FetchByte(); + BEQ(operand); + break; + } + + case 0x24: // BIT Direct Page + { + operand = DirectPage(); + BIT(operand); + break; + } + case 0x2C: // BIT Absolute + { + operand = Absolute(); + BIT(operand); + break; + } + case 0x34: // BIT DP Indexed, X + { + operand = DirectPageIndexedX(); + BIT(operand); + break; + } + case 0x3C: // BIT Absolute Indexed, X + { + operand = AbsoluteIndexedX(); + BIT(operand); + break; + } + case 0x89: // BIT Immediate + { + operand = Immediate(); + BIT(operand); + immediate = true; + break; + } + + case 0x30: // BMI Branch if minus (negative set) + { + operand = FetchByte(); + BMI(operand); + break; + } + + case 0xD0: // BNE Branch if not equal (zero clear) + { + operand = FetchSignedByte(); + BNE(operand); + break; + } + + case 0x10: // BPL Branch if plus (negative clear) + { + operand = FetchSignedByte(); + BPL(operand); + break; + } + + case 0x80: // BRA Branch always + { + operand = FetchByte(); + BRA(operand); + break; + } + + case 0x00: // BRK Break + { + BRK(); + std::cout << "BRK" << std::endl; + // Print all the registers + std::cout << "A: " << std::hex << std::setw(2) << std::setfill('0') + << (int)A << std::endl; + std::cout << "X: " << std::hex << std::setw(2) << std::setfill('0') + << (int)X << std::endl; + std::cout << "Y: " << std::hex << std::setw(2) << std::setfill('0') + << (int)Y << std::endl; + std::cout << "S: " << std::hex << std::setw(2) << std::setfill('0') + << (int)SP() << std::endl; + std::cout << "PC: " << std::hex << std::setw(4) << std::setfill('0') + << (int)PC << std::endl; + std::cout << "PB: " << std::hex << std::setw(2) << std::setfill('0') + << (int)PB << std::endl; + std::cout << "D: " << std::hex << std::setw(4) << std::setfill('0') + << (int)D << std::endl; + std::cout << "DB: " << std::hex << std::setw(2) << std::setfill('0') + << (int)DB << std::endl; + std::cout << "E: " << std::hex << std::setw(2) << std::setfill('0') + << (int)E << std::endl; + // status registers + std::cout << "C: " << std::hex << std::setw(2) << std::setfill('0') + << (int)status << std::endl; + break; + } + + case 0x82: // BRL Branch always long + { // operand = FetchSignedWord(); + operand = FetchWord(); + BRL(operand); + break; + } + + case 0x50: // BVC Branch if overflow clear + { + operand = FetchByte(); + BVC(operand); + break; + } + + case 0x70: // BVS Branch if overflow set + { + operand = FetchByte(); + BVS(operand); + break; + } + + case 0x18: // CLC Clear carry + { + CLC(); + break; + } + + case 0xD8: // CLD Clear decimal + { + CLD(); + break; + } + + case 0x58: // CLI Clear interrupt disable + { + CLI(); + break; + } + + case 0xB8: // CLV Clear overflow + { + CLV(); + break; + } + + case 0xC1: // CMP DP Indexed Indirect, X + { + operand = ReadByteOrWord(DirectPageIndexedIndirectX()); + CMP(operand); + break; + } + case 0xC3: // CMP Stack Relative + { + operand = StackRelative(); + CMP(operand); + break; + } + case 0xC5: // CMP Direct Page + { + operand = DirectPage(); + CMP(operand); + break; + } + case 0xC7: // CMP DP Indirect Long + { + operand = DirectPageIndirectLong(); + CMP(operand); + break; + } + case 0xC9: // CMP Immediate + { + operand = Immediate(); + immediate = true; + CMP(operand, immediate); + break; + } + case 0xCD: // CMP Absolute + { + operand = Absolute(AccessType::Data); + CMP(operand); + break; + } + case 0xCF: // CMP Absolute Long + { + operand = AbsoluteLong(); + CMP(operand); + break; + } + case 0xD1: // CMP DP Indirect Indexed, Y + { + operand = DirectPageIndirectIndexedY(); + CMP(operand); + break; + } + case 0xD2: // CMP DP Indirect + { + operand = DirectPageIndirect(); + CMP(operand); + break; + } + case 0xD3: // CMP SR Indirect Indexed, Y + { + operand = StackRelativeIndirectIndexedY(); + CMP(operand); + break; + } + case 0xD5: // CMP DP Indexed, X + { + operand = DirectPageIndexedX(); + CMP(operand); + break; + } + case 0xD7: // CMP DP Indirect Long Indexed, Y + { + operand = DirectPageIndirectLongIndexedY(); + CMP(operand); + break; + } + case 0xD9: // CMP Absolute Indexed, Y + { + operand = AbsoluteIndexedY(); + CMP(operand); + break; + } + case 0xDD: // CMP Absolute Indexed, X + { + operand = AbsoluteIndexedX(); + CMP(operand); + break; + } + case 0xDF: // CMP Absolute Long Indexed, X + { + operand = AbsoluteLongIndexedX(); + CMP(operand); + break; + } + + case 0x02: // COP + { + COP(); + break; + } + + case 0xE0: // CPX Immediate + { + operand = Immediate(/*index_size=*/true); + immediate = true; + CPX(operand, immediate); + break; + } + case 0xE4: // CPX Direct Page + { + operand = DirectPage(); + CPX(operand); + break; + } + case 0xEC: // CPX Absolute + { + operand = Absolute(); + CPX(operand); + break; + } + + case 0xC0: // CPY Immediate + { + operand = Immediate(); + immediate = true; + CPY(operand, immediate); + break; + } + case 0xC4: // CPY Direct Page + { + operand = DirectPage(); + CPY(operand); + break; + } + case 0xCC: // CPY Absolute + { + operand = Absolute(); + CPY(operand); + break; + } + + case 0x3A: // DEC Accumulator + { + DEC(A, /*accumulator=*/true); + break; + } + case 0xC6: // DEC Direct Page + { + operand = DirectPage(); + DEC(operand); + break; + } + case 0xCE: // DEC Absolute + { + operand = Absolute(); + DEC(operand); + break; + } + case 0xD6: // DEC DP Indexed, X + { + operand = DirectPageIndexedX(); + DEC(operand); + break; + } + case 0xDE: // DEC Absolute Indexed, X + { + operand = AbsoluteIndexedX(); + DEC(operand); + break; + } + + case 0xCA: // DEX + { + DEX(); + break; + } + + case 0x88: // DEY + { + DEY(); + break; + } + + case 0x41: // EOR DP Indexed Indirect, X + { + operand = DirectPageIndexedIndirectX(); + EOR(operand); + break; + } + case 0x43: // EOR Stack Relative + { + operand = StackRelative(); + EOR(operand); + break; + } + case 0x45: // EOR Direct Page + { + operand = DirectPage(); + EOR(operand); + break; + } + case 0x47: // EOR DP Indirect Long + { + operand = DirectPageIndirectLong(); + EOR(operand); + break; + } + case 0x49: // EOR Immediate + { + operand = Immediate(); + immediate = true; + EOR(operand, immediate); + break; + } + case 0x4D: // EOR Absolute + { + operand = Absolute(); + EOR(operand); + break; + } + case 0x4F: // EOR Absolute Long + { + operand = AbsoluteLong(); + EOR(operand); + break; + } + case 0x51: // EOR DP Indirect Indexed, Y + { + operand = DirectPageIndirectIndexedY(); + EOR(operand); + break; + } + case 0x52: // EOR DP Indirect + { + operand = DirectPageIndirect(); + EOR(operand); + break; + } + case 0x53: // EOR SR Indirect Indexed, Y + { + operand = StackRelativeIndirectIndexedY(); + EOR(operand); + break; + } + case 0x55: // EOR DP Indexed, X + { + operand = DirectPageIndexedX(); + EOR(operand); + break; + } + case 0x57: // EOR DP Indirect Long Indexed, Y + { + operand = ReadByteOrWord(DirectPageIndirectLongIndexedY()); + EOR(operand); + break; + } + case 0x59: // EOR Absolute Indexed, Y + { + operand = AbsoluteIndexedY(); + EOR(operand); + break; + } + case 0x5D: // EOR Absolute Indexed, X + { + operand = AbsoluteIndexedX(); + EOR(operand); + break; + } + case 0x5F: // EOR Absolute Long Indexed, X + { + operand = AbsoluteLongIndexedX(); + EOR(operand); + break; + } + + case 0x1A: // INC Accumulator + { + INC(A, /*accumulator=*/true); + break; + } + case 0xE6: // INC Direct Page + { + operand = DirectPage(); + INC(operand); + break; + } + case 0xEE: // INC Absolute + { + operand = Absolute(); + INC(operand); + break; + } + case 0xF6: // INC DP Indexed, X + { + operand = DirectPageIndexedX(); + INC(operand); + break; + } + case 0xFE: // INC Absolute Indexed, X + { + operand = AbsoluteIndexedX(); + INC(operand); + break; + } + + case 0xE8: // INX + { + INX(); + break; + } + + case 0xC8: // INY + { + INY(); + break; + } + + case 0x4C: // JMP Absolute + { + JMP(Absolute()); + break; + } + case 0x5C: // JMP Absolute Long + { + JML(AbsoluteLong()); + break; + } + case 0x6C: // JMP Absolute Indirect + { + JMP(AbsoluteIndirect()); + break; + } + case 0x7C: // JMP Absolute Indexed Indirect + { + JMP(AbsoluteIndexedIndirect()); + break; + } + case 0xDC: // JMP Absolute Indirect Long + { + operand = AbsoluteIndirectLong(); + JMP(operand); + PB = operand >> 16; + break; + } + + case 0x20: // JSR Absolute + { + operand = Absolute(AccessType::Control); + PB = (operand >> 16); + JSR(operand); + break; + } + + case 0x22: // JSL Absolute Long + { + JSL(AbsoluteLong()); + break; + } + + case 0xFC: // JSR Absolute Indexed Indirect + { + JSR(AbsoluteIndexedIndirect()); + break; + } + + case 0xA1: // LDA DP Indexed Indirect, X + { + operand = DirectPageIndexedIndirectX(); + LDA(operand); + break; + } + case 0xA3: // LDA Stack Relative + { + operand = StackRelative(); + LDA(operand); + break; + } + case 0xA5: // LDA Direct Page + { + operand = DirectPage(); + LDA(operand, false, true); + break; + } + case 0xA7: // LDA DP Indirect Long + { + operand = DirectPageIndirectLong(); + LDA(operand); + break; + } + case 0xA9: // LDA Immediate + { + operand = Immediate(); + immediate = true; + LDA(operand, immediate); + break; + } + case 0xAD: // LDA Absolute + { + operand = Absolute(); + LDA(operand); + break; + } + case 0xAF: // LDA Absolute Long + { + operand = AbsoluteLong(); + LDA(operand); + break; + } + case 0xB1: // LDA DP Indirect Indexed, Y + { + operand = DirectPageIndirectIndexedY(); + LDA(operand); + break; + } + case 0xB2: // LDA DP Indirect + { + operand = DirectPageIndirect(); + LDA(operand); + break; + } + case 0xB3: // LDA SR Indirect Indexed, Y + { + operand = StackRelativeIndirectIndexedY(); + LDA(operand); + break; + } + case 0xB5: // LDA DP Indexed, X + { + operand = DirectPageIndexedX(); + LDA(operand); + break; + } + case 0xB7: // LDA DP Indirect Long Indexed, Y + { + operand = DirectPageIndirectLongIndexedY(); + LDA(operand); + break; + } + case 0xB9: // LDA Absolute Indexed, Y + { + operand = AbsoluteIndexedY(); + LDA(operand); + break; + } + case 0xBD: // LDA Absolute Indexed, X + { + operand = AbsoluteIndexedX(); + LDA(operand, false, false, true); + break; + } + case 0xBF: // LDA Absolute Long Indexed, X + { + operand = AbsoluteLongIndexedX(); + LDA(operand); + break; + } + + case 0xA2: // LDX Immediate + { + operand = Immediate(); + immediate = true; + LDX(operand, immediate); + break; + } + case 0xA6: // LDX Direct Page + { + operand = DirectPage(); + LDX(operand); + break; + } + case 0xAE: // LDX Absolute + { + operand = Absolute(); + LDX(operand); + break; + } + case 0xB6: // LDX DP Indexed, Y + { + operand = DirectPageIndexedY(); + LDX(operand); + break; + } + case 0xBE: // LDX Absolute Indexed, Y + { + operand = AbsoluteIndexedY(); + LDX(operand); + break; + } + + case 0xA0: // LDY Immediate + { + operand = Immediate(); + immediate = true; + LDY(operand, immediate); + break; + } + case 0xA4: // LDY Direct Page + { + operand = DirectPage(); + LDY(operand); + break; + } + case 0xAC: // LDY Absolute + { + operand = Absolute(); + LDY(operand); + break; + } + case 0xB4: // LDY DP Indexed, X + { + operand = DirectPageIndexedX(); + LDY(operand); + break; + } + case 0xBC: // LDY Absolute Indexed, X + { + operand = AbsoluteIndexedX(); + LDY(operand); + break; + } + + case 0x46: // LSR Direct Page + { + operand = DirectPage(); + LSR(operand); + break; + } + case 0x4A: // LSR Accumulator + { + LSR(A, /*accumulator=*/true); + break; + } + case 0x4E: // LSR Absolute + { + operand = Absolute(); + LSR(operand); + break; + } + case 0x56: // LSR DP Indexed, X + { + operand = DirectPageIndexedX(); + LSR(operand); + break; + } + case 0x5E: // LSR Absolute Indexed, X + { + operand = AbsoluteIndexedX(); + LSR(operand); + break; + } + + case 0x54: + // MVN(); + break; + + case 0xEA: // NOP + NOP(); + break; + + case 0x01: // ORA DP Indexed Indirect, X + operand = DirectPageIndexedIndirectX(); + ORA(operand); + break; + case 0x03: // ORA Stack Relative + operand = StackRelative(); + ORA(operand); + break; + case 0x05: // ORA Direct Page + operand = DirectPage(); + ORA(operand); + break; + case 0x07: // ORA DP Indirect Long + operand = DirectPageIndirectLong(); + ORA(operand); + break; + case 0x09: // ORA Immediate + operand = Immediate(); + immediate = true; + ORA(operand, immediate); + break; + case 0x0D: // ORA Absolute + operand = Absolute(); + ORA(operand); + break; + case 0x0F: // ORA Absolute Long + operand = AbsoluteLong(); + ORA(operand); + break; + case 0x11: // ORA DP Indirect Indexed, Y + operand = DirectPageIndirectIndexedY(); + ORA(operand); + break; + case 0x12: // ORA DP Indirect + operand = DirectPageIndirect(); + ORA(operand); + break; + case 0x13: // ORA SR Indirect Indexed, Y + operand = StackRelativeIndirectIndexedY(); + ORA(operand); + break; + case 0x15: // ORA DP Indexed, X + operand = DirectPageIndexedX(); + ORA(operand); + break; + case 0x17: // ORA DP Indirect Long Indexed, Y + operand = DirectPageIndirectLongIndexedY(); + ORA(operand); + break; + case 0x19: // ORA Absolute Indexed, Y + operand = AbsoluteIndexedY(); + ORA(operand); + break; + case 0x1D: // ORA Absolute Indexed, X + operand = AbsoluteIndexedX(); + ORA(operand); + break; + case 0x1F: // ORA Absolute Long Indexed, X + operand = AbsoluteLongIndexedX(); + ORA(operand); + break; + + case 0xF4: // PEA Push Effective Absolute address + PEA(); + break; + + case 0xD4: // PEI Push Effective Indirect address + PEI(); + break; + + case 0x62: // PER Push Effective PC Relative Indirect address + PER(); + break; + + case 0x48: // PHA Push Accumulator + PHA(); + break; + + case 0x8B: // PHB Push Data Bank Register + PHB(); + break; + + case 0x0B: // PHD Push Direct Page Register + PHD(); + break; + + case 0x4B: // PHK Push Program Bank Register + PHK(); + break; + + case 0x08: // PHP Push Processor Status Register + PHP(); + break; + + case 0xDA: // PHX Push X register + PHX(); + break; + + case 0x5A: // PHY Push Y register + PHY(); + break; + + case 0x68: // PLA Pull Accumulator + PLA(); + break; + + case 0xAB: // PLB Pull Data Bank Register + PLB(); + break; + + case 0x2B: // PLD Pull Direct Page Register + PLD(); + break; + + case 0x28: // PLP Pull Processor Status Register + PLP(); + break; + + case 0xFA: // PLX Pull X register + PLX(); + break; + + case 0x7A: // PLY Pull Y register + PLY(); + break; + + case 0xC2: // REP Reset status bits + operand = FetchByte(); + immediate = true; + REP(); + break; + + case 0x26: // ROL Direct Page + operand = DirectPage(); + ROL(operand); + break; + case 0x2A: // ROL Accumulator + ROL(A, /*accumulator=*/true); + break; + case 0x2E: // ROL Absolute + operand = Absolute(); + ROL(operand); + break; + case 0x36: // ROL DP Indexed, X + operand = DirectPageIndexedX(); + ROL(operand); + break; + case 0x3E: // ROL Absolute Indexed, X + operand = AbsoluteIndexedX(); + ROL(operand); + break; + + case 0x66: // ROR Direct Page + operand = DirectPage(); + ROR(operand); + break; + case 0x6A: // ROR Accumulator + ROR(A, /*accumulator=*/true); + break; + case 0x6E: // ROR Absolute + operand = Absolute(); + ROR(operand); + break; + case 0x76: // ROR DP Indexed, X + operand = DirectPageIndexedX(); + ROR(operand); + break; + case 0x7E: // ROR Absolute Indexed, X + operand = AbsoluteIndexedX(); + ROR(operand); + break; + + case 0x40: // RTI Return from interrupt + RTI(); + break; + + case 0x6B: // RTL Return from subroutine long + RTL(); + break; + + case 0x60: // RTS Return from subroutine + RTS(); + break; + + case 0xE1: // SBC DP Indexed Indirect, X + operand = DirectPageIndexedIndirectX(); + SBC(operand); + break; + case 0xE3: // SBC Stack Relative + operand = StackRelative(); + SBC(operand); + break; + case 0xE5: // SBC Direct Page + operand = DirectPage(); + SBC(operand); + break; + case 0xE7: // SBC DP Indirect Long + operand = DirectPageIndirectLong(); + SBC(operand); + break; + case 0xE9: // SBC Immediate + operand = Immediate(); + immediate = true; + SBC(operand, immediate); + break; + case 0xED: // SBC Absolute + operand = Absolute(); + SBC(operand); + break; + case 0xEF: // SBC Absolute Long + operand = AbsoluteLong(); + SBC(operand); + break; + case 0xF1: // SBC DP Indirect Indexed, Y + operand = DirectPageIndirectIndexedY(); + SBC(operand); + break; + case 0xF2: // SBC DP Indirect + operand = DirectPageIndirect(); + SBC(operand); + break; + case 0xF3: // SBC SR Indirect Indexed, Y + operand = StackRelativeIndirectIndexedY(); + SBC(operand); + break; + case 0xF5: // SBC DP Indexed, X + operand = DirectPageIndexedX(); + SBC(operand); + break; + case 0xF7: // SBC DP Indirect Long Indexed, Y + operand = DirectPageIndirectLongIndexedY(); + SBC(operand); + break; + case 0xF9: // SBC Absolute Indexed, Y + operand = AbsoluteIndexedY(); + SBC(operand); + break; + case 0xFD: // SBC Absolute Indexed, X + operand = AbsoluteIndexedX(); + SBC(operand); + break; + case 0xFF: // SBC Absolute Long Indexed, X + operand = AbsoluteLongIndexedX(); + SBC(operand); + break; + + case 0x38: // SEC Set carry + SEC(); + break; + + case 0xF8: // SED Set decimal + SED(); + break; + + case 0x78: // SEI Set interrupt disable + SEI(); + break; + + case 0xE2: // SEP Set status bits + operand = FetchByte(); + immediate = true; + SEP(); + break; + + case 0x81: // STA DP Indexed Indirect, X + operand = DirectPageIndexedIndirectX(); + STA(operand); + break; + case 0x83: // STA Stack Relative + operand = StackRelative(); + STA(operand); + break; + case 0x85: // STA Direct Page + operand = DirectPage(); + STA(operand); + break; + case 0x87: // STA DP Indirect Long + operand = DirectPageIndirectLong(); + STA(operand); + break; + case 0x8D: // STA Absolute + operand = Absolute(AccessType::Data); + STA(operand); + break; + case 0x8F: // STA Absolute Long + operand = AbsoluteLong(); + STA(operand); + break; + case 0x91: // STA DP Indirect Indexed, Y + operand = DirectPageIndirectIndexedY(); + STA(operand); + break; + case 0x92: // STA DP Indirect + operand = DirectPageIndirect(); + STA(operand); + break; + case 0x93: // STA SR Indirect Indexed, Y + operand = StackRelativeIndirectIndexedY(); + STA(operand); + break; + case 0x95: // STA DP Indexed, X + operand = DirectPageIndexedX(); + STA(operand); + break; + case 0x97: // STA DP Indirect Long Indexed, Y + { + operand = DirectPageIndirectLongIndexedY(); + STA(operand); + break; + } + case 0x99: // STA Absolute Indexed, Y + operand = AbsoluteIndexedY(); + STA(operand); + break; + case 0x9D: // STA Absolute Indexed, X + operand = AbsoluteIndexedX(); + STA(operand); + break; + case 0x9F: // STA Absolute Long Indexed, X + operand = AbsoluteLongIndexedX(); + STA(operand); + break; + + case 0xDB: // STP Stop the processor + STP(); + break; + + case 0x86: // STX Direct Page + operand = DirectPage(); + STX(operand); + break; + case 0x8E: // STX Absolute + operand = Absolute(); + STX(operand); + break; + case 0x96: // STX DP Indexed, Y + operand = DirectPageIndexedY(); + STX(operand); + break; + + case 0x84: // STY Direct Page + operand = DirectPage(); + STY(operand); + break; + case 0x8C: // STY Absolute + operand = Absolute(); + STY(operand); + break; + case 0x94: // STY DP Indexed, X + operand = DirectPageIndexedX(); + STY(operand); + break; + + case 0x64: // STZ Direct Page + operand = DirectPage(); + STZ(operand); + break; + case 0x74: // STZ DP Indexed, X + operand = DirectPageIndexedX(); + STZ(operand); + break; + case 0x9C: // STZ Absolute + operand = Absolute(); + STZ(operand); + break; + case 0x9E: // STZ Absolute Indexed, X + operand = AbsoluteIndexedX(); + STZ(operand); + break; + + case 0xAA: // TAX Transfer accumulator to X + TAX(); + break; + + case 0xA8: // TAY Transfer accumulator to Y + TAY(); + break; + + case 0x5B: // TCD + TCD(); + break; + + case 0x1B: // TCS + TCS(); + break; + + case 0x7B: // TDC + TDC(); + break; + + case 0x14: // TRB Direct Page + operand = DirectPage(); + TRB(operand); + break; + case 0x1C: // TRB Absolute + operand = Absolute(); + TRB(operand); + break; + + case 0x04: // TSB Direct Page + operand = DirectPage(); + TSB(operand); + break; + case 0x0C: // TSB Absolute + operand = Absolute(); + TSB(operand); + break; + + case 0x3B: // TSC + TSC(); + break; + + case 0xBA: // TSX Transfer stack pointer to X + TSX(); + break; + + case 0x8A: // TXA Transfer X to accumulator + TXA(); + break; + + case 0x9A: // TXS Transfer X to stack pointer + TXS(); + break; + + case 0x9B: // TXY Transfer X to Y + TXY(); + break; + + case 0x98: // TYA Transfer Y to accumulator + TYA(); + break; + + case 0xBB: // TYX Transfer Y to X + TYX(); + break; + + case 0xCB: // WAI Wait for interrupt + WAI(); + break; + + case 0xEB: // XBA Exchange B and A + XBA(); + break; + + case 0xFB: // XCE Exchange carry and emulation bits + XCE(); + break; + default: + std::cerr << "Unknown instruction: " << std::hex + << static_cast(opcode) << std::endl; + break; + } + + LogInstructions(PC, opcode, operand, immediate, accumulator_mode); + instruction_length = GetInstructionLength(opcode); + UpdatePC(instruction_length); +} + +void CPU::LogInstructions(uint16_t PC, uint8_t opcode, uint16_t operand, + bool immediate, bool accumulator_mode) { + if (flags()->kLogInstructions) { + std::ostringstream oss; + oss << "$" << std::uppercase << std::setw(2) << std::setfill('0') + << static_cast(PB) << ":" << std::hex << PC << ": 0x" + << std::setw(2) << std::setfill('0') << std::hex + << static_cast(opcode) << " " << opcode_to_mnemonic.at(opcode) + << " "; + + // Log the operand. + std::string ops; + if (operand) { + if (immediate) { + ops += "#"; + } + std::ostringstream oss_ops; + oss_ops << "$"; + if (accumulator_mode) { + oss_ops << std::hex << std::setw(2) << std::setfill('0') + << static_cast(operand); + } else { + oss_ops << std::hex << std::setw(4) << std::setfill('0') + << static_cast(operand); + } + ops = oss_ops.str(); + } + + oss << ops << std::endl; + + InstructionEntry entry(PC, opcode, ops, oss.str()); + instruction_log_.push_back(entry); + } else { + // Log the address and opcode. + std::cout << "\033[1;36m" + << "$" << std::uppercase << std::setw(2) << std::setfill('0') + << static_cast(PB) << ":" << std::hex << PC; + std::cout << " \033[1;32m" + << ": 0x" << std::hex << std::uppercase << std::setw(2) + << std::setfill('0') << static_cast(opcode) << " "; + std::cout << " \033[1;35m" << opcode_to_mnemonic.at(opcode) << " " + << "\033[0m"; + + // Log the operand. + if (operand) { + if (immediate) { + std::cout << "#"; + } + std::cout << "$"; + if (accumulator_mode) { + std::cout << std::hex << std::setw(2) << std::setfill('0') << operand; + } else { + std::cout << std::hex << std::setw(4) << std::setfill('0') + << static_cast(operand); + } + + bool x_indexing, y_indexing; + auto x_indexed_instruction_opcodes = {0x15, 0x16, 0x17, 0x55, 0x56, + 0x57, 0xD5, 0xD6, 0xD7, 0xF5, + 0xF6, 0xF7, 0xBD}; + auto y_indexed_instruction_opcodes = {0x19, 0x97, 0x1D, 0x59, 0x5D, 0x99, + 0x9D, 0xB9, 0xD9, 0xDD, 0xF9, 0xFD}; + if (std::find(x_indexed_instruction_opcodes.begin(), + x_indexed_instruction_opcodes.end(), + opcode) != x_indexed_instruction_opcodes.end()) { + x_indexing = true; + } else { + x_indexing = false; + } + if (std::find(y_indexed_instruction_opcodes.begin(), + y_indexed_instruction_opcodes.end(), + opcode) != y_indexed_instruction_opcodes.end()) { + y_indexing = true; + } else { + y_indexing = false; + } + + if (x_indexing) { + std::cout << ", X"; + } + + if (y_indexing) { + std::cout << ", Y"; + } + } + + // Log the registers and flags. + std::cout << std::right; + std::cout << "\033[1;33m" + << " A:" << std::hex << std::setw(2) << std::setfill('0') + << static_cast(A); + std::cout << " X:" << std::hex << std::setw(2) << std::setfill('0') + << static_cast(X); + std::cout << " Y:" << std::hex << std::setw(2) << std::setfill('0') + << static_cast(Y); + std::cout << " S:" << std::hex << std::setw(2) << std::setfill('0') + << static_cast(status); + std::cout << " DB:" << std::hex << std::setw(2) << std::setfill('0') + << static_cast(DB); + std::cout << " D:" << std::hex << std::setw(2) << std::setfill('0') + << static_cast(D); + + std::cout << std::endl; + } +} + +uint8_t CPU::GetInstructionLength(uint8_t opcode) { + switch (opcode) { + case 0x00: // BRK + case 0x02: // COP + PC = next_pc_; + return 0; + + // TODO: Handle JMPs in logging. + case 0x20: // JSR Absolute + case 0x4C: // JMP Absolute + case 0x6C: // JMP Absolute Indirect + case 0x5C: // JMP Absolute Indexed Indirect + case 0x22: // JSL Absolute Long + case 0x7C: // JMP Absolute Indexed Indirect + case 0xFC: // JSR Absolute Indexed Indirect + case 0xDC: // JMP Absolute Indirect Long + case 0x6B: // RTL + case 0x82: // BRL Relative Long + PC = next_pc_; + return 0; + + case 0x80: // BRA Relative + PC += next_pc_; + return 2; + + case 0x60: // RTS + PC = last_call_frame_; + return 3; + + // Branch Instructions (BCC, BCS, BNE, BEQ, etc.) + case 0x90: // BCC near + if (!GetCarryFlag()) { + PC = next_pc_; + return 0; + } else { + return 2; + } + case 0xB0: // BCS near + if (GetCarryFlag()) { + PC = next_pc_; + return 0; + } else { + return 2; + } + case 0x30: // BMI near + if (GetNegativeFlag()) { + PC = next_pc_; + return 0; + } else { + return 2; + } + case 0xF0: // BEQ near + if (GetZeroFlag()) { + PC = next_pc_; + return 0; + } else { + return 2; + } + + case 0xD0: // BNE Relative + if (!GetZeroFlag()) { + PC += next_pc_; + } + return 2; + + case 0x10: // BPL Relative + if (!GetNegativeFlag()) { + PC = next_pc_; + return 0; + } else { + return 2; + } + + case 0x50: // BVC Relative + if (!GetOverflowFlag()) { + PC = next_pc_; + return 0; + } else { + return 2; + } + + case 0x70: // BVS Relative + if (GetOverflowFlag()) { + PC = next_pc_; + return 0; + } else { + return 2; + } + + case 0x18: // CLC + case 0xD8: // CLD + case 0x58: // CLI + case 0xB8: // CLV + case 0xCA: // DEX + case 0x88: // DEY + case 0xE8: // INX + case 0xC8: // INY + case 0xEA: // NOP + case 0x48: // PHA + case 0x8B: // PHB + case 0x0B: // PHD + case 0x4B: // PHK + case 0x08: // PHP + case 0xDA: // PHX + case 0x5A: // PHY + case 0x68: // PLA + case 0xAB: // PLB + case 0x2B: // PLD + case 0x28: // PLP + case 0xFA: // PLX + case 0x7A: // PLY + case 0x40: // RTI + case 0x38: // SEC + case 0xF8: // SED + case 0xBB: // TYX + case 0x78: // SEI + case 0xAA: // TAX + case 0xA8: // TAY + case 0xBA: // TSX + case 0x8A: // TXA + case 0x9B: // TXY + case 0x9A: // TXS + case 0x98: // TYA + case 0x0A: // ASL Accumulator + case 0x2A: // ROL Accumulator + case 0xFB: // XCE + case 0x5B: // TCD + case 0x1B: // TCS + case 0x3A: // DEC Accumulator + case 0x1A: // INC Accumulator + case 0x7B: // TDC + case 0x3B: // TSC + case 0xEB: // XBA + case 0xCB: // WAI + case 0xDB: // STP + case 0x4A: // LSR Accumulator + case 0x6A: // ROR Accumulator + return 1; + + case 0xC2: // REP + case 0xE2: // SEP + case 0xE4: // CPX Direct Page + case 0xC4: // CPY Direct Page + case 0xD6: // DEC Direct Page Indexed, X + case 0x45: // EOR Direct Page + case 0xA5: // LDA Direct Page + case 0x05: // ORA Direct Page + case 0x85: // STA Direct Page + case 0xC6: // DEC Direct Page + case 0x97: // STA Direct Page Indexed Y + case 0x25: // AND Direct Page + case 0x32: // AND Direct Page Indirect Indexed Y + case 0x27: // AND Direct Page Indirect Long + case 0x35: // AND Direct Page Indexed X + case 0x21: // AND Direct Page Indirect Indexed Y + case 0x31: // AND Direct Page Indirect Long Indexed Y + case 0x37: // AND Direct Page Indirect Long Indexed Y + case 0x23: // AND Direct Page Indirect Indexed X + case 0x33: // AND Direct Page Indirect Long Indexed Y + case 0xE6: // INC Direct Page + case 0x81: // STA Direct Page Indirect, X + case 0x01: // ORA Direct Page Indirect, X + case 0x19: // ORA Direct Page Indirect Indexed, Y + case 0x1D: // ORA Absolute Indexed, X + case 0x89: // BIT Immediate + case 0x91: // STA Direct Page Indirect Indexed, Y + case 0x65: // ADC Direct Page + case 0x72: // ADC Direct Page Indirect + case 0x67: // ADC Direct Page Indirect Long + case 0x75: // ADC Direct Page Indexed, X + case 0x61: // ADC Direct Page Indirect, X + case 0x71: // ADC DP Indirect Indexed, Y + case 0x77: // ADC DP Indirect Long Indexed, Y + case 0x63: // ADC Stack Relative + case 0x73: // ADC SR Indirect Indexed, Y + case 0x06: // ASL Direct Page + case 0x16: // ASL Direct Page Indexed, X + case 0xB2: // LDA Direct Page Indirect + case 0x57: // EOR Direct Page Indirect Long Indexed, Y + case 0xC1: // CMP Direct Page Indexed Indirect, X + case 0xC3: // CMP Stack Relative + case 0xC5: // CMP Direct Page + case 0x47: // EOR Direct Page Indirect Long + case 0x55: // EOR Direct Page Indexed, X + case 0x41: // EOR Direct Page Indirect, X + case 0x51: // EOR Direct Page Indirect Indexed, Y + case 0x43: // EOR Direct Page Indirect Indexed, X + case 0x53: // EOR Direct Page Indirect Long Indexed, Y + case 0xA1: // LDA Direct Page Indexed Indirect, X + case 0xA3: // LDA Stack Relative + case 0xA7: // LDA Direct Page Indirect Long + case 0xB5: // LDA Direct Page Indexed, X + case 0xB1: // LDA Direct Page Indirect Indexed, Y + case 0xB7: // LDA Direct Page Indirect Long Indexed, Y + case 0xB3: // LDA Direct Page Indirect Indexed, X + case 0xB6: // LDX Direct Page Indexed, Y + case 0xB4: // LDY Direct Page Indexed, X + case 0x46: // LSR Direct Page + case 0x56: // LSR Direct Page Indexed, X + case 0xE1: // SBC Direct Page Indexed Indirect, X + case 0xE3: // SBC Stack Relative + case 0xE5: // SBC Direct Page + case 0xE7: // SBC Direct Page Indirect Long + case 0xF2: // SBC Direct Page Indirect + case 0xF1: // SBC Direct Page Indirect Indexed, Y + case 0xF3: // SBC SR Indirect Indexed, Y + case 0xF5: // SBC Direct Page Indexed, X + case 0xF7: // SBC Direct Page Indirect Long Indexed, Y + case 0xF6: // INC Direct Page Indexed, X + case 0x86: // STX Direct Page + case 0x84: // STY Direct Page + case 0x64: // STZ Direct Page + case 0x74: // STZ Direct Page Indexed, X + case 0x04: // TSB Direct Page + case 0x14: // TRB Direct Page + case 0x44: // MVN + case 0x54: // MVP + case 0x24: // BIT Direct Page + case 0x34: // BIT Direct Page Indexed, X + case 0x94: // STY Direct Page Indexed, X + case 0x87: // STA Direct Page Indirect Long + case 0x92: // STA Direct Page Indirect + case 0x93: // STA SR Indirect Indexed, Y + case 0x95: // STA Direct Page Indexed, X + case 0x96: // STX Direct Page Indexed, Y + case 0xC7: // CMP Direct Page Indirect Long + case 0xD7: // CMP DP Indirect Long Indexed, Y + case 0xD2: // CMP DP Indirect + case 0xD1: // CMP DP Indirect Indexed, Y + case 0x03: // ORA Stack Relative + case 0x13: // ORA SR Indirect Indexed, Y + case 0x07: // ORA Direct Page Indirect Long + case 0x11: // ORA DP Indirect Indexed, Y + case 0x12: // ORA DP Indirect + case 0x15: // ORA DP Indexed, X + case 0x17: // ORA DP Indirect Long Indexed, Y + case 0x26: // ROL Direct Page + case 0x36: // ROL Direct Page Indexed, X + case 0x66: // ROR Direct Page + case 0x76: // ROR Direct Page Indexed, X + case 0x42: // WDM + case 0xD3: // CMP Stack Relative Indirect Indexed, Y + case 0x52: // EOR Direct Page Indirect + case 0xA4: // LDA Direct Page + case 0xA6: // LDX Direct Page + case 0xD4: // PEI + return 2; + + case 0x69: // ADC Immediate + case 0x29: // AND Immediate + case 0xC9: // CMP Immediate + case 0x49: // EOR Immediate + case 0xA9: // LDA Immediate + case 0x09: // ORA Immediate + case 0xE9: // SBC Immediate + return GetAccumulatorSize() ? 2 : 3; + + case 0xE0: // CPX Immediate + case 0xC0: // CPY Immediate + case 0xA2: // LDX Immediate + case 0xA0: // LDY Immediate + return GetIndexSize() ? 2 : 3; + + case 0x0E: // ASL Absolute + case 0x1E: // ASL Absolute Indexed, X + case 0x2D: // AND Absolute + case 0xCD: // CMP Absolute + case 0xEC: // CPX Absolute + case 0xCC: // CPY Absolute + case 0x4D: // EOR Absolute + case 0xAD: // LDA Absolute + case 0xAE: // LDX Absolute + case 0xAC: // LDY Absolute + case 0x0D: // ORA Absolute + case 0xED: // SBC Absolute + case 0x8D: // STA Absolute + case 0x8E: // STX Absolute + case 0x8C: // STY Absolute + case 0xBD: // LDA Absolute Indexed X + case 0xBC: // LDY Absolute Indexed X + case 0x3D: // AND Absolute Indexed X + case 0x39: // AND Absolute Indexed Y + case 0x9C: // STZ Absolute Indexed X + case 0x9D: // STA Absolute Indexed X + case 0x99: // STA Absolute Indexed Y + case 0x3C: // BIT Absolute Indexed X + case 0x7D: // ADC Absolute Indexed, X + case 0x79: // ADC Absolute Indexed, Y + case 0x6D: // ADC Absolute + case 0x5D: // EOR Absolute Indexed, X + case 0x59: // EOR Absolute Indexed, Y + case 0x83: // STA Stack Relative Indirect Indexed, Y + case 0xCE: // DEC Absolute + case 0xD5: // CMP DP Indexed, X + case 0xD9: // CMP Absolute Indexed, Y + case 0xDD: // CMP Absolute Indexed, X + case 0x0C: // TSB Absolute + case 0x1C: // TRB Absolute + case 0xF9: // SBC Absolute Indexed, Y + case 0xFD: // SBC Absolute Indexed, X + case 0x2C: // BIT Absolute + case 0x2E: // ROL Absolute + case 0x3E: // ROL Absolute Indexed, X + case 0x4E: // LSR Absolute + case 0x5E: // LSR Absolute Indexed, X + case 0xDE: // DEC Absolute Indexed, X + case 0xEE: // INC Absolute + case 0xB9: // LDA Absolute Indexed, Y + case 0xBE: // LDX Absolute Indexed, Y + case 0xFE: // INC Absolute Indexed, X + case 0xF4: // PEA + case 0x62: // PER + case 0x6E: // ROR Absolute + case 0x7E: // ROR Absolute Indexed, X + return 3; + + case 0x6F: // ADC Absolute Long + case 0x2F: // AND Absolute Long + case 0xCF: // CMP Absolute Long + case 0x4F: // EOR Absolute Long + case 0xAF: // LDA Absolute Long + case 0x0F: // ORA Absolute Long + case 0xEF: // SBC Absolute Long + case 0x8F: // STA Absolute Long + case 0x7F: // ADC Absolute Long Indexed, X + case 0x3F: // AND Absolute Long Indexed, X + case 0xDF: // CMP Absolute Long Indexed, X + case 0x5F: // EOR Absolute Long Indexed, X + case 0x9F: // STA Absolute Long Indexed, X + case 0x1F: // ORA Absolute Long Indexed, X + case 0xBF: // LDA Absolute Long Indexed, X + case 0x9E: // STZ Absolute Long Indexed, X + case 0xFF: // SBC Absolute Long Indexed, X + return 4; + + default: + auto mnemonic = opcode_to_mnemonic.at(opcode); + std::cerr << "Unknown instruction length: " << std::hex + << static_cast(opcode) << ", " << mnemonic << std::endl; + throw std::runtime_error("Unknown instruction length"); + return 1; // Default to 1 as a safe fallback + } +} + +// TODO: Implement 65816 interrupts. +void CPU::HandleInterrupts() { + if (GetInterruptFlag()) { + return; + } + + /** + if (GetIRQFlag()) { + if (GetEmulationFlag()) { + PushWord(PC); + PushByte(status); + SetInterruptFlag(true); + SetDecimalFlag(false); + SetIRQFlag(false); + SetEmulationFlag(true); + try { + PC = memory.ReadWord(0xFFFE); + } catch (const std::exception& e) { + std::cout << "IRQ: " << e.what() << std::endl; + } + } else { + PushWord(PC); + PushByte(status); + SetInterruptFlag(true); + SetDecimalFlag(false); + SetIRQFlag(false); + SetEmulationFlag(false); + try { + PC = memory.ReadWord(0xFFFE); + } catch (const std::exception& e) { + std::cout << "IRQ: " << e.what() << std::endl; + } + } + } + + if (GetNMIFlag()) { + if (GetEmulationFlag()) { + PushWord(PC); + PushByte(status); + SetInterruptFlag(true); + SetDecimalFlag(false); + SetNMIFlag(false); + SetEmulationFlag(true); + try { + PC = memory.ReadWord(0xFFFA); + } catch (const std::exception& e) { + std::cout << "NMI: " << e.what() << std::endl; + } + } else { + PushWord(PC); + PushByte(status); + SetInterruptFlag(true); + SetDecimalFlag(false); + SetNMIFlag(false); + SetEmulationFlag(false); + try { + PC = memory.ReadWord(0xFFFA); + } catch (const std::exception& e) { + std::cout << "NMI: " << e.what() << std::endl; + } + } + } + */ +} + +} // namespace emu +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/emu/cpu/cpu.h b/src/app/emu/cpu/cpu.h new file mode 100644 index 00000000..04707705 --- /dev/null +++ b/src/app/emu/cpu/cpu.h @@ -0,0 +1,726 @@ +#ifndef YAZE_APP_EMU_CPU_H_ +#define YAZE_APP_EMU_CPU_H_ + +#include +#include +#include +#include +#include + +#include "app/core/common.h" +#include "app/emu/cpu/clock.h" +#include "app/emu/cpu/internal/opcodes.h" +#include "app/emu/debug/log.h" +#include "app/emu/memory/memory.h" + +namespace yaze { +namespace app { +namespace emu { + +class InstructionEntry { + public: + // Constructor + InstructionEntry(uint32_t addr, uint8_t op, const std::string& ops, + const std::string& instr) + : address(addr), opcode(op), operands(ops), instruction(instr) {} + + // Getters for the class members + uint32_t GetAddress() const { return address; } + uint8_t GetOpcode() const { return opcode; } + const std::string& GetOperands() const { return operands; } + const std::string& GetInstruction() const { return instruction; } + + uint32_t address; // Memory address of the instruction + uint8_t opcode; // Opcode of the instruction + std::string operands; // Operand(s) of the instruction, if any + std::string instruction; // Human-readable instruction text +}; + +const int kCpuClockSpeed = 21477272; // 21.477272 MHz + +class CPU : public Memory, public Loggable, public core::ExperimentFlags { + public: + explicit CPU(Memory& mem, Clock& vclock) : memory(mem), clock(vclock) {} + enum class UpdateMode { Run, Step, Pause }; + + void Init(bool verbose = false) { clock.SetFrequency(kCpuClockSpeed); } + + void Update(UpdateMode mode = UpdateMode::Run, int stepCount = 1); + + void ExecuteInstruction(uint8_t opcode); + void LogInstructions(uint16_t PC, uint8_t opcode, uint16_t operand, + bool immediate, bool accumulator_mode); + + void UpdatePC(uint8_t instruction_length) { PC += instruction_length; } + + uint8_t GetInstructionLength(uint8_t opcode); + uint16_t SP() const override { return memory.SP(); } + void SetSP(uint16_t value) override { memory.SetSP(value); } + void set_next_pc(uint16_t value) { next_pc_ = value; } + void UpdateClock(int delta_time) { clock.UpdateClock(delta_time); } + + bool IsBreakpoint(uint32_t address) { + return std::find(breakpoints_.begin(), breakpoints_.end(), address) != + breakpoints_.end(); + } + void SetBreakpoint(uint32_t address) { breakpoints_.push_back(address); } + void ClearBreakpoint(uint32_t address) { + breakpoints_.erase( + std::remove(breakpoints_.begin(), breakpoints_.end(), address), + breakpoints_.end()); + } + void ClearBreakpoints() { + breakpoints_.clear(); + breakpoints_.shrink_to_fit(); + } + auto GetBreakpoints() { return breakpoints_; } + + std::vector breakpoints_; + std::vector instruction_log_; + + // ====================================================== + // Interrupt Vectors + // Emulation mode, e = 1 Native mode, e = 0 + // + // 0xFFFE,FF - IRQ/BRK 0xFFEE,EF - IRQ + // 0xFFFC,FD - RESET + // 0xFFFA,FB - NMI 0xFFEA,EB - NMI + // 0xFFF8,F9 - ABORT 0xFFE8,E9 - ABORT + // 0xFFE6,E7 - BRK + // 0xFFF4,F5 - COP 0xFFE4,E5 - COP + void HandleInterrupts(); + + // ====================================================== + // Registers + + uint16_t A = 0; // Accumulator + uint16_t X = 0; // X index register + uint16_t Y = 0; // Y index register + uint16_t D = 0; // Direct Page register + uint8_t DB = 0; // Data Bank register + uint8_t PB = 0; // Program Bank register + uint16_t PC = 0; // Program Counter + uint8_t E = 1; // Emulation mode flag + uint8_t status = 0b00110000; // Processor Status (P) + + // Mnemonic Value Binary Description + // N #$80 10000000 Negative + // V #$40 01000000 Overflow + // M #$20 00100000 Accumulator size (0 = 16-bit, 1 = 8-bit) + // X #$10 00010000 Index size (0 = 16-bit, 1 = 8-bit) + // D #$08 00001000 Decimal + // I #$04 00000100 IRQ disable + // Z #$02 00000010 Zero + // C #$01 00000001 Carry + // E 6502 emulation mode + // B #$10 00010000 Break (emulation mode only) + + // Setting flags in the status register + bool m() { return GetAccumulatorSize() ? 1 : 0; } + int GetAccumulatorSize() const { return status & 0x20; } + int GetIndexSize() const { return status & 0x10; } + void set_16_bit_mode() { + SetAccumulatorSize(true); + SetIndexSize(true); + } + void set_8_bit_mode() { + SetAccumulatorSize(false); + SetIndexSize(false); + } + void SetAccumulatorSize(bool set) { SetFlag(0x20, set); } + void SetIndexSize(bool set) { SetFlag(0x10, set); } + + // Set individual flags + void SetNegativeFlag(bool set) { SetFlag(0x80, set); } + void SetOverflowFlag(bool set) { SetFlag(0x40, set); } + void SetBreakFlag(bool set) { SetFlag(0x10, set); } + void SetDecimalFlag(bool set) { SetFlag(0x08, set); } + void SetInterruptFlag(bool set) { SetFlag(0x04, set); } + void SetZeroFlag(bool set) { SetFlag(0x02, set); } + void SetCarryFlag(bool set) { SetFlag(0x01, set); } + + // Get individual flags + bool GetNegativeFlag() const { return GetFlag(0x80); } + bool GetOverflowFlag() const { return GetFlag(0x40); } + bool GetBreakFlag() const { return GetFlag(0x10); } + bool GetDecimalFlag() const { return GetFlag(0x08); } + bool GetInterruptFlag() const { return GetFlag(0x04); } + bool GetZeroFlag() const { return GetFlag(0x02); } + bool GetCarryFlag() const { return GetFlag(0x01); } + + enum class AccessType { Control, Data }; + + // ========================================================================== + // Addressing Modes + + // Effective Address: + // Bank: Data Bank Register if locating data + // Program Bank Register if transferring control + // High: Second operand byte + // Low: First operand byte + // + // LDA addr + uint32_t Absolute(AccessType access_type = AccessType::Data); + + // Effective Address: + // The Data Bank Register is concatened with the 16-bit operand + // the 24-bit result is added to the X Index Register + // based on the emulation mode (16:X=0, 8:X=1) + // + // LDA addr, X + uint32_t AbsoluteIndexedX(); + + // Effective Address: + // The Data Bank Register is concatened with the 16-bit operand + // the 24-bit result is added to the Y Index Register + // based on the emulation mode (16:Y=0, 8:Y=1) + // + // LDA addr, Y + uint32_t AbsoluteIndexedY(); + + // Effective Address: + // Bank: Program Bank Register (PBR) + // High/low: The Indirect Address + // Indirect Address: Located in the Program Bank at the sum of + // the operand double byte and X based on the + // emulation mode + // JMP (addr, X) + uint16_t AbsoluteIndexedIndirect(); + + // Effective Address: + // Bank: Program Bank Register (PBR) + // High/low: The Indirect Address + // Indirect Address: Located in Bank Zero, at the operand double byte + // + // JMP (addr) + uint16_t AbsoluteIndirect(); + + // Effective Address: + // Bank/High/Low: The 24-bit Indirect Address + // Indirect Address: Located in Bank Zero, at the operand double byte + // + // JMP [addr] + uint32_t AbsoluteIndirectLong(); + + // Effective Address: + // Bank: Third operand byte + // High: Second operand byte + // Low: First operand byte + // + // LDA long + uint32_t AbsoluteLong(); + + // Effective Address: + // The 24-bit operand is added to X based on the emulation mode + // + // LDA long, X + uint32_t AbsoluteLongIndexedX(); + + // Source Effective Address: + // Bank: Second operand byte + // High/Low: The 16-bit value in X, if X is 8-bit high byte is 0 + // + // Destination Effective Address: + // Bank: First operand byte + // High/Low: The 16-bit value in Y, if Y is 8-bit high byte is 0 + // + // Length: + // The number of bytes to be moved: 16-bit value in Acculumator C plus 1. + // + // MVN src, dst + void BlockMove(uint16_t source, uint16_t dest, uint16_t length); + + // Effective Address: + // Bank: Zero + // High/low: Direct Page Register plus operand byte + // + // LDA dp + uint16_t DirectPage(); + + // Effective Address: + // Bank: Zero + // High/low: Direct Page Register plus operand byte plus X + // based on the emulation mode + // + // LDA dp, X + uint16_t DirectPageIndexedX(); + + // Effective Address: + // Bank: Zero + // High/low: Direct Page Register plus operand byte plus Y + // based on the emulation mode + // LDA dp, Y + uint16_t DirectPageIndexedY(); + + // Effective Address: + // Bank: Data bank register + // High/low: The indirect address + // Indirect Address: Located in the direct page at the sum of the direct page + // register, the operand byte, and X based on the emulation mode in bank zero. + // + // LDA (dp, X) + uint16_t DirectPageIndexedIndirectX(); + + // Effective Address: + // Bank: Data bank register + // High/low: The 16-bit indirect address + // Indirect Address: The operand byte plus the direct page register in bank + // zero. + // + // LDA (dp) + uint16_t DirectPageIndirect(); + + // Effective Address: + // Bank/High/Low: The 24-bit indirect address + // Indirect address: The operand byte plus the direct page + // register in bank zero. + // + // LDA [dp] + uint32_t DirectPageIndirectLong(); + + // Effective Address: + // Found by concatenating the data bank to the double-byte + // indirect address, then adding Y based on the emulation mode. + // + // Indirect Address: Located in the Direct Page at the sum of the direct page + // register and the operand byte, in bank zero. + // + // LDA (dp), Y + uint16_t DirectPageIndirectIndexedY(); + + // Effective Address: + // Found by adding to the triple-byte indirect address Y based on the + // emulation mode. Indrect Address: Located in the Direct Page at the sum + // of the direct page register and the operand byte in bank zero. + // Indirect Address: + // Located in the Direct Page at the sum of the direct page register and + // the operand byte in bank zero. + // + // LDA (dp), Y + uint32_t DirectPageIndirectLongIndexedY(); + + // 8-bit data: Data Operand Byte + // 16-bit data 65816 native mode m or x = 0 + // Data High: Second Operand Byte + // Data Low: First Operand Byte + // + // LDA #const + uint16_t Immediate(bool index_size = false); + + uint16_t StackRelative(); + + // Effective Address: + // The Data Bank Register is concatenated to the Indirect Address; + // the 24-bit result is added to Y (16 bits if x = 0; else 8 bits) + // Indirect Address: + // Located at the 16-bit sum of the 8-bit operand and the 16-bit stack + // pointer + // + // LDA (sr, S), Y + uint32_t StackRelativeIndirectIndexedY(); + + // Memory access routines + uint8_t ReadByte(uint32_t address) const override { + return memory.ReadByte(address); + } + uint16_t ReadWord(uint32_t address) const override { + return memory.ReadWord(address); + } + uint32_t ReadWordLong(uint32_t address) const override { + return memory.ReadWordLong(address); + } + + std::vector ReadByteVector(uint32_t address, + uint16_t size) const override { + return memory.ReadByteVector(address, size); + } + + void WriteByte(uint32_t address, uint8_t value) override { + memory.WriteByte(address, value); + } + + void WriteWord(uint32_t address, uint16_t value) override { + memory.WriteWord(address, value); + } + void WriteLong(uint32_t address, uint32_t value) override { + memory.WriteLong(address, value); + } + + uint8_t FetchByte() { + uint32_t address = (PB << 16) | PC + 1; + uint8_t byte = memory.ReadByte(address); + return byte; + } + + uint16_t FetchWord() { + uint32_t address = (PB << 16) | PC + 1; + uint16_t value = memory.ReadWord(address); + return value; + } + + uint32_t FetchLong() { + uint32_t value = memory.ReadWordLong((PB << 16) | PC + 1); + return value; + } + + int8_t FetchSignedByte() { return static_cast(FetchByte()); } + + int16_t FetchSignedWord() { + auto offset = static_cast(FetchWord()); + return offset; + } + + uint8_t FetchByteDirectPage(uint8_t operand) { + uint16_t distance = D * 0x100; + + // Calculate the effective address in the Direct Page + uint16_t effectiveAddress = operand + distance; + + // Fetch the byte from memory + uint8_t fetchedByte = memory.ReadByte(effectiveAddress); + + next_pc_ = PC + 1; + + return fetchedByte; + } + + uint16_t ReadByteOrWord(uint32_t address) { + if (GetAccumulatorSize()) { + // 8-bit mode + return memory.ReadByte(address) & 0xFF; + } else { + // 16-bit mode + return memory.ReadWord(address); + } + } + + // ====================================================== + // Instructions + + // ADC: Add with carry + void ADC(uint16_t operand); + + // AND: Logical AND + void AND(uint32_t address, bool immediate = false); + void ANDAbsoluteLong(uint32_t address); + + // ASL: Arithmetic shift left + void ASL(uint16_t address); + + // BCC: Branch if carry clear + void BCC(int8_t offset); + + // BCS: Branch if carry set + void BCS(int8_t offset); + + // BEQ: Branch if equal + void BEQ(int8_t offset); + + // BIT: Bit test + void BIT(uint16_t address); + + // BMI: Branch if minus + void BMI(int8_t offset); + + // BNE: Branch if not equal + void BNE(int8_t offset); + + // BPL: Branch if plus + void BPL(int8_t offset); + + // BRA: Branch always + void BRA(int8_t offset); + + // BRK: Force interrupt + void BRK(); + + // BRL: Branch always long + void BRL(int16_t offset); + + // BVC: Branch if overflow clear + void BVC(int8_t offset); + + // BVS: Branch if overflow set + void BVS(int8_t offset); + + // CLC: Clear carry flag + void CLC(); + + // CLD: Clear decimal mode + void CLD(); + + // CLI: Clear interrupt disable bit + void CLI(); + + // CLV: Clear overflow flag + void CLV(); + + // CMP: Compare + void CMP(uint32_t address, bool immediate = false); + + // COP: Coprocessor enable + void COP(); + + // CPX: Compare X register + void CPX(uint32_t address, bool immediate = false); + + // CPY: Compare Y register + void CPY(uint32_t address, bool immediate = false); + + // DEC: Decrement memory + void DEC(uint32_t address, bool accumulator = false); + + // DEX: Decrement X register + void DEX(); + + // DEY: Decrement Y register + void DEY(); + + // EOR: Exclusive OR + void EOR(uint32_t address, bool immediate = false); + + // INC: Increment memory + void INC(uint32_t address, bool accumulator = false); + + // INX: Increment X register + void INX(); + + // INY: Increment Y register + void INY(); + + // JMP: Jump + void JMP(uint16_t address); + + // JML: Jump long + void JML(uint32_t address); + + // JSR: Jump to subroutine + void JSR(uint16_t address); + + // JSL: Jump to subroutine long + void JSL(uint32_t address); + + // LDA: Load accumulator + void LDA(uint16_t address, bool immediate = false, bool direct_page = false, + bool data_bank = false); + + // LDX: Load X register + void LDX(uint16_t address, bool immediate = false); + + // LDY: Load Y register + void LDY(uint16_t address, bool immediate = false); + + // LSR: Logical shift right + void LSR(uint16_t address, bool accumulator = false); + + // MVN: Block move next + void MVN(uint16_t source, uint16_t dest, uint16_t length); + + // MVP: Block move previous + void MVP(uint16_t source, uint16_t dest, uint16_t length); + + // NOP: No operation + void NOP(); + + // ORA: Logical inclusive OR + void ORA(uint16_t address, bool immediate = false); + + // PEA: Push effective absolute address + void PEA(); + + // PEI: Push effective indirect address + void PEI(); + + // PER: Push effective relative address + void PER(); + + // PHA: Push accumulator + void PHA(); + + // PHB: Push data bank register + void PHB(); + + // PHD: Push direct page register + void PHD(); + + // PHK: Push program bank register + void PHK(); + + // PHP: Push processor status (flags) + void PHP(); + + // PHX: Push X register + void PHX(); + + // PHY: Push Y register + void PHY(); + + // PLA: Pull accumulator + void PLA(); + + // PLB: Pull data bank register + void PLB(); + + // PLD: Pull direct page register + void PLD(); + + // PLP: Pull processor status (flags) + void PLP(); + + // PLX: Pull X register + void PLX(); + + // PLY: Pull Y register + void PLY(); + + // REP: Reset processor status bits + void REP(); + + // ROL: Rotate left + void ROL(uint32_t address, bool accumulator = false); + + // ROR: Rotate right + void ROR(uint32_t address, bool accumulator = false); + + // RTI: Return from interrupt + void RTI(); + + // RTL: Return from subroutine long + void RTL(); + + // RTS: Return from subroutine + void RTS(); + + // SBC: Subtract with carry + void SBC(uint32_t operand, bool immediate = false); + + // SEC: Set carry flag + void SEC(); + + // SED: Set decimal mode + void SED(); + + // SEI: Set interrupt disable status + void SEI(); + + // SEP: Set processor status bits + void SEP(); + + // STA: Store accumulator + void STA(uint32_t address); + + // STP: Stop the processor + void STP(); + + // STX: Store X register + void STX(uint16_t address); + + // STY: Store Y register + void STY(uint16_t address); + + // STZ: Store zero + void STZ(uint16_t address); + + // TAX: Transfer accumulator to X + void TAX(); + + // TAY: Transfer accumulator to Y + void TAY(); + + // TCD: Transfer 16-bit accumulator to direct page register + void TCD(); + + // TCS: Transfer 16-bit accumulator to stack pointer + void TCS(); + + // TDC: Transfer direct page register to 16-bit accumulator + void TDC(); + + // TRB: Test and reset bits + void TRB(uint16_t address); + + // TSB: Test and set bits + void TSB(uint16_t address); + + // TSC: Transfer stack pointer to 16-bit accumulator + void TSC(); + + // TSX: Transfer stack pointer to X + void TSX(); + + // TXA: Transfer X to accumulator + void TXA(); + + // TXS: Transfer X to stack pointer + void TXS(); + + // TXY: Transfer X to Y + void TXY(); + + // TYA: Transfer Y to accumulator + void TYA(); + + // TYX: Transfer Y to X + void TYX(); + + // WAI: Wait for interrupt + void WAI(); + + // WDM: Reserved for future expansion + void WDM(); + + // XBA: Exchange B and A + void XBA(); + + // XCE: Exchange carry and emulation bits + void XCE(); + + private: + void compare(uint16_t register_value, uint16_t memory_value) { + uint16_t result; + if (GetIndexSize()) { + // 8-bit mode + uint8_t result8 = static_cast(register_value) - + static_cast(memory_value); + result = result8; + SetNegativeFlag(result & 0x80); // Negative flag for 8-bit + } else { + // 16-bit mode + result = register_value - memory_value; + SetNegativeFlag(result & 0x8000); // Negative flag for 16-bit + } + SetZeroFlag(result == 0); // Zero flag + SetCarryFlag(register_value >= memory_value); // Carry flag + } + + void SetFlag(uint8_t mask, bool set) { + if (set) { + status |= mask; // Set the bit + } else { + status &= ~mask; // Clear the bit + } + } + + bool GetFlag(uint8_t mask) const { return (status & mask) != 0; } + void PushByte(uint8_t value) override { memory.PushByte(value); } + void PushWord(uint16_t value) override { memory.PushWord(value); } + uint8_t PopByte() override { return memory.PopByte(); } + uint16_t PopWord() override { return memory.PopWord(); } + void PushLong(uint32_t value) override { memory.PushLong(value); } + uint32_t PopLong() override { return memory.PopLong(); } + void ClearMemory() override { memory.ClearMemory(); } + uint8_t operator[](int i) const override { return 0; } + uint8_t at(int i) const override { return 0; } + + uint16_t last_call_frame_; + uint16_t next_pc_; + + 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 new file mode 100644 index 00000000..685246ed --- /dev/null +++ b/src/app/emu/cpu/internal/addressing.cc @@ -0,0 +1,123 @@ +#include "app/emu/cpu/cpu.h" + +namespace yaze { +namespace app { +namespace emu { + +uint32_t CPU::Absolute(CPU::AccessType access_type) { + auto operand = FetchWord(); + uint32_t bank = + (access_type == CPU::AccessType::Data) ? (DB << 16) : (PB << 16); + return bank | (operand & 0xFFFF); +} + +uint32_t CPU::AbsoluteIndexedX() { + uint16_t address = memory.ReadWord((PB << 16) | (PC + 1)); + uint32_t effective_address = (DB << 16) | ((address + X) & 0xFFFF); + return effective_address; +} + +uint32_t CPU::AbsoluteIndexedY() { + uint16_t address = memory.ReadWord((PB << 16) | (PC + 1)); + uint32_t effective_address = (DB << 16) | address + Y; + return effective_address; +} + +uint16_t CPU::AbsoluteIndexedIndirect() { + uint16_t address = FetchWord() + X; + return memory.ReadWord((DB << 16) | address & 0xFFFF); +} + +uint16_t CPU::AbsoluteIndirect() { + uint16_t address = FetchWord(); + return memory.ReadWord((PB << 16) | address); +} + +uint32_t CPU::AbsoluteIndirectLong() { + uint16_t address = FetchWord(); + return memory.ReadWordLong((PB << 16) | address); +} + +uint32_t CPU::AbsoluteLong() { return FetchLong(); } + +uint32_t CPU::AbsoluteLongIndexedX() { return FetchLong() + X; } + +void CPU::BlockMove(uint16_t source, uint16_t dest, uint16_t length) { + for (int i = 0; i < length; i++) { + memory.WriteByte(dest + i, memory.ReadByte(source + i)); + } +} + +uint16_t CPU::DirectPage() { + uint8_t dp = FetchByte(); + return D + dp; +} + +uint16_t CPU::DirectPageIndexedX() { + uint8_t operand = FetchByte(); + uint16_t x_by_mode = GetAccumulatorSize() ? X : X & 0xFF; + return D + operand + x_by_mode; +} + +uint16_t CPU::DirectPageIndexedY() { + uint8_t operand = FetchByte(); + return (operand + Y) & 0xFF; +} + +uint16_t CPU::DirectPageIndexedIndirectX() { + uint8_t operand = FetchByte(); + uint16_t indirect_address = D + operand + X; + uint16_t effective_address = memory.ReadWord(indirect_address & 0xFFFF); + return effective_address; +} + +uint16_t CPU::DirectPageIndirect() { + uint8_t dp = FetchByte(); + uint16_t effective_address = D + dp; + return memory.ReadWord(effective_address); +} + +uint32_t CPU::DirectPageIndirectLong() { + uint8_t dp = FetchByte(); + uint16_t effective_address = D + dp; + return memory.ReadWordLong((0x00 << 0x10) | effective_address); +} + +uint16_t CPU::DirectPageIndirectIndexedY() { + uint8_t operand = FetchByte(); + uint16_t indirect_address = D + operand; + return memory.ReadWord(indirect_address) + Y; +} + +uint32_t CPU::DirectPageIndirectLongIndexedY() { + uint8_t operand = FetchByte(); + uint16_t indirect_address = D + operand; + uint16_t y_by_mode = GetAccumulatorSize() ? Y : Y & 0xFF; + uint32_t effective_address = + memory.ReadWordLong(indirect_address) + y_by_mode; + return effective_address; +} + +uint16_t CPU::Immediate(bool index_size) { + bool bit_mode = index_size ? GetIndexSize() : GetAccumulatorSize(); + if (bit_mode) { + return memory.ReadByte((PB << 16) | PC + 1); + } else { + return memory.ReadWord((PB << 16) | PC + 1); + } +} + +uint16_t CPU::StackRelative() { + uint8_t sr = FetchByte(); + uint16_t effective_address = SP() + sr; + return effective_address; +} + +uint32_t CPU::StackRelativeIndirectIndexedY() { + uint8_t sr = FetchByte(); + return (DB << 0x10) | (memory.ReadWord(SP() + sr) + Y); +} + +} // 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 new file mode 100644 index 00000000..a06ff7bd --- /dev/null +++ b/src/app/emu/cpu/internal/instructions.cc @@ -0,0 +1,820 @@ +#include +#include +#include + +#include "app/emu/cpu/cpu.h" + +namespace yaze { +namespace app { +namespace emu { + +/** + * 65816 Instruction Set + * + * TODO: STP, WDM + */ + +void CPU::ADC(uint16_t operand) { + bool C = GetCarryFlag(); + if (GetAccumulatorSize()) { // 8-bit mode + uint16_t result = static_cast(A & 0xFF) + + static_cast(operand) + (C ? 1 : 0); + SetCarryFlag(result > 0xFF); // Update the carry flag + + // Update the overflow flag + bool overflow = (~(A ^ operand) & (A ^ result) & 0x80) != 0; + SetOverflowFlag(overflow); + + // Update the accumulator with proper wrap-around + A = (A & 0xFF00) | (result & 0xFF); + + SetZeroFlag((A & 0xFF) == 0); + SetNegativeFlag(A & 0x80); + } else { + uint32_t result = + static_cast(A) + static_cast(operand) + (C ? 1 : 0); + SetCarryFlag(result > 0xFFFF); // Update the carry flag + + // Update the overflow flag + bool overflow = (~(A ^ operand) & (A ^ result) & 0x8000) != 0; + SetOverflowFlag(overflow); + + // Update the accumulator + A = result & 0xFFFF; + + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x8000); + } +} + +void CPU::AND(uint32_t value, bool isImmediate) { + uint16_t operand; + if (GetAccumulatorSize()) { // 8-bit mode + operand = isImmediate ? value : memory.ReadByte(value); + A &= operand; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); + } else { // 16-bit mode + operand = isImmediate ? value : memory.ReadWord(value); + A &= operand; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x8000); + } +} + +// New function for absolute long addressing mode +void CPU::ANDAbsoluteLong(uint32_t address) { + uint32_t operand32 = memory.ReadWordLong(address); + A &= operand32; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x8000); +} + +void CPU::ASL(uint16_t address) { + uint8_t value = memory.ReadByte(address); + SetCarryFlag(!(value & 0x80)); // Set carry flag if bit 7 is set + value <<= 1; // Shift left + value &= 0xFE; // Clear bit 0 + memory.WriteByte(address, value); + SetNegativeFlag(!value); + SetZeroFlag(value); +} + +void CPU::BCC(int8_t offset) { + if (!GetCarryFlag()) { // If the carry flag is clear + next_pc_ = offset; + } +} + +void CPU::BCS(int8_t offset) { + if (GetCarryFlag()) { // If the carry flag is set + next_pc_ = offset; + } +} + +void CPU::BEQ(int8_t offset) { + if (GetZeroFlag()) { // If the zero flag is set + next_pc_ = offset; + } +} + +void CPU::BIT(uint16_t address) { + uint8_t value = memory.ReadByte(address); + SetNegativeFlag(value & 0x80); + SetOverflowFlag(value & 0x40); + SetZeroFlag((A & value) == 0); +} + +void CPU::BMI(int8_t offset) { + if (GetNegativeFlag()) { // If the negative flag is set + next_pc_ = offset; + } +} + +void CPU::BNE(int8_t offset) { + if (!GetZeroFlag()) { // If the zero flag is clear + // PC += offset; + next_pc_ = offset; + } +} + +void CPU::BPL(int8_t offset) { + if (!GetNegativeFlag()) { // If the negative flag is clear + next_pc_ = offset; + } +} + +void CPU::BRA(int8_t offset) { next_pc_ = offset; } + +void CPU::BRK() { + next_pc_ = PC + 2; // Increment the program counter by 2 + memory.PushWord(next_pc_); + memory.PushByte(status); + SetInterruptFlag(true); + try { + next_pc_ = memory.ReadWord(0xFFFE); + } catch (const std::exception& e) { + std::cout << "BRK: " << e.what() << std::endl; + } +} + +void CPU::BRL(int16_t offset) { next_pc_ = offset; } + +void CPU::BVC(int8_t offset) { + if (!GetOverflowFlag()) { // If the overflow flag is clear + next_pc_ = offset; + } +} + +void CPU::BVS(int8_t offset) { + if (GetOverflowFlag()) { // If the overflow flag is set + next_pc_ = offset; + } +} + +void CPU::CLC() { status &= ~0x01; } + +void CPU::CLD() { status &= ~0x08; } + +void CPU::CLI() { status &= ~0x04; } + +void CPU::CLV() { status &= ~0x40; } + +// n Set if MSB of result is set; else cleared +// z Set if result is zero; else cleared +// c Set if no borrow; else cleared +void CPU::CMP(uint32_t value, bool isImmediate) { + if (GetAccumulatorSize()) { // 8-bit + uint8_t result; + if (isImmediate) { + result = A - (value & 0xFF); + } else { + uint8_t memory_value = memory.ReadByte(value); + result = A - memory_value; + } + SetZeroFlag(result == 0); + SetNegativeFlag(result & 0x80); + SetCarryFlag(A >= (value & 0xFF)); + } else { // 16-bit + uint16_t result; + if (isImmediate) { + result = A - (value & 0xFFFF); + } else { + uint16_t memory_value = memory.ReadWord(value); + result = A - memory_value; + } + SetZeroFlag(result == 0); + SetNegativeFlag(result & 0x8000); + SetCarryFlag(A >= (value & 0xFFFF)); + } +} + +void CPU::COP() { + next_pc_ += 2; // Increment the program counter by 2 + memory.PushWord(next_pc_); + memory.PushByte(status); + SetInterruptFlag(true); + if (E) { + next_pc_ = memory.ReadWord(0xFFF4); + } else { + next_pc_ = memory.ReadWord(0xFFE4); + } + SetDecimalFlag(false); +} + +void CPU::CPX(uint32_t value, bool isImmediate) { + if (GetIndexSize()) { // 8-bit + uint8_t memory_value = isImmediate ? value : memory.ReadByte(value); + compare(X, memory_value); + } else { // 16-bit + uint16_t memory_value = isImmediate ? value : memory.ReadWord(value); + compare(X, memory_value); + } +} + +void CPU::CPY(uint32_t value, bool isImmediate) { + if (GetIndexSize()) { // 8-bit + uint8_t memory_value = isImmediate ? value : memory.ReadByte(value); + compare(Y, memory_value); + } else { // 16-bit + uint16_t memory_value = isImmediate ? value : memory.ReadWord(value); + compare(Y, memory_value); + } +} + +void CPU::DEC(uint32_t address, bool accumulator) { + if (accumulator) { + if (GetAccumulatorSize()) { // 8-bit + A = (A - 1) & 0xFF; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); + } else { // 16-bit + A = (A - 1) & 0xFFFF; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x8000); + } + return; + } + + if (GetAccumulatorSize()) { + uint8_t value = memory.ReadByte(address); + value--; + memory.WriteByte(address, value); + SetZeroFlag(value == 0); + SetNegativeFlag(value & 0x80); + } else { + uint16_t value = memory.ReadWord(address); + value--; + memory.WriteWord(address, value); + SetZeroFlag(value == 0); + SetNegativeFlag(value & 0x8000); + } +} + +void CPU::DEX() { + if (GetIndexSize()) { // 8-bit + X = static_cast(X - 1); + SetZeroFlag(X == 0); + SetNegativeFlag(X & 0x80); + } else { // 16-bit + X = static_cast(X - 1); + SetZeroFlag(X == 0); + SetNegativeFlag(X & 0x8000); + } +} + +void CPU::DEY() { + if (GetIndexSize()) { // 8-bit + Y = static_cast(Y - 1); + SetZeroFlag(Y == 0); + SetNegativeFlag(Y & 0x80); + } else { // 16-bit + Y = static_cast(Y - 1); + SetZeroFlag(Y == 0); + SetNegativeFlag(Y & 0x8000); + } +} + +void CPU::EOR(uint32_t address, bool isImmediate) { + if (GetAccumulatorSize()) { + A ^= isImmediate ? address : memory.ReadByte(address); + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); + } else { + A ^= isImmediate ? address : memory.ReadWord(address); + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x8000); + } +} + +void CPU::INC(uint32_t address, bool accumulator) { + if (accumulator) { + if (GetAccumulatorSize()) { // 8-bit + A = (A + 1) & 0xFF; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); + } else { // 16-bit + A = (A + 1) & 0xFFFF; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x8000); + } + return; + } + + if (GetAccumulatorSize()) { + uint8_t value = memory.ReadByte(address); + value++; + memory.WriteByte(address, value); + SetNegativeFlag(value & 0x80); + SetZeroFlag(value == 0); + } else { + uint16_t value = memory.ReadWord(address); + value++; + memory.WriteWord(address, value); + SetNegativeFlag(value & 0x8000); + SetZeroFlag(value == 0); + } +} + +void CPU::INX() { + if (GetIndexSize()) { // 8-bit + X = static_cast(X + 1); + SetZeroFlag(X == 0); + SetNegativeFlag(X & 0x80); + } else { // 16-bit + X = static_cast(X + 1); + SetZeroFlag(X == 0); + SetNegativeFlag(X & 0x8000); + } +} + +void CPU::INY() { + if (GetIndexSize()) { // 8-bit + Y = static_cast(Y + 1); + SetZeroFlag(Y == 0); + SetNegativeFlag(Y & 0x80); + } else { // 16-bit + Y = static_cast(Y + 1); + SetZeroFlag(Y == 0); + SetNegativeFlag(Y & 0x8000); + } +} + +void CPU::JMP(uint16_t address) { + next_pc_ = address; // Set program counter to the new address +} + +void CPU::JML(uint32_t address) { + next_pc_ = static_cast(address & 0xFFFF); + // Set the PBR to the upper 8 bits of the address + PB = static_cast((address >> 16) & 0xFF); +} + +void CPU::JSR(uint16_t address) { + memory.PushWord(PC); // Push the program counter onto the stack + next_pc_ = address; // Set program counter to the new address +} + +void CPU::JSL(uint32_t address) { + memory.PushLong(PC); // Push the program counter onto the stack as a long + // value (24 bits) + next_pc_ = address; // Set program counter to the new address +} + +void CPU::LDA(uint16_t address, bool isImmediate, bool direct_page, bool data_bank) { + uint8_t bank = PB; + if (direct_page) { + bank = 0; + } + if (GetAccumulatorSize()) { + A = isImmediate ? address : memory.ReadByte((bank << 16) | address); + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); + } else { + A = isImmediate ? address : memory.ReadWord((bank << 16) | address); + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x8000); + } +} + +void CPU::LDX(uint16_t address, bool isImmediate) { + if (GetIndexSize()) { + X = isImmediate ? address : memory.ReadByte(address); + SetZeroFlag(X == 0); + SetNegativeFlag(X & 0x80); + } else { + X = isImmediate ? address : memory.ReadWord(address); + SetZeroFlag(X == 0); + SetNegativeFlag(X & 0x8000); + } +} + +void CPU::LDY(uint16_t address, bool isImmediate) { + if (GetIndexSize()) { + Y = isImmediate ? address : memory.ReadByte(address); + SetZeroFlag(Y == 0); + SetNegativeFlag(Y & 0x80); + } else { + Y = isImmediate ? address : memory.ReadWord(address); + SetZeroFlag(Y == 0); + SetNegativeFlag(Y & 0x8000); + } +} + +void CPU::LSR(uint16_t address, bool accumulator) { + if (accumulator) { + if (GetAccumulatorSize()) { // 8-bit + SetCarryFlag(A & 0x01); + A >>= 1; + SetZeroFlag(A == 0); + SetNegativeFlag(false); + } else { // 16-bit + SetCarryFlag(A & 0x0001); + A >>= 1; + SetZeroFlag(A == 0); + SetNegativeFlag(false); + } + return; + } + uint8_t value = memory.ReadByte(address); + SetCarryFlag(value & 0x01); + value >>= 1; + memory.WriteByte(address, value); + SetNegativeFlag(false); + SetZeroFlag(value == 0); +} + +void CPU::MVN(uint16_t source, uint16_t dest, uint16_t length) { + for (uint16_t i = 0; i < length; i++) { + memory.WriteByte(dest, memory.ReadByte(source)); + source++; + dest++; + } +} + +void CPU::MVP(uint16_t source, uint16_t dest, uint16_t length) { + for (uint16_t i = 0; i < length; i++) { + memory.WriteByte(dest, memory.ReadByte(source)); + source--; + dest--; + } +} + +void CPU::NOP() { + // Do nothing +} + +void CPU::ORA(uint16_t address, bool isImmediate) { + if (GetAccumulatorSize()) { + A |= isImmediate ? address : memory.ReadByte(address); + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); + } else { + A |= isImmediate ? address : memory.ReadWord(address); + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x8000); + } +} + +void CPU::PEA() { + uint16_t address = FetchWord(); + memory.PushWord(address); +} + +void CPU::PEI() { + uint16_t address = FetchWord(); + memory.PushWord(memory.ReadWord(address)); +} + +void CPU::PER() { + uint16_t address = FetchWord(); + memory.PushWord(PC + address); +} + +void CPU::PHA() { + if (GetAccumulatorSize()) { + memory.PushByte(static_cast(A)); + } else { + memory.PushWord(A); + } +} + +void CPU::PHB() { memory.PushByte(DB); } + +void CPU::PHD() { memory.PushWord(D); } + +void CPU::PHK() { memory.PushByte(PB); } + +void CPU::PHP() { memory.PushByte(status); } + +void CPU::PHX() { + if (GetIndexSize()) { + memory.PushByte(static_cast(X)); + } else { + memory.PushWord(X); + } +} + +void CPU::PHY() { + if (GetIndexSize()) { + memory.PushByte(static_cast(Y)); + } else { + memory.PushWord(Y); + } +} + +void CPU::PLA() { + if (GetAccumulatorSize()) { + A = memory.PopByte(); + SetNegativeFlag((A & 0x80) != 0); + } else { + A = memory.PopWord(); + SetNegativeFlag((A & 0x8000) != 0); + } + SetZeroFlag(A == 0); +} + +void CPU::PLB() { + DB = memory.PopByte(); + SetNegativeFlag((DB & 0x80) != 0); + SetZeroFlag(DB == 0); +} + +// Pull Direct Page Register from Stack +void CPU::PLD() { + D = memory.PopWord(); + SetNegativeFlag((D & 0x8000) != 0); + SetZeroFlag(D == 0); +} + +// Pull Processor Status Register from Stack +void CPU::PLP() { status = memory.PopByte(); } + +void CPU::PLX() { + if (GetIndexSize()) { + X = memory.PopByte(); + SetNegativeFlag((A & 0x80) != 0); + } else { + X = memory.PopWord(); + SetNegativeFlag((A & 0x8000) != 0); + } + + SetZeroFlag(X == 0); +} + +void CPU::PLY() { + if (GetIndexSize()) { + Y = memory.PopByte(); + SetNegativeFlag((A & 0x80) != 0); + } else { + Y = memory.PopWord(); + SetNegativeFlag((A & 0x8000) != 0); + } + SetZeroFlag(Y == 0); +} + +void CPU::REP() { + auto byte = FetchByte(); + status &= ~byte; +} + +void CPU::ROL(uint32_t address, bool accumulator) { + if (accumulator) { + if (GetAccumulatorSize()) { // 8-bit + uint8_t carry = GetCarryFlag() ? 0x01 : 0x00; + SetCarryFlag(A & 0x80); + A <<= 1; + A |= carry; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); + } else { // 16-bit + uint8_t carry = GetCarryFlag() ? 0x01 : 0x00; + SetCarryFlag(A & 0x8000); + A <<= 1; + A |= carry; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x8000); + } + return; + } + + uint8_t value = memory.ReadByte(address); + uint8_t carry = GetCarryFlag() ? 0x01 : 0x00; + SetCarryFlag(value & 0x80); + value <<= 1; + value |= carry; + memory.WriteByte(address, value); + SetNegativeFlag(value & 0x80); + SetZeroFlag(value == 0); +} + +void CPU::ROR(uint32_t address, bool accumulator) { + if (accumulator) { + if (GetAccumulatorSize()) { // 8-bit + uint8_t carry = GetCarryFlag() ? 0x80 : 0x00; + SetCarryFlag(A & 0x01); + A >>= 1; + A |= carry; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); + } else { // 16-bit + uint8_t carry = GetCarryFlag() ? 0x8000 : 0x00; + SetCarryFlag(A & 0x0001); + A >>= 1; + A |= carry; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x8000); + } + return; + } + + uint8_t value = memory.ReadByte(address); + uint8_t carry = GetCarryFlag() ? 0x80 : 0x00; + SetCarryFlag(value & 0x01); + value >>= 1; + value |= carry; + memory.WriteByte(address, value); + SetNegativeFlag(value & 0x80); + SetZeroFlag(value == 0); +} + +void CPU::RTI() { + status = memory.PopByte(); + PC = memory.PopWord(); +} + +void CPU::RTL() { + next_pc_ = memory.PopWord(); + PB = memory.PopByte(); +} + +void CPU::RTS() { + last_call_frame_ = memory.PopWord(); +} + +void CPU::SBC(uint32_t value, bool isImmediate) { + uint16_t operand; + if (!GetAccumulatorSize()) { // 16-bit mode + operand = isImmediate ? value : memory.ReadWord(value); + uint16_t result = A - operand - (GetCarryFlag() ? 0 : 1); + SetCarryFlag(!(result > 0xFFFF)); // Update the carry flag + + // Update the overflow flag + bool overflow = ((A ^ operand) & (A ^ result) & 0x8000) != 0; + SetOverflowFlag(overflow); + + // Update the accumulator + A = result & 0xFFFF; + + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x8000); + } else { // 8-bit mode + operand = isImmediate ? value : memory.ReadByte(value); + uint8_t result = A - operand - (GetCarryFlag() ? 0 : 1); + SetCarryFlag(!(result > 0xFF)); // Update the carry flag + + // Update the overflow flag + bool overflow = ((A ^ operand) & (A ^ result) & 0x80) != 0; + SetOverflowFlag(overflow); + + // Update the accumulator + A = result & 0xFF; + + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); + } +} + +void CPU::SEC() { status |= 0x01; } + +void CPU::SED() { status |= 0x08; } + +void CPU::SEI() { status |= 0x04; } + +void CPU::SEP() { + auto byte = FetchByte(); + status |= byte; +} + +void CPU::STA(uint32_t address) { + if (GetAccumulatorSize()) { + memory.WriteByte(address, static_cast(A)); + } else { + memory.WriteWord(address, A); + } +} + +// TODO: Make this work with the Clock class of the CPU + +void CPU::STP() { + // During the next phase 2 clock cycle, stop the processors oscillator input + // The processor is effectively shut down until a reset occurs (RES` pin). +} + +void CPU::STX(uint16_t address) { + if (GetIndexSize()) { + memory.WriteByte(address, static_cast(X)); + } else { + memory.WriteWord(address, X); + } +} + +void CPU::STY(uint16_t address) { + if (GetIndexSize()) { + memory.WriteByte(address, static_cast(Y)); + } else { + memory.WriteWord(address, Y); + } +} + +void CPU::STZ(uint16_t address) { + if (GetAccumulatorSize()) { + memory.WriteByte(address, 0x00); + } else { + memory.WriteWord(address, 0x0000); + } +} + +void CPU::TAX() { + X = A; + SetZeroFlag(X == 0); + SetNegativeFlag(X & 0x80); +} + +void CPU::TAY() { + Y = A; + SetZeroFlag(Y == 0); + SetNegativeFlag(Y & 0x80); +} + +void CPU::TCD() { + D = A; + SetZeroFlag(D == 0); + SetNegativeFlag(D & 0x80); +} + +void CPU::TCS() { memory.SetSP(A); } + +void CPU::TDC() { + A = D; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); +} + +void CPU::TRB(uint16_t address) { + uint8_t value = memory.ReadByte(address); + SetZeroFlag((A & value) == 0); + value &= ~A; + memory.WriteByte(address, value); +} + +void CPU::TSB(uint16_t address) { + uint8_t value = memory.ReadByte(address); + SetZeroFlag((A & value) == 0); + value |= A; + memory.WriteByte(address, value); +} + +void CPU::TSC() { + A = SP(); + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); +} + +void CPU::TSX() { + X = SP(); + SetZeroFlag(X == 0); + SetNegativeFlag(X & 0x80); +} + +void CPU::TXA() { + A = X; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); +} + +void CPU::TXS() { memory.SetSP(X); } + +void CPU::TXY() { + Y = X; + SetZeroFlag(X == 0); + SetNegativeFlag(X & 0x80); +} + +void CPU::TYA() { + A = Y; + SetZeroFlag(A == 0); + SetNegativeFlag(A & 0x80); +} + +void CPU::TYX() { + X = Y; + SetZeroFlag(Y == 0); + SetNegativeFlag(Y & 0x80); +} + +// TODO: Make this communicate with the SNES class + +void CPU::WAI() { + // Pull the RDY pin low + // Power consumption is reduced(?) + // RDY remains low until an external hardware interupt + // (NMI, IRQ, ABORT, or RESET) is received from the SNES class +} + +void CPU::XBA() { + uint8_t lowByte = A & 0xFF; + uint8_t highByte = (A >> 8) & 0xFF; + A = (lowByte << 8) | highByte; +} + +void CPU::XCE() { + uint8_t carry = status & 0x01; + status &= ~0x01; + status |= E; + E = carry; +} + +} // namespace emu +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/emu/cpu/internal/opcodes.h b/src/app/emu/cpu/internal/opcodes.h new file mode 100644 index 00000000..ab112903 --- /dev/null +++ b/src/app/emu/cpu/internal/opcodes.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +const std::unordered_map opcode_to_mnemonic = { + {0x00, "BRK"}, {0x01, "ORA"}, {0x02, "COP"}, {0x03, "ORA"}, {0x04, "TSB"}, + {0x05, "ORA"}, {0x06, "ASL"}, {0x07, "ORA"}, {0x08, "PHP"}, {0x09, "ORA"}, + {0x0A, "ASL"}, {0x0B, "PHD"}, {0x0C, "TSB"}, {0x0D, "ORA"}, {0x0E, "ASL"}, + {0x0F, "ORA"}, {0x10, "BPL"}, {0x11, "ORA"}, {0x12, "ORA"}, {0x13, "ORA"}, + {0x14, "TRB"}, {0x15, "ORA"}, {0x16, "ASL"}, {0x17, "ORA"}, {0x18, "CLC"}, + {0x19, "ORA"}, {0x1A, "INC"}, {0x1B, "TCS"}, {0x1C, "TRB"}, {0x1D, "ORA"}, + {0x1E, "ASL"}, {0x1F, "ORA"}, {0x20, "JSR"}, {0x21, "AND"}, {0x22, "JSL"}, + {0x23, "AND"}, {0x24, "BIT"}, {0x25, "AND"}, {0x26, "ROL"}, {0x27, "AND"}, + {0x28, "PLP"}, {0x29, "AND"}, {0x2A, "ROL"}, {0x2B, "PLD"}, {0x2C, "BIT"}, + {0x2D, "AND"}, {0x2E, "ROL"}, {0x2F, "AND"}, {0x30, "BMI"}, {0x31, "AND"}, + {0x32, "AND"}, {0x33, "AND"}, {0x34, "BIT"}, {0x35, "AND"}, {0x36, "ROL"}, + {0x37, "AND"}, {0x38, "SEC"}, {0x39, "AND"}, {0x3A, "DEC"}, {0x3B, "TSC"}, + {0x3C, "BIT"}, {0x3D, "AND"}, {0x3E, "ROL"}, {0x3F, "AND"}, {0x40, "RTI"}, + {0x41, "EOR"}, {0x42, "WDM"}, {0x43, "EOR"}, {0x44, "MVP"}, {0x45, "EOR"}, + {0x46, "LSR"}, {0x47, "EOR"}, {0x48, "PHA"}, {0x49, "EOR"}, {0x4A, "LSR"}, + {0x4B, "PHK"}, {0x4C, "JMP"}, {0x4D, "EOR"}, {0x4E, "LSR"}, {0x4F, "EOR"}, + {0x50, "BVC"}, {0x51, "EOR"}, {0x52, "EOR"}, {0x53, "EOR"}, {0x54, "MVN"}, + {0x55, "EOR"}, {0x56, "LSR"}, {0x57, "EOR"}, {0x58, "CLI"}, {0x59, "EOR"}, + {0x5A, "PHY"}, {0x5B, "TCD"}, {0x5C, "JMP"}, {0x5D, "EOR"}, {0x5E, "LSR"}, + {0x5F, "EOR"}, {0x60, "RTS"}, {0x61, "ADC"}, {0x62, "PER"}, {0x63, "ADC"}, + {0x64, "STZ"}, {0x65, "ADC"}, {0x66, "ROR"}, {0x67, "ADC"}, {0x68, "PLA"}, + {0x69, "ADC"}, {0x6A, "ROR"}, {0x6B, "RTL"}, {0x6C, "JMP"}, {0x6D, "ADC"}, + {0x6E, "ROR"}, {0x6F, "ADC"}, {0x70, "BVS"}, {0x71, "ADC"}, {0x72, "ADC"}, + {0x73, "ADC"}, {0x74, "STZ"}, {0x75, "ADC"}, {0x76, "ROR"}, {0x77, "ADC"}, + {0x78, "SEI"}, {0x79, "ADC"}, {0x7A, "PLY"}, {0x7B, "TDC"}, {0x7C, "JMP"}, + {0x7D, "ADC"}, {0x7E, "ROR"}, {0x7F, "ADC"}, {0x80, "BRA"}, {0x81, "STA"}, + {0x82, "BRL"}, {0x83, "STA"}, {0x84, "STY"}, {0x85, "STA"}, {0x86, "STX"}, + {0x87, "STA"}, {0x88, "DEY"}, {0x89, "BIT"}, {0x8A, "TXA"}, {0x8B, "PHB"}, + {0x8C, "STY"}, {0x8D, "STA"}, {0x8E, "STX"}, {0x8F, "STA"}, {0x90, "BCC"}, + {0x91, "STA"}, {0x92, "STA"}, {0x93, "STA"}, {0x94, "STY"}, {0x95, "STA"}, + {0x96, "STX"}, {0x97, "STA"}, {0x98, "TYA"}, {0x99, "STA"}, {0x9A, "TXS"}, + {0x9B, "TXY"}, {0x9C, "STZ"}, {0x9D, "STA"}, {0x9E, "STZ"}, {0x9F, "STA"}, + {0xA0, "LDY"}, {0xA1, "LDA"}, {0xA2, "LDX"}, {0xA3, "LDA"}, {0xA4, "LDY"}, + {0xA5, "LDA"}, {0xA6, "LDX"}, {0xA7, "LDA"}, {0xA8, "TAY"}, {0xA9, "LDA"}, + {0xAA, "TAX"}, {0xAB, "PLB"}, {0xAC, "LDY"}, {0xAD, "LDA"}, {0xAE, "LDX"}, + {0xAF, "LDA"}, {0xB0, "BCS"}, {0xB1, "LDA"}, {0xB2, "LDA"}, {0xB3, "LDA"}, + {0xB4, "LDY"}, {0xB5, "LDA"}, {0xB6, "LDX"}, {0xB7, "LDA"}, {0xB8, "CLV"}, + {0xB9, "LDA"}, {0xBA, "TSX"}, {0xBB, "TYX"}, {0xBC, "LDY"}, {0xBD, "LDA"}, + {0xBE, "LDX"}, {0xBF, "LDA"}, {0xC0, "CPY"}, {0xC1, "CMP"}, {0xC2, "REP"}, + {0xC3, "CMP"}, {0xC4, "CPY"}, {0xC5, "CMP"}, {0xC6, "DEC"}, {0xC7, "CMP"}, + {0xC8, "INY"}, {0xC9, "CMP"}, {0xCA, "DEX"}, {0xCB, "WAI"}, {0xCC, "CPY"}, + {0xCD, "CMP"}, {0xCE, "DEC"}, {0xCF, "CMP"}, {0xD0, "BNE"}, {0xD1, "CMP"}, + {0xD2, "CMP"}, {0xD3, "CMP"}, {0xD4, "PEI"}, {0xD5, "CMP"}, {0xD6, "DEC"}, + {0xD7, "CMP"}, {0xD8, "CLD"}, {0xD9, "CMP"}, {0xDA, "PHX"}, {0xDB, "STP"}, + {0xDC, "JMP"}, {0xDD, "CMP"}, {0xDE, "DEC"}, {0xDF, "CMP"}, {0xE0, "CPX"}, + {0xE1, "SBC"}, {0xE2, "SEP"}, {0xE3, "SBC"}, {0xE4, "CPX"}, {0xE5, "SBC"}, + {0xE6, "INC"}, {0xE7, "SBC"}, {0xE8, "INX"}, {0xE9, "SBC"}, {0xEA, "NOP"}, + {0xEB, "XBA"}, {0xEC, "CPX"}, {0xED, "SBC"}, {0xEE, "INC"}, {0xEF, "SBC"}, + {0xF0, "BEQ"}, {0xF1, "SBC"}, {0xF2, "SBC"}, {0xF3, "SBC"}, {0xF4, "PEA"}, + {0xF5, "SBC"}, {0xF6, "INC"}, {0xF7, "SBC"}, {0xF8, "SED"}, {0xF9, "SBC"}, + {0xFA, "PLX"}, {0xFB, "XCE"}, {0xFC, "JSR"}, {0xFD, "SBC"}, {0xFE, "INC"}, + {0xFF, "SBC"} + +}; \ No newline at end of file diff --git a/src/app/emu/debug/asm_parser.h b/src/app/emu/debug/asm_parser.h new file mode 100644 index 00000000..5ea1fe07 --- /dev/null +++ b/src/app/emu/debug/asm_parser.h @@ -0,0 +1,118 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "app/emu/cpu/internal/opcodes.h" + +namespace yaze { +namespace app { +namespace emu { + +class AsmParser { + public: + std::vector Parse(const std::string& instruction) { + std::smatch match; + if (!std::regex_match(instruction, match, instruction_regex_)) { + throw std::runtime_error("Invalid instruction format: " + instruction); + } + + std::string mnemonic = match[1]; + std::string addressing_mode = match[2]; + std::string operand = match[3]; + + std::string lookup_string = mnemonic.substr(0, 3); + + auto opcode_entry = mnemonic_to_opcode_.find(mnemonic); + if (opcode_entry == mnemonic_to_opcode_.end()) { + throw std::runtime_error( + "Unknown mnemonic or addressing mode: " + mnemonic + addressing_mode); + } + + std::vector bytes = {opcode_entry->second}; + // AppendOperandBytes(bytes, operand, addressing_mode); + + return bytes; + } + + void CreateInternalOpcodeMap() { + for (const auto& opcode_entry : opcode_to_mnemonic) { + std::string name = opcode_entry.second; + uint8_t opcode = opcode_entry.first; + mnemonic_to_opcode_[name] = opcode; + } + } + + private: + void AppendOperandBytes(std::vector& bytes, + const std::string& operand, + const std::string& addressing_mode) { + if (addressing_mode == ".b") { + bytes.push_back(static_cast(std::stoi(operand, nullptr, 16))); + } else if (addressing_mode == ".w") { + uint16_t word_operand = + static_cast(std::stoi(operand, nullptr, 16)); + bytes.push_back(static_cast(word_operand & 0xFF)); + bytes.push_back(static_cast((word_operand >> 8) & 0xFF)); + } else if (addressing_mode == ".l") { + uint32_t long_operand = + static_cast(std::stoul(operand, nullptr, 16)); + bytes.push_back(static_cast(long_operand & 0xFF)); + bytes.push_back(static_cast((long_operand >> 8) & 0xFF)); + bytes.push_back(static_cast((long_operand >> 16) & 0xFF)); + } + } + + enum class AddressingMode { + kAbsolute, + kAbsoluteLong, + kAbsoluteIndexedIndirect, + kAbsoluteIndexedX, + kAbsoluteIndexedY, + kAbsoluteIndirect, + kAbsoluteIndirectLong, + kAbsoluteLongIndexedX, + kAccumulator, + kBlockMove, + kDirectPage, + kDirectPageIndexedX, + kDirectPageIndexedY, + kDirectPageIndirect, + kDirectPageIndirectIndexedY, + kDirectPageIndirectLong, + kDirectPageIndirectLongIndexedY, + kDirectPageIndirectIndexedX, + kDirectPageIndirectLongIndexedX, + kImmediate, + kImplied, + kProgramCounterRelative, + kProgramCounterRelativeLong, + kStackRelative, + kStackRelativeIndirectIndexedY, + kStackRelativeIndirectIndexedYLong, + kStack, + kStackRelativeIndexedY, + }; + + AddressingMode InferAddressingModeFromOperand(const std::string& operand) { + if (operand[0] == '$') { + return AddressingMode::kAbsolute; + } else if (operand[0] == '#') { + return AddressingMode::kImmediate; + } else { + return AddressingMode::kImplied; + } + } + + const std::regex instruction_regex_{R"((\w+)\s*(\.\w)?\s*(\$\w+|\#\w+|\w+))"}; + std::unordered_map mnemonic_to_opcode_; +}; + +} // namespace emu +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/emu/debug/debugger.h b/src/app/emu/debug/debugger.h new file mode 100644 index 00000000..a6a4dc3e --- /dev/null +++ b/src/app/emu/debug/debugger.h @@ -0,0 +1,56 @@ +#ifndef YAZE_APP_EMU_DEBUG_DEBUGGER_H_ +#define YAZE_APP_EMU_DEBUG_DEBUGGER_H_ + +#include "app/emu/audio/apu.h" +#include "app/emu/cpu/cpu.h" +#include "app/emu/video/ppu.h" + +namespace yaze { +namespace app { +namespace emu { + +class Debugger { + public: + Debugger() = default; + // Attach the debugger to the emulator + // Debugger(CPU &cpu, PPU &ppu, APU &apu); + + // Set a breakpoint + void SetBreakpoint(uint16_t address); + + // Remove a breakpoint + void RemoveBreakpoint(uint16_t address); + + // Step through the code + void Step(); + + // Inspect memory + uint8_t InspectMemory(uint16_t address); + + // Modify memory + void ModifyMemory(uint16_t address, uint8_t value); + + // Inspect registers + uint8_t InspectRegister(uint8_t reg); + + // Modify registers + void ModifyRegister(uint8_t reg, uint8_t value); + + // Handle other debugger tasks + // ... + + private: + // References to the emulator's components + // CPU &cpu; + // PPU &ppu; + // APU &apu; + + // Breakpoints, watchpoints, etc. + // ... +}; + +} // namespace emu +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_EMU_DBG_H_ \ No newline at end of file diff --git a/src/app/emu/debug/log.h b/src/app/emu/debug/log.h new file mode 100644 index 00000000..92c7649f --- /dev/null +++ b/src/app/emu/debug/log.h @@ -0,0 +1,43 @@ +#ifndef YAZE_APP_EMU_LOG_H_ +#define YAZE_APP_EMU_LOG_H_ + +#include +#include + +namespace yaze { +namespace app { +namespace emu { + +// Logger.h +class Logger { + public: + static Logger& GetInstance() { + static Logger instance; + return instance; + } + + void Log(const std::string& message) const { + // Write log messages to a file or console + std::cout << message << std::endl; + } + + private: + Logger() = default; + Logger(const Logger&) = delete; + Logger& operator=(const Logger&) = delete; +}; + +// Loggable.h +class Loggable { + protected: + Logger& logger_ = Logger::GetInstance(); + + virtual ~Loggable() = default; + virtual void LogMessage(const std::string& message) { logger_.Log(message); } +}; + +} // namespace emu +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_EMU_LOG_H_ \ No newline at end of file diff --git a/src/app/emu/emulator.cc b/src/app/emu/emulator.cc new file mode 100644 index 00000000..54458877 --- /dev/null +++ b/src/app/emu/emulator.cc @@ -0,0 +1,387 @@ +#include "app/emu/emulator.h" + +#include +#include + +#include +#include + +#include "app/core/constants.h" +#include "app/emu/snes.h" +#include "app/gui/icons.h" +#include "app/gui/input.h" +#include "app/rom.h" + +namespace yaze { +namespace app { +namespace emu { + +namespace { +bool ShouldDisplay(const InstructionEntry& entry, const char* filter, + bool showAll) { + // Implement logic to determine if the entry should be displayed based on the + // filter and showAll flag + return true; +} + +} // namespace + +using ImGui::NextColumn; +using ImGui::SameLine; +using ImGui::Separator; +using ImGui::TableNextColumn; +using ImGui::Text; + +void Emulator::Run() { + if (!snes_.running() && rom()->isLoaded()) { + snes_.SetupMemory(*rom()); + snes_.Init(*rom()); + } + + RenderNavBar(); + + if (running_) { + HandleEvents(); + if (!step_) { + snes_.Run(); + } + } + + RenderEmulator(); +} + +void Emulator::RenderNavBar() { + MENU_BAR() + if (ImGui::BeginMenu("Options")) { + MENU_ITEM("Input") {} + MENU_ITEM("Audio") {} + MENU_ITEM("Video") {} + ImGui::EndMenu(); + } + END_MENU_BAR() + + if (ImGui::Button(ICON_MD_PLAY_ARROW)) { + loading_ = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Start Emulation"); + } + SameLine(); + + if (ImGui::Button(ICON_MD_PAUSE)) { + snes_.SetCpuMode(1); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Pause Emulation"); + } + SameLine(); + + if (ImGui::Button(ICON_MD_SKIP_NEXT)) { + // Step through Code logic + snes_.StepRun(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Step Through Code"); + } + SameLine(); + + if (ImGui::Button(ICON_MD_REFRESH)) { + // Reset Emulator logic + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Reset Emulator"); + } + SameLine(); + + if (ImGui::Button(ICON_MD_STOP)) { + // Stop Emulation logic + running_ = false; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Stop Emulation"); + } + SameLine(); + + if (ImGui::Button(ICON_MD_SAVE)) { + // Save State logic + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Save State"); + } + SameLine(); + + if (ImGui::Button(ICON_MD_SYSTEM_UPDATE_ALT)) { + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Load State"); + } + + // Additional elements + SameLine(); + if (ImGui::Button(ICON_MD_SETTINGS)) { + // Settings logic + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Settings"); + } + + SameLine(); + if (ImGui::Button(ICON_MD_INFO)) { + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("About Debugger"); + } + // About Debugger logic + } + static bool show_memory_viewer = false; + + SameLine(); + if (ImGui::Button(ICON_MD_MEMORY)) { + show_memory_viewer = !show_memory_viewer; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Memory Viewer"); + } + + if (show_memory_viewer) { + ImGui::Begin("Memory Viewer", &show_memory_viewer); + RenderMemoryViewer(); + ImGui::End(); + } +} + +void Emulator::HandleEvents() { + // Handle user input events + // ... +} + +void Emulator::RenderEmulator() { + if (ImGui::BeginTable("##Emulator", 3, + ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) { + ImGui::TableSetupColumn("CPU"); + ImGui::TableSetupColumn("PPU"); + ImGui::TableHeadersRow(); + + TableNextColumn(); + RenderCpuInstructionLog(snes_.cpu().instruction_log_); + + TableNextColumn(); + RenderSnesPpu(); + RenderBreakpointList(); + + TableNextColumn(); + ImGui::BeginChild("##", ImVec2(0, 0), true, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar); + RenderCpuState(snes_.cpu()); + ImGui::EndChild(); + + ImGui::EndTable(); + } +} + +void Emulator::RenderSnesPpu() { + ImVec2 size = ImVec2(320, 240); + if (snes_.running()) { + ImGui::BeginChild("EmulatorOutput", ImVec2(0, 240), true, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar); + ImGui::SetCursorPosX((ImGui::GetWindowSize().x - size.x) * 0.5f); + ImGui::SetCursorPosY((ImGui::GetWindowSize().y - size.y) * 0.5f); + ImGui::Image((void*)snes_.ppu().GetScreen()->texture(), size, ImVec2(0, 0), + ImVec2(1, 1)); + ImGui::EndChild(); + + } else { + ImGui::Text("Emulator output not available."); + ImGui::BeginChild("EmulatorOutput", ImVec2(0, 240), true, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar); + ImGui::SetCursorPosX(((ImGui::GetWindowSize().x * 0.5f) - size.x) * 0.5f); + ImGui::SetCursorPosY(((ImGui::GetWindowSize().y * 0.5f) - size.y) * 0.5f); + ImGui::Dummy(size); + ImGui::EndChild(); + } + ImGui::Separator(); +} + +void Emulator::RenderBreakpointList() { + Text("Breakpoints"); + Separator(); + static char breakpoint_input[10] = ""; + static int current_memory_mode = 0; + + static bool read_mode = false; + static bool write_mode = false; + static bool execute_mode = false; + + if (ImGui::Combo("##TypeOfMemory", ¤t_memory_mode, "PRG\0RAM\0")) { + } + + ImGui::Checkbox("Read", &read_mode); + SameLine(); + ImGui::Checkbox("Write", &write_mode); + SameLine(); + ImGui::Checkbox("Execute", &execute_mode); + + // Breakpoint input fields and buttons + if (ImGui::InputText("##BreakpointInput", breakpoint_input, 10, + ImGuiInputTextFlags_EnterReturnsTrue)) { + int breakpoint = std::stoi(breakpoint_input, nullptr, 16); + snes_.cpu().SetBreakpoint(breakpoint); + memset(breakpoint_input, 0, sizeof(breakpoint_input)); + } + SameLine(); + if (ImGui::Button("Add")) { + int breakpoint = std::stoi(breakpoint_input, nullptr, 16); + snes_.cpu().SetBreakpoint(breakpoint); + memset(breakpoint_input, 0, sizeof(breakpoint_input)); + } + SameLine(); + if (ImGui::Button("Clear")) { + snes_.cpu().ClearBreakpoints(); + } + Separator(); + auto breakpoints = snes_.cpu().GetBreakpoints(); + if (!breakpoints.empty()) { + Text("Breakpoints:"); + ImGui::BeginChild("BreakpointsList", ImVec2(0, 100), true); + for (auto breakpoint : breakpoints) { + if (ImGui::Selectable(absl::StrFormat("0x%04X", breakpoint).c_str())) { + // Jump to breakpoint + // snes_.Cpu().JumpToBreakpoint(breakpoint); + } + } + ImGui::EndChild(); + } + Separator(); + gui::InputHexByte("PB", &manual_pb_, 1, 25.f); + gui::InputHexWord("PC", &manual_pc_, 25.f); + if (ImGui::Button("Set Current Address")) { + snes_.cpu().PC = manual_pc_; + snes_.cpu().PB = manual_pb_; + } +} + +void Emulator::RenderCpuState(CPU& cpu) { + if (ImGui::CollapsingHeader("Register Values", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Columns(2, "RegistersColumns"); + Separator(); + Text("A: 0x%04X", cpu.A); + NextColumn(); + Text("D: 0x%04X", cpu.D); + NextColumn(); + Text("X: 0x%04X", cpu.X); + NextColumn(); + Text("DB: 0x%02X", cpu.DB); + NextColumn(); + Text("Y: 0x%04X", cpu.Y); + NextColumn(); + Text("PB: 0x%02X", cpu.PB); + NextColumn(); + Text("PC: 0x%04X", cpu.PC); + NextColumn(); + Text("E: %d", cpu.E); + NextColumn(); + ImGui::Columns(1); + Separator(); + } + + // Call Stack + if (ImGui::CollapsingHeader("Call Stack", ImGuiTreeNodeFlags_DefaultOpen)) { + // For each return address in the call stack: + Text("Return Address: 0x%08X", 0xFFFFFF); // Placeholder + } + + snes_.SetCpuMode(0); +} + +void Emulator::RenderMemoryViewer() { + static MemoryEditor mem_edit; + if (ImGui::Button("RAM")) { + mem_edit.GotoAddrAndHighlight(0x7E0000, 0x7E0001); + } + + if (ImGui::BeginTable("MemoryViewerTable", 2, + ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) { + ImGui::TableSetupColumn("Bookmarks"); + ImGui::TableSetupColumn("Memory"); + ImGui::TableHeadersRow(); + + TableNextColumn(); + if (ImGui::CollapsingHeader("Bookmarks", ImGuiTreeNodeFlags_DefaultOpen)) { + // Input for adding a new bookmark + static char nameBuf[256]; + static uint64_t uint64StringBuf; + ImGui::InputText("Name", nameBuf, IM_ARRAYSIZE(nameBuf)); + gui::InputHex("Address", &uint64StringBuf); + if (ImGui::Button("Add Bookmark")) { + bookmarks.push_back({nameBuf, uint64StringBuf}); + memset(nameBuf, 0, sizeof(nameBuf)); + uint64StringBuf = 0; + } + + // Tree view of bookmarks + for (const auto& bookmark : bookmarks) { + if (ImGui::TreeNode(bookmark.name.c_str(), ICON_MD_STAR)) { + auto bookmark_string = absl::StrFormat( + "%s: 0x%08X", bookmark.name.c_str(), bookmark.value); + if (ImGui::Selectable(bookmark_string.c_str())) { + mem_edit.GotoAddrAndHighlight(static_cast(bookmark.value), + 1); + } + SameLine(); + if (ImGui::Button("Delete")) { + // Logic to delete the bookmark + bookmarks.erase(std::remove_if(bookmarks.begin(), bookmarks.end(), + [&](const Bookmark& b) { + return b.name == bookmark.name && + b.value == bookmark.value; + }), + bookmarks.end()); + } + ImGui::TreePop(); + } + } + } + + TableNextColumn(); + mem_edit.DrawContents((void*)snes_.Memory()->data(), + snes_.Memory()->size()); + + ImGui::EndTable(); + } +} + +void Emulator::RenderCpuInstructionLog( + const std::vector& instructionLog) { + if (ImGui::CollapsingHeader("CPU Instruction Log")) { + // Filtering options + static char filterBuf[256]; + ImGui::InputText("Filter", filterBuf, IM_ARRAYSIZE(filterBuf)); + SameLine(); + if (ImGui::Button("Clear")) { /* Clear filter logic */ + } + + // Toggle for showing all opcodes + static bool showAllOpcodes = true; + ImGui::Checkbox("Show All Opcodes", &showAllOpcodes); + + // Instruction list + ImGui::BeginChild("InstructionList", ImVec2(0, 0), + ImGuiChildFlags_None); + for (const auto& entry : instructionLog) { + if (ShouldDisplay(entry, filterBuf, showAllOpcodes)) { + if (ImGui::Selectable( + absl::StrFormat("%06X: %s %s", entry.address, + opcode_to_mnemonic.at(entry.opcode), + entry.operands) + .c_str())) { + // Logic to handle click (e.g., jump to address, set breakpoint) + } + } + } + ImGui::EndChild(); + } +} + +} // namespace emu +} // namespace app +} // namespace yaze diff --git a/src/app/emu/emulator.h b/src/app/emu/emulator.h new file mode 100644 index 00000000..f4467589 --- /dev/null +++ b/src/app/emu/emulator.h @@ -0,0 +1,53 @@ +#ifndef YAZE_APP_CORE_EMULATOR_H +#define YAZE_APP_CORE_EMULATOR_H + +#include + +#include +#include + +#include "app/emu/snes.h" +#include "app/rom.h" + +namespace yaze { +namespace app { +namespace emu { + +class Emulator : public SharedROM { + public: + void Run(); + + private: + void RenderNavBar(); + void HandleEvents(); + + void RenderEmulator(); + void RenderSnesPpu(); + void RenderBreakpointList(); + void RenderCpuState(CPU& cpu); + void RenderMemoryViewer(); + + struct Bookmark { + std::string name; + uint64_t value; + }; + std::vector bookmarks; + + void RenderCpuInstructionLog( + const std::vector& instructionLog); + + SNES snes_; + uint16_t manual_pc_ = 0; + uint8_t manual_pb_ = 0; + + bool power_ = false; + bool loading_ = false; + bool running_ = false; + bool step_ = true; +}; + +} // namespace emu +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_CORE_EMULATOR_H \ No newline at end of file diff --git a/src/app/emu/memory/dma.cc b/src/app/emu/memory/dma.cc new file mode 100644 index 00000000..1f334f46 --- /dev/null +++ b/src/app/emu/memory/dma.cc @@ -0,0 +1,75 @@ +#include "app/emu/memory/dma.h" + +#include + +namespace yaze { +namespace app { +namespace emu { + +void DMA::StartDMATransfer(uint8_t channelMask) { + for (int i = 0; i < 8; ++i) { + if ((channelMask & (1 << i)) != 0) { + Channel& ch = channels[i]; + + // Validate channel parameters (e.g., DMAPn, BBADn, A1Tn, DASn) + // ... + + // Determine the transfer direction based on the DMAPn register + bool fromMemory = (ch.DMAPn & 0x80) != 0; + + // Determine the transfer size based on the DMAPn register + bool transferTwoBytes = (ch.DMAPn & 0x40) != 0; + + // Perform the DMA transfer based on the channel parameters + std::cout << "Starting DMA transfer for channel " << i << std::endl; + + for (uint16_t j = 0; j < ch.DASn; ++j) { + // Read a byte or two bytes from memory based on the transfer size + // ... + + // Write the data to the B-bus address (BBADn) if transferring from + // memory + // ... + + // Update the A1Tn register based on the transfer direction + if (fromMemory) { + ch.A1Tn += transferTwoBytes ? 2 : 1; + } else { + ch.A1Tn -= transferTwoBytes ? 2 : 1; + } + } + + // Update the channel registers after the transfer (e.g., A1Tn, DASn) + // ... + } + } + MDMAEN = channelMask; // Set the MDMAEN register to the channel mask +} + +void DMA::EnableHDMATransfers(uint8_t channelMask) { + for (int i = 0; i < 8; ++i) { + if ((channelMask & (1 << i)) != 0) { + Channel& ch = channels[i]; + + // Validate channel parameters (e.g., DMAPn, BBADn, A1Tn, A2An, NLTRn) + // ... + + // Perform the HDMA setup based on the channel parameters + std::cout << "Enabling HDMA transfer for channel " << i << std::endl; + + // Read the HDMA table from memory starting at A1Tn + // ... + + // Update the A2An register based on the HDMA table + // ... + + // Update the NLTRn register based on the HDMA table + // ... + } + } + HDMAEN = channelMask; // Set the HDMAEN register to the channel mask +} + +} // namespace emu +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/emu/memory/dma.h b/src/app/emu/memory/dma.h new file mode 100644 index 00000000..2566898d --- /dev/null +++ b/src/app/emu/memory/dma.h @@ -0,0 +1,59 @@ +#ifndef YAZE_APP_EMU_MEMORY_DMA_H +#define YAZE_APP_EMU_MEMORY_DMA_H + +#include + +namespace yaze { +namespace app { +namespace emu { + +// Direct Memory Address +class DMA { + public: + DMA() { + // Initialize DMA and HDMA channels + for (int i = 0; i < 8; ++i) { + channels[i].DMAPn = 0; + channels[i].BBADn = 0; + channels[i].UNUSEDn = 0; + channels[i].A1Tn = 0xFFFFFF; + channels[i].DASn = 0xFFFF; + channels[i].A2An = 0xFFFF; + channels[i].NLTRn = 0xFF; + } + } + + // DMA Transfer Modes + enum class DMA_TRANSFER_TYPE { + OAM, + PPUDATA, + CGDATA, + FILL_VRAM, + CLEAR_VRAM, + RESET_VRAM + }; + + // Functions for handling DMA and HDMA transfers + void StartDMATransfer(uint8_t channels); + void EnableHDMATransfers(uint8_t channels); + + // Structure for DMA and HDMA channel registers + struct Channel { + uint8_t DMAPn; // DMA/HDMA parameters + uint8_t BBADn; // B-bus address + uint8_t UNUSEDn; // Unused byte + uint32_t A1Tn; // DMA Current Address / HDMA Table Start Address + uint16_t DASn; // DMA Byte-Counter / HDMA indirect table address + uint16_t A2An; // HDMA Table Current Address + uint8_t NLTRn; // HDMA Line-Counter + }; + Channel channels[8]; + + uint8_t MDMAEN = 0; // Start DMA transfer + uint8_t HDMAEN = 0; // Enable HDMA transfers +}; +} // namespace emu +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_EMU_MEMORY_DMA_H \ No newline at end of file diff --git a/src/app/emu/memory/memory.cc b/src/app/emu/memory/memory.cc new file mode 100644 index 00000000..fa2cbb6b --- /dev/null +++ b/src/app/emu/memory/memory.cc @@ -0,0 +1,82 @@ +#include "app/emu/memory/memory.h" + +#include + +#include +#include +#include +#include + +#include "app/emu/debug/log.h" + +namespace yaze { +namespace app { +namespace emu { + +void DrawSnesMemoryMapping(const MemoryImpl& memory) { + // Using those as a base value to create width/height that are factor of the + // size of our font + const float TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x; + const float TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing(); + const char* column_names[] = { + "Offset", "0x00", "0x01", "0x02", "0x03", "0x04", "0x05", "0x06", "0x07", + "0x08", "0x09", "0x0A", "0x0B", "0x0C", "0x0D", "0x0E", "0x0F", "0x10", + "0x11", "0x12", "0x13", "0x14", "0x15", "0x16", "0x17", "0x18", "0x19", + "0x1A", "0x1B", "0x1C", "0x1D", "0x1E", "0x1F"}; + const int columns_count = IM_ARRAYSIZE(column_names); + const int rows_count = 16; + + static ImGuiTableFlags table_flags = + ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollX | + ImGuiTableFlags_ScrollY | ImGuiTableFlags_BordersOuter | + ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_Hideable | + ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | + ImGuiTableFlags_HighlightHoveredColumn; + static bool bools[columns_count * rows_count] = {}; + static int frozen_cols = 1; + static int frozen_rows = 2; + ImGui::CheckboxFlags("_ScrollX", &table_flags, ImGuiTableFlags_ScrollX); + ImGui::CheckboxFlags("_ScrollY", &table_flags, ImGuiTableFlags_ScrollY); + ImGui::CheckboxFlags("_NoBordersInBody", &table_flags, + ImGuiTableFlags_NoBordersInBody); + ImGui::CheckboxFlags("_HighlightHoveredColumn", &table_flags, + ImGuiTableFlags_HighlightHoveredColumn); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::SliderInt("Frozen columns", &frozen_cols, 0, 2); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::SliderInt("Frozen rows", &frozen_rows, 0, 2); + + if (ImGui::BeginTable("table_angled_headers", columns_count, table_flags, + ImVec2(0.0f, TEXT_BASE_HEIGHT * 12))) { + ImGui::TableSetupColumn( + column_names[0], + ImGuiTableColumnFlags_NoHide | ImGuiTableColumnFlags_NoReorder); + for (int n = 1; n < columns_count; n++) + ImGui::TableSetupColumn(column_names[n], + ImGuiTableColumnFlags_AngledHeader | + ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupScrollFreeze(frozen_cols, frozen_rows); + + ImGui::TableAngledHeadersRow(); + ImGui::TableHeadersRow(); + for (int row = 0; row < rows_count; row++) { + ImGui::PushID(row); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::Text("Offset 0x%04X", row); + for (int column = 1; column < columns_count; column++) + if (ImGui::TableSetColumnIndex(column)) { + ImGui::PushID(column); + ImGui::Checkbox("", &bools[row * columns_count + column]); + ImGui::PopID(); + } + ImGui::PopID(); + } + ImGui::EndTable(); + } +} + +} // namespace emu +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/emu/memory/memory.h b/src/app/emu/memory/memory.h new file mode 100644 index 00000000..ed91a36f --- /dev/null +++ b/src/app/emu/memory/memory.h @@ -0,0 +1,403 @@ +#ifndef MEM_H +#define MEM_H + +#include +#include +#include +#include + +#include "app/emu/debug/log.h" + +// LoROM (Mode 20): + +// Banks Offset Purpose +// 00-3F 0000-1FFF LowRAM (shadowed from 7E) +// 2000-2FFF PPU1, APU +// 3000-3FFF SFX, DSP, etc. +// 4000-41FF Controller +// 4200-5FFF PPU2, DMA, etc. +// 6000-7FFF Expansion RAM (reserved) +// 8000-FFFF 32k ROM Chunk +// 40-7C 0000-7FFF 32k ROM Chunk +// 8000-FFFF 32k ROM Chunk +// 7D 0000-FFFF SRAM +// 7E 0000-1FFF LowRAM +// 2000-FFFF System RAM +// 7F 0000-FFFF System RAM + +namespace yaze { +namespace app { +namespace emu { + +enum ROMSpeed { SLOW_ROM = 0x00, FAST_ROM = 0x07 }; + +enum BankSize { LOW_ROM = 0x00, HI_ROM = 0x01 }; + +enum ROMType { + ROM_DEFAULT = 0x00, + ROM_RAM = 0x01, + ROM_SRAM = 0x02, + ROM_DSP1 = 0x03, + ROM_DSP1_RAM = 0x04, + ROM_DSP1_SRAM = 0x05, + FX = 0x06 +}; + +enum ROMSize { + SIZE_2_MBIT = 0x08, + SIZE_4_MBIT = 0x09, + SIZE_8_MBIT = 0x0A, + SIZE_16_MBIT = 0x0B, + SIZE_32_MBIT = 0x0C +}; + +enum SRAMSize { + NO_SRAM = 0x00, + SRAM_16_KBIT = 0x01, + SRAM_32_KBIT = 0x02, + SRAM_64_KBIT = 0x03 +}; + +enum CountryCode { + JAPAN = 0x00, + USA = 0x01, + EUROPE_OCEANIA_ASIA = 0x02, + // ... and other countries +}; + +enum License { + INVALID = 0, + NINTENDO = 1, + ZAMUSE = 5, + CAPCOM = 8, + // ... and other licenses +}; + +class ROMInfo { + public: + std::string title; + ROMSpeed romSpeed; + BankSize bankSize; + ROMType romType; + ROMSize romSize; + SRAMSize sramSize; + CountryCode countryCode; + License license; + uint8_t version; + uint16_t checksumComplement; + uint16_t checksum; + uint16_t nmiVblVector; + uint16_t resetVector; +}; + +class Observer { + public: + virtual ~Observer() = default; + virtual void Notify(uint32_t address, uint8_t data) = 0; +}; + +constexpr uint32_t kROMStart = 0x008000; +constexpr uint32_t kROMSize = 0x200000; +constexpr uint32_t kRAMStart = 0x7E0000; +constexpr uint32_t kRAMSize = 0x20000; +constexpr uint32_t kVRAMStart = 0x210000; +constexpr uint32_t kVRAMSize = 0x10000; +constexpr uint32_t kOAMStart = 0x218000; +constexpr uint32_t kOAMSize = 0x220; + +// memory.h +class Memory { + public: + virtual ~Memory() = default; + virtual uint8_t ReadByte(uint32_t address) const = 0; + virtual uint16_t ReadWord(uint32_t address) const = 0; + virtual uint32_t ReadWordLong(uint32_t address) const = 0; + virtual std::vector ReadByteVector(uint32_t address, + uint16_t length) const = 0; + + virtual void WriteByte(uint32_t address, uint8_t value) = 0; + virtual void WriteWord(uint32_t address, uint16_t value) = 0; + virtual void WriteLong(uint32_t address, uint32_t value) = 0; + + virtual void PushByte(uint8_t value) = 0; + virtual uint8_t PopByte() = 0; + virtual void PushWord(uint16_t value) = 0; + virtual uint16_t PopWord() = 0; + virtual void PushLong(uint32_t value) = 0; + virtual uint32_t PopLong() = 0; + + virtual uint16_t SP() const = 0; + virtual void SetSP(uint16_t value) = 0; + + virtual void ClearMemory() = 0; + + virtual uint8_t operator[](int i) const = 0; + virtual uint8_t at(int i) const = 0; +}; + +enum class MemoryMapping { SNES_LOROM = 0, PC_ADDRESS = 1 }; + +class MemoryImpl : public Memory, public Loggable { + public: + void Initialize(const std::vector& romData, bool verbose = false, + MemoryMapping mapping = MemoryMapping::SNES_LOROM) { + verbose_ = verbose; + mapping_ = mapping; + if (mapping == MemoryMapping::PC_ADDRESS) { + memory_.resize(romData.size()); + std::copy(romData.begin(), romData.end(), memory_.begin()); + return; + } + + memory_.resize(0x1000000); // 16 MB + + const size_t ROM_CHUNK_SIZE = 0x8000; // 32 KB + const size_t SRAM_SIZE = 0x10000; // 64 KB + const size_t SYSTEM_RAM_SIZE = 0x20000; // 128 KB + const size_t EXPANSION_RAM_SIZE = 0x2000; // 8 KB + const size_t HARDWARE_REGISTERS_SIZE = 0x4000; // 16 KB + + // Clear memory + std::fill(memory_.begin(), memory_.end(), 0); + + // Load ROM data into memory based on LoROM mapping + size_t romSize = romData.size(); + size_t romAddress = 0; + for (size_t bank = 0x00; bank <= 0x3F; ++bank) { + for (size_t offset = 0x8000; offset <= 0xFFFF; offset += ROM_CHUNK_SIZE) { + if (romAddress < romSize) { + std::copy(romData.begin() + romAddress, + romData.begin() + romAddress + ROM_CHUNK_SIZE, + memory_.begin() + (bank << 16) + offset); + romAddress += ROM_CHUNK_SIZE; + } + } + } + + // Initialize SRAM at banks 0x7D and 0xFD + std::fill(memory_.begin() + (0x7D << 16), memory_.begin() + (0x7E << 16), + 0); + std::fill(memory_.begin() + (0xFD << 16), memory_.begin() + (0xFE << 16), + 0); + + // Initialize System RAM at banks 0x7E and 0x7F + std::fill(memory_.begin() + (0x7E << 16), + memory_.begin() + (0x7E << 16) + SYSTEM_RAM_SIZE, 0); + + // Initialize Shadow RAM at banks 0x00-0x3F and 0x80-0xBF + for (size_t bank = 0x00; bank <= 0xBF; bank += 0x80) { + std::fill(memory_.begin() + (bank << 16), + memory_.begin() + (bank << 16) + 0x2000, 0); + } + + // Initialize Hardware Registers at banks 0x00-0x3F and 0x80-0xBF + for (size_t bank = 0x00; bank <= 0xBF; bank += 0x80) { + std::fill( + memory_.begin() + (bank << 16) + 0x2000, + memory_.begin() + (bank << 16) + 0x2000 + HARDWARE_REGISTERS_SIZE, 0); + } + + // Initialize Expansion RAM at banks 0x00-0x3F and 0x80-0xBF + for (size_t bank = 0x00; bank <= 0xBF; bank += 0x80) { + std::fill(memory_.begin() + (bank << 16) + 0x6000, + memory_.begin() + (bank << 16) + 0x6000 + EXPANSION_RAM_SIZE, + 0); + } + + // Initialize Reset and NMI Vectors at bank 0xFF + std::fill(memory_.begin() + (0xFF << 16) + 0xFF00, + memory_.begin() + (0xFF << 16) + 0xFFFF + 1, 0); + + // Copy data into rom_ vector + rom_.resize(kROMSize); + std::copy(memory_.begin() + kROMStart, + memory_.begin() + kROMStart + kROMSize, rom_.begin()); + + // Copy data into ram_ vector + ram_.resize(kRAMSize); + std::copy(memory_.begin() + kRAMStart, + memory_.begin() + kRAMStart + kRAMSize, ram_.begin()); + + // Copy data into vram_ vector + vram_.resize(kVRAMSize); + std::copy(memory_.begin() + kVRAMStart, + memory_.begin() + kVRAMStart + kVRAMSize, vram_.begin()); + + // Copy data into oam_ vector + oam_.resize(kOAMSize); + std::copy(memory_.begin() + kOAMStart, + memory_.begin() + kOAMStart + kOAMSize, oam_.begin()); + } + + uint8_t ReadByte(uint32_t address) const override { + uint32_t mapped_address = GetMappedAddress(address); + NotifyObservers(mapped_address, /*data=*/0); + return memory_.at(mapped_address); + } + uint16_t ReadWord(uint32_t address) const override { + uint32_t mapped_address = GetMappedAddress(address); + NotifyObservers(mapped_address, /*data=*/0); + return static_cast(memory_.at(mapped_address)) | + (static_cast(memory_.at(mapped_address + 1)) << 8); + } + uint32_t ReadWordLong(uint32_t address) const override { + uint32_t mapped_address = GetMappedAddress(address); + NotifyObservers(mapped_address, /*data=*/0); + return static_cast(memory_.at(mapped_address)) | + (static_cast(memory_.at(mapped_address + 1)) << 8) | + (static_cast(memory_.at(mapped_address + 2)) << 16); + } + std::vector ReadByteVector(uint32_t address, + uint16_t length) const override { + uint32_t mapped_address = GetMappedAddress(address); + NotifyObservers(mapped_address, /*data=*/0); + return std::vector(memory_.begin() + mapped_address, + memory_.begin() + mapped_address + length); + } + + void WriteByte(uint32_t address, uint8_t value) override { + uint32_t mapped_address = GetMappedAddress(address); + memory_[mapped_address] = value; + } + void WriteWord(uint32_t address, uint16_t value) override { + uint32_t mapped_address = GetMappedAddress(address); + memory_.at(mapped_address) = value & 0xFF; + memory_.at(mapped_address + 1) = (value >> 8) & 0xFF; + } + void WriteLong(uint32_t address, uint32_t value) override { + uint32_t mapped_address = GetMappedAddress(address); + memory_.at(mapped_address) = value & 0xFF; + memory_.at(mapped_address + 1) = (value >> 8) & 0xFF; + memory_.at(mapped_address + 2) = (value >> 16) & 0xFF; + } + + // Stack operations + void PushByte(uint8_t value) override { + if (SP_ > 0x0100) { + memory_.at(SP_--) = value; + } else { + // Handle stack underflow + std::cout << "Stack underflow!" << std::endl; + throw std::runtime_error("Stack underflow!"); + } + } + + uint8_t PopByte() override { + if (SP_ < 0x1FF) { + return memory_.at(++SP_); + } else { + // Handle stack overflow + std::cout << "Stack overflow!" << std::endl; + throw std::runtime_error("Stack overflow!"); + } + } + + void PushWord(uint16_t value) override { + PushByte(value >> 8); + PushByte(value & 0xFF); + } + + uint16_t PopWord() override { + uint8_t low = PopByte(); + uint8_t high = PopByte(); + return (static_cast(high) << 8) | low; + } + + void PushLong(uint32_t value) override { + PushByte(value >> 16); + PushByte(value >> 8); + PushByte(value & 0xFF); + } + + uint32_t PopLong() override { + uint8_t low = PopByte(); + uint8_t mid = PopByte(); + uint8_t high = PopByte(); + return (static_cast(high) << 16) | + (static_cast(mid) << 8) | low; + } + + void AddObserver(Observer* observer) { observers_.push_back(observer); } + + // Stack Pointer access. + uint16_t SP() const override { return SP_; } + void SetSP(uint16_t value) override { SP_ = value; } + void ClearMemory() override { std::fill(memory_.begin(), memory_.end(), 0); } + + uint8_t at(int i) const override { return memory_[i]; } + uint8_t operator[](int i) const override { + if (i > memory_.size()) { + std::cout << i << " out of bounds \n"; + return memory_[0]; + } + return memory_[i]; + } + + auto size() const { return memory_.size(); } + auto begin() const { return memory_.begin(); } + auto end() const { return memory_.end(); } + auto data() const { return memory_.data(); } + + // Define memory regions + std::vector rom_; + std::vector ram_; + std::vector vram_; + std::vector oam_; + + private: + uint32_t GetMappedAddress(uint32_t address) const { + uint8_t bank = address >> 16; + uint32_t offset = address & 0xFFFF; + + if (mapping_ == MemoryMapping::PC_ADDRESS) { + return address; + } + + if (bank <= 0x3F) { + if (address <= 0x1FFF) { + return (0x7E << 16) + offset; // Shadow RAM + } else if (address <= 0x5FFF) { + return (bank << 16) + (offset - 0x2000) + 0x2000; // Hardware Registers + } else if (address <= 0x7FFF) { + return offset - 0x6000 + 0x6000; // Expansion RAM + } else { + // Return lorom mapping + return (bank << 16) + (offset - 0x8000) + 0x8000; // ROM + } + } else if (bank == 0x7D) { + return offset + 0x7D0000; // SRAM + } else if (bank == 0x7E || bank == 0x7F) { + return offset + 0x7E0000; // System RAM + } else if (bank >= 0x80) { + // Handle HiROM and mirrored areas + } + + return address; // Return the original address if no mapping is defined + } + + void NotifyObservers(uint32_t address, uint8_t data) const { + for (auto observer : observers_) { + observer->Notify(address, data); + } + } + + bool verbose_ = false; + + std::vector observers_; + + // Memory (64KB) + std::vector memory_; + + // Stack Pointer + uint16_t SP_ = 0x01FF; + + MemoryMapping mapping_ = MemoryMapping::SNES_LOROM; +}; + +void DrawSnesMemoryMapping(const MemoryImpl& memory); + +} // namespace emu +} // namespace app +} // namespace yaze + +#endif // MEM_H \ No newline at end of file diff --git a/src/app/emu/memory/mock_memory.h b/src/app/emu/memory/mock_memory.h new file mode 100644 index 00000000..e1a43d69 --- /dev/null +++ b/src/app/emu/memory/mock_memory.h @@ -0,0 +1,189 @@ +#ifndef YAZE_TEST_MOCK_MOCK_MEMORY_H +#define YAZE_TEST_MOCK_MOCK_MEMORY_H + +#include +#include + +#include "app/emu/cpu/clock.h" +#include "app/emu/cpu/cpu.h" +#include "app/emu/memory/memory.h" + +using yaze::app::emu::Clock; +using yaze::app::emu::CPU; +using yaze::app::emu::Memory; + +class MockClock : public Clock { + public: + MOCK_METHOD(void, UpdateClock, (double delta), (override)); + MOCK_METHOD(unsigned long long, GetCycleCount, (), (const, override)); + MOCK_METHOD(void, ResetAccumulatedTime, (), (override)); + MOCK_METHOD(void, SetFrequency, (float new_frequency), (override)); + MOCK_METHOD(float, GetFrequency, (), (const, override)); +}; + +// 0x1000000 is 16 MB, simplifying the memory layout for testing +// 2 MB is = 0x200000 + +class MockMemory : public Memory { + public: + MOCK_CONST_METHOD1(ReadByte, uint8_t(uint32_t address)); + MOCK_CONST_METHOD1(ReadWord, uint16_t(uint32_t address)); + MOCK_CONST_METHOD1(ReadWordLong, uint32_t(uint32_t address)); + MOCK_METHOD(std::vector, ReadByteVector, + (uint32_t address, uint16_t length), (const, override)); + + MOCK_METHOD2(WriteByte, void(uint32_t address, uint8_t value)); + MOCK_METHOD2(WriteWord, void(uint32_t address, uint16_t value)); + MOCK_METHOD2(WriteLong, void(uint32_t address, uint32_t value)); + + MOCK_METHOD1(PushByte, void(uint8_t value)); + MOCK_METHOD0(PopByte, uint8_t()); + MOCK_METHOD1(PushWord, void(uint16_t value)); + MOCK_METHOD0(PopWord, uint16_t()); + MOCK_METHOD1(PushLong, void(uint32_t value)); + MOCK_METHOD0(PopLong, uint32_t()); + + MOCK_CONST_METHOD0(SP, uint16_t()); + MOCK_METHOD1(SetSP, void(uint16_t value)); + + MOCK_METHOD1(SetMemory, void(const std::vector& data)); + MOCK_METHOD1(LoadData, void(const std::vector& data)); + + MOCK_METHOD0(ClearMemory, void()); + + MOCK_CONST_METHOD1(at, uint8_t(int i)); + uint8_t operator[](int i) const override { return memory_[i]; } + + void SetMemoryContents(const std::vector& data) { + if (data.size() > memory_.size()) { + memory_.resize(data.size()); + } + std::copy(data.begin(), data.end(), memory_.begin()); + } + + void SetMemoryContents(const std::vector& data) { + if (data.size() > memory_.size()) { + memory_.resize(data.size()); + } + int i = 0; + for (const auto& each : data) { + memory_[i] = each & 0xFF; + memory_[i + 1] = (each >> 8) & 0xFF; + i += 2; + } + } + + void InsertMemory(const uint64_t address, const std::vector& data) { + if (address > memory_.size()) { + memory_.resize(address + data.size()); + } + + int i = 0; + for (const auto& each : data) { + memory_[address + i] = each; + i++; + } + } + + void Initialize(const std::vector& romData) { + // 16 MB, simplifying the memory layout for testing + memory_.resize(0x1000000); + + // Clear memory + std::fill(memory_.begin(), memory_.end(), 0); + + // Load ROM data into mock memory + size_t romSize = romData.size(); + size_t romAddress = 0; + const size_t ROM_CHUNK_SIZE = 0x8000; // 32 KB + for (size_t bank = 0x00; bank <= 0xBF; bank += 0x80) { + for (size_t offset = 0x8000; offset <= 0xFFFF; offset += ROM_CHUNK_SIZE) { + if (romAddress < romSize) { + std::copy(romData.begin() + romAddress, + romData.begin() + romAddress + ROM_CHUNK_SIZE, + memory_.begin() + (bank << 16) + offset); + romAddress += ROM_CHUNK_SIZE; + } + } + } + } + + void Init() { + ON_CALL(*this, ReadByte(::testing::_)) + .WillByDefault( + [this](uint32_t address) { return memory_.at(address); }); + ON_CALL(*this, ReadWord(::testing::_)) + .WillByDefault([this](uint32_t address) { + return static_cast(memory_.at(address)) | + (static_cast(memory_.at(address + 1)) << 8); + }); + ON_CALL(*this, ReadWordLong(::testing::_)) + .WillByDefault([this](uint32_t address) { + return static_cast(memory_.at(address)) | + (static_cast(memory_.at(address + 1)) << 8) | + (static_cast(memory_.at(address + 2)) << 16); + }); + ON_CALL(*this, ReadByteVector(::testing::_, ::testing::_)) + .WillByDefault([this](uint32_t address, uint16_t length) { + std::vector data; + for (int i = 0; i < length; i++) { + data.push_back(memory_.at(address + i)); + } + return data; + }); + ON_CALL(*this, WriteByte(::testing::_, ::testing::_)) + .WillByDefault([this](uint32_t address, uint8_t value) { + memory_[address] = value; + }); + ON_CALL(*this, WriteWord(::testing::_, ::testing::_)) + .WillByDefault([this](uint32_t address, uint16_t value) { + memory_[address] = value & 0xFF; + memory_[address + 1] = (value >> 8) & 0xFF; + }); + ON_CALL(*this, PushByte(::testing::_)).WillByDefault([this](uint8_t value) { + memory_.at(SP_--) = value; + }); + ON_CALL(*this, PopByte()).WillByDefault([this]() { + uint8_t value = memory_.at(SP_); + this->SetSP(SP_ + 1); + return value; + }); + ON_CALL(*this, PushWord(::testing::_)) + .WillByDefault([this](uint16_t value) { + memory_.at(SP_) = value & 0xFF; + memory_.at(SP_ + 1) = (value >> 8) & 0xFF; + this->SetSP(SP_ - 2); + }); + ON_CALL(*this, PopWord()).WillByDefault([this]() { + uint16_t value = static_cast(memory_.at(SP_)) | + (static_cast(memory_.at(SP_ + 1)) << 8); + this->SetSP(SP_ + 2); + return value; + }); + ON_CALL(*this, PushLong(::testing::_)) + .WillByDefault([this](uint32_t value) { + memory_.at(SP_) = value & 0xFF; + memory_.at(SP_ + 1) = (value >> 8) & 0xFF; + memory_.at(SP_ + 2) = (value >> 16) & 0xFF; + }); + ON_CALL(*this, PopLong()).WillByDefault([this]() { + uint32_t value = static_cast(memory_.at(SP_)) | + (static_cast(memory_.at(SP_ + 1)) << 8) | + (static_cast(memory_.at(SP_ + 2)) << 16); + this->SetSP(SP_ + 3); + return value; + }); + ON_CALL(*this, SP()).WillByDefault([this]() { return SP_; }); + ON_CALL(*this, SetSP(::testing::_)).WillByDefault([this](uint16_t value) { + SP_ = value; + }); + ON_CALL(*this, ClearMemory()).WillByDefault([this]() { + memory_.resize(64000, 0x00); + }); + } + + std::vector memory_; + uint16_t SP_ = 0x01FF; +}; + +#endif // YAZE_TEST_MOCK_MOCK_MEMORY_H \ No newline at end of file diff --git a/src/app/emu/snes.cc b/src/app/emu/snes.cc new file mode 100644 index 00000000..13f5960a --- /dev/null +++ b/src/app/emu/snes.cc @@ -0,0 +1,376 @@ +#include "app/emu/snes.h" + +#include + +#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/debug/debugger.h" +#include "app/emu/memory/memory.h" +#include "app/emu/video/ppu.h" +#include "app/rom.h" + +namespace yaze { +namespace app { +namespace emu { + +namespace { + +uint16_t GetHeaderOffset(const Memory& memory) { + uint8_t mapMode = memory[(0x00 << 16) + 0xFFD5]; + uint16_t offset; + + switch (mapMode & 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."); + } + + return offset; +} + +void audio_callback(void* userdata, uint8_t* stream, int len) { + auto* apu = static_cast(userdata); + auto* buffer = reinterpret_cast(stream); + + for (int i = 0; i < len / 2; i++) { // Assuming 16-bit samples + buffer[i] = apu->GetNextSample(); // This function should be implemented in + // APU to fetch the next sample + } +} + +} // namespace + +ROMInfo SNES::ReadRomHeader(uint32_t offset) { + ROMInfo romInfo; + + // Read cartridge title + char title[22]; + for (int i = 0; i < 21; ++i) { + title[i] = cpu_.ReadByte(offset + i); + } + title[21] = '\0'; // Null-terminate the string + romInfo.title = std::string(title); + + // Read ROM speed and memory map mode + uint8_t romSpeedAndMapMode = cpu_.ReadByte(offset + 0x15); + romInfo.romSpeed = (ROMSpeed)(romSpeedAndMapMode & 0x07); + romInfo.bankSize = (BankSize)((romSpeedAndMapMode >> 5) & 0x01); + + // Read ROM type + romInfo.romType = (ROMType)cpu_.ReadByte(offset + 0x16); + + // Read ROM size + romInfo.romSize = (ROMSize)cpu_.ReadByte(offset + 0x17); + + // Read RAM size + romInfo.sramSize = (SRAMSize)cpu_.ReadByte(offset + 0x18); + + // Read country code + romInfo.countryCode = (CountryCode)cpu_.ReadByte(offset + 0x19); + + // Read license + romInfo.license = (License)cpu_.ReadByte(offset + 0x1A); + + // Read ROM version + romInfo.version = cpu_.ReadByte(offset + 0x1B); + + // Read checksum complement + romInfo.checksumComplement = cpu_.ReadWord(offset + 0x1E); + + // Read checksum + romInfo.checksum = cpu_.ReadWord(offset + 0x1C); + + // Read NMI VBL vector + romInfo.nmiVblVector = cpu_.ReadWord(offset + 0x3E); + + // Read reset vector + romInfo.resetVector = cpu_.ReadWord(offset + 0x3C); + + return romInfo; +} + +void SNES::Init(ROM& rom) { + // Perform a long jump into a FastROM bank (if the ROM speed is FastROM) + // Disable the emulation flag (switch to 65816 native mode) + cpu_.E = 0; + + // Initialize CPU + cpu_.Init(); + + // Read the ROM header + auto header_offset = GetHeaderOffset(memory_); + rom_info_ = ReadRomHeader((0x00 << 16) + header_offset); + cpu_.PB = 0x00; + cpu_.PC = 0x8000; + + // Initialize PPU + ppu_.Init(); + + // Initialize APU + apu_.Init(); + + // Initialize SDL_Mixer to play the audio samples + // Mix_HookMusic(audio_callback, &apu); + + // Disable interrupts and rendering + memory_.WriteByte(0x4200, 0x00); // NMITIMEN + memory_.WriteByte(0x420C, 0x00); // HDMAEN + + // Disable screen + memory_.WriteByte(0x2100, 0x8F); // INIDISP + + // Fill Work-RAM with zeros using two 64KiB fixed address DMA transfers to + // WMDATA + // TODO: Make this load from work ram, potentially in Memory class + std::memset((void*)memory_.ram_.data(), 0, sizeof(memory_.ram_)); + + // Reset PPU registers to a known good state + memory_.WriteByte(0x4201, 0xFF); // WRIO + + // Objects + memory_.WriteByte(0x2101, 0x00); // OBSEL + memory_.WriteByte(0x2102, 0x00); // OAMADDL + memory_.WriteByte(0x2103, 0x00); // OAMADDH + + // Backgrounds + memory_.WriteByte(0x2105, 0x00); // BGMODE + memory_.WriteByte(0x2106, 0x00); // MOSAIC + + memory_.WriteByte(0x2107, 0x00); // BG1SC + memory_.WriteByte(0x2108, 0x00); // BG2SC + memory_.WriteByte(0x2109, 0x00); // BG3SC + memory_.WriteByte(0x210A, 0x00); // BG4SC + + memory_.WriteByte(0x210B, 0x00); // BG12NBA + memory_.WriteByte(0x210C, 0x00); // BG34NBA + + // Scroll Registers + memory_.WriteByte(0x210D, 0x00); // BG1HOFS + memory_.WriteByte(0x210E, 0xFF); // BG1VOFS + + memory_.WriteByte(0x210F, 0x00); // BG2HOFS + memory_.WriteByte(0x2110, 0xFF); // BG2VOFS + + memory_.WriteByte(0x2111, 0x00); // BG3HOFS + memory_.WriteByte(0x2112, 0xFF); // BG3VOFS + + memory_.WriteByte(0x2113, 0x00); // BG4HOFS + memory_.WriteByte(0x2114, 0xFF); // BG4VOFS + + // VRAM Registers + memory_.WriteByte(0x2115, 0x80); // VMAIN + + // Mode 7 + memory_.WriteByte(0x211A, 0x00); // M7SEL + memory_.WriteByte(0x211B, 0x01); // M7A + memory_.WriteByte(0x211C, 0x00); // M7B + memory_.WriteByte(0x211D, 0x00); // M7C + memory_.WriteByte(0x211E, 0x01); // M7D + memory_.WriteByte(0x211F, 0x00); // M7X + memory_.WriteByte(0x2120, 0x00); // M7Y + + // Windows + memory_.WriteByte(0x2123, 0x00); // W12SEL + memory_.WriteByte(0x2124, 0x00); // W34SEL + memory_.WriteByte(0x2125, 0x00); // WOBJSEL + memory_.WriteByte(0x2126, 0x00); // WH0 + memory_.WriteByte(0x2127, 0x00); // WH1 + memory_.WriteByte(0x2128, 0x00); // WH2 + memory_.WriteByte(0x2129, 0x00); // WH3 + memory_.WriteByte(0x212A, 0x00); // WBGLOG + memory_.WriteByte(0x212B, 0x00); // WOBJLOG + + // Layer Enable + memory_.WriteByte(0x212C, 0x00); // TM + memory_.WriteByte(0x212D, 0x00); // TS + memory_.WriteByte(0x212E, 0x00); // TMW + memory_.WriteByte(0x212F, 0x00); // TSW + + // Color Math + memory_.WriteByte(0x2130, 0x30); // CGWSEL + memory_.WriteByte(0x2131, 0x00); // CGADSUB + memory_.WriteByte(0x2132, 0xE0); // COLDATA + + // Misc + memory_.WriteByte(0x2133, 0x00); // SETINI + + // Psuedo-Init + memory_.WriteWord(0x2140, 0xBBAA); + + running_ = true; + scanline = 0; +} + +void SNES::Run() { + const double targetFPS = 60.0; // 60 frames per second + const double frame_time = 1.0 / targetFPS; + double frame_accumulated_time = 0.0; + + auto last_time = std::chrono::high_resolution_clock::now(); + + if (running_) { + auto current_time = std::chrono::high_resolution_clock::now(); + double delta_time = + std::chrono::duration(current_time - last_time).count(); + last_time = current_time; + + frame_accumulated_time += delta_time; + + // Update the CPU + cpu_.UpdateClock(delta_time); + cpu_.Update(GetCpuMode()); + + // Update the PPU + ppu_.UpdateClock(delta_time); + ppu_.Update(); + + // Update the APU + apu_.UpdateClock(delta_time); + apu_.Update(); + + if (frame_accumulated_time >= frame_time) { + // renderer.Render(); + frame_accumulated_time -= frame_time; + } + + HandleInput(); + } +} + +void SNES::StepRun() { + // Update the CPU + cpu_.UpdateClock(0.0); + cpu_.Update(CPU::UpdateMode::Step); + + // Update the PPU + ppu_.UpdateClock(0.0); + ppu_.Update(); + + // Update the APU + apu_.UpdateClock(0.0); + apu_.Update(); + + HandleInput(); +} + +// Enable NMI Interrupts +void SNES::EnableVBlankInterrupts() { + v_blank_flag_ = false; + + // Clear the RDNMI VBlank flag + memory_.ReadByte(0x4210); // RDNMI + + // Enable vblank NMI interrupts and Joypad auto-read + memory_.WriteByte(0x4200, 0x81); // NMITIMEN +} + +// Wait until the VBlank routine has been processed +void SNES::WaitForVBlank() { + v_blank_flag_ = true; + + // Loop until `v_blank_flag_` is clear + while (v_blank_flag_) { + std::this_thread::yield(); + } +} + +// NMI Interrupt Service Routine +void SNES::NmiIsr() { + // Switch to a FastROM bank (assuming NmiIsr is in bank 0x80) + // ... + + // Push CPU registers to stack + cpu_.PHP(); + + // Reset DB and DP registers + cpu_.DB = 0x80; // Assuming bank 0x80, can be changed to 0x00 + cpu_.D = 0; + + if (v_blank_flag_) { + VBlankRoutine(); + + // Clear `v_blank_flag_` + v_blank_flag_ = false; + } + + // Increment 32-bit frame_counter_ + frame_counter_++; + + // Restore CPU registers + cpu_.PHB(); +} + +// VBlank routine +void SNES::VBlankRoutine() { + // Read the joypad state + // ... + + // Update the PPU + // ... + + // Update the APU + // ... +} + +void SNES::StartApuDataTransfer() { + // 2. Setting the starting address + const uint16_t startAddress = 0x0200; + memory_.WriteByte(0x2142, startAddress & 0xFF); // Lower byte + memory_.WriteByte(0x2143, startAddress >> 8); // Upper byte + memory_.WriteByte(0x2141, 0xCC); // Any non-zero value + memory_.WriteByte(0x2140, 0xCC); // Signal to start + + const int DATA_SIZE = 0x1000; // 4 KiB + + // 3. Sending data (simplified) + // Assuming a buffer `audioData` containing the audio program/data + uint8_t audioData[DATA_SIZE]; // Define DATA_SIZE and populate audioData as + // needed + for (int i = 0; i < DATA_SIZE; ++i) { + memory_.WriteByte(0x2141, audioData[i]); + memory_.WriteByte(0x2140, i & 0xFF); + while (memory_.ReadByte(0x2140) != (i & 0xFF)) + ; // Wait for acknowledgment + } + + // 4. Running the SPC700 program + memory_.WriteByte(0x2142, startAddress & 0xFF); // Lower byte + memory_.WriteByte(0x2143, startAddress >> 8); // Upper byte + memory_.WriteByte(0x2141, 0x00); // Zero to start the program + memory_.WriteByte(0x2140, 0xCE); // Increment by 2 + while (memory_.ReadByte(0x2140) != 0xCE) + ; // Wait for acknowledgment +} + +void SNES::HandleInput() { + // ... +} + +void SNES::SaveState(const std::string& path) { + // ... +} + +void SNES::LoadState(const std::string& path) { + // ... +} + +} // namespace emu +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/emu/snes.h b/src/app/emu/snes.h new file mode 100644 index 00000000..a2c39d4f --- /dev/null +++ b/src/app/emu/snes.h @@ -0,0 +1,120 @@ +#ifndef YAZE_APP_EMU_SNES_H +#define YAZE_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/debug/debugger.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 { + +class SNES : public DMA { + public: + SNES() = default; + ~SNES() = default; + + ROMInfo ReadRomHeader(uint32_t offset); + + // Initialization + void Init(ROM& rom); + + // Main emulation loop + void Run(); + + // Step through a single instruction + void StepRun(); + + // Enable NMI Interrupts + void EnableVBlankInterrupts(); + + // Wait until the VBlank routine has been processed + void WaitForVBlank(); + + // NMI Interrupt Service Routine + void NmiIsr(); + + // VBlank routine + void VBlankRoutine(); + + // Boot the APU with the IPL ROM + void BootApuWithIPL(); + void StartApuDataTransfer(); + + // Controller input handling + void HandleInput(); + + // Save/Load game state + void SaveState(const std::string& path); + void LoadState(const std::string& path); + + bool running() const { return running_; } + + auto cpu() -> CPU& { return cpu_; } + auto ppu() -> Ppu& { return ppu_; } + auto Memory() -> MemoryImpl* { return &memory_; } + + void SetCpuMode(int mode) { cpu_mode_ = mode; } + CPU::UpdateMode GetCpuMode() const { + return static_cast(cpu_mode_); + } + + void SetupMemory(ROM& rom) { + // Setup observers for the memory space + memory_.AddObserver(&apu_); + memory_.AddObserver(&ppu_); + + // Load the ROM into memory and set up the memory mapping + rom_data = rom.vector(); + memory_.Initialize(rom_data); + } + + private: + void WriteToRegister(uint16_t address, uint8_t value) { + memory_.WriteByte(address, value); + } + + // Components of the SNES + MemoryImpl memory_; + ClockImpl clock_; + AudioRamImpl audio_ram_; + + CPU cpu_{memory_, clock_}; + Ppu ppu_{memory_, clock_}; + APU apu_{memory_, audio_ram_, clock_}; + + // Helper classes + ROMInfo rom_info_; + Debugger debugger; + + // Currently loaded ROM + std::vector rom_data; + + // Byte flag to indicate if the VBlank routine should be executed or not + std::atomic v_blank_flag_; + + // 32-bit counter to track the number of NMI interrupts + std::atomic frame_counter_; + + // Other private member variables + bool running_ = false; + int scanline; + int cpu_mode_ = 0; +}; + +} // namespace emu +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_EMU_SNES_H \ No newline at end of file diff --git a/src/app/emu/video/ppu.cc b/src/app/emu/video/ppu.cc new file mode 100644 index 00000000..c0f6bb26 --- /dev/null +++ b/src/app/emu/video/ppu.cc @@ -0,0 +1,435 @@ +#include "app/emu/video/ppu.h" + +#include +#include +#include + +#include "app/emu/memory/memory.h" + +namespace yaze { +namespace app { +namespace emu { + +using namespace PpuRegisters; + +void Ppu::Update() { + auto cycles_to_run = clock_.GetCycleCount(); + + UpdateInternalState(cycles_to_run); + + // Render however many scanlines we're supposed to. + if (current_scanline_ < visibleScanlines) { + // Render the current scanline + RenderScanline(); + + // Increment the current scanline + current_scanline_++; + } +} + +void Ppu::UpdateInternalState(int cycles) { + // Update the Ppu's internal state based on the number of cycles + cycle_count_ += cycles; + + // Check if it's time to move to the next scanline + if (cycle_count_ >= cyclesPerScanline) { + current_scanline_++; + cycle_count_ -= cyclesPerScanline; + + // If we've reached the end of the frame, reset to the first scanline + if (current_scanline_ >= totalScanlines) { + current_scanline_ = 0; + } + } +} + +void Ppu::RenderScanline() { + for (int y = 0; y < 240; ++y) { + for (int x = 0; x < 256; ++x) { + // Calculate the color index based on the x and y coordinates + uint8_t color_index = (x + y) % 8; + + // Set the pixel in the frame buffer to the calculated color index + frame_buffer_[y * 256 + x] = color_index; + } + } + + // Fetch the tile data from VRAM, tile map data from memory, and palette data + // from CGRAM + // UpdateTileData(); // Fetches the tile data from VRAM and stores it in an + // internal buffer + UpdateTileMapData(); // Fetches the tile map data from memory and stores it + // in an internal buffer + UpdatePaletteData(); // Fetches the palette data from CGRAM and stores it in + // an internal buffer + + // Render the background layers, taking into account the current mode and + // layer priorities + for (int layer = 1; layer <= 4; ++layer) { + RenderBackground(layer); // Renders the specified background layer into an + // internal layer buffer + } + + // Render the sprite layer, taking into account sprite priorities and + // transparency + RenderSprites(); // Renders the sprite layer into an internal sprite buffer + + // Apply effects to the layers, such as scaling, rotation, and blending + ApplyEffects(); // Applies effects to the layers based on the current mode + // and register settings + + // Combine the layers into a single image and store it in the frame buffer + ComposeLayers(); // Combines the layers into a single image and stores it in + // the frame buffer + + // Display the frame buffer on the screen + DisplayFrameBuffer(); +} + +void Ppu::Notify(uint32_t address, uint8_t data) { + // Handle communication in the Ppu. + if (address >= 0x2100 && address <= 0x213F) { + // Handle register notification + switch (address) { + case INIDISP: + enable_forced_blanking_ = (data >> 7) & 0x01; + break; + case OBJSEL: + oam_size_.base_selection = (data >> 2) & 0x03; + oam_size_.name_selection = (data >> 4) & 0x07; + oam_size_.object_size = data & 0x03; + break; + case OAMADDL: + oam_address_.oam_address_low = data; + break; + case OAMADDH: + oam_address_.oam_address_msb = data & 0x01; + oam_address_.oam_priority_rotation = (data >> 1) & 0x01; + break; + case OAMDATA: + // Write the data to OAM + break; + case BGMODE: + // Update the Ppu mode settings + UpdateModeSettings(); + break; + case MOSAIC: + mosaic_.bg_enable = (data >> 7) & 0x01; + mosaic_.mosaic_size = data & 0x0F; + break; + case BG1SC: + bgsc_[0] = BGSC(data); + break; + case BG2SC: + bgsc_[1] = BGSC(data); + break; + case BG3SC: + bgsc_[2] = BGSC(data); + break; + case BG4SC: + bgsc_[3] = BGSC(data); + break; + case BG12NBA: + bgnba_[0] = BGNBA(data); + break; + case BG34NBA: + bgnba_[1] = BGNBA(data); + break; + case BG1HOFS: + bghofs_[0].horizontal_scroll = data; + break; + case BG2HOFS: + bghofs_[1].horizontal_scroll = data; + break; + case BG3HOFS: + bghofs_[2].horizontal_scroll = data; + break; + case BG4HOFS: + bghofs_[3].horizontal_scroll = data; + break; + case BG1VOFS: + bgvofs_[0].vertical_scroll = data; + break; + case BG2VOFS: + bgvofs_[1].vertical_scroll = data; + break; + case BG3VOFS: + bgvofs_[2].vertical_scroll = data; + break; + case BG4VOFS: + bgvofs_[3].vertical_scroll = data; + break; + case VMAIN: + vmain_.increment_size = data & 0x03; + vmain_.remapping = (data >> 2) & 0x03; + vmain_.address_increment_mode = (data >> 4) & 0x01; + break; + case VMADDL: + vmaddl_.address_low = data; + break; + case VMADDH: + vmaddh_.address_high = data; + break; + case M7SEL: + m7sel_.flip_horizontal = data & 0x01; + m7sel_.flip_vertical = (data >> 1) & 0x01; + m7sel_.fill = (data >> 2) & 0x01; + m7sel_.tilemap_repeat = (data >> 3) & 0x01; + break; + case M7A: + m7a_.matrix_a = data; + break; + case M7B: + m7b_.matrix_b = data; + break; + case M7C: + m7c_.matrix_c = data; + break; + case M7D: + m7d_.matrix_d = data; + break; + case M7X: + m7x_.center_x = data; + break; + case M7Y: + m7y_.center_y = data; + break; + case CGADD: + cgadd_.address = data; + break; + case CGDATA: + // Write the data to CGRAM + break; + case W12SEL: + w12sel_.enable_bg1_a = data & 0x01; + w12sel_.invert_bg1_a = (data >> 1) & 0x01; + w12sel_.enable_bg1_b = (data >> 2) & 0x01; + w12sel_.invert_bg1_b = (data >> 3) & 0x01; + w12sel_.enable_bg2_c = (data >> 4) & 0x01; + w12sel_.invert_bg2_c = (data >> 5) & 0x01; + w12sel_.enable_bg2_d = (data >> 6) & 0x01; + w12sel_.invert_bg2_d = (data >> 7) & 0x01; + break; + case W34SEL: + w34sel_.enable_bg3_e = data & 0x01; + w34sel_.invert_bg3_e = (data >> 1) & 0x01; + w34sel_.enable_bg3_f = (data >> 2) & 0x01; + w34sel_.invert_bg3_f = (data >> 3) & 0x01; + w34sel_.enable_bg4_g = (data >> 4) & 0x01; + w34sel_.invert_bg4_g = (data >> 5) & 0x01; + w34sel_.enable_bg4_h = (data >> 6) & 0x01; + w34sel_.invert_bg4_h = (data >> 7) & 0x01; + break; + case WOBJSEL: + wobjsel_.enable_obj_i = data & 0x01; + wobjsel_.invert_obj_i = (data >> 1) & 0x01; + wobjsel_.enable_obj_j = (data >> 2) & 0x01; + wobjsel_.invert_obj_j = (data >> 3) & 0x01; + wobjsel_.enable_color_k = (data >> 4) & 0x01; + wobjsel_.invert_color_k = (data >> 5) & 0x01; + wobjsel_.enable_color_l = (data >> 6) & 0x01; + wobjsel_.invert_color_l = (data >> 7) & 0x01; + break; + case WH0: + wh0_.left_position = data; + break; + case WH1: + wh1_.right_position = data; + break; + case WH2: + wh2_.left_position = data; + break; + case WH3: + wh3_.right_position = data; + break; + case TM: + tm_.enable_layer = (data >> 5) & 0x01; // + break; + case TS: + ts_.enable_layer = (data >> 5) & 0x01; + break; + case TMW: + tmw_.enable_window = (data >> 5) & 0x01; + break; + case TSW: + tsw_.enable_window = (data >> 5) & 0x01; + break; + } + } +} + +void Ppu::UpdateModeSettings() { + // Read the Ppu mode settings from the Ppu registers + uint8_t modeRegister = memory_.ReadByte(PpuRegisters::INIDISP); + + // Mode is stored in the lower 3 bits + auto mode = static_cast(modeRegister & 0x07); + + // Update the tilemap, tile data, and palette settings + switch (mode) { + case BackgroundMode::Mode0: + // Mode 0: 4 layers, each 2bpp (4 colors) + break; + + case BackgroundMode::Mode1: + // Mode 1: 2 layers, 4bpp (16 colors), 1 layer, 2bpp (4 colors) + break; + + case BackgroundMode::Mode2: + // Mode 2: 2 layers, 4bpp (16 colors), 1 layer for offset-per-tile + break; + + case BackgroundMode::Mode3: + // Mode 3: 1 layer, 8bpp (256 colors), 1 layer, 4bpp (16 colors) + break; + + case BackgroundMode::Mode4: + // Mode 4: 1 layer, 8bpp (256 colors), 1 layer, 2bpp (4 colors) + break; + + case BackgroundMode::Mode5: + // Mode 5: 1 layer, 4bpp (16 colors), 1 layer, 2bpp (4 colors) hi-res + break; + + case BackgroundMode::Mode6: + // Mode 6: 1 layer, 4bpp (16 colors), 1 layer for offset-per-tile, hi-res + break; + + case BackgroundMode::Mode7: + // Mode 7: 1 layer, 8bpp (256 colors), rotation/scaling + break; + + default: + // Invalid mode setting, handle the error or set default settings + // ... + break; + } + + // Update the internal state of the Ppu based on the mode settings + // Update tile data, tilemaps, sprites, and palette based on the mode settings + UpdateTileData(); + UpdatePaletteData(); +} + +// Internal methods to handle Ppu rendering and operations +void Ppu::UpdateTileData() { + // Fetch tile data from VRAM and store it in the internal buffer + for (uint16_t address = 0; address < tile_data_size_; ++address) { + tile_data_[address] = memory_.ReadByte(vram_base_address_ + address); + } + + // Update the tilemap entries based on the fetched tile data + for (uint16_t entryIndex = 0; entryIndex < tilemap_.entries.size(); + ++entryIndex) { + uint16_t tilemapAddress = + tilemap_base_address_ + entryIndex * sizeof(TilemapEntry); + // Assume ReadWord reads a 16-bit value from VRAM + uint16_t tileData = memory_.ReadWord(tilemapAddress); + + // Extract tilemap entry attributes from the tile data + TilemapEntry entry; + // Tile number is stored in the lower 10 bits + entry.tileNumber = tileData & 0x03FF; + + // Palette is stored in bits 10-12 + entry.palette = (tileData >> 10) & 0x07; + + // Priority is stored in bit 13 + entry.priority = (tileData >> 13) & 0x01; + + // Horizontal flip is stored in bit 14 + entry.hFlip = (tileData >> 14) & 0x01; + + // Vertical flip is stored in bit 15 + entry.vFlip = (tileData >> 15) & 0x01; + + tilemap_.entries[entryIndex] = entry; + } + + // Update the sprites based on the fetched tile data + for (uint16_t spriteIndex = 0; spriteIndex < sprites_.size(); ++spriteIndex) { + uint16_t spriteAddress = spriteIndex * sizeof(SpriteAttributes); + uint16_t spriteData = memory_.ReadWord(spriteAddress); + + // Extract sprite attributes from the sprite data + SpriteAttributes sprite; + + sprite.x = memory_.ReadByte(spriteAddress); + sprite.y = memory_.ReadByte(spriteAddress + 1); + + // Tile number is stored in the lower 9 + sprite.tile = spriteData & 0x01FF; + + // bits Palette is stored in bits 9-11 + sprite.palette = (spriteData >> 9) & 0x07; + + // Priority is stored in bits 12-13 + sprite.priority = (spriteData >> 12) & 0x03; + + // Horizontal flip is stored in bit 14 + sprite.hFlip = (spriteData >> 14) & 0x01; + + // Vertical flip is stored in bit 15 + sprite.vFlip = (spriteData >> 15) & 0x01; + + sprites_[spriteIndex] = sprite; + } +} + +void Ppu::UpdateTileMapData() {} + +void Ppu::RenderBackground(int layer) { + auto bg1_tilemap_info = BGSC(0); + auto bg1_chr_data = BGNBA(0); + auto bg2_tilemap_info = BGSC(0); + auto bg2_chr_data = BGNBA(0); + auto bg3_tilemap_info = BGSC(0); + auto bg3_chr_data = BGNBA(0); + auto bg4_tilemap_info = BGSC(0); + auto bg4_chr_data = BGNBA(0); + + switch (layer) { + case 1: + // Render the first background layer + bg1_tilemap_info = BGSC(memory_.ReadByte(BG1SC)); + bg1_chr_data = BGNBA(memory_.ReadByte(BG12NBA)); + break; + case 2: + // Render the second background layer + bg2_tilemap_info = BGSC(memory_.ReadByte(BG2SC)); + bg2_chr_data = BGNBA(memory_.ReadByte(BG12NBA)); + break; + case 3: + // Render the third background layer + bg3_tilemap_info = BGSC(memory_.ReadByte(BG3SC)); + bg3_chr_data = BGNBA(memory_.ReadByte(BG34NBA)); + break; + case 4: + // Render the fourth background layer + bg4_tilemap_info = BGSC(memory_.ReadByte(BG4SC)); + bg4_chr_data = BGNBA(memory_.ReadByte(BG34NBA)); + break; + default: + // Invalid layer, do nothing + break; + } +} + +void Ppu::RenderSprites() {} + +void Ppu::UpdatePaletteData() {} + +void Ppu::ApplyEffects() {} + +void Ppu::ComposeLayers() {} + +void Ppu::DisplayFrameBuffer() { + if (!screen_->IsActive()) { + screen_->Create(256, 240, 24, frame_buffer_); + rom()->RenderBitmap(screen_.get()); + } +} + +} // namespace emu +} // namespace app +} // namespace yaze diff --git a/src/app/emu/video/ppu.h b/src/app/emu/video/ppu.h new file mode 100644 index 00000000..def76a17 --- /dev/null +++ b/src/app/emu/video/ppu.h @@ -0,0 +1,393 @@ +#ifndef YAZE_APP_EMU_PPU_H +#define YAZE_APP_EMU_PPU_H + +#include +#include +#include + +#include "app/emu/cpu/clock.h" +#include "app/emu/memory/memory.h" +#include "app/emu/video/ppu_registers.h" +#include "app/rom.h" + +namespace yaze { +namespace app { +namespace emu { + +using namespace yaze::app::emu::PpuRegisters; + +class PpuInterface { + public: + virtual ~PpuInterface() = default; + + // Memory Interactions + virtual void Write(uint16_t address, uint8_t data) = 0; + virtual uint8_t Read(uint16_t address) const = 0; + + // Rendering Controls + virtual void RenderFrame() = 0; + virtual void RenderScanline() = 0; + virtual void RenderBackground(int layer) = 0; + virtual void RenderSprites() = 0; + + // State Management + virtual void Init() = 0; + virtual void Reset() = 0; + virtual void Update(double deltaTime) = 0; + virtual void UpdateClock(double deltaTime) = 0; + virtual void UpdateInternalState(int cycles) = 0; + + // Data Access + virtual const std::vector& GetFrameBuffer() const = 0; + virtual std::shared_ptr GetScreen() const = 0; + + // Mode and Setting Updates + virtual void UpdateModeSettings() = 0; + virtual void UpdateTileData() = 0; + virtual void UpdateTileMapData() = 0; + virtual void UpdatePaletteData() = 0; + + // Layer Composition + virtual void ApplyEffects() = 0; + virtual void ComposeLayers() = 0; + + // Display Output + virtual void DisplayFrameBuffer() = 0; + + // Notification (Observer pattern) + virtual void Notify(uint32_t address, uint8_t data) = 0; +}; + +// Enum representing different background modes +enum class BackgroundMode { + Mode0, // 4 layers, each 2bpp (4 colors) + Mode1, // 2 layers, 4bpp (16 colors), 1 layer, 2bpp (4 colors) + Mode2, // 2 layers, 4bpp (16 colors), 1 layer for offset-per-tile + Mode3, // 1 layer, 8bpp (256 colors), 1 layer, 4bpp (16 colors) + Mode4, // 1 layer, 8bpp (256 colors), 1 layer, 2bpp (4 colors) + // 1 layer for offset-per-tile + Mode5, // 1 layer, 4bpp (16 colors), 1 layer, 2bpp (4 colors) hi-res + Mode6, // 1 layer, 4bpp (16 colors), 1 layer for offset-per-tile, hi-res + Mode7, // 1 layer, 8bpp (256 colors), rotation/scaling +}; + +// Enum representing sprite sizes +enum class SpriteSize { Size8x8, Size16x16, Size32x32, Size64x64 }; + +// Struct representing a sprite's attributes +struct SpriteAttributes { + uint8_t x; // X position of the sprite + uint8_t y; // Y position of the sprite + uint16_t tile; // Tile number for the sprite + uint8_t palette; // Palette number for the sprite + uint8_t priority; // Priority for the sprite + bool hFlip; // Horizontal flip flag + bool vFlip; // Vertical flip flag +}; + +// Struct representing a tilemap entry +struct TilemapEntry { + uint16_t tileNumber; // Tile number for the tile + uint8_t palette; // Palette number for the tile + uint8_t priority; // Priority for the tile + bool hFlip; // Horizontal flip flag + bool vFlip; // Vertical flip flag +}; + +// Struct representing a tilemap +struct Tilemap { + std::vector entries; // Entries for the tilemap +}; + +// Struct representing a color +struct Color { + uint8_t r; // Red component + uint8_t g; // Green component + uint8_t b; // Blue component +}; + +// Registers +struct OAMSize { + uint8_t base_selection : 3; + uint8_t name_selection : 2; + uint8_t object_size : 3; +}; + +struct OAMAddress { + uint8_t oam_address_low : 8; + uint8_t oam_address_msb : 1; + uint8_t oam_priority_rotation : 1; + uint8_t unused : 6; +}; + +struct TileMapLocation { + uint8_t SC_size : 2; + uint8_t tile_map_address : 5; + uint8_t unused : 1; +}; + +struct CharacterLocation { + uint8_t BG1_address : 4; + uint8_t BG2_address : 4; + uint8_t BG3_address : 4; + uint8_t BG4_address : 4; +}; + +struct VideoPortControl { + uint8_t increment_rate : 2; + uint8_t full_graphic : 2; + uint8_t increment_mode : 1; + uint8_t unused : 3; +}; + +struct ScreenDisplay { + uint8_t brightness : 4; + uint8_t disable_screen : 1; + uint8_t unused : 3; +}; + +struct ScreenMode { + uint8_t general_screen_mode : 3; + uint8_t priority : 1; + uint8_t BG1_tile_size : 1; + uint8_t BG2_tile_size : 1; + uint8_t BG3_tile_size : 1; + uint8_t BG4_tile_size : 1; +}; + +struct ScrollRegister { + uint8_t offset : 8; + uint8_t mode7_bits : 3; + uint8_t unused : 5; +}; + +struct MainSubScreenDesignation { + uint8_t BG1_enable : 1; + uint8_t BG2_enable : 1; + uint8_t BG3_enable : 1; + uint8_t BG4_enable : 1; + uint8_t sprites_enable : 1; + uint8_t unused : 3; +}; + +struct WindowMaskSettings { + uint8_t BG1_clip_in_out : 1; + uint8_t BG1_enable : 1; + uint8_t BG2_clip_in_out : 1; + uint8_t BG2_enable : 1; + uint8_t BG3_clip_in_out : 1; + uint8_t BG3_enable : 1; + uint8_t BG4_clip_in_out : 1; + uint8_t BG4_enable : 1; +}; + +struct WindowMaskSettings2 { + uint8_t sprites_clip_in_out : 1; + uint8_t sprites_enable : 1; + uint8_t color_windows_clip_in_out : 1; + uint8_t color_windows_enable : 1; + uint8_t unused : 4; +}; + +struct WindowPosition { + uint8_t position : 8; +}; + +struct MaskLogicSettings { + uint8_t BG1_mask_logic : 2; + uint8_t BG2_mask_logic : 2; + uint8_t BG3_mask_logic : 2; + uint8_t BG4_mask_logic : 2; +}; + +// Counter/IRQ/NMI Registers +struct CounterIrqNmiRegisters { + uint8_t softwareLatchHvCounter; // Register $2137 + uint16_t horizontalScanLocation; // Register $213C + uint16_t verticalScanLocation; // Register $213D + uint8_t counterEnable; // Register $4200 + uint16_t horizontalIrqTrigger; // Register $4207/$4208 + uint16_t verticalIrqTrigger; // Register $4209/$420A + uint8_t nmiRegister; // Register $4210 + uint8_t irqRegister; // Register $4211 + uint8_t statusRegisterIrq; // Register $4212 +}; + +// Joypad Registers +struct JoypadRegisters { + uint16_t joypadData[4]; // Register $4218 to $421F + uint8_t oldStyleJoypadRegisters[2]; // Registers $4016/$4217 +}; + +// DMA Registers +struct DmaRegisters { + uint8_t startDmaTransfer; // Register $420B + uint8_t enableHDmaTransfer; // Register $420C + uint8_t dmacontrol_register_ister[8]; // Register $43?0 + uint8_t dmaDestinationAddress[8]; // Register $43?1 + uint32_t dmaSourceAddress[8]; // Register $43?2/$43?3/$43?4 + uint16_t bytesToTransfer[8]; // Register $43?5/$43?6/$43?7 + uint16_t hdmaCountPointer[8]; // Register $43?8/$43?9 + uint8_t scanlinesLeft[8]; // Register $43?A +}; + +// WRAM access Registers +struct WramAccessRegisters { + uint8_t dataByte; // Register $2180 + uint32_t address; // Register $2181/$2182/$2183 +}; + +struct Tile { + uint16_t index; // Index of the tile in VRAM + uint8_t palette; // Palette number used for this tile + bool flip_x; // Horizontal flip flag + bool flip_y; // Vertical flip flag + uint8_t priority; // Priority of this tile +}; + +struct BackgroundLayer { + enum class Size { SIZE_32x32, SIZE_64x32, SIZE_32x64, SIZE_64x64 }; + + enum class ColorDepth { BPP_2, BPP_4, BPP_8 }; + + Size size; // Size of the background layer + ColorDepth color_depth; // Color depth of the background layer + std::vector tilemap; // Tilemap data + std::vector tile_data; // Tile data in VRAM + uint16_t tilemap_base_address; // Base address of the tilemap in VRAM + uint16_t tile_data_base_address; // Base address of the tile data in VRAM + uint8_t scroll_x; // Horizontal scroll offset + uint8_t scroll_y; // Vertical scroll offset + bool enabled; // Whether the background layer is enabled +}; + +const int kPpuClockSpeed = 5369318; // 5.369318 MHz + +class Ppu : public Observer, public SharedROM { + public: + // Initializes the PPU with the necessary resources and dependencies + Ppu(Memory& memory, Clock& clock) : memory_(memory), clock_(clock) {} + + // Initialize the frame buffer + void Init() { + clock_.SetFrequency(kPpuClockSpeed); + frame_buffer_.resize(256 * 240, 0); + screen_ = std::make_shared(256, 240, 8, 0x100); + screen_->SetActive(false); + } + + // Resets the PPU to its initial state + void Reset() { std::fill(frame_buffer_.begin(), frame_buffer_.end(), 0); } + + // Runs the PPU for one frame. + void Update(); + void UpdateClock(double delta_time) { clock_.UpdateClock(delta_time); } + void UpdateInternalState(int cycles); + + // Renders a scanline of the screen + void RenderScanline(); + + void Notify(uint32_t address, uint8_t data) override; + + // Returns the pixel data for the current frame + const std::vector& GetFrameBuffer() const { return frame_buffer_; } + + auto GetScreen() const { return screen_; } + + private: + // Updates internal state based on PPU register settings + void UpdateModeSettings(); + + // Internal methods to handle PPU rendering and operations + void UpdateTileData(); + + // Fetches the tile map data from memory and stores it in an internal buffer + void UpdateTileMapData(); + + // Renders a background layer + void RenderBackground(int layer); + + // Renders sprites (also known as objects) + void RenderSprites(); + + // Fetches the palette data from CGRAM and stores it in an internal buffer + void UpdatePaletteData(); + + // Applies effects to the layers based on the current mode and register + void ApplyEffects(); + + // Combines the layers into a single image and stores it in the frame buffer + void ComposeLayers(); + + // Sends the frame buffer to the display hardware (e.g., SDL2) + void DisplayFrameBuffer(); + + // =========================================================== + // Member variables to store internal PPU state and resources + Memory& memory_; + Clock& clock_; + + // PPU registers + OAMSize oam_size_; + OAMAddress oam_address_; + Mosaic mosaic_; + std::array bgsc_; + std::array bgnba_; + std::array bghofs_; + std::array bgvofs_; + struct VMAIN vmain_; + struct VMADDL vmaddl_; + struct VMADDH vmaddh_; + // struct VMDATAL vmdatal_; + // struct VMDATAH vmdatah_; + struct M7SEL m7sel_; + struct M7A m7a_; + struct M7B m7b_; + struct M7C m7c_; + struct M7D m7d_; + struct M7X m7x_; + struct M7Y m7y_; + struct CGADD cgadd_; + struct CGDATA cgdata_; + struct W12SEL w12sel_; + struct W34SEL w34sel_; + struct WOBJSEL wobjsel_; + struct WH0 wh0_; + struct WH1 wh1_; + struct WH2 wh2_; + struct WH3 wh3_; + struct WBGLOG wbglog_; + struct WOBJLOG wobjlog_; + struct TM tm_; + struct TS ts_; + struct TSW tsw_; + struct TMW tmw_; + struct SETINI setini_; + + Tilemap tilemap_; + BackgroundMode bg_mode_; + std::array bg_layers_; + std::vector sprites_; + std::vector tile_data_; + std::vector frame_buffer_; + std::shared_ptr screen_; + + uint16_t tile_data_size_; + uint16_t vram_base_address_; + uint16_t tilemap_base_address_; + uint16_t screen_brightness_ = 0x00; + + bool enable_forced_blanking_ = false; + + int cycle_count_ = 0; + int current_scanline_ = 0; + const int cyclesPerScanline = 341; // SNES PPU has 341 cycles per scanline + const int totalScanlines = 262; // SNES PPU has 262 scanlines per frame + const int visibleScanlines = 224; // SNES PPU renders 224 visible scanlines +}; + +} // namespace emu +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_EMU_PPU_H \ No newline at end of file diff --git a/src/app/emu/video/ppu_registers.h b/src/app/emu/video/ppu_registers.h new file mode 100644 index 00000000..ae6c1c17 --- /dev/null +++ b/src/app/emu/video/ppu_registers.h @@ -0,0 +1,422 @@ +#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 PpuRegisters { + +constexpr uint16_t INIDISP = 0x2100; + +// OAM Size Register ($2101): Controls the size of the object/sprite, the base +// address, and the name selection for the OAM (Object Attribute Memory). +constexpr uint16_t OBJSEL = 0x2101; + +// OAM Address Register ($2102-$2103): Sets the address for accessing OAM data. +constexpr uint16_t OAMADDL = 0x2102; +constexpr uint16_t OAMADDH = 0x2103; + +// OAM Data Register ($2104): Holds the data to be written to the OAM at a +// specified address. +constexpr uint16_t OAMDATA = 0x2104; + +// OAM Data Read Register ($2138): Allows reading data from the OAM. + +// Screen Display Register ($2100): Controls screen on/off and brightness. + +// Screen Mode Register ($2105): Defines the screen mode and character size for +// each background layer. +constexpr uint16_t BGMODE = 0x2105; + +// Screen Pixelation Register ($2106): Sets the pixel size and screen +// designation for the mosaic display. +constexpr uint16_t MOSAIC = 0x2106; + +// BGx VRAM Location Registers ($2107-$210A) +// Define the location in VRAM where the background screen data is stored. +constexpr uint16_t BG1SC = 0x2107; +constexpr uint16_t BG2SC = 0x2108; +constexpr uint16_t BG3SC = 0x2109; +constexpr uint16_t BG4SC = 0x210A; + +// BGx & BGy VRAM Location Registers ($210B-$210C): +// Set the base address for BG character data in VRAM. +constexpr uint16_t BG12NBA = 0x210B; +constexpr uint16_t BG34NBA = 0x210C; + +// BGx Scroll Registers ($210D-$2114): Control the horizontal and vertical +// scroll values for each background layer. +constexpr uint16_t BG1HOFS = 0x210D; +constexpr uint16_t BG1VOFS = 0x210E; +constexpr uint16_t BG2HOFS = 0x210F; +constexpr uint16_t BG2VOFS = 0x2110; +constexpr uint16_t BG3HOFS = 0x2111; +constexpr uint16_t BG3VOFS = 0x2112; +constexpr uint16_t BG4HOFS = 0x2113; +constexpr uint16_t BG4VOFS = 0x2114; + +// Video Port Control Register ($2115): Designates the VRAM address increment +// value. +constexpr uint16_t VMAIN = 0x2115; + +// Video Port Address Register ($2116-$2117): Sets the initial address for +// reading from or writing to VRAM. +constexpr uint16_t VMADDL = 0x2116; +constexpr uint16_t VMADDH = 0x2117; + +constexpr uint16_t VMDATAL = 0x2118; +constexpr uint16_t VMDATAH = 0x2119; +constexpr uint16_t M7SEL = 0x211A; +constexpr uint16_t M7A = 0x211B; +constexpr uint16_t M7B = 0x211C; +constexpr uint16_t M7C = 0x211D; +constexpr uint16_t M7D = 0x211E; +constexpr uint16_t M7X = 0x211F; +constexpr uint16_t M7Y = 0x2120; +constexpr uint16_t CGADD = 0x2121; +constexpr uint16_t CGDATA = 0x2122; +constexpr uint16_t W12SEL = 0x2123; +constexpr uint16_t W34SEL = 0x2124; +constexpr uint16_t WOBJSEL = 0x2125; +constexpr uint16_t WH0 = 0x2126; +constexpr uint16_t WH1 = 0x2127; +constexpr uint16_t WH2 = 0x2128; +constexpr uint16_t WH3 = 0x2129; +constexpr uint16_t WBGLOG = 0x212A; +constexpr uint16_t WOBJLOG = 0x212B; +constexpr uint16_t TM = 0x212C; +constexpr uint16_t TS = 0x212D; +constexpr uint16_t TMW = 0x212E; +constexpr uint16_t TSW = 0x212F; +constexpr uint16_t CGWSEL = 0x2130; +constexpr uint16_t CGADSUB = 0x2131; +constexpr uint16_t COLDATA = 0x2132; +constexpr uint16_t SETINI = 0x2133; +constexpr uint16_t MPYL = 0x2134; +constexpr uint16_t MPYM = 0x2135; +constexpr uint16_t MPYH = 0x2136; +constexpr uint16_t SLHV = 0x2137; +constexpr uint16_t OAMDATAREAD = 0x2138; +constexpr uint16_t VMDATALREAD = 0x2139; +constexpr uint16_t VMDATAHREAD = 0x213A; +constexpr uint16_t CGDATAREAD = 0x213B; +constexpr uint16_t OPHCT = 0x213C; +constexpr uint16_t OPVCT = 0x213D; +constexpr uint16_t STAT77 = 0x213E; +constexpr uint16_t STAT78 = 0x213F; + +struct INIDISP { + uint8_t brightness : 4; + uint8_t forced_blanking : 1; + uint8_t unused : 3; +}; + +struct OBJSEL { + uint8_t name_base_address : 2; + uint8_t name_secondary_select : 1; + uint8_t sprite_size : 2; + uint8_t unused : 3; +}; + +struct OAMADDL { + uint8_t address : 8; +}; + +struct OAMADDH { + uint8_t high_bit : 1; + uint8_t priority_rotation : 1; + uint8_t unused : 6; +}; + +struct OAMDATA { + uint8_t data : 8; +}; + +struct BGMODE { + uint8_t bg_mode : 3; + uint8_t bg3_priority : 1; + uint8_t tile_size : 4; +}; + +struct Mosaic { + uint8_t bg_enable : 4; + uint8_t mosaic_size : 4; +}; + +struct BGSC { + BGSC() = default; + ~BGSC() = default; + explicit BGSC(uint8_t value) + : horizontal_tilemap_count(value & 0x01), + vertical_tilemap_count((value >> 1) & 0x01), + vram_address((value >> 2) & 0x3F) {} + uint8_t horizontal_tilemap_count : 1; + uint8_t vertical_tilemap_count : 1; + uint8_t vram_address : 6; +}; + +struct BGNBA { + BGNBA() = default; + ~BGNBA() = default; + explicit BGNBA(uint8_t value) + : chr_base_address_2(value & 0x0F), + chr_base_address_1((value >> 4) & 0x0F) {} + uint8_t chr_base_address_2 : 4; + uint8_t chr_base_address_1 : 4; +}; + +struct BGHOFS { + uint16_t horizontal_scroll : 10; + uint8_t unused : 6; +}; + +struct BGVOFS { + uint16_t vertical_scroll : 10; + uint8_t unused : 6; +}; + +struct VMAIN { + uint8_t increment_size : 2; + uint8_t remapping : 2; + uint8_t address_increment_mode : 1; + uint8_t unused : 3; +}; + +struct VMADDL { + uint8_t address_low : 8; +}; + +struct VMADDH { + uint8_t address_high : 8; +}; + +struct VMDATA { + uint8_t data : 8; +}; + +struct M7SEL { + uint8_t flip_horizontal : 1; + uint8_t flip_vertical : 1; + uint8_t fill : 1; + uint8_t tilemap_repeat : 1; + uint8_t unused : 4; +}; + +struct M7A { + int16_t matrix_a : 16; +}; + +struct M7B { + int16_t matrix_b : 16; +}; + +struct M7C { + int16_t matrix_c : 16; +}; + +struct M7D { + int16_t matrix_d : 16; +}; + +struct M7X { + uint16_t center_x : 13; + uint8_t unused : 3; +}; + +struct M7Y { + uint16_t center_y : 13; + uint8_t unused : 3; +}; + +struct CGADD { + uint8_t address : 8; +}; + +struct CGDATA { + uint16_t data : 15; + uint8_t unused : 1; +}; + +struct W12SEL { + uint8_t enable_bg1_a : 1; + uint8_t invert_bg1_a : 1; + uint8_t enable_bg1_b : 1; + uint8_t invert_bg1_b : 1; + uint8_t enable_bg2_c : 1; + uint8_t invert_bg2_c : 1; + uint8_t enable_bg2_d : 1; + uint8_t invert_bg2_d : 1; +}; + +struct W34SEL { + uint8_t enable_bg3_e : 1; + uint8_t invert_bg3_e : 1; + uint8_t enable_bg3_f : 1; + uint8_t invert_bg3_f : 1; + uint8_t enable_bg4_g : 1; + uint8_t invert_bg4_g : 1; + uint8_t enable_bg4_h : 1; + uint8_t invert_bg4_h : 1; +}; + +struct WOBJSEL { + uint8_t enable_obj_i : 1; + uint8_t invert_obj_i : 1; + uint8_t enable_obj_j : 1; + uint8_t invert_obj_j : 1; + uint8_t enable_color_k : 1; + uint8_t invert_color_k : 1; + uint8_t enable_color_l : 1; + uint8_t invert_color_l : 1; +}; + +struct WH0 { + uint8_t left_position : 8; +}; + +struct WH1 { + uint8_t right_position : 8; +}; + +struct WH2 { + uint8_t left_position : 8; +}; + +struct WH3 { + uint8_t right_position : 8; +}; + +struct WBGLOG { + uint8_t mask_logic_bg4 : 2; + uint8_t mask_logic_bg3 : 2; + uint8_t mask_logic_bg2 : 2; + uint8_t mask_logic_bg1 : 2; +}; + +struct WOBJLOG { + uint8_t unused : 4; + uint8_t mask_logic_color : 2; + uint8_t mask_logic_obj : 2; +}; + +struct TM { + uint8_t enable_layer : 5; + uint8_t unused : 3; +}; + +struct TS { + uint8_t enable_layer : 5; + uint8_t unused : 3; +}; + +struct TMW { + uint8_t enable_window : 5; + uint8_t unused : 3; +}; + +struct TSW { + uint8_t enable_window : 5; + uint8_t unused : 3; +}; + +struct CGWSEL { + uint8_t direct_color : 1; + uint8_t fixed_subscreen : 1; + uint8_t sub_color_window : 2; + uint8_t main_color_window : 2; + uint8_t unused : 2; +}; + +struct CGADSUB { + uint8_t enable_layer : 5; + uint8_t backdrop : 1; + uint8_t half : 1; + uint8_t add_subtract : 1; +}; + +struct COLDATA { + uint8_t value : 4; + uint8_t channel_select : 3; + uint8_t unused : 1; +}; + +struct SETINI { + uint8_t screen_interlace : 1; + uint8_t obj_interlace : 1; + uint8_t overscan : 1; + uint8_t hi_res : 1; + uint8_t extbg : 1; + uint8_t external_sync : 1; + uint8_t unused : 2; +}; + +struct MPYL { + uint8_t multiplication_result_low : 8; +}; + +struct MPYM { + uint8_t multiplication_result_mid : 8; +}; + +struct MPYH { + uint8_t multiplication_result_high : 8; +}; + +struct SLHV { + uint8_t software_latch : 8; +}; + +struct OAMDATAREAD { + uint8_t oam_data_read : 8; +}; + +struct VMDATALREAD { + uint8_t vram_data_read_low : 8; +}; + +struct VMDATAHREAD { + uint8_t vram_data_read_high : 8; +}; + +struct CGDATAREAD { + uint8_t cgram_data_read : 8; +}; + +struct OPHCT { + uint16_t horizontal_counter_output : 9; + uint8_t unused : 7; +}; + +struct OPVCT { + uint16_t vertical_counter_output : 9; + uint8_t unused : 7; +}; + +struct STAT77 { + uint8_t ppu1_version : 4; + uint8_t master_slave : 1; + uint8_t sprite_tile_overflow : 1; + uint8_t sprite_overflow : 1; + uint8_t unused : 1; +}; + +struct STAT78 { + uint8_t ppu2_version : 4; + uint8_t ntsc_pal : 1; + uint8_t counter_latch_value : 1; + uint8_t interlace_field : 1; + uint8_t unused : 1; +}; + +} // namespace PpuRegisters + +} // namespace emu +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_EMU_VIDEO_PPU_REGISTERS_H \ No newline at end of file diff --git a/src/app/gfx/bitmap.cc b/src/app/gfx/bitmap.cc index c4ab757b..ea6cd549 100644 --- a/src/app/gfx/bitmap.cc +++ b/src/app/gfx/bitmap.cc @@ -1,6 +1,7 @@ #include "bitmap.h" #include +#include #include #include @@ -23,35 +24,180 @@ void GrayscalePalette(SDL_Palette *palette) { palette->colors[i].b = i * 31; } } -} // namespace -Bitmap::Bitmap(int width, int height, int depth, uchar *data) { - Create(width, height, depth, data); +void PngWriteCallback(png_structp png_ptr, png_bytep data, png_size_t length) { + std::vector *p = (std::vector *)png_get_io_ptr(png_ptr); + p->insert(p->end(), data, data + length); } +bool ConvertSurfaceToPNG(SDL_Surface *surface, std::vector &buffer) { + png_structp png_ptr = png_create_write_struct("1.6.40", NULL, NULL, NULL); + if (!png_ptr) { + SDL_Log("Failed to create PNG write struct"); + return false; + } + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_write_struct(&png_ptr, (png_infopp)NULL); + SDL_Log("Failed to create PNG info struct"); + return false; + } + + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_write_struct(&png_ptr, &info_ptr); + SDL_Log("Error during PNG write"); + return false; + } + + png_set_write_fn(png_ptr, &buffer, PngWriteCallback, NULL); + + png_colorp pal_ptr; + + /* Prepare chunks */ + int colortype = PNG_COLOR_MASK_COLOR; + int i = 0; + SDL_Palette *pal; + if (surface->format->BytesPerPixel > 0 && + surface->format->BytesPerPixel <= 8 && (pal = surface->format->palette)) { + SDL_Log("Writing PNG image with palette"); + colortype |= PNG_COLOR_MASK_PALETTE; + pal_ptr = (png_colorp)malloc(pal->ncolors * sizeof(png_color)); + for (i = 0; i < pal->ncolors; i++) { + pal_ptr[i].red = pal->colors[i].r; + pal_ptr[i].green = pal->colors[i].g; + pal_ptr[i].blue = pal->colors[i].b; + } + png_set_PLTE(png_ptr, info_ptr, pal_ptr, pal->ncolors); + free(pal_ptr); + } + + auto depth = surface->format->BitsPerPixel; + + // Set image attributes. + png_set_IHDR(png_ptr, info_ptr, surface->w, surface->h, depth, colortype, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + + png_set_bgr(png_ptr); + + // Write the image data. + std::vector row_pointers(surface->h); + for (int y = 0; y < surface->h; ++y) { + row_pointers[y] = (png_bytep)(surface->pixels) + y * surface->pitch; + } + + png_set_rows(png_ptr, info_ptr, row_pointers.data()); + + SDL_Log("Writing PNG image..."); + png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL); + SDL_Log("PNG image write complete"); + + png_destroy_write_struct(&png_ptr, &info_ptr); + + return true; +} + +void PngReadCallback(png_structp png_ptr, png_bytep outBytes, + png_size_t byteCountToRead) { + png_voidp io_ptr = png_get_io_ptr(png_ptr); + if (!io_ptr) return; + + std::vector *png_data = + reinterpret_cast *>(io_ptr); + size_t pos = png_data->size() - byteCountToRead; + memcpy(outBytes, png_data->data() + pos, byteCountToRead); + png_data->resize(pos); // Reduce the buffer size +} + +void ConvertPngToSurface(const std::vector &png_data, + SDL_Surface **outSurface) { + std::vector data(png_data); + png_structp png_ptr = png_create_read_struct("1.6.40", NULL, NULL, NULL); + if (!png_ptr) { + throw std::runtime_error("Failed to create PNG read struct"); + } + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_read_struct(&png_ptr, NULL, NULL); + throw std::runtime_error("Failed to create PNG info struct"); + } + + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + throw std::runtime_error("Error during PNG read"); + } + + // Set our custom read function + png_set_read_fn(png_ptr, &data, PngReadCallback); + + // Read the PNG info + png_read_info(png_ptr, info_ptr); + + uint32_t width = png_get_image_width(png_ptr, info_ptr); + uint32_t height = png_get_image_height(png_ptr, info_ptr); + png_byte color_type = png_get_color_type(png_ptr, info_ptr); + png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr); + + // Set up transformations, e.g., strip 16-bit PNGs down to 8-bit, expand + // palettes, etc. + if (bit_depth == 16) { + png_set_strip_16(png_ptr); + } + + if (color_type == PNG_COLOR_TYPE_PALETTE) { + png_set_palette_to_rgb(png_ptr); + } + + // PNG files pack pixels, expand them + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) { + png_set_expand_gray_1_2_4_to_8(png_ptr); + } + + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(png_ptr); + } + + if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || + color_type == PNG_COLOR_TYPE_PALETTE) { + png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER); + } + + if (color_type == PNG_COLOR_TYPE_GRAY || + color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + png_set_gray_to_rgb(png_ptr); + } + + // Update info structure with transformations + png_read_update_info(png_ptr, info_ptr); + + // Read the file + std::vector row_pointers(height); + std::vector raw_data(width * height * + 4); // Assuming 4 bytes per pixel (RGBA) + for (size_t y = 0; y < height; y++) { + row_pointers[y] = &raw_data[y * width * 4]; + } + + png_read_image(png_ptr, row_pointers.data()); + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + + // Create SDL_Surface from raw pixel data + *outSurface = SDL_CreateRGBSurfaceWithFormatFrom( + raw_data.data(), width, height, 32, width * 4, SDL_PIXELFORMAT_RGBA32); + if (*outSurface == nullptr) { + SDL_Log("SDL_CreateRGBSurfaceWithFormatFrom failed: %s\n", SDL_GetError()); + } else { + SDL_Log("Successfully created SDL_Surface from PNG data"); + } +} +} // namespace + Bitmap::Bitmap(int width, int height, int depth, int data_size) { Create(width, height, depth, data_size); } -Bitmap::Bitmap(int width, int height, int depth, uchar *data, int data_size) { - Create(width, height, depth, data, data_size); -} - -// Pass raw pixel data directly to the surface -void Bitmap::Create(int width, int height, int depth, uchar *data) { - active_ = true; - width_ = width; - height_ = height; - depth_ = depth; - pixel_data_ = data; - surface_ = std::unique_ptr( - SDL_CreateRGBSurfaceWithFormat(0, width_, height_, depth_, - SDL_PIXELFORMAT_INDEX8), - SDL_Surface_Deleter()); - surface_->pixels = pixel_data_; - GrayscalePalette(surface_->format->palette); -} - // Reserves data to later draw to surface via pointer void Bitmap::Create(int width, int height, int depth, int size) { active_ = true; @@ -69,23 +215,7 @@ void Bitmap::Create(int width, int height, int depth, int size) { GrayscalePalette(surface_->format->palette); } -// Pass raw pixel data directly to the surface -void Bitmap::Create(int width, int height, int depth, uchar *data, int size) { - active_ = true; - width_ = width; - height_ = height; - depth_ = depth; - pixel_data_ = data; - data_size_ = size; - surface_ = std::unique_ptr( - SDL_CreateRGBSurfaceWithFormat(0, width_, height_, depth_, - SDL_PIXELFORMAT_INDEX8), - SDL_Surface_Deleter()); - surface_->pixels = pixel_data_; - GrayscalePalette(surface_->format->palette); -} - -void Bitmap::Create(int width, int height, int depth, Bytes data) { +void Bitmap::Create(int width, int height, int depth, const Bytes &data) { active_ = true; width_ = width; height_ = height; @@ -100,9 +230,62 @@ void Bitmap::Create(int width, int height, int depth, Bytes data) { GrayscalePalette(surface_->format->palette); } -void Bitmap::Apply(Bytes data) { - pixel_data_ = data.data(); - data_ = data; +// Creates the texture that will be displayed to the screen. +void Bitmap::CreateTexture(SDL_Renderer *renderer) { + // Ensure width and height are non-zero + if (width_ <= 0 || height_ <= 0) { + SDL_Log("Invalid texture dimensions: width=%d, height=%d\n", width_, + height_); + return; + } + + texture_ = std::shared_ptr{ + SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB888, + SDL_TEXTUREACCESS_STREAMING, width_, height_), + SDL_Texture_Deleter{}}; + if (texture_ == nullptr) { + SDL_Log("SDL_CreateTextureFromSurface failed: %s\n", SDL_GetError()); + } + + SDL_Surface *converted_surface = + SDL_ConvertSurfaceFormat(surface_.get(), SDL_PIXELFORMAT_ARGB8888, 0); + if (converted_surface) { + // Create texture from the converted surface + converted_surface_ = std::unique_ptr( + converted_surface, SDL_Surface_Deleter()); + } else { + // Handle the error + SDL_Log("SDL_ConvertSurfaceFormat failed: %s\n", SDL_GetError()); + } + + SDL_LockTexture(texture_.get(), nullptr, (void **)&texture_pixels, + &converted_surface_->pitch); + + memcpy(texture_pixels, converted_surface_->pixels, + converted_surface_->h * converted_surface_->pitch); + + SDL_UnlockTexture(texture_.get()); +} + +void Bitmap::UpdateTexture(SDL_Renderer *renderer) { + SDL_Surface *converted_surface = + SDL_ConvertSurfaceFormat(surface_.get(), SDL_PIXELFORMAT_ARGB8888, 0); + if (converted_surface) { + // Create texture from the converted surface + converted_surface_ = std::unique_ptr( + converted_surface, SDL_Surface_Deleter()); + } else { + // Handle the error + SDL_Log("SDL_ConvertSurfaceFormat failed: %s\n", SDL_GetError()); + } + + SDL_LockTexture(texture_.get(), nullptr, (void **)&texture_pixels, + &converted_surface_->pitch); + + memcpy(texture_pixels, converted_surface_->pixels, + converted_surface_->h * converted_surface_->pitch); + + SDL_UnlockTexture(texture_.get()); } // Creates the texture that will be displayed to the screen. @@ -112,22 +295,107 @@ void Bitmap::CreateTexture(std::shared_ptr renderer) { SDL_Texture_Deleter{}}; } +void Bitmap::UpdateTexture(std::shared_ptr renderer) { + // SDL_DestroyTexture(texture_.get()); + // texture_ = nullptr; + texture_ = std::shared_ptr{ + SDL_CreateTextureFromSurface(renderer.get(), surface_.get()), + SDL_Texture_Deleter{}}; +} + +void Bitmap::SaveSurfaceToFile(std::string_view filename) { + SDL_SaveBMP(surface_.get(), filename.data()); +} + +void Bitmap::SetSurface(SDL_Surface *surface) { + surface_ = std::unique_ptr( + surface, SDL_Surface_Deleter()); +} + +std::vector Bitmap::GetPngData() { + ConvertSurfaceToPNG(surface_.get(), png_data_); + return png_data_; +} + +void Bitmap::LoadFromPngData(const std::vector &png_data, int width, + int height) { + width_ = width; + height_ = height; + SDL_Surface *surface = surface_.get(); + ConvertPngToSurface(png_data, &surface); + surface_.reset(surface); +} + // Convert SNESPalette to SDL_Palette for surface. void Bitmap::ApplyPalette(const SNESPalette &palette) { palette_ = palette; - for (int i = 0; i < palette.size_; ++i) { - if (palette.GetColor(i).transparent) { + SDL_UnlockSurface(surface_.get()); + for (int i = 0; i < palette.size(); ++i) { + if (palette.GetColor(i).IsTransparent()) { surface_->format->palette->colors[i].r = 0; surface_->format->palette->colors[i].g = 0; surface_->format->palette->colors[i].b = 0; surface_->format->palette->colors[i].a = 0; } else { - surface_->format->palette->colors[i].r = palette.GetColor(i).rgb.x; - surface_->format->palette->colors[i].g = palette.GetColor(i).rgb.y; - surface_->format->palette->colors[i].b = palette.GetColor(i).rgb.z; - surface_->format->palette->colors[i].a = palette.GetColor(i).rgb.w; + surface_->format->palette->colors[i].r = palette.GetColor(i).GetRGB().x; + surface_->format->palette->colors[i].g = palette.GetColor(i).GetRGB().y; + surface_->format->palette->colors[i].b = palette.GetColor(i).GetRGB().z; + surface_->format->palette->colors[i].a = palette.GetColor(i).GetRGB().w; } } + SDL_LockSurface(surface_.get()); +} + +void Bitmap::ApplyPaletteWithTransparent(const SNESPalette &palette, + int index) { + auto start_index = index * 7; + palette_ = palette.sub_palette(start_index, start_index + 7); + std::vector colors; + colors.push_back(ImVec4(0, 0, 0, 0)); + for (int i = start_index; i < start_index + 7; ++i) { + colors.push_back(palette.GetColor(i).GetRGB()); + } + + SDL_UnlockSurface(surface_.get()); + int i = 0; + for (auto &each : colors) { + surface_->format->palette->colors[i].r = each.x; + surface_->format->palette->colors[i].g = each.y; + surface_->format->palette->colors[i].b = each.z; + surface_->format->palette->colors[i].a = each.w; + i++; + } + SDL_LockSurface(surface_.get()); +} + +void Bitmap::ApplyPalette(const std::vector &palette) { + SDL_UnlockSurface(surface_.get()); + for (int i = 0; i < palette.size(); ++i) { + surface_->format->palette->colors[i].r = palette[i].r; + surface_->format->palette->colors[i].g = palette[i].g; + surface_->format->palette->colors[i].b = palette[i].b; + surface_->format->palette->colors[i].a = palette[i].a; + } + SDL_LockSurface(surface_.get()); +} + +void Bitmap::InitializeFromData(uint32_t width, uint32_t height, uint32_t depth, + const Bytes &data) { + active_ = true; + width_ = width; + height_ = height; + depth_ = depth; + data_ = data; + data_size_ = data.size(); + pixel_data_ = data_.data(); + + surface_ = std::unique_ptr( + SDL_CreateRGBSurfaceWithFormat(0, width_, height_, depth_, + SDL_PIXELFORMAT_INDEX8), + SDL_Surface_Deleter()); + + surface_->pixels = pixel_data_; + GrayscalePalette(surface_->format->palette); } } // namespace gfx diff --git a/src/app/gfx/bitmap.h b/src/app/gfx/bitmap.h index 9ae90ee3..32d126dc 100644 --- a/src/app/gfx/bitmap.h +++ b/src/app/gfx/bitmap.h @@ -6,6 +6,7 @@ #include #include +#include "absl/container/flat_hash_map.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" @@ -19,33 +20,136 @@ namespace gfx { class Bitmap { public: Bitmap() = default; - Bitmap(int width, int height, int depth, uchar *data); + Bitmap(int width, int height, int depth, int data_size); - Bitmap(int width, int height, int depth, uchar *data, int data_size); - - void Create(int width, int height, int depth, uchar *data); - void Create(int width, int height, int depth, int data_size); - void Create(int width, int height, int depth, uchar *data, int data_size); - void Create(int width, int height, int depth, Bytes data); - - void Apply(Bytes data); - - void CreateTexture(std::shared_ptr renderer); - - void ApplyPalette(const SNESPalette &palette); - - void WriteToPixel(int position, uchar value) { - this->pixel_data_[position] = value; + Bitmap(int width, int height, int depth, const Bytes &data) + : width_(width), height_(height), depth_(depth), data_(data) { + InitializeFromData(width, height, depth, data); } - int GetWidth() const { return width_; } - int GetHeight() const { return height_; } - auto GetSize() const { return data_size_; } - auto GetData() const { return pixel_data_; } - auto GetByte(int i) const { return pixel_data_[i]; } - auto GetTexture() const { return texture_.get(); } - auto GetSurface() const { return surface_.get(); } + void Create(int width, int height, int depth, int data_size); + void Create(int width, int height, int depth, const Bytes &data); + + void InitializeFromData(uint32_t width, uint32_t height, uint32_t depth, + const Bytes &data); + + void CreateTexture(std::shared_ptr renderer); + void UpdateTexture(std::shared_ptr renderer); + void CreateTexture(SDL_Renderer *renderer); + void UpdateTexture(SDL_Renderer *renderer); + + void SaveSurfaceToFile(std::string_view filename); + void SetSurface(SDL_Surface *surface); + std::vector GetPngData(); + void LoadFromPngData(const std::vector &png_data, int width, + int height); + + void ApplyPalette(const SNESPalette &palette); + void ApplyPaletteWithTransparent(const SNESPalette &palette, int index); + void ApplyPalette(const std::vector &palette); + + void WriteToPixel(int position, uchar value) { + if (pixel_data_ == nullptr) { + pixel_data_ = data_.data(); + } + pixel_data_[position] = value; + modified_ = true; + } + + void WriteWordToPixel(int position, uint16_t value) { + if (pixel_data_ == nullptr) { + pixel_data_ = data_.data(); + } + pixel_data_[position] = value & 0xFF; + pixel_data_[position + 1] = (value >> 8) & 0xFF; + modified_ = true; + } + + void Get8x8Tile(int tile_index, int x, int y, std::vector &tile_data, + int &tile_data_offset) { + int tile_offset = tile_index * 64; + int tile_x = x * 8; + int tile_y = y * 8; + for (int i = 0; i < 8; i++) { + int row_offset = tile_offset + (i * 8); + for (int j = 0; j < 8; j++) { + int pixel_offset = row_offset + j; + int pixel_value = data_[pixel_offset]; + tile_data[tile_data_offset] = pixel_value; + tile_data_offset++; + } + } + } + + void WriteColor(int position, const ImVec4 &color) { + // Convert ImVec4 (RGBA) to SDL_Color (RGBA) + SDL_Color sdl_color; + sdl_color.r = static_cast(color.x * 255); + sdl_color.g = static_cast(color.y * 255); + sdl_color.b = static_cast(color.z * 255); + sdl_color.a = static_cast(color.w * 255); + + // Map SDL_Color to the nearest color index in the surface's palette + Uint8 index = + SDL_MapRGB(surface_->format, sdl_color.r, sdl_color.g, sdl_color.b); + + // Write the color index to the pixel data + pixel_data_[position] = index; + modified_ = true; + } + + void Cleanup() { + // Reset texture_ + if (texture_) { + texture_.reset(); + } + + // Reset surface_ and its pixel data + if (surface_) { + surface_->pixels = nullptr; + surface_.reset(); + } + + // Reset data_ + data_.clear(); + + // Reset other members if necessary + active_ = false; + pixel_data_ = nullptr; + width_ = 0; + height_ = 0; + depth_ = 0; + data_size_ = 0; + palette_.Clear(); + } + + auto sdl_palette() { + if (surface_ == nullptr) { + throw std::runtime_error("Surface is null."); + } + return surface_->format->palette; + } + auto palette() const { return palette_; } + auto palette_size() const { return palette_.size(); } + + int width() const { return width_; } + int height() const { return height_; } + auto depth() const { return depth_; } + auto size() const { return data_size_; } + auto data() const { return data_.data(); } + auto &mutable_data() { return data_; } + auto mutable_pixel_data() { return pixel_data_; } + auto surface() const { return surface_.get(); } + auto mutable_surface() { return surface_.get(); } + void set_data(const Bytes &data) { data_ = data; } + + auto vector() const { return data_; } + auto at(int i) const { return data_[i]; } + auto texture() const { return texture_.get(); } + auto modified() const { return modified_; } + void set_modified(bool modified) { modified_ = modified; } auto IsActive() const { return active_; } + auto SetActive(bool active) { active_ = active; } private: struct SDL_Texture_Deleter { @@ -71,13 +175,75 @@ class Bitmap { int height_ = 0; int depth_ = 0; int data_size_ = 0; + bool freed_ = false; bool active_ = false; + bool modified_ = false; + void *texture_pixels = nullptr; + uchar *pixel_data_; Bytes data_; + + std::vector png_data_; + gfx::SNESPalette palette_; std::shared_ptr texture_ = nullptr; std::shared_ptr surface_ = nullptr; + std::shared_ptr converted_surface_ = nullptr; +}; + +using BitmapTable = std::unordered_map; + +class BitmapManager { + private: + std::unordered_map> bitmap_cache_; + + public: + void LoadBitmap(int id, const Bytes &data, int width, int height, int depth) { + bitmap_cache_[id] = + std::make_shared(width, height, depth, data); + } + + std::shared_ptr const &CopyBitmap(const gfx::Bitmap &bitmap, + int id) { + auto new_bitmap = std::make_shared( + bitmap.width(), bitmap.height(), bitmap.depth(), bitmap.vector()); + bitmap_cache_[id] = new_bitmap; + return new_bitmap; + } + + std::shared_ptr const &operator[](int id) { + auto it = bitmap_cache_.find(id); + if (it != bitmap_cache_.end()) { + return it->second; + } + return nullptr; + } + + auto mutable_bitmap(int id) { return bitmap_cache_[id]; } + + using value_type = std::pair>; + using iterator = + std::unordered_map>::iterator; + using const_iterator = + std::unordered_map>::const_iterator; + + iterator begin() noexcept { return bitmap_cache_.begin(); } + iterator end() noexcept { return bitmap_cache_.end(); } + const_iterator begin() const noexcept { return bitmap_cache_.begin(); } + const_iterator end() const noexcept { return bitmap_cache_.end(); } + const_iterator cbegin() const noexcept { return bitmap_cache_.cbegin(); } + const_iterator cend() const noexcept { return bitmap_cache_.cend(); } + + std::shared_ptr const &GetBitmap(int id) { + auto it = bitmap_cache_.find(id); + if (it != bitmap_cache_.end()) { + return it->second; + } + return nullptr; // or handle the error accordingly + } + + void ClearCache() { bitmap_cache_.clear(); } }; } // namespace gfx diff --git a/src/app/gfx/compression.cc b/src/app/gfx/compression.cc new file mode 100644 index 00000000..2481aa2d --- /dev/null +++ b/src/app/gfx/compression.cc @@ -0,0 +1,1186 @@ +#include "compression.h" + +#include +#include +#include + +#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 { + +// Compression commands + +void PrintCompressionPiece(const CompressionPiecePointer& piece) { + std::cout << "Command: " << std::to_string(piece->command) << "\n"; + std::cout << "Command Length: " << piece->length << "\n"; + std::cout << "Argument: "; + auto arg_size = piece->argument.size(); + for (int i = 0; i < arg_size; ++i) { + printf("%02X ", piece->argument.at(i)); + } + std::cout << "\nArgument Length: " << piece->argument_length << "\n"; +} + +void PrintCompressionChain(const CompressionPiecePointer& chain_head) { + auto compressed_chain = chain_head->next; + while (compressed_chain != nullptr) { + std::cout << "- Compression Piece -\n"; + PrintCompressionPiece(compressed_chain); + compressed_chain = compressed_chain->next; + } +} + +void CheckByteRepeat(const uchar* rom_data, DataSizeArray& data_size_taken, + CommandArgumentArray& cmd_args, uint& src_data_pos, + const uint last_pos) { + uint pos = src_data_pos; + char byte_to_repeat = rom_data[pos]; + while (pos <= last_pos && rom_data[pos] == byte_to_repeat) { + data_size_taken[kCommandByteFill]++; + pos++; + } + cmd_args[kCommandByteFill][0] = byte_to_repeat; +} + +void CheckWordRepeat(const uchar* rom_data, DataSizeArray& data_size_taken, + CommandArgumentArray& cmd_args, uint& src_data_pos, + const uint last_pos) { + if (src_data_pos + 2 <= last_pos && + rom_data[src_data_pos] != rom_data[src_data_pos + 1]) { + uint pos = src_data_pos; + char byte1 = rom_data[pos]; + char byte2 = rom_data[pos + 1]; + pos += 2; + data_size_taken[kCommandWordFill] = 2; + while (pos + 1 <= last_pos) { + if (rom_data[pos] == byte1 && rom_data[pos + 1] == byte2) + data_size_taken[kCommandWordFill] += 2; + else + break; + pos += 2; + } + cmd_args[kCommandWordFill][0] = byte1; + cmd_args[kCommandWordFill][1] = byte2; + } +} + +void CheckIncByte(const uchar* rom_data, DataSizeArray& data_size_taken, + CommandArgumentArray& cmd_args, uint& src_data_pos, + const uint last_pos) { + uint pos = src_data_pos; + char byte = rom_data[pos]; + pos++; + data_size_taken[kCommandIncreasingFill] = 1; + byte++; + while (pos <= last_pos && byte == rom_data[pos]) { + data_size_taken[kCommandIncreasingFill]++; + byte++; + pos++; + } + cmd_args[kCommandIncreasingFill][0] = rom_data[src_data_pos]; +} + +void CheckIntraCopy(const uchar* rom_data, DataSizeArray& data_size_taken, + CommandArgumentArray& cmd_args, uint& src_data_pos, + const uint last_pos, uint start) { + if (src_data_pos != start) { + uint searching_pos = start; + uint current_pos_u = src_data_pos; + uint copied_size = 0; + uint search_start = start; + + while (searching_pos < src_data_pos && current_pos_u <= last_pos) { + while (rom_data[current_pos_u] != rom_data[searching_pos] && + searching_pos < src_data_pos) + searching_pos++; + search_start = searching_pos; + while (current_pos_u <= last_pos && + rom_data[current_pos_u] == rom_data[searching_pos] && + searching_pos < src_data_pos) { + copied_size++; + current_pos_u++; + searching_pos++; + } + if (copied_size > data_size_taken[kCommandRepeatingBytes]) { + search_start -= start; + printf("- Found repeat of %d at %d\n", copied_size, search_start); + data_size_taken[kCommandRepeatingBytes] = copied_size; + cmd_args[kCommandRepeatingBytes][0] = search_start & kSnesByteMax; + cmd_args[kCommandRepeatingBytes][1] = search_start >> 8; + } + current_pos_u = src_data_pos; + copied_size = 0; + } + } +} + +// Check if a command managed to pick up `max_win` or more bytes +// Avoids being even with copy command, since it's possible to merge copy +void ValidateForByteGain(const DataSizeArray& data_size_taken, + const CommandSizeArray& cmd_size, uint& max_win, + uint& cmd_with_max) { + for (uint cmd_i = 1; cmd_i < 5; cmd_i++) { + uint cmd_size_taken = data_size_taken[cmd_i]; + // TODO(@scawful): Replace conditional with table of command sizes + // "Table that is even with copy but all other cmd are 2" + auto table_check = + !(cmd_i == kCommandRepeatingBytes && cmd_size_taken == 3); + if (cmd_size_taken > max_win && cmd_size_taken > cmd_size[cmd_i] && + table_check) { + printf("==> C:%d / S:%d\n", cmd_i, cmd_size_taken); + cmd_with_max = cmd_i; + max_win = cmd_size_taken; + } + } +} + +void CompressionCommandAlternative(const uchar* rom_data, + CompressionPiecePointer& compressed_chain, + const CommandSizeArray& cmd_size, + const CommandArgumentArray& cmd_args, + uint& src_data_pos, uint& comp_accumulator, + uint& cmd_with_max, uint& max_win) { + printf("- Ok we get a gain from %d\n", cmd_with_max); + std::string buffer; + buffer.push_back(cmd_args[cmd_with_max][0]); + if (cmd_size[cmd_with_max] == 2) { + buffer.push_back(cmd_args[cmd_with_max][1]); + } + + auto new_comp_piece = std::make_shared( + cmd_with_max, max_win, buffer, cmd_size[cmd_with_max]); + PrintCompressionPiece(new_comp_piece); + // If we let non compressed stuff, we need to add a copy chunk before + if (comp_accumulator != 0) { + std::string copy_buff; + copy_buff.resize(comp_accumulator); + for (int i = 0; i < comp_accumulator; ++i) { + copy_buff[i] = rom_data[i + src_data_pos - comp_accumulator]; + } + auto copy_chunk = std::make_shared( + kCommandDirectCopy, comp_accumulator, copy_buff, comp_accumulator); + compressed_chain->next = copy_chunk; + compressed_chain = copy_chunk; + } else { + compressed_chain->next = new_comp_piece; + compressed_chain = new_comp_piece; + } + src_data_pos += max_win; + comp_accumulator = 0; +} + +// ============================================================================ +// Compression V2 + +void CheckByteRepeatV2(const uchar* data, uint& src_pos, const uint last_pos, + CompressionCommand& cmd) { + uint i = 0; + while (src_pos + i < last_pos && data[src_pos] == data[src_pos + i]) { + ++i; + } + cmd.data_size[kCommandByteFill] = i; + cmd.arguments[kCommandByteFill][0] = data[src_pos]; +} + +void CheckWordRepeatV2(const uchar* data, uint& src_pos, const uint last_pos, + CompressionCommand& cmd) { + if (src_pos + 2 <= last_pos && data[src_pos] != data[src_pos + 1]) { + uint pos = src_pos; + char byte1 = data[pos]; + char byte2 = data[pos + 1]; + pos += 2; + cmd.data_size[kCommandWordFill] = 2; + while (pos + 1 <= last_pos) { + if (data[pos] == byte1 && data[pos + 1] == byte2) + cmd.data_size[kCommandWordFill] += 2; + else + break; + pos += 2; + } + cmd.arguments[kCommandWordFill][0] = byte1; + cmd.arguments[kCommandWordFill][1] = byte2; + } +} + +void CheckIncByteV2(const uchar* rom_data, uint& src_data_pos, + const uint last_pos, CompressionCommand& cmd) { + uint pos = src_data_pos; + char byte = rom_data[pos]; + pos++; + cmd.data_size[kCommandIncreasingFill] = 1; + byte++; + while (pos <= last_pos && byte == rom_data[pos]) { + cmd.data_size[kCommandIncreasingFill]++; + byte++; + pos++; + } + cmd.arguments[kCommandIncreasingFill][0] = rom_data[src_data_pos]; +} + +void CheckIntraCopyV2(const uchar* rom_data, uint& src_data_pos, + const uint last_pos, uint start, + CompressionCommand& cmd) { + if (src_data_pos != start) { + uint searching_pos = start; + uint current_pos_u = src_data_pos; + uint copied_size = 0; + uint search_start = start; + + while (searching_pos < src_data_pos && current_pos_u <= last_pos) { + while (rom_data[current_pos_u] != rom_data[searching_pos] && + searching_pos < src_data_pos) + searching_pos++; + search_start = searching_pos; + while (current_pos_u <= last_pos && + rom_data[current_pos_u] == rom_data[searching_pos] && + searching_pos < src_data_pos) { + copied_size++; + current_pos_u++; + searching_pos++; + } + if (copied_size > cmd.data_size[kCommandRepeatingBytes]) { + search_start -= start; + printf("- Found repeat of %d at %d\n", copied_size, search_start); + cmd.data_size[kCommandRepeatingBytes] = copied_size; + cmd.arguments[kCommandRepeatingBytes][0] = search_start & kSnesByteMax; + cmd.arguments[kCommandRepeatingBytes][1] = search_start >> 8; + } + current_pos_u = src_data_pos; + copied_size = 0; + } + } +} + +// Table indicating command sizes, in bytes +const std::array kCommandSizes = {1, 2, 2, 2, 3}; + +// TODO(@scawful): TEST ME +void ValidateForByteGainV2(const CompressionCommand& cmd, uint& max_win, + uint& cmd_with_max) { + for (uint cmd_i = 1; cmd_i < 5; cmd_i++) { + uint cmd_size_taken = cmd.data_size[cmd_i]; + // Check if the command size exceeds the maximum win and the size in the + // command sizes table, except for the repeating bytes command when the size + // taken is 3 + if (cmd_size_taken > max_win && cmd_size_taken > kCommandSizes[cmd_i] && + !(cmd_i == kCommandRepeatingBytes && cmd_size_taken == 3)) { + printf("==> C:%d / S:%d\n", cmd_i, cmd_size_taken); + cmd_with_max = cmd_i; + max_win = cmd_size_taken; + } + } +} + +void CompressionCommandAlternativeV2(const uchar* rom_data, + const CompressionCommand& cmd, + CompressionPiecePointer& compressed_chain, + uint& src_data_pos, uint& comp_accumulator, + uint& cmd_with_max, uint& max_win) { + printf("- Ok we get a gain from %d\n", cmd_with_max); + std::string buffer; + buffer.push_back(cmd.arguments[cmd_with_max][0]); + if (cmd.cmd_size[cmd_with_max] == 2) { + buffer.push_back(cmd.arguments[cmd_with_max][1]); + } + + auto new_comp_piece = std::make_shared( + cmd_with_max, max_win, buffer, cmd.cmd_size[cmd_with_max]); + PrintCompressionPiece(new_comp_piece); + // If we let non compressed stuff, we need to add a copy chunk before + if (comp_accumulator != 0) { + std::string copy_buff; + copy_buff.resize(comp_accumulator); + for (int i = 0; i < comp_accumulator; ++i) { + copy_buff[i] = rom_data[i + src_data_pos - comp_accumulator]; + } + auto copy_chunk = std::make_shared( + kCommandDirectCopy, comp_accumulator, copy_buff, comp_accumulator); + compressed_chain->next = copy_chunk; + compressed_chain = copy_chunk; + } else { + compressed_chain->next = new_comp_piece; + compressed_chain = new_comp_piece; + } + src_data_pos += max_win; + comp_accumulator = 0; +} + +void AddAlternativeCompressionCommand( + const uchar* rom_data, CompressionPiecePointer& compressed_chain, + const CompressionCommand& command, uint& source_data_position, + uint& uncompressed_data_size, uint& best_command, uint& best_command_gain) { + std::cout << "- Identified a gain from command: " << best_command + << std::endl; + + // Create a buffer to store the arguments for the best command. + std::string argument_buffer; + argument_buffer.push_back(command.arguments[best_command][0]); + if (command.cmd_size[best_command] == 2) { + argument_buffer.push_back(command.arguments[best_command][1]); + } + + // Create a new compression piece for the best command. + auto new_compression_piece = std::make_shared( + best_command, best_command_gain, argument_buffer, + command.cmd_size[best_command]); + PrintCompressionPiece(new_compression_piece); + + // If there is uncompressed data, create a direct copy compression piece for + // it. + if (uncompressed_data_size != 0) { + std::string copy_buffer(uncompressed_data_size, 0); + for (int i = 0; i < uncompressed_data_size; ++i) { + copy_buffer[i] = + rom_data[i + source_data_position - uncompressed_data_size]; + } + auto direct_copy_piece = std::make_shared( + kCommandDirectCopy, uncompressed_data_size, copy_buffer, + uncompressed_data_size); + + // Append the direct copy piece to the chain. + compressed_chain->next = direct_copy_piece; + compressed_chain = direct_copy_piece; + } + + // Append the new compression piece to the chain. + compressed_chain->next = new_compression_piece; + compressed_chain = new_compression_piece; + + // Update the position in the source data and reset the uncompressed data + // size. + source_data_position += best_command_gain; + uncompressed_data_size = 0; +} + +absl::StatusOr SplitCompressionPiece( + CompressionPiecePointer& piece, int mode) { + CompressionPiecePointer new_piece; + uint length_left = piece->length - kMaxLengthCompression; + piece->length = kMaxLengthCompression; + + switch (piece->command) { + case kCommandByteFill: + case kCommandWordFill: + new_piece = std::make_shared( + piece->command, length_left, piece->argument, piece->argument_length); + break; + case kCommandIncreasingFill: + new_piece = std::make_shared( + piece->command, length_left, piece->argument, piece->argument_length); + new_piece->argument[0] = + (char)(piece->argument[0] + kMaxLengthCompression); + break; + case kCommandDirectCopy: + piece->argument_length = kMaxLengthCompression; + new_piece = std::make_shared( + piece->command, length_left, nullptr, 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); + new_piece = std::make_shared( + piece->command, length_left, piece->argument, piece->argument_length); + if (mode == kNintendoMode2) { + new_piece->argument[0] = + (offset + kMaxLengthCompression) & kSnesByteMax; + new_piece->argument[1] = (offset + kMaxLengthCompression) >> 8; + } + if (mode == kNintendoMode1) { + new_piece->argument[1] = + (offset + kMaxLengthCompression) & kSnesByteMax; + new_piece->argument[0] = (offset + kMaxLengthCompression) >> 8; + } + } break; + default: { + return absl::InvalidArgumentError( + "SplitCompressionCommand: Invalid Command"); + } + } + return new_piece; +} + +Bytes CreateCompressionString(CompressionPiecePointer& start, int mode) { + uint pos = 0; + auto piece = start; + Bytes output; + + while (piece != nullptr) { + if (piece->length <= kMaxLengthNormalHeader) { // Normal header + output.push_back(BUILD_HEADER(piece->command, piece->length)); + pos++; + } else { + if (piece->length <= kMaxLengthCompression) { + output.push_back(kCompressionStringMod | ((uchar)piece->command << 2) | + (((piece->length - 1) & 0xFF00) >> 8)); + pos++; + printf("Building extended header : cmd: %d, length: %d - %02X\n", + piece->command, piece->length, output[pos - 1]); + output.push_back(((piece->length - 1) & 0x00FF)); // (char) + pos++; + } else { + // We need to split the command + auto new_piece = SplitCompressionPiece(piece, mode); + if (!new_piece.ok()) { + std::cout << new_piece.status().ToString() << std::endl; + } + printf("New added piece\n"); + auto piece_data = new_piece.value(); + PrintCompressionPiece(piece_data); + piece_data->next = piece->next; + piece->next = piece_data; + continue; + } + } + + if (piece->command == kCommandRepeatingBytes) { + char tmp[2]; + tmp[0] = piece->argument[0]; + tmp[1] = piece->argument[1]; + if (mode == kNintendoMode1) { + tmp[0] = piece->argument[1]; + tmp[1] = piece->argument[0]; + } + for (const auto& each : tmp) { + output.push_back(each); + pos++; + } + } else { + for (int i = 0; i < piece->argument_length; ++i) { + output.push_back(piece->argument[i]); + pos++; + } + } + pos += piece->argument_length; + piece = piece->next; + } + output.push_back(kSnesByteMax); + return output; +} + +absl::Status ValidateCompressionResult(CompressionPiecePointer& chain_head, + int mode, int start, int src_data_pos) { + if (chain_head->next != nullptr) { + ROM temp_rom; + RETURN_IF_ERROR( + temp_rom.LoadFromBytes(CreateCompressionString(chain_head->next, mode))) + ASSIGN_OR_RETURN(auto decomp_data, + DecompressV2(temp_rom.data(), 0, temp_rom.size())) + if (!std::equal(decomp_data.begin() + start, decomp_data.end(), + temp_rom.begin())) { + return absl::InternalError(absl::StrFormat( + "Compressed data does not match uncompressed data at %d\n", + (uint)(src_data_pos - start))); + } + } + return absl::OkStatus(); +} + +// Merge consecutive copy if possible +CompressionPiecePointer MergeCopy(CompressionPiecePointer& start) { + CompressionPiecePointer piece = start; + + while (piece != nullptr) { + if (piece->command == kCommandDirectCopy && piece->next != nullptr && + piece->next->command == kCommandDirectCopy && + piece->length + piece->next->length <= kMaxLengthCompression) { + uint previous_length = piece->length; + piece->length = piece->length + piece->next->length; + + for (int i = 0; i < piece->next->argument_length; ++i) { + piece->argument[i + previous_length] = piece->next->argument[i]; + } + piece->argument_length = piece->length; + PrintCompressionPiece(piece); + + auto p_next_next = piece->next->next; + piece->next = p_next_next; + continue; // Next could be another copy + } + piece = piece->next; + } + return start; +} + +// TODO TEST compressed data border for each cmd +absl::StatusOr CompressV2(const uchar* data, const int start, + const int length, int mode, bool check) { + // Surely there's no need to compress zero... + if (length == 0) { + return Bytes(); + } + + // Worst case should be a copy of the string with extended header + auto compressed_chain = std::make_shared(1, 1, "aaa", 2); + auto compressed_chain_start = compressed_chain; + + CompressionCommand current_cmd = {/*argument*/ {{}}, + /*cmd_size*/ {0, 1, 2, 1, 2}, + /*data_size*/ {0, 0, 0, 0, 0}}; + + uint src_pos = start; + uint last_pos = start + length - 1; + uint comp_accumulator = 0; // Used when skipping using copy + + while (true) { + current_cmd.data_size.fill({}); + current_cmd.arguments.fill({{}}); + + CheckByteRepeatV2(data, src_pos, last_pos, current_cmd); + CheckWordRepeatV2(data, src_pos, last_pos, current_cmd); + CheckIncByteV2(data, src_pos, last_pos, current_cmd); + CheckIntraCopyV2(data, src_pos, last_pos, start, current_cmd); + + uint max_win = 2; + uint cmd_with_max = kCommandDirectCopy; + ValidateForByteGain(current_cmd.data_size, current_cmd.cmd_size, max_win, + cmd_with_max); + // ValidateForByteGainV2(current_cmd, max_win, cmd_with_max); + + if (cmd_with_max == kCommandDirectCopy) { + // This is the worst case scenario + // Progress through the next byte, in case there's a different + // compression command we can implement before we hit 32 bytes. + src_pos++; + comp_accumulator++; + + // Arbitrary choice to do a 32 bytes grouping for copy. + if (comp_accumulator == 32 || src_pos > last_pos) { + std::string buffer = SetBuffer(data, src_pos, comp_accumulator); + auto new_comp_piece = std::make_shared( + kCommandDirectCopy, comp_accumulator, buffer, comp_accumulator); + compressed_chain->next = new_comp_piece; + comp_accumulator = 0; + } + } else { + AddAlternativeCompressionCommand(data, compressed_chain, current_cmd, + src_pos, comp_accumulator, cmd_with_max, + max_win); + } + + if (src_pos > last_pos) { + printf("Breaking compression loop\n"); + break; + } + + if (check) { + RETURN_IF_ERROR(ValidateCompressionResult(compressed_chain_start, mode, + start, src_pos)) + } + } + + // Skipping compression chain header + MergeCopy(compressed_chain_start->next); + PrintCompressionChain(compressed_chain_start); + return CreateCompressionString(compressed_chain_start->next, mode); +} + +absl::StatusOr CompressGraphics(const uchar* data, const int pos, + const int length) { + return CompressV2(data, pos, length, kNintendoMode2); +} + +absl::StatusOr CompressOverworld(const uchar* data, const int pos, + const int length) { + return CompressV2(data, pos, length, kNintendoMode1); +} + +// ============================================================================ +// Compression V3 + +void CheckByteRepeatV3(CompressionContext& context) { + uint pos = context.src_pos; + + // Ensure the sequence does not start with an uncompressable byte + if (pos == 0 || context.data[pos - 1] != context.data[pos]) { + char byte_to_repeat = context.data[pos]; + while (pos <= context.last_pos && context.data[pos] == byte_to_repeat) { + context.current_cmd.data_size[kCommandByteFill]++; + pos++; + } + + context.current_cmd.arguments[kCommandByteFill][0] = byte_to_repeat; + + // Added debug log + DEBUG_LOG("CheckByteRepeatV3: byte_to_repeat = " + << (int)byte_to_repeat << ", size = " + << context.current_cmd.data_size[kCommandByteFill]); + } +} + +void CheckWordRepeatV3(CompressionContext& context) { + if (context.src_pos + 1 <= context.last_pos) { // Changed the condition here + uint pos = context.src_pos; + char byte1 = context.data[pos]; + char byte2 = context.data[pos + 1]; + pos += 2; + context.current_cmd.data_size[kCommandWordFill] = 2; + while (pos + 1 <= context.last_pos) { + if (context.data[pos] == byte1 && context.data[pos + 1] == byte2) + context.current_cmd.data_size[kCommandWordFill] += 2; + else + break; + pos += 2; + } + + context.current_cmd.arguments[kCommandWordFill][0] = byte1; + context.current_cmd.arguments[kCommandWordFill][1] = byte2; + } + + DEBUG_LOG("CheckWordRepeatV3: byte1 = " + << (int)context.current_cmd.arguments[kCommandWordFill][0] + << ", byte2 = " + << (int)context.current_cmd.arguments[kCommandWordFill][1] + << ", size = " << context.current_cmd.data_size[kCommandWordFill]); +} + +void CheckIncByteV3(CompressionContext& context) { + uint pos = context.src_pos; + uint8_t byte = context.data[pos]; + pos++; + context.current_cmd.data_size[kCommandIncreasingFill] = 1; + byte++; + + while (pos <= context.last_pos && byte == context.data[pos]) { + context.current_cmd.data_size[kCommandIncreasingFill]++; + byte++; + pos++; + } + + // Let's see if the sequence is surrounded by identical bytes and if so, + // consider if a direct copy is better. + if (context.current_cmd.data_size[kCommandIncreasingFill] == 3 && + context.src_pos > 0 && pos < context.data.size() && + context.data[context.src_pos - 1] == context.data[pos]) { + context.current_cmd.data_size[kCommandIncreasingFill] = + 0; // Reset the size to 0 to prioritize direct copy + return; + } + + context.current_cmd.arguments[kCommandIncreasingFill][0] = + context.data[context.src_pos]; + + DEBUG_LOG("CheckIncByteV3: byte = " + << (int)context.current_cmd.arguments[kCommandIncreasingFill][0] + << ", size = " + << context.current_cmd.data_size[kCommandIncreasingFill]); +} + +void CheckIntraCopyV3(CompressionContext& context) { + const int window_size = + 32; // This can be adjusted for optimal performance and results + + // We'll only search for repeating sequences if we're not at the very + // beginning + if (context.src_pos > 0 && + context.src_pos + window_size <= context.data.size()) { + uint max_copied_size = 0; + uint best_search_start = 0; + + // Slide the window over the source data + for (int win_pos = 1; win_pos < window_size && win_pos < context.src_pos; + ++win_pos) { + auto start_search_from = context.data.begin() + context.src_pos - win_pos; + auto search_end = context.data.begin() + context.src_pos; + + // Use std::search to find the sequence in the window in the previous + // source data + auto found_pos = std::search( + start_search_from, search_end, context.data.begin() + context.src_pos, + context.data.begin() + context.src_pos + win_pos); + + if (found_pos != search_end) { + // Check the entire length of the match + uint len = 0; + while (context.src_pos + len < context.data.size() && + context.data[context.src_pos + len] == *(found_pos + len)) { + len++; + } + + if (len > max_copied_size) { + max_copied_size = len; + best_search_start = found_pos - context.data.begin(); + } + } + } + + if (max_copied_size > + context.current_cmd.data_size[kCommandRepeatingBytes]) { + DEBUG_LOG("CheckIntraCopyV3: Detected repeating sequence of length " + << max_copied_size << " starting from " << best_search_start); + context.current_cmd.data_size[kCommandRepeatingBytes] = max_copied_size; + context.current_cmd.arguments[kCommandRepeatingBytes][0] = + best_search_start & kSnesByteMax; + context.current_cmd.arguments[kCommandRepeatingBytes][1] = + best_search_start >> 8; + } + + DEBUG_LOG("CheckIntraCopyV3: max_copied_size = " << max_copied_size + << ", best_search_start = " + << best_search_start); + } +} + +void InitializeCompression(CompressionContext& context) { + // Initialize the current_cmd with default values. + context.current_cmd = {/*argument*/ {{}}, + /*cmd_size*/ {0, 1, 2, 1, 2}, + /*data_size*/ {0, 0, 0, 0, 0}}; +} + +void CheckAvailableCompressionCommands(CompressionContext& context) { + // Reset the data_size and arguments for a fresh check. + context.current_cmd.data_size.fill({}); + context.current_cmd.arguments.fill({{}}); + + CheckByteRepeatV3(context); + CheckWordRepeatV3(context); + CheckIncByteV3(context); + CheckIntraCopyV3(context); + + DEBUG_LOG("CheckAvailableCompressionCommands: src_pos = " << context.src_pos); +} + +void DetermineBestCompression(CompressionContext& context) { + int max_net_savings = -1; // Adjusted the bias to consider any savings + + // Start with the default scenario. + context.cmd_with_max = kCommandDirectCopy; + + for (uint cmd_i = 1; cmd_i < 5; cmd_i++) { + uint cmd_size_taken = context.current_cmd.data_size[cmd_i]; + int net_savings = cmd_size_taken - context.current_cmd.cmd_size[cmd_i]; + + // Skip commands that aren't efficient. + if (cmd_size_taken <= 2 && cmd_i != kCommandDirectCopy) { + continue; + } + + // Check surrounding data for optimization. + if (context.src_pos > 0 && + context.src_pos + cmd_size_taken < context.data.size()) { + char prev_byte = context.data[context.src_pos - 1]; + char next_byte = context.data[context.src_pos + cmd_size_taken]; + if (prev_byte != next_byte && cmd_size_taken == 3) { + continue; + } + } + + // Check if the current command offers more net savings. + if (net_savings > max_net_savings) { + context.cmd_with_max = cmd_i; + max_net_savings = net_savings; + } + } + + DEBUG_LOG("DetermineBestCompression: cmd_with_max = " + << context.cmd_with_max << ", data_size = " + << context.current_cmd.data_size[context.cmd_with_max]); +} + +void HandleDirectCopy(CompressionContext& context) { + // If the next best compression method isn't direct copy and we have bytes + // accumulated for direct copy, flush them out. + if (context.cmd_with_max != kCommandDirectCopy && + context.comp_accumulator > 0) { + uint8_t header = BUILD_HEADER(kCommandDirectCopy, context.comp_accumulator); + context.compressed_data.push_back(header); + std::vector uncompressed_data( + context.data.begin() + context.src_pos - context.comp_accumulator, + context.data.begin() + context.src_pos); + context.compressed_data.insert(context.compressed_data.end(), + uncompressed_data.begin(), + uncompressed_data.end()); + context.comp_accumulator = 0; + return; + } + + // If the next best compression method is not direct copy and we haven't + // accumulated any bytes, treat it as a single byte direct copy. + if (context.cmd_with_max != kCommandDirectCopy && + context.comp_accumulator == 0) { + context.compressed_data.push_back( + 0x00); // Command for a single byte direct copy + context.compressed_data.push_back( + context.data[context.src_pos]); // The single byte + context.src_pos++; + return; + } + + // If we reach here, accumulate bytes for a direct copy. + context.src_pos++; + context.comp_accumulator++; + + // If we've accumulated the maximum bytes for a direct copy command or + // reached the end, flush them. + if (context.comp_accumulator >= 32 || context.src_pos > context.last_pos) { + uint8_t header = BUILD_HEADER(kCommandDirectCopy, context.comp_accumulator); + context.compressed_data.push_back(header); + std::vector uncompressed_data( + context.data.begin() + context.src_pos - context.comp_accumulator, + context.data.begin() + context.src_pos); + context.compressed_data.insert(context.compressed_data.end(), + uncompressed_data.begin(), + uncompressed_data.end()); + context.comp_accumulator = 0; + } + + DEBUG_LOG("HandleDirectCopy: src_pos = " << context.src_pos + << ", compressed_data size = " + << context.compressed_data.size()); +} + +void AddCompressionToChain(CompressionContext& context) { + DEBUG_LOG("AddCompressionToChain: Adding command arguments: "); + + // If there's uncompressed data, add a copy chunk before the compression + // command + if (context.comp_accumulator != 0) { + uint8_t header = BUILD_HEADER(kCommandDirectCopy, context.comp_accumulator); + context.compressed_data.push_back(header); + std::vector uncompressed_data( + context.data.begin() + context.src_pos - context.comp_accumulator, + context.data.begin() + context.src_pos); + context.compressed_data.insert(context.compressed_data.end(), + uncompressed_data.begin(), + uncompressed_data.end()); + context.comp_accumulator = 0; + } + + // Now, add the compression command + uint8_t header = + BUILD_HEADER(context.cmd_with_max, + context.current_cmd.data_size[context.cmd_with_max]); + context.compressed_data.push_back(header); + + DEBUG_LOG("AddCompressionToChain: (Before) src_pos = " + << context.src_pos + << ", compressed_data size = " << context.compressed_data.size()); + + // Add the command arguments to the compressed_data vector + context.compressed_data.push_back( + context.current_cmd.arguments[context.cmd_with_max][0]); + if (context.current_cmd.cmd_size[context.cmd_with_max] == 2) { + context.compressed_data.push_back( + context.current_cmd.arguments[context.cmd_with_max][1]); + } + + context.src_pos += context.current_cmd.data_size[context.cmd_with_max]; + context.comp_accumulator = 0; + + DEBUG_LOG("AddCompressionToChain: (After) src_pos = " + << context.src_pos + << ", compressed_data size = " << context.compressed_data.size()); +} + +absl::Status ValidateCompressionResultV3(const CompressionContext& context) { + if (!context.compressed_data.empty()) { + ROM temp_rom; + RETURN_IF_ERROR(temp_rom.LoadFromBytes(context.compressed_data)); + ASSIGN_OR_RETURN(auto decomp_data, + DecompressV2(temp_rom.data(), 0, temp_rom.size())) + + if (!std::equal(decomp_data.begin() + context.start, decomp_data.end(), + temp_rom.begin())) { + return absl::InternalError(absl::StrFormat( + "Compressed data does not match uncompressed data at %d\n", + (context.src_pos - context.start))); + } + } + return absl::OkStatus(); +} + +absl::StatusOr SplitCompressionPieceV3( + CompressionPiece& piece, int mode) { + CompressionPiece new_piece; + uint length_left = piece.length - kMaxLengthCompression; + piece.length = kMaxLengthCompression; + + switch (piece.command) { + case kCommandByteFill: + case kCommandWordFill: + new_piece = CompressionPiece(piece.command, length_left, piece.argument, + piece.argument_length); + break; + case kCommandIncreasingFill: + new_piece = CompressionPiece(piece.command, length_left, piece.argument, + piece.argument_length); + new_piece.argument[0] = (char)(piece.argument[0] + kMaxLengthCompression); + break; + case kCommandDirectCopy: + piece.argument_length = kMaxLengthCompression; + new_piece = + CompressionPiece(piece.command, length_left, nullptr, 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); + new_piece = CompressionPiece(piece.command, length_left, piece.argument, + piece.argument_length); + if (mode == kNintendoMode2) { + new_piece.argument[0] = (offset + kMaxLengthCompression) & kSnesByteMax; + new_piece.argument[1] = (offset + kMaxLengthCompression) >> 8; + } + if (mode == kNintendoMode1) { + new_piece.argument[1] = (offset + kMaxLengthCompression) & kSnesByteMax; + new_piece.argument[0] = (offset + kMaxLengthCompression) >> 8; + } + } break; + default: { + return absl::InvalidArgumentError( + "SplitCompressionCommand: Invalid Command"); + } + } + + return new_piece; +} + +void FinalizeCompression(CompressionContext& context) { + uint pos = 0; + + for (CompressionPiece& piece : context.compression_pieces) { + if (piece.length <= kMaxLengthNormalHeader) { // Normal Header + context.compression_string.push_back( + BUILD_HEADER(piece.command, piece.length)); + pos++; + } else { + if (piece.length <= kMaxLengthCompression) { + context.compression_string.push_back( + kCompressionStringMod | ((uchar)piece.command << 2) | + (((piece.length - 1) & 0xFF00) >> 8)); + pos++; + std::cout << "Building extended header : cmd: " << piece.command + << ", length: " << piece.length << " - " + << (int)context.compression_string[pos - 1] << std::endl; + context.compression_string.push_back( + ((piece.length - 1) & 0x00FF)); // (char) + } else { + // We need to split the command + auto new_piece = SplitCompressionPieceV3(piece, context.mode); + if (!new_piece.ok()) { + std::cout << new_piece.status().ToString() << std::endl; + } + context.compression_pieces.insert( + context.compression_pieces.begin() + pos + 1, new_piece.value()); + continue; + } + } + + if (piece.command == kCommandRepeatingBytes) { + char tmp[2]; + tmp[0] = piece.argument[0]; + tmp[1] = piece.argument[1]; + if (context.mode == kNintendoMode1) { + tmp[0] = piece.argument[1]; + tmp[1] = piece.argument[0]; + } + for (const auto& each : tmp) { + context.compression_string.push_back(each); + pos++; + } + } else { + for (int i = 0; i < piece.argument_length; ++i) { + context.compression_string.push_back(piece.argument[i]); + pos++; + } + } + pos += piece.argument_length; + } + + // Add any remaining uncompressed data + if (context.comp_accumulator > 0) { + context.compressed_data.insert( + context.compressed_data.end(), + context.data.begin() + context.src_pos - context.comp_accumulator, + context.data.begin() + context.src_pos); + context.comp_accumulator = 0; + } + + // Add the end marker to the compressed data + context.compressed_data.push_back(kSnesByteMax); + DEBUG_LOG("FinalizeCompression: compressed_data size = " + << context.compressed_data.size()); +} + +absl::StatusOr CompressV3(const std::vector data, + const int start, const int length, int mode, + bool check) { + if (length == 0) { + return Bytes(); + } + + CompressionContext context(data, start, length, mode); + InitializeCompression(context); + + while (context.src_pos <= context.last_pos) { + CheckAvailableCompressionCommands(context); + DetermineBestCompression(context); + + DEBUG_LOG("CompressV3 Loop: cmd_with_max = " << context.cmd_with_max); + + if (context.cmd_with_max == kCommandDirectCopy) { + HandleDirectCopy(context); + } else { + AddCompressionToChain(context); + } + + if (check) { + RETURN_IF_ERROR(ValidateCompressionResultV3(context)) + } + } + + FinalizeCompression(context); + return Bytes(context.compressed_data.begin(), 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) { + buffer.push_back(data[i + src_pos - comp_accumulator]); + } + return buffer; +} + +std::string SetBuffer(const std::vector& data, int src_pos, + int comp_accumulator) { + std::string buffer; + for (int i = 0; i < comp_accumulator; ++i) { + buffer.push_back(data[i + src_pos - comp_accumulator]); + } + return buffer; +} + +void memfill(const uchar* data, Bytes& buffer, int buffer_pos, int offset, + int length) { + auto a = data[offset]; + auto b = data[offset + 1]; + for (int i = 0; i < length; i = i + 2) { + buffer[buffer_pos + i] = a; + if ((i + 1) < length) buffer[buffer_pos + i + 1] = b; + } +} + +absl::StatusOr DecompressV2(const uchar* data, int offset, int size, + int mode) { + if (size == 0) { + return Bytes(); + } + + Bytes buffer(size, 0); + uint length = 0; + uint buffer_pos = 0; + uchar command = 0; + uchar header = data[offset]; + + while (header != kSnesByteMax) { + if ((header & kExpandedMod) == kExpandedMod) { + // Expanded Command + command = ((header >> 2) & kCommandMod); + length = (((header << 8) | data[offset + 1]) & kExpandedLengthMod); + offset += 2; // Advance 2 bytes in ROM + } else { + // Normal Command + command = ((header >> 5) & kCommandMod); + length = (header & kNormalLengthMod); + offset += 1; // Advance 1 byte in ROM + } + length += 1; // each commands is at least of size 1 even if index 00 + + switch (command) { + case kCommandDirectCopy: // Does not advance in the ROM + memcpy(buffer.data() + buffer_pos, data + offset, length); + buffer_pos += length; + offset += length; + break; + case kCommandByteFill: + memset(buffer.data() + buffer_pos, (int)(data[offset]), length); + buffer_pos += length; + offset += 1; // Advances 1 byte in the ROM + break; + case kCommandWordFill: + memfill(data, buffer, buffer_pos, offset, length); + buffer_pos += length; + offset += 2; // Advance 2 byte in the ROM + break; + case kCommandIncreasingFill: { + auto inc_byte = data[offset]; + for (int i = 0; i < length; i++) { + buffer[buffer_pos] = inc_byte++; + buffer_pos++; + } + offset += 1; // Advance 1 byte in the ROM + } break; + case kCommandRepeatingBytes: { + ushort s1 = ((data[offset + 1] & kSnesByteMax) << 8); + ushort s2 = (data[offset] & kSnesByteMax); + int addr = (s1 | s2); + if (mode == kNintendoMode1) { // Reversed byte order for + // overworld maps + addr = (data[offset + 1] & kSnesByteMax) | + ((data[offset] & kSnesByteMax) << 8); + } + if (addr > offset) { + return absl::InternalError( + absl::StrFormat("Decompress: Offset for command copy exceeds " + "current position " + "(Offset : %#04x | Pos : %#06x)\n", + addr, offset)); + } + if (buffer_pos + length >= size) { + size *= 2; + buffer.resize(size); + } + memcpy(buffer.data() + buffer_pos, buffer.data() + addr, length); + buffer_pos += length; + offset += 2; + } break; + default: { + std::cout << absl::StrFormat( + "Decompress: Invalid header (Offset : %#06x, Command: %#04x)\n", + offset, command); + } break; + } + // check next byte + header = data[offset]; + } + + return buffer; +} + +absl::StatusOr DecompressGraphics(const uchar* data, int pos, int size) { + return DecompressV2(data, pos, size, kNintendoMode2); +} + +absl::StatusOr DecompressOverworld(const uchar* data, int pos, + int size) { + return DecompressV2(data, pos, size, kNintendoMode1); +} + +absl::StatusOr DecompressOverworld(const std::vector data, + int pos, int size) { + return DecompressV2(data.data(), pos, size, kNintendoMode1); +} + +} // namespace lc_lz2 +} // namespace gfx +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/gfx/compression.h b/src/app/gfx/compression.h new file mode 100644 index 00000000..f532cc39 --- /dev/null +++ b/src/app/gfx/compression.h @@ -0,0 +1,213 @@ +#ifndef YAZE_APP_GFX_COMPRESSION_H +#define YAZE_APP_GFX_COMPRESSION_H + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "app/core/constants.h" + +#define BUILD_HEADER(command, length) (command << 5) + (length - 1) + +namespace yaze { +namespace app { +namespace gfx { + +namespace lc_lz2 { + +constexpr int kCommandDirectCopy = 0; +constexpr int kCommandByteFill = 1; +constexpr int kCommandWordFill = 2; +constexpr int kCommandIncreasingFill = 3; +constexpr int kCommandRepeatingBytes = 4; +constexpr int kCommandLongLength = 7; +constexpr int kMaxLengthNormalHeader = 32; +constexpr int kMaxLengthCompression = 1024; +constexpr int kNintendoMode1 = 0; +constexpr int kNintendoMode2 = 1; +constexpr int kSnesByteMax = 0xFF; +constexpr int kCommandMod = 0x07; +constexpr int kExpandedMod = 0xE0; +constexpr int kExpandedLengthMod = 0x3FF; +constexpr int kNormalLengthMod = 0x1F; +constexpr int kCompressionStringMod = 7 << 5; + +// Represents a command in the compression algorithm. +struct CompressionCommand { + // The command arguments for each possible command. + std::array, 5> arguments; + + // The size of each possible command. + std::array cmd_size; + + // The size of the data processed by each possible command. + std::array data_size; +}; + +using CommandArgumentArray = std::array, 5>; +using CommandSizeArray = std::array; +using DataSizeArray = std::array; + +// Represents a piece of compressed data. +struct CompressionPiece { + char command; + int length; + int argument_length; + std::string argument; + std::shared_ptr next = nullptr; + CompressionPiece() = default; + 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; +using CompressionPiecePointer = std::shared_ptr; + +void PrintCompressionPiece(const CompressionPiecePointer& piece); + +void PrintCompressionChain(const CompressionPiecePointer& chain_head); + +// Compression V1 + +void CheckByteRepeat(const uchar* rom_data, DataSizeArray& data_size_taken, + CommandArgumentArray& cmd_args, uint& src_data_pos, + const uint last_pos); + +void CheckWordRepeat(const uchar* rom_data, DataSizeArray& data_size_taken, + CommandArgumentArray& cmd_args, uint& src_data_pos, + const uint last_pos); + +void CheckIncByte(const uchar* rom_data, DataSizeArray& data_size_taken, + CommandArgumentArray& cmd_args, uint& src_data_pos, + const uint last_pos); + +void CheckIntraCopy(const uchar* rom_data, DataSizeArray& data_size_taken, + CommandArgumentArray& cmd_args, uint& src_data_pos, + const uint last_pos, uint start); + +void ValidateForByteGain(const DataSizeArray& data_size_taken, + const CommandSizeArray& cmd_size, uint& max_win, + uint& cmd_with_max); + +void CompressionCommandAlternative(const uchar* rom_data, + CompressionPiecePointer& compressed_chain, + const CommandSizeArray& cmd_size, + const CommandArgumentArray& cmd_args, + uint& src_data_pos, uint& comp_accumulator, + uint& cmd_with_max, uint& max_win); + +// Compression V2 + +void CheckByteRepeatV2(const uchar* data, uint& src_pos, const uint last_pos, + CompressionCommand& cmd); + +void CheckWordRepeatV2(const uchar* data, uint& src_pos, const uint last_pos, + CompressionCommand& cmd); + +void CheckIncByteV2(const uchar* data, uint& src_pos, const uint last_pos, + CompressionCommand& cmd); + +void CheckIntraCopyV2(const uchar* data, uint& src_pos, const uint last_pos, + uint start, CompressionCommand& cmd); + +void ValidateForByteGainV2(const CompressionCommand& cmd, uint& max_win, + uint& cmd_with_max); + +void CompressionCommandAlternativeV2(const uchar* data, + const CompressionCommand& cmd, + CompressionPiecePointer& compressed_chain, + uint& src_pos, uint& comp_accumulator, + uint& cmd_with_max, uint& max_win); + +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 SplitCompressionPiece( + CompressionPiecePointer& piece, int mode); + +Bytes 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; + std::vector compression_pieces; + std::vector compression_string; + uint src_pos; + uint last_pos; + uint start; + uint comp_accumulator = 0; + uint cmd_with_max = kCommandDirectCopy; + uint max_win = 0; + CompressionCommand current_cmd = {}; + int mode; + + // Constructor to initialize the context + CompressionContext(const std::vector& data_, const int start, + const int length) + : data(data_), src_pos(start), last_pos(start + length - 1), mode(0) {} + + // Constructor to initialize the context + CompressionContext(const std::vector& data_, const int start, + const int length, int mode_) + : data(data_), + src_pos(start), + last_pos(start + length - 1), + mode(mode_) {} +}; + +void CheckByteRepeatV3(CompressionContext& context); +void CheckWordRepeatV3(CompressionContext& context); +void CheckIncByteV3(CompressionContext& context); +void CheckIntraCopyV3(CompressionContext& context); + +void InitializeCompression(CompressionContext& context); +void CheckAvailableCompressionCommands(CompressionContext& context); +void DetermineBestCompression(CompressionContext& context); +void HandleDirectCopy(CompressionContext& context); +void AddCompressionToChain(CompressionContext& context); +absl::Status ValidateCompressionResultV3(const CompressionContext& context); + +absl::StatusOr SplitCompressionPieceV3( + CompressionPiece& piece, int mode); +void FinalizeCompression(CompressionContext& context); + +absl::StatusOr CompressV3(const std::vector data, + const int start, const int length, + int mode = 1, bool check = false); + +// Decompression + +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, Bytes& buffer, int buffer_pos, int offset, + int length); + +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); + +} // namespace lc_lz2 + +} // namespace gfx +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_GFX_COMPRESSION_H \ No newline at end of file diff --git a/src/app/gfx/scad_format.cc b/src/app/gfx/scad_format.cc new file mode 100644 index 00000000..6626ae14 --- /dev/null +++ b/src/app/gfx/scad_format.cc @@ -0,0 +1,281 @@ +#include "scad_format.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "app/core/constants.h" +#include "app/gfx/snes_tile.h" + +namespace yaze { +namespace app { +namespace gfx { + +void FindMetastamp() { + int matching_position = -1; + bool matched = false; + Bytes cgx_rom; + Bytes raw_data_; + for (int i = 0; + i < cgx_rom.size() - sizeof(kMatchedBytes) - kOffsetFromMatchedBytesEnd; + i++) { + raw_data_.push_back(cgx_rom[i]); + bool is_match = std::equal(std::begin(kMatchedBytes), + std::end(kMatchedBytes), &cgx_rom[i]); + if (is_match) { + matching_position = i; + matched = true; + break; + } + } + if (matched) { + int bpp_marker_position = + matching_position + sizeof(kMatchedBytes) + kOffsetFromMatchedBytesEnd; + int bpp_marker = cgx_rom[bpp_marker_position]; + std::string bpp_type = (bpp_marker == 0x31) ? "8bpp" : "4bpp"; + } +} + +absl::Status LoadCgx(uint8_t bpp, std::string_view filename, + std::vector& cgx_data, + std::vector& cgx_loaded, + std::vector& cgx_header) { + std::ifstream file(filename.data(), std::ios::binary); + if (!file.is_open()) { + return absl::NotFoundError("CGX file not found."); + } + std::vector file_content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + cgx_data = + std::vector(file_content.begin(), file_content.end() - 0x500); + file.seekg(cgx_data.size() + 0x100); + cgx_header = std::vector((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + file.close(); + + if (bpp > 8) { + cgx_loaded = gfx::Bpp8SnesToIndexed(cgx_data, 40); + return absl::OkStatus(); + } + cgx_loaded = gfx::Bpp8SnesToIndexed(cgx_data, bpp); + return absl::OkStatus(); +} + +absl::Status LoadScr(std::string_view filename, uint8_t input_value, + std::vector& map_data) { + std::ifstream file(filename.data(), std::ios::binary); + if (!file.is_open()) { + return absl::NotFoundError("SCR/PNL/MAP file not found."); + } + + // Check if file extension is PNL + bool pnl = false; + if (filename.find("PNL") != std::string::npos) { + std::vector scr_data; + map_data.resize(0x8000); + scr_data.resize(0x8000); + + // Read from file for 0x8000 bytes + std::vector file_content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + scr_data = std::vector(file_content.begin(), file_content.end()); + + int md = 0x100; + + for (int i = input_value * 0x400; i < 0x1000 + input_value * 0x400; + i += 2) { + auto b1_pos = (i - (input_value * 0x400)); + map_data[b1_pos] = gfx::TileInfoToShort( + gfx::GetTilesInfo((ushort)scr_data[md + (i * 2)])); + + auto b2_pos = (i - (input_value * 0x400) + 1); + map_data[b2_pos] = gfx::TileInfoToShort( + gfx::GetTilesInfo((ushort)scr_data[md + (i * 2) + 2])); + } + // 0x900 + + } else { + int offset = 0; + std::vector scr_data; + map_data.resize(0x2000); + scr_data.resize(0x2000); + + // read from file for 0x2000 bytes + std::vector file_content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + scr_data = std::vector(file_content.begin(), file_content.end()); + + for (int i = 0; i < 0x1000 - offset; i++) { + map_data[i] = gfx::TileInfoToShort( + gfx::GetTilesInfo((ushort)scr_data[((i + offset) * 2)])); + } + } + return absl::OkStatus(); +} + +absl::Status DrawScrWithCgx(uint8_t bpp, std::vector& map_data, + std::vector& map_bitmap_data, + std::vector& cgx_loaded) { + const std::vector dimensions = {0x000, 0x400, 0x800, 0xC00}; + uint8_t p = 0; + for (const auto each_dimension : dimensions) { + p = each_dimension; + // for each tile on the tile buffer + for (int i = 0; i < 0x400; i++) { + if (map_data[i + p] != 0xFFFF) { + auto t = gfx::GetTilesInfo(map_data[i + p]); + + for (auto yl = 0; yl < 8; yl++) { + for (auto xl = 0; xl < 8; xl++) { + int mx = xl * (1 - t.horizontal_mirror_) + + (7 - xl) * (t.horizontal_mirror_); + int my = + yl * (1 - t.vertical_mirror_) + (7 - yl) * (t.vertical_mirror_); + + int ty = (t.id_ / 16) * 1024; + int tx = (t.id_ % 16) * 8; + auto pixel = cgx_loaded[(tx + ty) + (yl * 128) + xl]; + + int index = (((i % 32) * 8) + ((i / 32) * 2048) + mx + (my * 256)); + + if (bpp != 8) { + map_bitmap_data[index] = + (uchar)((pixel & 0xFF) + t.palette_ * 16); + } else { + map_bitmap_data[index] = (uchar)(pixel & 0xFF); + } + } + } + } + } + } + return absl::OkStatus(); +} + +std::vector DecodeColFile(const std::string_view filename) { + std::vector decoded_col; + std::ifstream file(filename.data(), std::ios::binary | std::ios::ate); + + if (!file.is_open()) { + return decoded_col; // Return an empty vector if the file couldn't be + // opened. + } + + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector buffer(size); + if (file.read(buffer.data(), size)) { + buffer.resize(size - 0x200); + + int k = 0; + for (size_t i = 0; i < buffer.size() / 2; i++) { + uint16_t current_color = static_cast(buffer[k]) | + (static_cast(buffer[k + 1]) << 8); + + SDL_Color color; + color.r = (current_color & 31) << 3; + color.g = ((current_color >> 5) & 31) << 3; + color.b = ((current_color >> 10) & 31) << 3; + color.a = (i & 0xF) == 0 ? 0 : 255; + + decoded_col.push_back(color); + k += 2; + } + } + + return decoded_col; +} + +absl::Status DecodeObjFile( + std::string_view filename, std::vector& obj_data, + std::vector actual_obj_data, + std::unordered_map> decoded_obj, + std::vector& decoded_extra_obj, int& obj_loaded) { + std::vector header_obj; + int obj_range; + int expected_cut; + if (obj_loaded == 0) { + obj_range = 0x180; + expected_cut = 0x500; + } else { + obj_range = 0x300; + expected_cut = 0x900; + } + + std::ifstream file(filename.data(), std::ios::binary); + if (!file.is_open()) { + return absl::NotFoundError("OBJ file not found."); + } + + std::vector file_content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + obj_data = file_content; + file.close(); + + int cut = obj_data.size() & 0x0FFF; + actual_obj_data = + std::vector(obj_data.begin(), obj_data.end() - cut); + decoded_extra_obj = + std::vector(obj_data.begin() + actual_obj_data.size(), + obj_data.begin() + actual_obj_data.size() + 0x100); + header_obj = std::vector( + actual_obj_data.begin() + actual_obj_data.size(), actual_obj_data.end()); + + if (cut > expected_cut) { + std::vector scad_data; + int j = 0; + int k = (obj_loaded == 0) ? 63 : 127; + + for (size_t i = 0; i < (actual_obj_data.size() / 6); i++) { + std::vector data = { + actual_obj_data[k * 6 + 0 + j], // display + actual_obj_data[k * 6 + 1 + j], // unknown + actual_obj_data[k * 6 + 2 + j], // y-disp + actual_obj_data[k * 6 + 3 + j], // x-disp + actual_obj_data[k * 6 + 5 + j], // props + actual_obj_data[k * 6 + 4 + j] // tile + }; + scad_data.insert(scad_data.end(), data.begin(), data.end()); + + k = k - 1; + if (k == -1) { + k = (obj_loaded == 0) ? 63 : 127; + j = j + ((k + 1) * 6); + } + } + + int extra_data_range = 0x400 * (obj_loaded + 1) + 0x100; + for (int i = 0; i < extra_data_range; i++) { + scad_data.push_back(header_obj[i]); + } + + obj_data = scad_data; + actual_obj_data = + std::vector(obj_data.begin(), obj_data.end() - cut); + } + + decoded_obj.clear(); + for (int k = 0; k < 128; k++) { + decoded_obj["frame " + std::to_string(k)] = std::vector(obj_range); + for (int i = 0; i < obj_range; i++) { + try { + decoded_obj["frame " + std::to_string(k)][i] = + obj_data[i + (obj_range * k)]; + } catch (...) { + decoded_obj["frame " + std::to_string(k)][i] = 0; + } + } + } + + return absl::OkStatus(); +} + +} // namespace gfx +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/gfx/scad_format.h b/src/app/gfx/scad_format.h new file mode 100644 index 00000000..977308df --- /dev/null +++ b/src/app/gfx/scad_format.h @@ -0,0 +1,78 @@ +#ifndef YAZE_APP_GFX_scad_format_H +#define YAZE_APP_GFX_scad_format_H + +#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 { + +// キャラクタ(.SCH)ファイル +// ヘッダー情報 +// アドレス 説明 +// 00000 - 00003 ファイルタイプ "SCH" +// 00004 - 00008 ビットモード "?BIT" +// 00009 - 00013 バージョンナンバー "Ver-????\n" +// 00014 - 00017 ヘッダーサイズ +// 00018 - 0001B ハード名 "SFC" or "CGB" or "GB" +// 0001C - 0001C BG/OBJフラグ(AGBの時) +// 0001D - 0001D Color Pallette Number +// 0001D - 000FF 予約 +// 00100 - 001FF Color Path +struct CgxHeader { + char file_type[4]; + char bit_mode[5]; + char version_number[9]; + uint32_t header_size; + char hardware_name[4]; + uint8_t bg_obj_flag; + uint8_t color_palette_number; + uint8_t reserved[0xE3]; + uint8_t color_path[0x100]; +}; + +constexpr uint16_t kMatchedBytes[] = {0x4E, 0x41, 0x4B, 0x31, 0x39, 0x38, 0x39}; +constexpr uint16_t kOffsetFromMatchedBytesEnd = 0x1D; + +void FindMetastamp(); + +absl::Status LoadScr(std::string_view filename, uint8_t input_value, + std::vector& map_data); + +absl::Status LoadCgx(uint8_t bpp, std::string_view filename, + std::vector& cgx_data, + std::vector& cgx_loaded, + std::vector& cgx_header); + +absl::Status DrawScrWithCgx(uint8_t bpp, std::vector& map_bitmap_data, + std::vector& map_data, + std::vector& cgx_loaded); + +std::vector DecodeColFile(const std::string_view filename); + +absl::Status DecodeObjFile( + std::string_view filename, std::vector& obj_data, + std::vector actual_obj_data, + std::unordered_map> decoded_obj, + std::vector& decoded_extra_obj, int& obj_loaded); + +} // namespace gfx +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_GFX_scad_format_H \ No newline at end of file diff --git a/src/app/gfx/snes_palette.cc b/src/app/gfx/snes_palette.cc index 5ff1c878..fd68d029 100644 --- a/src/app/gfx/snes_palette.cc +++ b/src/app/gfx/snes_palette.cc @@ -10,91 +10,129 @@ #include #include +#include "absl/container/flat_hash_map.h" // for flat_hash_map +#include "absl/status/status.h" // for Status #include "app/core/constants.h" namespace yaze { namespace app { namespace gfx { -ushort ConvertRGBtoSNES(const snes_color color) { - uchar red = color.red / 8; - uchar green = color.green / 8; - uchar blue = color.blue / 8; - return blue * 1024 + green * 32 + red; +// Define a hash map to hold the addresses of different palette groups +const absl::flat_hash_map paletteGroupAddresses = { + {"ow_main", core::overworldPaletteMain}, + {"ow_aux", core::overworldPaletteAuxialiary}, + {"ow_animated", core::overworldPaletteAnimated}, + {"hud", core::hudPalettes}, + {"global_sprites", core::globalSpritePalettesLW}, + {"armors", core::armorPalettes}, + {"swords", core::swordPalettes}, + {"shields", core::shieldPalettes}, + {"sprites_aux1", core::spritePalettesAux1}, + {"sprites_aux2", core::spritePalettesAux2}, + {"sprites_aux3", core::spritePalettesAux3}, + {"dungeon_main", core::dungeonMainPalettes}, + {"grass", core::hardcodedGrassLW}, + {"3d_object", core::triforcePalette}, + {"ow_mini_map", core::overworldMiniMapPalettes}, +}; + +// Define a hash map to hold the number of colors in each palette group +const absl::flat_hash_map paletteGroupColorCounts = { + {"ow_main", 35}, {"ow_aux", 21}, {"ow_animated", 7}, + {"hud", 32}, {"global_sprites", 60}, {"armors", 15}, + {"swords", 3}, {"shields", 4}, {"sprites_aux1", 7}, + {"sprites_aux2", 7}, {"sprites_aux3", 7}, {"dungeon_main", 90}, + {"grass", 1}, {"3d_object", 8}, {"ow_mini_map", 128}, +}; + +constexpr uint16_t SNES_RED_MASK = 32; +constexpr uint16_t SNES_GREEN_MASK = 32; +constexpr uint16_t SNES_BLUE_MASK = 32; + +constexpr uint16_t SNES_GREEN_SHIFT = 32; +constexpr uint16_t SNES_BLUE_SHIFT = 1024; + +uint16_t ConvertRGBtoSNES(const snes_color& color) { + uint16_t red = color.red / 8; + uint16_t green = color.green / 8; + uint16_t blue = color.blue / 8; + return (blue * SNES_BLUE_SHIFT) + (green * SNES_GREEN_SHIFT) + red; } -snes_color ConvertSNEStoRGB(const ushort color) { - snes_color toret; - - toret.red = ((color) % 32) * 8; - toret.green = ((color / 32) % 32) * 8; - toret.blue = ((color / 1024) % 32) * 8; - - toret.red = toret.red + toret.red / 32; - toret.green = toret.green + toret.green / 32; - toret.blue = toret.blue + toret.blue / 32; - return toret; +uint16_t ConvertRGBtoSNES(const ImVec4& color) { + snes_color new_color; + new_color.red = color.x * 255; + new_color.green = color.y * 255; + new_color.blue = color.z * 255; + return ConvertRGBtoSNES(new_color); } -snes_palette* Extract(const char* data, const unsigned int offset, - const unsigned int palette_size) { - snes_palette* toret = nullptr; // palette_create(palette_size, 0) - unsigned colnum = 0; - for (int i = 0; i < palette_size * 2; i += 2) { - unsigned short snes_color; - snes_color = ((uchar)data[offset + i + 1]) << 8; - snes_color = snes_color | ((uchar)data[offset + i]); - toret->colors[colnum] = ConvertSNEStoRGB(snes_color); - colnum++; +snes_color ConvertSNEStoRGB(uint16_t color_snes) { + snes_color result; + + result.red = (color_snes % SNES_RED_MASK) * 8; + result.green = ((color_snes / SNES_GREEN_MASK) % SNES_GREEN_MASK) * 8; + result.blue = ((color_snes / SNES_BLUE_SHIFT) % SNES_BLUE_MASK) * 8; + + result.red += result.red / SNES_RED_MASK; + result.green += result.green / SNES_GREEN_MASK; + result.blue += result.blue / SNES_BLUE_MASK; + + return result; +} + +std::vector Extract(const char* data, unsigned int offset, + unsigned int palette_size) { + std::vector palette(palette_size); + for (unsigned int i = 0; i < palette_size * 2; i += 2) { + uint16_t snes_color = (static_cast(data[offset + i + 1]) << 8) | + static_cast(data[offset + i]); + palette[i / 2] = ConvertSNEStoRGB(snes_color); } - return toret; + return palette; } -char* Convert(const snes_palette pal) { - char* toret = (char*)malloc(pal.size * 2); - for (unsigned int i = 0; i < pal.size; i++) { - unsigned short snes_data = ConvertRGBtoSNES(pal.colors[i]); - toret[i * 2] = snes_data & 0xFF; - toret[i * 2 + 1] = snes_data >> 8; +std::vector Convert(const std::vector& palette) { + std::vector data(palette.size() * 2); + for (unsigned int i = 0; i < palette.size(); i++) { + uint16_t snes_data = ConvertRGBtoSNES(palette[i]); + data[i * 2] = snes_data & 0xFF; + data[i * 2 + 1] = snes_data >> 8; } + return data; +} + +SNESColor ReadColorFromROM(int offset, const uchar* rom) { + short color = (ushort)((rom[offset + 1]) << 8) | rom[offset]; + snes_color new_color; + new_color.red = (color & 0x1F) * 8; + new_color.green = ((color >> 5) & 0x1F) * 8; + new_color.blue = ((color >> 10) & 0x1F) * 8; + SNESColor snes_color(new_color); + return snes_color; +} + +SNESColor GetCgxColor(uint16_t color) { + ImVec4 rgb; + rgb.x = (color & 0x1F) * 8; + rgb.y = ((color & 0x3E0) >> 5) * 8; + rgb.z = ((color & 0x7C00) >> 10) * 8; + SNESColor toret; + toret.SetRGB(rgb); return toret; } -// ============================================================================ +std::vector GetColFileData(uchar* data) { + std::vector colors; + colors.reserve(256); + colors.resize(256); -SNESColor::SNESColor() : rgb(ImVec4(0.f, 0.f, 0.f, 0.f)) {} + for (int i = 0; i < 512; i += 2) { + colors[i / 2] = GetCgxColor((uint16_t)((data[i + 1] << 8) + data[i])); + } -SNESColor::SNESColor(snes_color val) { - rgb.x = val.red; - rgb.y = val.green; - rgb.z = val.blue; -} - -SNESColor::SNESColor(ImVec4 val) : rgb(val) { - snes_color col; - col.red = (uchar)val.x; - col.blue = (uchar)val.y; - col.green = (uchar)val.z; - snes = ConvertRGBtoSNES(col); -} - -void SNESColor::setRgb(ImVec4 val) { - rgb = val; - snes_color col; - col.red = val.x; - col.blue = val.y; - col.green = val.z; - snes = ConvertRGBtoSNES(col); -} - -void SNESColor::setSNES(snes_color val) { - rgb = ImVec4(val.red, val.green, val.blue, 255.f); -} - -void SNESColor::setSNES(uint16_t val) { - snes = val; - snes_color col = ConvertSNEStoRGB(val); - rgb = ImVec4(col.red, col.green, col.blue, 0.f); + return colors; } // ============================================================================ @@ -110,10 +148,10 @@ SNESPalette::SNESPalette(char* data) : size_(sizeof(data) / 2) { assert((sizeof(data) % 4 == 0) && (sizeof(data) <= 32)); for (unsigned i = 0; i < sizeof(data); i += 2) { SNESColor col; - col.snes = static_cast(data[i + 1]) << 8; - col.snes = col.snes | static_cast(data[i]); - snes_color mColor = ConvertSNEStoRGB(col.snes); - col.rgb = ImVec4(mColor.red, mColor.green, mColor.blue, 1.f); + col.SetSNES(static_cast(data[i + 1]) << 8); + col.SetSNES(col.GetSNES() | static_cast(data[i])); + snes_color mColor = ConvertSNEStoRGB(col.GetSNES()); + col.SetRGB(ImVec4(mColor.red, mColor.green, mColor.blue, 1.f)); colors.push_back(col); } } @@ -123,10 +161,10 @@ SNESPalette::SNESPalette(const unsigned char* snes_pal) assert((sizeof(snes_pal) % 4 == 0) && (sizeof(snes_pal) <= 32)); for (unsigned i = 0; i < sizeof(snes_pal); i += 2) { SNESColor col; - col.snes = snes_pal[i + 1] << (uint16_t)8; - col.snes = col.snes | snes_pal[i]; - snes_color mColor = ConvertSNEStoRGB(col.snes); - col.rgb = ImVec4(mColor.red, mColor.green, mColor.blue, 1.f); + col.SetSNES(snes_pal[i + 1] << (uint16_t)8); + col.SetSNES(col.GetSNES() | snes_pal[i]); + snes_color mColor = ConvertSNEStoRGB(col.GetSNES()); + col.SetRGB(ImVec4(mColor.red, mColor.green, mColor.blue, 1.f)); colors.push_back(col); } } @@ -134,7 +172,7 @@ SNESPalette::SNESPalette(const unsigned char* snes_pal) SNESPalette::SNESPalette(const std::vector& cols) { for (const auto& each : cols) { SNESColor scol; - scol.setRgb(each); + scol.SetRGB(each); colors.push_back(scol); } size_ = cols.size(); @@ -143,7 +181,7 @@ SNESPalette::SNESPalette(const std::vector& cols) { SNESPalette::SNESPalette(const std::vector& cols) { for (const auto& each : cols) { SNESColor scol; - scol.setSNES(each); + scol.SetSNES(ConvertRGBtoSNES(each)); colors.push_back(scol); } size_ = cols.size(); @@ -156,32 +194,15 @@ SNESPalette::SNESPalette(const std::vector& cols) { size_ = cols.size(); } -void SNESPalette::Create(const std::vector& cols) { - for (const auto each : cols) { - colors.push_back(each); - } - size_ = cols.size(); -} - -char* SNESPalette::encode() { - auto data = new char[size_ * 2]; - for (unsigned int i = 0; i < size_; i++) { - std::cout << colors[i].snes << std::endl; - data[i * 2] = (char)(colors[i].snes & 0xFF); - data[i * 2 + 1] = (char)(colors[i].snes >> 8); - } - return data; -} - SDL_Palette* SNESPalette::GetSDL_Palette() { auto sdl_palette = std::make_shared(); sdl_palette->ncolors = size_; auto color = std::vector(size_); for (int i = 0; i < size_; i++) { - color[i].r = (uint8_t)colors[i].rgb.x * 100; - color[i].g = (uint8_t)colors[i].rgb.y * 100; - color[i].b = (uint8_t)colors[i].rgb.z * 100; + color[i].r = (uint8_t)colors[i].GetRGB().x * 100; + color[i].g = (uint8_t)colors[i].GetRGB().y * 100; + color[i].b = (uint8_t)colors[i].GetRGB().z * 100; color[i].a = 0; std::cout << "Color " << i << " added (R:" << color[i].r << " G:" << color[i].g << " B:" << color[i].b << ")" << std::endl; @@ -190,7 +211,87 @@ SDL_Palette* SNESPalette::GetSDL_Palette() { return sdl_palette.get(); } -PaletteGroup::PaletteGroup(uint8_t mSize) : size(mSize) {} +SNESPalette ReadPaletteFromROM(int offset, int num_colors, const uchar* rom) { + int color_offset = 0; + std::vector colors(num_colors); + + while (color_offset < num_colors) { + short color = (ushort)((rom[offset + 1]) << 8) | rom[offset]; + gfx::snes_color new_color; + new_color.red = (color & 0x1F) * 8; + new_color.green = ((color >> 5) & 0x1F) * 8; + new_color.blue = ((color >> 10) & 0x1F) * 8; + colors[color_offset].SetSNES(ConvertRGBtoSNES(new_color)); + if (color_offset == 0) { + colors[color_offset].SetTransparent(true); + } + color_offset++; + offset += 2; + } + + gfx::SNESPalette palette(colors); + return palette; +} + +uint32_t GetPaletteAddress(const std::string& group_name, size_t palette_index, + size_t color_index) { + // Retrieve the base address for the palette group + uint32_t base_address = paletteGroupAddresses.at(group_name); + + // Retrieve the number of colors for each palette in the group + uint32_t colors_per_palette = paletteGroupColorCounts.at(group_name); + + // Calculate the address for thes specified color in the ROM + uint32_t address = base_address + (palette_index * colors_per_palette * 2) + + (color_index * 2); + + return address; +} + +std::array ToFloatArray(const SNESColor& color) { + std::array colorArray; + colorArray[0] = color.GetRGB().x / 255.0f; + colorArray[1] = color.GetRGB().y / 255.0f; + colorArray[2] = color.GetRGB().z / 255.0f; + colorArray[3] = color.GetRGB().w; + return colorArray; +} + +PaletteGroup::PaletteGroup(uint8_t mSize) : size_(mSize) {} + +PaletteGroup CreatePaletteGroupFromColFile( + std::vector& palette_rows) { + PaletteGroup toret; + + for (int i = 0; i < palette_rows.size(); i += 8) { + SNESPalette palette; + for (int j = 0; j < 8; j++) { + palette.AddColor(palette_rows[i + j].GetRomRGB()); + } + toret.AddPalette(palette); + } + return toret; +} + +// Take a SNESPalette with N many colors and divide it into palettes of 8 colors +// each +PaletteGroup CreatePaletteGroupFromLargePalette(SNESPalette& palette) { + PaletteGroup toret; + + std::cout << "Palette size is " << palette.size() << std::endl; + + for (int i = 0; i < palette.size(); i += 8) { + SNESPalette new_palette; + if (i + 8 < palette.size()) { + for (int j = 0; j < 8; j++) { + new_palette.AddColor(palette[i + j]); + } + } + + toret.AddPalette(new_palette); + } + return toret; +} } // namespace gfx } // namespace app diff --git a/src/app/gfx/snes_palette.h b/src/app/gfx/snes_palette.h index 1c0e4143..3affce6b 100644 --- a/src/app/gfx/snes_palette.h +++ b/src/app/gfx/snes_palette.h @@ -11,6 +11,8 @@ #include #include +#include "absl/base/casts.h" +#include "absl/status/status.h" #include "app/core/constants.h" namespace yaze { @@ -18,47 +20,111 @@ namespace app { namespace gfx { struct snes_color { - uchar red; - uchar blue; - uchar green; + uint16_t red; /**< Red component of the color. */ + uint16_t blue; /**< Blue component of the color. */ + uint16_t green; /**< Green component of the color. */ }; using snes_color = struct snes_color; struct snes_palette { - uint id; - uint size; - snes_color* colors; + uint id; /**< ID of the palette. */ + uint size; /**< Size of the palette. */ + snes_color* colors; /**< Pointer to the colors in the palette. */ }; using snes_palette = struct snes_palette; -ushort ConvertRGBtoSNES(const snes_color color); -snes_color ConvertSNEStoRGB(const ushort snes_color); -snes_palette* Extract(const char* data, const unsigned int offset, - const unsigned int palette_size); -char* Convert(const snes_palette pal); +uint16_t ConvertRGBtoSNES(const snes_color& color); +uint16_t ConvertRGBtoSNES(const ImVec4& color); +snes_color ConvertSNEStoRGB(uint16_t snes_color); + +/** + * @brief Extracts a vector of SNES colors from a data buffer. + * + * @param data The data buffer to extract from. + * @param offset The offset in the buffer to start extracting from. + * @param palette_size The size of the palette to extract. + * @return A vector of SNES colors extracted from the buffer. + */ +std::vector Extract(const char* data, unsigned int offset, + unsigned int palette_size); + +/** + * @brief Converts a vector of SNES colors to a vector of characters. + * + * @param palette The vector of SNES colors to convert. + * @return A vector of characters representing the converted SNES colors. + */ +std::vector Convert(const std::vector& palette); struct SNESColor { - SNESColor(); - explicit SNESColor(ImVec4); - explicit SNESColor(snes_color); + SNESColor() : rgb(0.f, 0.f, 0.f, 0.f), snes(0) {} - void setRgb(ImVec4); - void setSNES(snes_color); - void setSNES(uint16_t); - void setTransparent(bool t) { transparent = t; } - - auto RGB() { - return ImVec4(rgb.x / 255, rgb.y / 255, rgb.z / 255, rgb.w); + explicit SNESColor(const ImVec4 val) : rgb(val) { + snes_color color; + color.red = val.x / 255; + color.green = val.y / 255; + color.blue = val.z / 255; + snes = ConvertRGBtoSNES(color); } - bool transparent = false; - uint16_t snes = 0; + explicit SNESColor(const snes_color val) + : rgb(val.red, val.green, val.blue, 255.f), + snes(ConvertRGBtoSNES(val)), + rom_color(val) {} + + ImVec4 GetRGB() const { return rgb; } + void SetRGB(const ImVec4 val) { + rgb.x = val.x / 255; + rgb.y = val.y / 255; + rgb.z = val.z / 255; + snes_color color; + color.red = val.x; + color.green = val.y; + color.blue = val.z; + rom_color = color; + snes = ConvertRGBtoSNES(color); + modified = true; + } + + snes_color GetRomRGB() const { return rom_color; } + + uint16_t GetSNES() const { return snes; } + void SetSNES(uint16_t val) { + snes = val; + snes_color col = ConvertSNEStoRGB(val); + rgb = ImVec4(col.red, col.green, col.blue, 0.f); + modified = true; + } + + bool IsModified() const { return modified; } + bool IsTransparent() const { return transparent; } + void SetTransparent(bool t) { transparent = t; } + void SetModified(bool m) { modified = m; } + + private: ImVec4 rgb; + uint16_t snes; + snes_color rom_color; + bool modified = false; + bool transparent = false; }; +gfx::SNESColor ReadColorFromROM(int offset, const uchar* rom); + +SNESColor GetCgxColor(uint16_t color); +std::vector GetColFileData(uchar* data); + class SNESPalette { public: + template + explicit SNESPalette(const std::vector& data) { + for (const auto& item : data) { + colors.push_back(SNESColor(item)); + } + } + SNESPalette() = default; + explicit SNESPalette(uint8_t mSize); explicit SNESPalette(char* snesPal); explicit SNESPalette(const unsigned char* snes_pal); @@ -66,50 +132,146 @@ class SNESPalette { explicit SNESPalette(const std::vector&); explicit SNESPalette(const std::vector&); - void Create(const std::vector&); - void AddColor(SNESColor color) { colors.push_back(color); } - auto GetColor(int i) const { return colors[i]; } + SDL_Palette* GetSDL_Palette(); - SNESColor operator[](int i) { + void Create(const std::vector& cols) { + for (const auto& each : cols) { + colors.push_back(each); + } + size_ = cols.size(); + } + + void AddColor(SNESColor color) { + colors.push_back(color); + size_++; + } + + void AddColor(snes_color color) { + colors.emplace_back(color); + size_++; + } + + auto GetColor(int i) const { if (i > size_) { - std::cout << "SNESPalette: Index out of bounds" << std::endl; - return colors[0]; + throw std::out_of_range("SNESPalette: Index out of bounds"); } return colors[i]; } - char* encode(); - SDL_Palette* GetSDL_Palette(); + void Clear() { + colors.clear(); + size_ = 0; + } - int size_ = 0; - std::vector colors; + auto size() const { return colors.size(); } + + SNESColor& operator[](int i) { + if (i > size_) { + throw std::out_of_range("SNESPalette: Index out of bounds"); + } + return colors[i]; + } + + void operator()(int i, const SNESColor& color) { + if (i >= size_) { + throw std::out_of_range("SNESPalette: Index out of bounds"); + } + colors[i] = color; + } + + void operator()(int i, const ImVec4& color) { + if (i >= size_) { + throw std::out_of_range("SNESPalette: Index out of bounds"); + } + colors[i].SetRGB(color); + colors[i].SetModified(true); + } + + SNESPalette sub_palette(int start, int end) const { + SNESPalette pal; + for (int i = start; i < end; i++) { + pal.AddColor(colors[i]); + } + return pal; + } + + private: + int size_ = 0; /**< The size of the palette. */ + std::vector colors; /**< The colors in the palette. */ }; +SNESPalette ReadPaletteFromROM(int offset, int num_colors, const uchar* rom); +uint32_t GetPaletteAddress(const std::string& group_name, size_t palette_index, + size_t color_index); +std::array ToFloatArray(const SNESColor& color); + struct PaletteGroup { PaletteGroup() = default; + explicit PaletteGroup(uint8_t mSize); - void AddPalette(SNESPalette pal) { + + absl::Status AddPalette(SNESPalette pal) { palettes.emplace_back(pal); - size = palettes.size(); + size_ = palettes.size(); + return absl::OkStatus(); } - void AddColor(SNESColor color) { - if (size == 0) { - SNESPalette empty_pal; - palettes.emplace_back(empty_pal); + + absl::Status AddColor(SNESColor color) { + if (size_ == 0) { + palettes.emplace_back(); } palettes[0].AddColor(color); + return absl::OkStatus(); } + + void Clear() { + palettes.clear(); + size_ = 0; + } + + auto size() const { return palettes.size(); } + SNESPalette operator[](int i) { - if (i > size) { + if (i > size_) { std::cout << "PaletteGroup: Index out of bounds" << std::endl; return palettes[0]; } return palettes[i]; } - int size = 0; + + const SNESPalette& operator[](int i) const { + if (i > size_) { + std::cout << "PaletteGroup: Index out of bounds" << std::endl; + return palettes[0]; + } + return palettes[i]; + } + + absl::Status operator()(int i, const SNESColor& color) { + if (i >= size_) { + return absl::InvalidArgumentError("PaletteGroup: Index out of bounds"); + } + palettes[i](0, color); + return absl::OkStatus(); + } + + absl::Status operator()(int i, const ImVec4& color) { + if (i >= size_) { + return absl::InvalidArgumentError("PaletteGroup: Index out of bounds"); + } + palettes[i](0, color); + return absl::OkStatus(); + } + + private: + int size_ = 0; std::vector palettes; }; +PaletteGroup CreatePaletteGroupFromColFile(std::vector& colors); + +PaletteGroup CreatePaletteGroupFromLargePalette(SNESPalette& palette); + } // namespace gfx } // namespace app } // namespace yaze diff --git a/src/app/gfx/snes_tile.cc b/src/app/gfx/snes_tile.cc index 3e02f34b..6be31b4b 100644 --- a/src/app/gfx/snes_tile.cc +++ b/src/app/gfx/snes_tile.cc @@ -9,6 +9,327 @@ namespace yaze { namespace app { namespace gfx { +tile8 UnpackBppTile(const Bytes& data, const uint32_t offset, + const uint32_t bpp) { + tile8 tile; + assert(bpp >= 1 && bpp <= 8); + unsigned int bpp_pos[8]; // More for conveniance and readibility + for (int col = 0; col < 8; col++) { + for (int row = 0; row < 8; row++) { + if (bpp == 1) { + tile.data[col * 8 + row] = (data[offset + col] >> (7 - row)) & 0x01; + continue; + } + /* SNES bpp format interlace each byte of the first 2 bitplanes. + * | byte 1 of first bitplane | byte 1 of second bitplane | + * | byte 2 of first bitplane | byte 2 of second bitplane | .. + */ + bpp_pos[0] = offset + col * 2; + bpp_pos[1] = offset + col * 2 + 1; + char mask = 1 << (7 - row); + tile.data[col * 8 + row] = (data[bpp_pos[0]] & mask) == mask; + tile.data[col * 8 + row] |= (uchar)((data[bpp_pos[1]] & mask) == mask) + << 1; + if (bpp == 3) { + // When we have 3 bitplanes, the bytes for the third bitplane are after + // the 16 bytes of the 2 bitplanes. + bpp_pos[2] = offset + 16 + col; + tile.data[col * 8 + row] |= (uchar)((data[bpp_pos[2]] & mask) == mask) + << 2; + } + if (bpp >= 4) { + // For 4 bitplanes, the 2 added bitplanes are interlaced like the first + // two. + bpp_pos[2] = offset + 16 + col * 2; + bpp_pos[3] = offset + 16 + col * 2 + 1; + tile.data[col * 8 + row] |= (uchar)((data[bpp_pos[2]] & mask) == mask) + << 2; + tile.data[col * 8 + row] |= (uchar)((data[bpp_pos[3]] & mask) == mask) + << 3; + } + if (bpp == 8) { + bpp_pos[4] = offset + 32 + col * 2; + bpp_pos[5] = offset + 32 + col * 2 + 1; + bpp_pos[6] = offset + 48 + col * 2; + bpp_pos[7] = offset + 48 + col * 2 + 1; + tile.data[col * 8 + row] |= (uchar)((data[bpp_pos[4]] & mask) == mask) + << 4; + tile.data[col * 8 + row] |= (uchar)((data[bpp_pos[5]] & mask) == mask) + << 5; + tile.data[col * 8 + row] |= (uchar)((data[bpp_pos[6]] & mask) == mask) + << 6; + tile.data[col * 8 + row] |= (uchar)((data[bpp_pos[7]] & mask) == mask) + << 7; + } + } + } + return tile; +} + +Bytes PackBppTile(const tile8& tile, const uint32_t bpp) { + // Allocate memory for output data + std::vector output(bpp * 8, 0); // initialized with 0 + unsigned maxcolor = 2 << bpp; + + // Iterate over all columns and rows of the tile + for (unsigned int col = 0; col < 8; col++) { + for (unsigned int row = 0; row < 8; row++) { + uchar color = tile.data[col * 8 + row]; + if (color > maxcolor) { + throw std::invalid_argument("Invalid color value."); + } + + // 1bpp format + if (bpp == 1) output[col] += (uchar)((color & 1) << (7 - row)); + + // 2bpp format + if (bpp >= 2) { + output[col * 2] += (uchar)((color & 1) << (7 - row)); + output[col * 2 + 1] += (uchar)((uchar)((color & 2) == 2) << (7 - row)); + } + + // 3bpp format + if (bpp == 3) + output[16 + col] += (uchar)(((color & 4) == 4) << (7 - row)); + + // 4bpp format + if (bpp >= 4) { + output[16 + col * 2] += (uchar)(((color & 4) == 4) << (7 - row)); + output[16 + col * 2 + 1] += (uchar)(((color & 8) == 8) << (7 - row)); + } + + // 8bpp format + if (bpp == 8) { + output[32 + col * 2] += (uchar)(((color & 16) == 16) << (7 - row)); + output[32 + col * 2 + 1] += (uchar)(((color & 32) == 32) << (7 - row)); + output[48 + col * 2] += (uchar)(((color & 64) == 64) << (7 - row)); + output[48 + col * 2 + 1] += + (uchar)(((color & 128) == 128) << (7 - row)); + } + } + } + return output; +} + +std::vector ConvertBpp(const std::vector& tiles, + uint32_t from_bpp, uint32_t to_bpp) { + unsigned int nb_tile = tiles.size() / (from_bpp * 8); + std::vector converted(nb_tile * to_bpp * 8); + + for (unsigned int i = 0; i < nb_tile; i++) { + tile8 tile = UnpackBppTile(tiles, i * from_bpp * 8, from_bpp); + std::vector packed_tile = PackBppTile(tile, to_bpp); + std::memcpy(converted.data() + i * to_bpp * 8, packed_tile.data(), + to_bpp * 8); + } + return converted; +} + +std::vector Convert3bppTo4bpp(const std::vector& tiles) { + return ConvertBpp(tiles, 3, 4); +} + +std::vector Convert4bppTo3bpp(const std::vector& tiles) { + return ConvertBpp(tiles, 4, 3); +} + +Bytes SnesTo8bppSheet(Bytes sheet, int bpp) { + int xx = 0; // positions where we are at on the sheet + int yy = 0; + int pos = 0; + int ypos = 0; + int num_tiles = 64; + int buffer_size = 0x1000; + if (bpp == 2) { + bpp = 16; + num_tiles = 128; + buffer_size = 0x2000; + } else if (bpp == 3) { + bpp = 24; + } + Bytes sheet_buffer_out(buffer_size); + + for (int i = 0; i < num_tiles; i++) { // for each tiles, 16 per line + for (int y = 0; y < 8; y++) { // for each line + for (int x = 0; x < 8; x++) { //[0] + [1] + [16] + auto b1 = (sheet[(y * 2) + (bpp * pos)] & (kGraphicsBitmap[x])); + auto b2 = (sheet[((y * 2) + (bpp * pos)) + 1] & (kGraphicsBitmap[x])); + auto b3 = (sheet[(16 + y) + (bpp * pos)] & (kGraphicsBitmap[x])); + unsigned char b = 0; + if (b1 != 0) { + b |= 1; + } + if (b2 != 0) { + b |= 2; + } + if (b3 != 0 && bpp != 16) { + b |= 4; + } + sheet_buffer_out[x + xx + (y * 128) + (yy * 1024)] = b; + } + } + pos++; + ypos++; + xx += 8; + if (ypos >= 16) { + yy++; + xx = 0; + ypos = 0; + } + } + return sheet_buffer_out; +} + +Bytes Bpp8SnesToIndexed(Bytes data, uint64_t bpp) { + // 3BPP + // [r0,bp1],[r0,bp2],[r1,bp1],[r1,bp2],[r2,bp1],[r2,bp2],[r3,bp1],[r3,bp2] + // [r4,bp1],[r4,bp2],[r5,bp1],[r5,bp2],[r6,bp1],[r6,bp2],[r7,bp1],[r7,bp2] + // [r0,bp3],[r0,bp4],[r1,bp3],[r1,bp4],[r2,bp3],[r2,bp4],[r3,bp3],[r3,bp4] + // [r4,bp3],[r4,bp4],[r5,bp3],[r5,bp4],[r6,bp3],[r6,bp4],[r7,bp3],[r7,bp4] + // [r0,bp5],[r0,bp6],[r1,bp5],[r1,bp6],[r2,bp5],[r2,bp6],[r3,bp5],[r3,bp6] + // [r4,bp5],[r4,bp6],[r5,bp5],[r5,bp6],[r6,bp5],[r6,bp6],[r7,bp5],[r7,bp6] + // [r0,bp7],[r0,bp8],[r1,bp7],[r1,bp8],[r2,bp7],[r2,bp8],[r3,bp7],[r3,bp8] + // [r4,bp7],[r4,bp8],[r5,bp7],[r5,bp8],[r6,bp7],[r6,bp8],[r7,bp7],[r7,bp8] + + // 16 tiles = 1024 bytes + auto buffer = Bytes(data.size()); + std::vector> bitmap_data; + bitmap_data.resize(0x80); + for (auto& each : bitmap_data) { + each.reserve(0x800); + } + int yy = 0; + int xx = 0; + int pos = 0; + + const uint16_t sheet_width = 128; + + // 64 = 4096 bytes + // 16 = 1024? + int ypos = 0; + // for each tiles //16 per lines + for (int i = 0; i < 4096; i++) { + // for each lines + for (int y = 0; y < 8; y++) { + //[0] + [1] + [16] + for (int x = 0; x < 8; x++) { + const uint16_t bitmask[] = {0x80, 0x40, 0x20, 0x10, + 0x08, 0x04, 0x02, 0x01}; + auto b1 = (data[(y * 2) + ((bpp * 8) * pos)] & (bitmask[x])); + auto b2 = (data[((y * 2) + ((bpp * 8) * pos)) + 1] & (bitmask[x])); + auto b3 = (data[(y * 2) + ((bpp * 8) * pos) + 16] & (bitmask[x])); + auto b4 = (data[(y * 2) + ((bpp * 8) * pos) + 17] & (bitmask[x])); + auto b5 = (data[(y * 2) + ((bpp * 8) * pos) + 32] & (bitmask[x])); + auto b6 = (data[(y * 2) + ((bpp * 8) * pos) + 33] & (bitmask[x])); + auto b7 = (data[(y * 2) + ((bpp * 8) * pos) + 48] & (bitmask[x])); + auto b8 = (data[(y * 2) + ((bpp * 8) * pos) + 49] & (bitmask[x])); + + auto b = 0; + if (b1 != 0) { + b |= 1; + } + if (b2 != 0) { + b |= 2; + } + if (bpp >= 4) { + if (b3 != 0) { + b |= 4; + } + if (b4 != 0) { + b |= 8; + } + } + if (bpp >= 8) { + if (b5 != 0) { + b |= 0x10; + } + if (b6 != 0) { + b |= 0x20; + } + if (b7 != 0) { + b |= 0x40; + } + if (b8 != 0) { + b |= 0x80; + } + } + // bitmap_data[((x + xx) * sheet_width) + y + (yy * 8)] = b; + bitmap_data[x + xx][y + (yy * 8)] = b; + } + } + pos++; + ypos++; + xx += 8; + if (ypos >= 16) { + yy++; + xx = 0; + ypos = 0; + } + } + + int n = 0; + + for (int y = 0; y < (data.size() / 64); y++) { + for (int x = 0; x < sheet_width; x++) { // 128 assumption + if (n < data.size()) { + // buffer[n] = bitmap_data[(x * sheet_width) + y]; + buffer[n] = bitmap_data[x][y]; + n++; + } + } + } + return buffer; +} + +uint16_t TileInfoToWord(TileInfo tile_info) { + uint16_t result = 0; + + // Copy the id_ value + result |= tile_info.id_ & 0x3FF; // ids are 10 bits + + // Set the vertical_mirror_, horizontal_mirror_, and over_ flags + result |= (tile_info.vertical_mirror_ ? 1 : 0) << 15; + result |= (tile_info.horizontal_mirror_ ? 1 : 0) << 14; + result |= (tile_info.over_ ? 1 : 0) << 13; + + // Set the palette_ + result |= (tile_info.palette_ & 0x07) << 10; // palettes are 3 bits + + return result; +} + +TileInfo WordToTileInfo(uint16_t word) { + // Extract the id_ value + uint16_t id = word & 0x3FF; // ids are 10 bits + + // Extract the vertical_mirror_, horizontal_mirror_, and over_ flags + bool vertical_mirror = (word >> 15) & 0x01; + bool horizontal_mirror = (word >> 14) & 0x01; + bool over = (word >> 13) & 0x01; + + // Extract the palette_ + uint8_t palette = (word >> 10) & 0x07; // palettes are 3 bits + + return TileInfo(id, palette, vertical_mirror, horizontal_mirror, over); +} + +ushort TileInfoToShort(TileInfo tile_info) { + ushort result = 0; + + // Copy the id_ value + result |= tile_info.id_ & 0x3FF; // ids are 10 bits + + // Set the vertical_mirror_, horizontal_mirror_, and over_ flags + result |= (tile_info.vertical_mirror_ ? 1 : 0) << 10; + result |= (tile_info.horizontal_mirror_ ? 1 : 0) << 11; + result |= (tile_info.over_ ? 1 : 0) << 12; + + // Set the palette_ + result |= (tile_info.palette_ & 0x07) << 13; // palettes are 3 bits + + return result; +} + TileInfo GetTilesInfo(ushort tile) { // vhopppcc cccccccc bool o = false; diff --git a/src/app/gfx/snes_tile.h b/src/app/gfx/snes_tile.h index 72764333..a117a785 100644 --- a/src/app/gfx/snes_tile.h +++ b/src/app/gfx/snes_tile.h @@ -2,6 +2,7 @@ #define YAZE_APP_GFX_SNES_TILE_H #include +#include #include #include "app/core/constants.h" @@ -10,13 +11,30 @@ namespace yaze { namespace app { namespace gfx { +constexpr uchar kGraphicsBitmap[8] = {0x80, 0x40, 0x20, 0x10, + 0x08, 0x04, 0x02, 0x01}; + +Bytes SnesTo8bppSheet(Bytes sheet, int bpp); +Bytes Bpp8SnesToIndexed(Bytes data, uint64_t bpp = 0); + struct tile8 { - unsigned int id; + uint32_t id; char data[64]; - unsigned int palette_id; + uint32_t palette_id; }; using tile8 = struct tile8; +tile8 UnpackBppTile(const Bytes& data, const uint32_t offset, + const uint32_t bpp); + +Bytes PackBppTile(const tile8& tile, const uint32_t bpp); + +std::vector ConvertBpp(const std::vector& tiles, + uint32_t from_bpp, uint32_t to_bpp); + +std::vector Convert3bppTo4bpp(const std::vector& tiles); +std::vector Convert4bppTo3bpp(const std::vector& tiles); + // vhopppcc cccccccc // [0, 1] // [2, 3] @@ -34,19 +52,66 @@ class TileInfo { vertical_mirror_(v), horizontal_mirror_(h), palette_(palette) {} + + bool operator==(const TileInfo& other) const { + return id_ == other.id_ && over_ == other.over_ && + vertical_mirror_ == other.vertical_mirror_ && + horizontal_mirror_ == other.horizontal_mirror_ && + palette_ == other.palette_; + } }; +uint16_t TileInfoToWord(TileInfo tile_info); +TileInfo WordToTileInfo(uint16_t word); +ushort TileInfoToShort(TileInfo tile_info); + TileInfo GetTilesInfo(ushort tile); class Tile32 { public: - ushort tile0_; - ushort tile1_; - ushort tile2_; - ushort tile3_; + uint16_t tile0_; + uint16_t tile1_; + uint16_t tile2_; + uint16_t tile3_; - Tile32(ushort t0, ushort t1, ushort t2, ushort t3) + // Default constructor + Tile32() : tile0_(0), tile1_(0), tile2_(0), tile3_(0) {} + + // Parameterized constructor + Tile32(uint16_t t0, uint16_t t1, uint16_t t2, uint16_t t3) : tile0_(t0), tile1_(t1), tile2_(t2), tile3_(t3) {} + + // Copy constructor + Tile32(const Tile32& other) + : tile0_(other.tile0_), + tile1_(other.tile1_), + tile2_(other.tile2_), + tile3_(other.tile3_) {} + + // Constructor from packed value + Tile32(uint64_t packedVal) { + tile0_ = (packedVal >> 48) & 0xFFFF; + tile1_ = (packedVal >> 32) & 0xFFFF; + tile2_ = (packedVal >> 16) & 0xFFFF; + tile3_ = packedVal & 0xFFFF; + } + + // Equality operator + bool operator==(const Tile32& other) const { + return tile0_ == other.tile0_ && tile1_ == other.tile1_ && + tile2_ == other.tile2_ && tile3_ == other.tile3_; + } + + // Inequality operator + bool operator!=(const Tile32& other) const { return !(*this == other); } + + // Get packed uint64_t representation + uint64_t GetPackedValue() const { + return (static_cast(tile0_) << 48) | + (static_cast(tile1_) << 32) | + (static_cast(tile2_) << 16) | + static_cast(tile3_); + } }; class Tile16 { @@ -65,6 +130,13 @@ class Tile16 { tiles_info.push_back(tile2_); tiles_info.push_back(tile3_); } + + bool operator==(const Tile16& other) const { + return tile0_ == other.tile0_ && tile1_ == other.tile1_ && + tile2_ == other.tile2_ && tile3_ == other.tile3_; + } + + bool operator!=(const Tile16& other) const { return !(*this == other); } }; class OAMTile { diff --git a/src/app/gui/canvas.cc b/src/app/gui/canvas.cc new file mode 100644 index 00000000..d1658abc --- /dev/null +++ b/src/app/gui/canvas.cc @@ -0,0 +1,487 @@ +#include "canvas.h" + +#include + +#include +#include + +#include "app/gfx/bitmap.h" +#include "app/rom.h" + +namespace yaze { +namespace app { +namespace gui { + +constexpr uint32_t kRectangleColor = IM_COL32(32, 32, 32, 255); +constexpr uint32_t kRectangleBorder = IM_COL32(255, 255, 255, 255); +constexpr ImGuiButtonFlags kMouseFlags = + ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight; + +void Canvas::Update(const gfx::Bitmap &bitmap, ImVec2 bg_size, int tile_size, + float scale, float grid_size) { + if (scale != 1.0f) { + bg_size.x *= scale / 2; + bg_size.y *= scale / 2; + } + DrawBackground(bg_size); + DrawContextMenu(); + DrawTileSelector(tile_size); + DrawBitmap(bitmap, 2, scale); + DrawGrid(grid_size); + DrawOverlay(); +} + +void Canvas::UpdateColorPainter(const gfx::Bitmap &bitmap, const ImVec4 &color, + const std::function &event, + ImVec2 bg_size, int tile_size, float scale, + float grid_size) { + global_scale_ = scale; + DrawBackground(bg_size); + DrawContextMenu(); + DrawBitmap(bitmap, 2, scale); + if (DrawSolidTilePainter(color, tile_size)) { + event(); + } + DrawGrid(grid_size); + DrawOverlay(); +} + +void Canvas::UpdateEvent(const std::function &event, ImVec2 bg_size, + int tile_size, float scale, float grid_size) { + DrawBackground(bg_size); + DrawContextMenu(); + event(); + DrawGrid(grid_size); + DrawOverlay(); +} + +void Canvas::DrawBackground(ImVec2 canvas_size) { + canvas_p0_ = ImGui::GetCursorScreenPos(); + if (!custom_canvas_size_) canvas_sz_ = ImGui::GetContentRegionAvail(); + if (canvas_size.x != 0) canvas_sz_ = canvas_size; + canvas_p1_ = ImVec2(canvas_p0_.x + (canvas_sz_.x * global_scale_), + canvas_p0_.y + (canvas_sz_.y * global_scale_)); + draw_list_ = ImGui::GetWindowDrawList(); // Draw border and background color + draw_list_->AddRectFilled(canvas_p0_, canvas_p1_, kRectangleColor); + draw_list_->AddRect(canvas_p0_, canvas_p1_, kRectangleBorder); +} + +void Canvas::DrawContextMenu() { + const ImGuiIO &io = ImGui::GetIO(); + auto scaled_sz = + ImVec2(canvas_sz_.x * global_scale_, canvas_sz_.y * global_scale_); + ImGui::InvisibleButton("canvas", scaled_sz, kMouseFlags); + const bool is_active = ImGui::IsItemActive(); // Held + const ImVec2 origin(canvas_p0_.x + scrolling_.x, + canvas_p0_.y + scrolling_.y); // Lock scrolled origin + const ImVec2 mouse_pos(io.MousePos.x - origin.x, io.MousePos.y - origin.y); + + // Pan (we use a zero mouse threshold when there's no context menu) + if (const float mouse_threshold_for_pan = enable_context_menu_ ? -1.0f : 0.0f; + is_active && + ImGui::IsMouseDragging(ImGuiMouseButton_Right, mouse_threshold_for_pan)) { + scrolling_.x += io.MouseDelta.x; + scrolling_.y += io.MouseDelta.y; + } + + // Context menu (under default mouse threshold) + if (ImVec2 drag_delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Right); + enable_context_menu_ && drag_delta.x == 0.0f && drag_delta.y == 0.0f) + ImGui::OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + + // Contents of the Context Menu + if (ImGui::BeginPopup("context")) { + ImGui::MenuItem("Show Grid", nullptr, &enable_grid_); + ImGui::Selectable("Show Labels", &enable_hex_tile_labels_); + if (ImGui::MenuItem("Reset Position", nullptr, false)) { + scrolling_.x = 0; + scrolling_.y = 0; + } + ImGui::Separator(); + if (ImGui::MenuItem("8x8", nullptr, custom_step_ == 8.0f)) { + custom_step_ = 8.0f; + } + if (ImGui::MenuItem("16x16", nullptr, custom_step_ == 16.0f)) { + custom_step_ = 16.0f; + } + if (ImGui::MenuItem("32x32", nullptr, custom_step_ == 32.0f)) { + custom_step_ = 32.0f; + } + if (ImGui::MenuItem("64x64", nullptr, custom_step_ == 64.0f)) { + custom_step_ = 64.0f; + } + // Display bitmap metadata such as canvas size and global scale + ImGui::Separator(); + ImGui::Text("Canvas Size: %.0f x %.0f", canvas_sz_.x, canvas_sz_.y); + ImGui::Text("Global Scale: %.1f", global_scale_); + ImGui::Text("Mouse Position: %.0f x %.0f", mouse_pos.x, mouse_pos.y); + + ImGui::EndPopup(); + } +} + +bool Canvas::DrawTilePainter(const Bitmap &bitmap, int size, float scale) { + const ImGuiIO &io = ImGui::GetIO(); + const bool is_hovered = ImGui::IsItemHovered(); + is_hovered_ = is_hovered; + // Lock scrolled origin + const ImVec2 origin(canvas_p0_.x + scrolling_.x, canvas_p0_.y + scrolling_.y); + const ImVec2 mouse_pos(io.MousePos.x - origin.x, io.MousePos.y - origin.y); + + if (is_hovered) { + // Reset the previous tile hover + if (!points_.empty()) { + points_.clear(); + } + + // Calculate the coordinates of the mouse + ImVec2 painter_pos; + painter_pos.x = std::floor((double)mouse_pos.x / size) * size; + painter_pos.y = std::floor((double)mouse_pos.y / size) * size; + + auto painter_pos_end = ImVec2(painter_pos.x + size, painter_pos.y + size); + points_.push_back(painter_pos); + points_.push_back(painter_pos_end); + + if (bitmap.IsActive()) { + draw_list_->AddImage( + (void *)bitmap.texture(), + ImVec2(origin.x + painter_pos.x, origin.y + painter_pos.y), + ImVec2(origin.x + painter_pos.x + bitmap.width() * scale, + origin.y + painter_pos.y + bitmap.height() * scale)); + } + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + // Draw the currently selected tile on the overworld here + // Save the coordinates of the selected tile. + drawn_tile_pos_ = io.MousePos; + SDL_Log("Drawn tile position: %.0f, %.0f", drawn_tile_pos_.x, + drawn_tile_pos_.y); + return true; + } + + } else { + // Erase the hover when the mouse is not in the canvas window. + points_.clear(); + } + return false; +} + +bool Canvas::DrawSolidTilePainter(const ImVec4 &color, int tile_size) { + const ImGuiIO &io = ImGui::GetIO(); + const bool is_hovered = ImGui::IsItemHovered(); + is_hovered_ = is_hovered; + // Lock scrolled origin + const ImVec2 origin(canvas_p0_.x + scrolling_.x, canvas_p0_.y + scrolling_.y); + const ImVec2 mouse_pos(io.MousePos.x - origin.x, io.MousePos.y - origin.y); + auto scaled_tile_size = tile_size * global_scale_; + + static bool is_dragging = false; + static ImVec2 start_drag_pos; + + if (is_hovered) { + // Reset the previous tile hover + if (!points_.empty()) { + points_.clear(); + } + + // Calculate the coordinates of the mouse + ImVec2 painter_pos; + painter_pos.x = + std::floor((double)mouse_pos.x / scaled_tile_size) * scaled_tile_size; + painter_pos.y = + std::floor((double)mouse_pos.y / scaled_tile_size) * scaled_tile_size; + + // Clamp the size to a grid + painter_pos.x = + std::clamp(painter_pos.x, 0.0f, canvas_sz_.x * global_scale_); + painter_pos.y = + std::clamp(painter_pos.y, 0.0f, canvas_sz_.y * global_scale_); + + auto painter_pos_end = ImVec2(painter_pos.x + scaled_tile_size, + painter_pos.y + scaled_tile_size); + points_.push_back(painter_pos); + points_.push_back(painter_pos_end); + + draw_list_->AddRectFilled( + ImVec2(origin.x + painter_pos.x + 1, origin.y + painter_pos.y + 1), + ImVec2(origin.x + painter_pos.x + scaled_tile_size, + origin.y + painter_pos.y + scaled_tile_size), + IM_COL32(color.x * 255, color.y * 255, color.z * 255, 255)); + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + is_dragging = true; + start_drag_pos = painter_pos; + } + + if (is_dragging && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + is_dragging = false; + drawn_tile_pos_ = start_drag_pos; + return true; + } + + } else { + // Erase the hover when the mouse is not in the canvas window. + points_.clear(); + } + return false; +} + +void Canvas::DrawTileOnBitmap(int tile_size, gfx::Bitmap &bitmap, + ImVec4 color) { + const ImVec2 position = drawn_tile_pos_; + int tile_index_x = static_cast(position.x / global_scale_) / tile_size; + int tile_index_y = static_cast(position.y / global_scale_) / tile_size; + + ImVec2 start_position(tile_index_x * tile_size, tile_index_y * tile_size); + + // Update the bitmap's pixel data based on the start_position and color + for (int y = 0; y < tile_size; ++y) { + for (int x = 0; x < tile_size; ++x) { + // Calculate the actual pixel index in the bitmap + int pixel_index = + (start_position.y + y) * bitmap.width() + (start_position.x + x); + + // Write the color to the pixel + bitmap.WriteColor(pixel_index, color); + } + } +} + +void Canvas::DrawTileSelector(int size) { + const ImGuiIO &io = ImGui::GetIO(); + const bool is_hovered = ImGui::IsItemHovered(); + const ImVec2 origin(canvas_p0_.x + scrolling_.x, canvas_p0_.y + scrolling_.y); + const ImVec2 mouse_pos(io.MousePos.x - origin.x, io.MousePos.y - origin.y); + + if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + if (!points_.empty()) { + points_.clear(); + } + ImVec2 painter_pos; + painter_pos.x = std::floor((double)mouse_pos.x / size) * size; + painter_pos.y = std::floor((double)mouse_pos.y / size) * size; + + points_.push_back(painter_pos); + points_.push_back(ImVec2(painter_pos.x + size, painter_pos.y + size)); + } +} + +void Canvas::HandleTileEdits(Canvas &blockset_canvas, + std::vector &source_blockset, + gfx::Bitmap &destination, int ¤t_tile, + float scale, int tile_painter_size, + int tiles_per_row) { + if (!blockset_canvas.Points().empty()) { + uint16_t x = blockset_canvas.Points().front().x / 32; + uint16_t y = blockset_canvas.Points().front().y / 32; + current_tile = x + (y * tiles_per_row); + if (DrawTilePainter(source_blockset[current_tile], tile_painter_size, + scale)) { + RenderUpdatedBitmap(drawn_tile_position(), + source_blockset[current_tile].mutable_data(), + destination); + } + } +} + +void Canvas::RenderUpdatedBitmap(const ImVec2 &click_position, + const Bytes &tile_data, + gfx::Bitmap &destination) { + // Calculate the tile position relative to the current active map + constexpr int tile_size = 16; // Tile size is 16x16 pixels + + // Calculate the tile index for x and y based on the click_position + int tile_index_x = (static_cast(click_position.x) % 512) / tile_size; + int tile_index_y = (static_cast(click_position.y) % 512) / tile_size; + + // Calculate the pixel start position based on tile index and tile size + ImVec2 start_position; + start_position.x = tile_index_x * tile_size; + start_position.y = tile_index_y * tile_size; + + // Update the bitmap's pixel data based on the start_position and tile_data + for (int y = 0; y < tile_size; ++y) { + for (int x = 0; x < tile_size; ++x) { + int pixel_index = + (start_position.y + y) * destination.width() + (start_position.x + x); + destination.WriteToPixel(pixel_index, tile_data[y * tile_size + x]); + } + } +} + +void Canvas::DrawBitmap(const Bitmap &bitmap, int border_offset, bool ready) { + if (ready) { + draw_list_->AddImage( + (void *)bitmap.texture(), + ImVec2(canvas_p0_.x + border_offset, canvas_p0_.y + border_offset), + ImVec2(canvas_p0_.x + (bitmap.width() * 2), + canvas_p0_.y + (bitmap.height() * 2))); + } +} + +void Canvas::DrawBitmap(const Bitmap &bitmap, int border_offset, float scale) { + draw_list_->AddImage((void *)bitmap.texture(), + ImVec2(canvas_p0_.x, canvas_p0_.y), + ImVec2(canvas_p0_.x + (bitmap.width() * scale), + canvas_p0_.y + (bitmap.height() * scale))); + draw_list_->AddRect(canvas_p0_, canvas_p1_, kRectangleBorder); +} + +void Canvas::DrawBitmap(const Bitmap &bitmap, int x_offset, int y_offset, + float scale) { + draw_list_->AddImage( + (void *)bitmap.texture(), + ImVec2(canvas_p0_.x + x_offset + scrolling_.x, + canvas_p0_.y + y_offset + scrolling_.y), + ImVec2( + canvas_p0_.x + x_offset + scrolling_.x + (bitmap.width() * scale), + canvas_p0_.y + y_offset + scrolling_.y + (bitmap.height() * scale))); +} + +// TODO: Add parameters for sizing and positioning +void Canvas::DrawBitmapTable(const BitmapTable &gfx_bin) { + for (const auto &[key, value] : gfx_bin) { + int offset = 0x40 * (key + 1); + int top_left_y = canvas_p0_.y + 2; + if (key >= 1) { + top_left_y = canvas_p0_.y + 0x40 * key; + } + draw_list_->AddImage((void *)value.texture(), + ImVec2(canvas_p0_.x + 2, top_left_y), + ImVec2(canvas_p0_.x + 0x100, canvas_p0_.y + offset)); + } +} + +void Canvas::DrawOutline(int x, int y, int w, int h) { + ImVec2 origin(canvas_p0_.x + scrolling_.x + x, + canvas_p0_.y + scrolling_.y + y); + ImVec2 size(canvas_p0_.x + scrolling_.x + x + w, + canvas_p0_.y + scrolling_.y + y + h); + draw_list_->AddRect(origin, size, IM_COL32(255, 255, 255, 255)); +} + +void Canvas::DrawSelectRect(int tile_size, float scale) { + const ImGuiIO &io = ImGui::GetIO(); + static ImVec2 drag_start_pos; + static bool dragging = false; + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + if (!points_.empty()) { + points_.clear(); + } + // Snap the start position to the nearest grid point with scaling + // consideration + drag_start_pos.x = + std::floor(io.MousePos.x / (tile_size * scale)) * tile_size * scale; + drag_start_pos.y = + std::floor(io.MousePos.y / (tile_size * scale)) * tile_size * scale; + dragging = true; + } + + if (dragging) { + ImVec2 current_pos = io.MousePos; + ImVec2 grid_pos; + grid_pos.x = + std::floor(current_pos.x / (tile_size * scale)) * tile_size * scale; + grid_pos.y = + std::floor(current_pos.y / (tile_size * scale)) * tile_size * scale; + + // Calculate rect_min and rect_max considering the drag direction + ImVec2 rect_min, rect_max; + rect_min.x = + (grid_pos.x < drag_start_pos.x) ? grid_pos.x : drag_start_pos.x; + rect_min.y = + (grid_pos.y < drag_start_pos.y) ? grid_pos.y : drag_start_pos.y; + rect_max.x = (grid_pos.x >= drag_start_pos.x) + ? grid_pos.x + tile_size * scale + : drag_start_pos.x + tile_size * scale; + rect_max.y = (grid_pos.y >= drag_start_pos.y) + ? grid_pos.y + tile_size * scale + : drag_start_pos.y + tile_size * scale; + + draw_list_->AddRect(rect_min, rect_max, kRectangleBorder); + + if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { + dragging = false; + // Convert the coordinates to scale-independent form + ImVec2 scaled_rect_min, scaled_rect_max; + scaled_rect_min.x = rect_min.x * scale; + scaled_rect_min.y = rect_min.y * scale; + scaled_rect_max.x = rect_max.x * scale; + scaled_rect_max.y = rect_max.y * scale; + + points_.push_back(scaled_rect_min); + points_.push_back(scaled_rect_max); + } + } +} + +void Canvas::DrawRect(int x, int y, int w, int h, ImVec4 color) { + ImVec2 origin(canvas_p0_.x + scrolling_.x + x, + canvas_p0_.y + scrolling_.y + y); + ImVec2 size(canvas_p0_.x + scrolling_.x + x + w, + canvas_p0_.y + scrolling_.y + y + h); + draw_list_->AddRectFilled(origin, size, + IM_COL32(color.x, color.y, color.z, color.w)); +} + +void Canvas::DrawText(std::string text, int x, int y) { + draw_list_->AddText( + ImVec2(canvas_p0_.x + scrolling_.x + x, canvas_p0_.y + scrolling_.y + y), + IM_COL32(255, 255, 255, 255), text.data()); +} + +void Canvas::DrawGrid(float grid_step) { + // Draw grid + all lines in the canvas + draw_list_->PushClipRect(canvas_p0_, canvas_p1_, true); + if (enable_grid_) { + if (custom_step_ != 0.f) grid_step = custom_step_; + + grid_step *= global_scale_; // Apply global scale to grid step + for (float x = fmodf(scrolling_.x, grid_step); + x < canvas_sz_.x * global_scale_; x += grid_step) + draw_list_->AddLine(ImVec2(canvas_p0_.x + x, canvas_p0_.y), + ImVec2(canvas_p0_.x + x, canvas_p1_.y), + IM_COL32(200, 200, 200, 50), 0.5f); + for (float y = fmodf(scrolling_.y, grid_step); + y < canvas_sz_.y * global_scale_; y += grid_step) + draw_list_->AddLine(ImVec2(canvas_p0_.x, canvas_p0_.y + y), + ImVec2(canvas_p1_.x, canvas_p0_.y + y), + IM_COL32(200, 200, 200, 50), 0.5f); + + if (enable_hex_tile_labels_) { + // Draw the hex ID of the tile in the center of the tile square + for (float x = fmodf(scrolling_.x, grid_step); + x < canvas_sz_.x * global_scale_; x += grid_step) { + for (float y = fmodf(scrolling_.y, grid_step); + y < canvas_sz_.y * global_scale_; y += grid_step) { + int tile_x = (x - scrolling_.x) / grid_step; + int tile_y = (y - scrolling_.y) / grid_step; + int tile_id = tile_x + (tile_y * 16); + std::string hex_id = absl::StrFormat("%02X", tile_id); + draw_list_->AddText(ImVec2(canvas_p0_.x + x + (grid_step / 2) - 4, + canvas_p0_.y + y + (grid_step / 2) - 4), + IM_COL32(255, 255, 255, 255), hex_id.data()); + } + } + } + } +} + +void Canvas::DrawOverlay() { + const ImVec2 origin(canvas_p0_.x + scrolling_.x, + canvas_p0_.y + scrolling_.y); // Lock scrolled origin + for (int n = 0; n < points_.Size; n += 2) { + draw_list_->AddRect( + ImVec2(origin.x + points_[n].x, origin.y + points_[n].y), + ImVec2(origin.x + points_[n + 1].x, origin.y + points_[n + 1].y), + IM_COL32(255, 255, 255, 255), 1.0f); + } + + draw_list_->PopClipRect(); +} + +} // namespace gui +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/gui/canvas.h b/src/app/gui/canvas.h new file mode 100644 index 00000000..3c89aeb5 --- /dev/null +++ b/src/app/gui/canvas.h @@ -0,0 +1,122 @@ +#ifndef YAZE_GUI_CANVAS_H +#define YAZE_GUI_CANVAS_H + +#include + +#include +#include + +#include "app/gfx/bitmap.h" +#include "app/rom.h" + +namespace yaze { +namespace app { +namespace gui { + +using app::gfx::Bitmap; +using app::gfx::BitmapTable; + +class Canvas { + public: + Canvas() = default; + explicit Canvas(ImVec2 canvas_size) + : custom_canvas_size_(true), canvas_sz_(canvas_size) {} + + void Update(const gfx::Bitmap& bitmap, ImVec2 bg_size, int tile_size, + float scale = 1.0f, float grid_size = 64.0f); + + void UpdateColorPainter(const gfx::Bitmap& bitmap, const ImVec4& color, + const std::function& event, ImVec2 bg_size, + int tile_size, float scale = 1.0f, + float grid_size = 64.0f); + + void UpdateEvent(const std::function& event, ImVec2 bg_size, + int tile_size, float scale = 1.0f, float grid_size = 64.0f); + + // Background for the Canvas represents region without any content drawn to + // it, but can be controlled by the user. + void DrawBackground(ImVec2 canvas_size = ImVec2(0, 0)); + + // Context Menu refers to what happens when the right mouse button is pressed + // This routine also handles the scrolling for the canvas. + void DrawContextMenu(); + + // Tile painter shows a preview of the currently selected tile + // and allows the user to left click to paint the tile or right + // click to select a new tile to paint with. + bool DrawTilePainter(const Bitmap& bitmap, int size, float scale = 1.0f); + bool DrawSolidTilePainter(const ImVec4& color, int size); + + // Draws a tile on the canvas at the specified position + void DrawTileOnBitmap(int tile_size, gfx::Bitmap& bitmap, ImVec4 color); + + // Dictates which tile is currently selected based on what the user clicks + // in the canvas window. Represented and split apart into a grid of tiles. + void DrawTileSelector(int size); + + void HandleTileEdits(Canvas& blockset_canvas, + std::vector& source_blockset, + gfx::Bitmap& destination, int& current_tile, + float scale = 1.0f, int tile_painter_size = 16, + int tiles_per_row = 8); + + void RenderUpdatedBitmap(const ImVec2& click_position, const Bytes& tile_data, + gfx::Bitmap& destination); + + // Draws the contents of the Bitmap image to the Canvas + void DrawBitmap(const Bitmap& bitmap, int border_offset = 0, + bool ready = true); + void DrawBitmap(const Bitmap& bitmap, int border_offset, float scale); + void DrawBitmap(const Bitmap& bitmap, int x_offset = 0, int y_offset = 0, + float scale = 1.0f); + + void DrawBitmapTable(const BitmapTable& gfx_bin); + void DrawOutline(int x, int y, int w, int h); + void DrawSelectRect(int tile_size, float scale = 1.0f); + void DrawRect(int x, int y, int w, int h, ImVec4 color); + void DrawText(std::string text, int x, int y); + void DrawGrid(float grid_step = 64.0f); + void DrawOverlay(); // last + + auto Points() const { return points_; } + auto GetDrawList() const { return draw_list_; } + auto zero_point() const { return canvas_p0_; } + auto Scrolling() const { return scrolling_; } + auto drawn_tile_position() const { return drawn_tile_pos_; } + auto canvas_size() const { return canvas_sz_; } + void SetCanvasSize(ImVec2 canvas_size) { + canvas_sz_ = canvas_size; + custom_canvas_size_ = true; + } + auto IsMouseHovering() const { return is_hovered_; } + void ZoomIn() { global_scale_ += 0.1f; } + void ZoomOut() { global_scale_ -= 0.1f; } + + void set_global_scale(float scale) { global_scale_ = scale; } + auto global_scale() const { return global_scale_; } + + private: + bool enable_grid_ = true; + bool enable_hex_tile_labels_ = false; + bool enable_context_menu_ = true; + bool custom_canvas_size_ = false; + bool is_hovered_ = false; + + float custom_step_ = 0.0f; + float global_scale_ = 1.0f; + + ImDrawList* draw_list_; + ImVector points_; + ImVec2 scrolling_; + ImVec2 canvas_sz_; + ImVec2 canvas_p0_; + ImVec2 canvas_p1_; + ImVec2 mouse_pos_in_canvas_; + ImVec2 drawn_tile_pos_; +}; + +} // namespace gui +} // namespace app +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/gui/color.cc b/src/app/gui/color.cc similarity index 68% rename from src/gui/color.cc rename to src/app/gui/color.cc index 22c836a1..4871cd7e 100644 --- a/src/gui/color.cc +++ b/src/app/gui/color.cc @@ -9,7 +9,31 @@ #include "app/gfx/snes_palette.h" namespace yaze { +namespace app { namespace gui { + +ImVec4 ConvertSNESColorToImVec4(const SNESColor& color) { + return ImVec4(static_cast(color.GetRGB().x) / 255.0f, + static_cast(color.GetRGB().y) / 255.0f, + static_cast(color.GetRGB().z) / 255.0f, + 1.0f // Assuming alpha is always fully opaque for SNES colors, + // adjust if necessary + ); +} + +IMGUI_API bool SNESColorButton(absl::string_view id, SNESColor& color, + ImGuiColorEditFlags flags, + const ImVec2& size_arg) { + // Convert the SNES color values to ImGui color values (normalized to 0-1 + // range) + ImVec4 displayColor = ConvertSNESColorToImVec4(color); + + // Call the original ImGui::ColorButton with the converted color + bool pressed = ImGui::ColorButton(id.data(), displayColor, flags, size_arg); + + return pressed; +} + void DisplayPalette(app::gfx::SNESPalette& palette, bool loaded) { static ImVec4 color = ImVec4(0, 0, 0, 255.f); ImGuiColorEditFlags misc_flags = ImGuiColorEditFlags_AlphaPreview | @@ -20,10 +44,10 @@ void DisplayPalette(app::gfx::SNESPalette& palette, bool loaded) { static bool init = false; static ImVec4 saved_palette[32] = {}; if (loaded && !init) { - for (int n = 0; n < palette.size_; n++) { - saved_palette[n].x = palette.GetColor(n).rgb.x / 255; - saved_palette[n].y = palette.GetColor(n).rgb.y / 255; - saved_palette[n].z = palette.GetColor(n).rgb.z / 255; + for (int n = 0; n < palette.size(); n++) { + saved_palette[n].x = palette.GetColor(n).GetRGB().x / 255; + saved_palette[n].y = palette.GetColor(n).GetRGB().y / 255; + saved_palette[n].z = palette.GetColor(n).GetRGB().z / 255; saved_palette[n].w = 255; // Alpha } init = true; @@ -47,7 +71,7 @@ void DisplayPalette(app::gfx::SNESPalette& palette, bool loaded) { color = backup_color; ImGui::Separator(); -ImGui::BeginGroup(); // Lock X position + ImGui::BeginGroup(); // Lock X position ImGui::Text("Palette"); for (int n = 0; n < IM_ARRAYSIZE(saved_palette); n++) { ImGui::PushID(n); @@ -80,5 +104,7 @@ ImGui::BeginGroup(); // Lock X position misc_flags | ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoSmallPreview); } + } // namespace gui +} // namespace app } // namespace yaze \ No newline at end of file diff --git a/src/app/gui/color.h b/src/app/gui/color.h new file mode 100644 index 00000000..7e944a1e --- /dev/null +++ b/src/app/gui/color.h @@ -0,0 +1,33 @@ +#ifndef YAZE_GUI_COLOR_H +#define YAZE_GUI_COLOR_H + +#include + +#include +#include + +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_palette.h" + +namespace yaze { +namespace app { +namespace gui { + +using gfx::SNESColor; + +// A utility function to convert an SNESColor object to an ImVec4 with +// normalized color values +ImVec4 ConvertSNESColorToImVec4(const SNESColor& color); + +// The wrapper function for ImGui::ColorButton that takes a SNESColor reference +IMGUI_API bool SNESColorButton(absl::string_view id, SNESColor& color, + ImGuiColorEditFlags flags = 0, + const ImVec2& size_arg = ImVec2(0, 0)); + +void DisplayPalette(app::gfx::SNESPalette& palette, bool loaded); + +} // namespace gui +} // namespace app +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/gui/icons.h b/src/app/gui/icons.h similarity index 100% rename from src/gui/icons.h rename to src/app/gui/icons.h diff --git a/src/app/gui/input.cc b/src/app/gui/input.cc new file mode 100644 index 00000000..7c2dd82b --- /dev/null +++ b/src/app/gui/input.cc @@ -0,0 +1,181 @@ +#include "input.h" + +#include +#include + +#include "absl/strings/string_view.h" + +namespace ImGui { + +static inline ImGuiInputTextFlags InputScalar_DefaultCharsFilter( + ImGuiDataType data_type, const char* format) { + if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) + return ImGuiInputTextFlags_CharsScientific; + const char format_last_char = format[0] ? format[strlen(format) - 1] : 0; + return (format_last_char == 'x' || format_last_char == 'X') + ? ImGuiInputTextFlags_CharsHexadecimal + : ImGuiInputTextFlags_CharsDecimal; +} + +bool InputScalarLeft(const char* label, ImGuiDataType data_type, void* p_data, + const void* p_step, const void* p_step_fast, + const char* format, float input_width, + ImGuiInputTextFlags flags) { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) return false; + + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + + if (format == NULL) format = DataTypeGetInfo(data_type)->PrintFmt; + + char buf[64]; + DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format); + + if (g.ActiveId == 0 && (flags & (ImGuiInputTextFlags_CharsDecimal | + ImGuiInputTextFlags_CharsHexadecimal | + ImGuiInputTextFlags_CharsScientific)) == 0) + flags |= InputScalar_DefaultCharsFilter(data_type, format); + flags |= ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoMarkEdited; + + bool value_changed = false; + if (p_step == NULL) { + ImGui::SetNextItemWidth(input_width); + if (InputText(label, buf, IM_ARRAYSIZE(buf), flags)) + value_changed = DataTypeApplyFromText(buf, data_type, p_data, format); + } else { + const float button_size = GetFrameHeight(); + + BeginGroup(); // The only purpose of the group here is to allow the caller + // to query item data e.g. IsItemActive() + PushID(label); + SetNextItemWidth(ImMax( + 1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2)); + + // Place the label on the left of the input field + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, + ImVec2{style.ItemSpacing.x, style.ItemSpacing.y}); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + ImVec2{style.FramePadding.x, style.FramePadding.y}); + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", label); + ImGui::SameLine(); + ImGui::SetNextItemWidth(input_width); + if (InputText("", buf, IM_ARRAYSIZE(buf), + flags)) // PushId(label) + "" gives us the expected ID + // from outside point of view + value_changed = DataTypeApplyFromText(buf, data_type, p_data, format); + IMGUI_TEST_ENGINE_ITEM_INFO( + g.LastItemData.ID, label, + g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable); + + // Step buttons + const ImVec2 backup_frame_padding = style.FramePadding; + style.FramePadding.x = style.FramePadding.y; + ImGuiButtonFlags button_flags = + ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups; + if (flags & ImGuiInputTextFlags_ReadOnly) BeginDisabled(); + SameLine(0, style.ItemInnerSpacing.x); + if (ButtonEx("-", ImVec2(button_size, button_size), button_flags)) { + DataTypeApplyOp(data_type, '-', p_data, p_data, + g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); + value_changed = true; + } + SameLine(0, style.ItemInnerSpacing.x); + if (ButtonEx("+", ImVec2(button_size, button_size), button_flags)) { + DataTypeApplyOp(data_type, '+', p_data, p_data, + g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); + value_changed = true; + } + if (flags & ImGuiInputTextFlags_ReadOnly) EndDisabled(); + + style.FramePadding = backup_frame_padding; + + PopID(); + EndGroup(); + ImGui::PopStyleVar(2); + } + if (value_changed) MarkItemEdited(g.LastItemData.ID); + + return value_changed; +} +} // namespace ImGui + +namespace yaze { +namespace app { +namespace gui { + +const int kStepOneHex = 0x01; +const int kStepFastHex = 0x0F; + +bool InputHex(const char* label, uint64_t* data) { + return ImGui::InputScalar(label, ImGuiDataType_U64, data, &kStepOneHex, + &kStepFastHex, "%06X", + ImGuiInputTextFlags_CharsHexadecimal); +} + +bool InputHexShort(const char* label, uint32_t* data) { + return ImGui::InputScalar(label, ImGuiDataType_U32, data, &kStepOneHex, + &kStepFastHex, "%06X", + ImGuiInputTextFlags_CharsHexadecimal); +} + +bool InputHexWord(const char* label, uint16_t* data, float input_width) { + return ImGui::InputScalarLeft(label, ImGuiDataType_U16, data, &kStepOneHex, + &kStepFastHex, "%04X", input_width, + ImGuiInputTextFlags_CharsHexadecimal); +} + +bool InputHexByte(const char* label, uint8_t* data, uint8_t step, + float input_width) { + return ImGui::InputScalarLeft(label, ImGuiDataType_U8, data, &step, + &kStepFastHex, "%02X", input_width, + ImGuiInputTextFlags_CharsHexadecimal); +} + +void ItemLabel(absl::string_view title, ItemLabelFlags flags) { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + const ImVec2 lineStart = ImGui::GetCursorScreenPos(); + const ImGuiStyle& style = ImGui::GetStyle(); + float fullWidth = ImGui::GetContentRegionAvail().x; + float itemWidth = ImGui::CalcItemWidth() + style.ItemSpacing.x; + ImVec2 textSize = ImGui::CalcTextSize(title.begin(), title.end()); + ImRect textRect; + textRect.Min = ImGui::GetCursorScreenPos(); + if (flags & ItemLabelFlag::Right) textRect.Min.x = textRect.Min.x + itemWidth; + textRect.Max = textRect.Min; + textRect.Max.x += fullWidth - itemWidth; + textRect.Max.y += textSize.y; + + ImGui::SetCursorScreenPos(textRect.Min); + + ImGui::AlignTextToFramePadding(); + // Adjust text rect manually because we render it directly into a drawlist + // instead of using public functions. + textRect.Min.y += window->DC.CurrLineTextBaseOffset; + textRect.Max.y += window->DC.CurrLineTextBaseOffset; + + ImGui::ItemSize(textRect); + if (ImGui::ItemAdd( + textRect, window->GetID(title.data(), title.data() + title.size()))) { + ImGui::RenderTextEllipsis( + ImGui::GetWindowDrawList(), textRect.Min, textRect.Max, textRect.Max.x, + textRect.Max.x, title.data(), title.data() + title.size(), &textSize); + + if (textRect.GetWidth() < textSize.x && ImGui::IsItemHovered()) + ImGui::SetTooltip("%.*s", (int)title.size(), title.data()); + } + if (flags & ItemLabelFlag::Left) { + ImVec2 result; + auto other = ImVec2{0, textSize.y + window->DC.CurrLineTextBaseOffset}; + result.x = textRect.Max.x - other.x; + result.y = textRect.Max.y - other.y; + ImGui::SetCursorScreenPos(result); + ImGui::SameLine(); + } else if (flags & ItemLabelFlag::Right) + ImGui::SetCursorScreenPos(lineStart); +} + +} // namespace gui +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/gui/input.h b/src/app/gui/input.h new file mode 100644 index 00000000..1542a702 --- /dev/null +++ b/src/app/gui/input.h @@ -0,0 +1,37 @@ +#ifndef YAZE_APP_CORE_INPUT_H +#define YAZE_APP_CORE_INPUT_H + +#include + +#include +#include + +#include "absl/strings/string_view.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 InputHexShort(const char* label, uint32_t* data); +IMGUI_API bool InputHexWord(const char* label, uint16_t* data, + float input_width = 50.f); +IMGUI_API bool InputHexByte(const char* label, uint8_t* data, uint8_t step = 0x01, + float input_width = 50.f); + +using ItemLabelFlags = enum ItemLabelFlag { + Left = 1u << 0u, + Right = 1u << 1u, + Default = Left, +}; + +IMGUI_API void ItemLabel(absl::string_view title, ItemLabelFlags flags); + +} // namespace gui +} // namespace app +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/app/gui/pipeline.cc b/src/app/gui/pipeline.cc new file mode 100644 index 00000000..d91713c0 --- /dev/null +++ b/src/app/gui/pipeline.cc @@ -0,0 +1,189 @@ +#include "pipeline.h" + +#include +#include +#include +#include + +#include +#include + +#include "absl/strings/string_view.h" +#include "app/core/common.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_palette.h" +#include "app/gui/canvas.h" +#include "app/gui/color.h" +#include "app/gui/input.h" +#include "app/rom.h" + +namespace yaze { +namespace app { +namespace gui { + +void SelectablePalettePipeline(uint64_t& palette_id, bool& refresh_graphics, + gfx::SNESPalette& palette) { + const auto palette_row_size = 7; + if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)100); + ImGui::BeginChild(child_id, ImGui::GetContentRegionAvail(), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + ImGui::BeginGroup(); // Lock X position + ImGui::Text("Palette"); + for (int n = 0; n < palette.size(); n++) { + ImGui::PushID(n); + if ((n % palette_row_size) != 0) + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y); + + // Check if the current row is selected + bool is_selected = (palette_id == n / palette_row_size); + + // Add outline rectangle to the selected row + if (is_selected) { + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 0.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); + } + + if (gui::SNESColorButton("##palette", palette[n], + ImGuiColorEditFlags_NoAlpha | + ImGuiColorEditFlags_NoPicker | + ImGuiColorEditFlags_NoTooltip, + ImVec2(20, 20))) { + palette_id = n / palette_row_size; + refresh_graphics = true; + } + + if (is_selected) { + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + } + + ImGui::PopID(); + } + ImGui::EndGroup(); + } + ImGui::EndChild(); +} + +void GraphicsBinCanvasPipeline(int width, int height, int tile_size, + int num_sheets_to_load, int canvas_id, + bool is_loaded, gfx::BitmapTable& graphics_bin) { + gui::Canvas canvas; + + if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)canvas_id); + ImGui::BeginChild(child_id, ImGui::GetContentRegionAvail(), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + canvas.DrawBackground(ImVec2(width + 1, num_sheets_to_load * height + 1)); + canvas.DrawContextMenu(); + if (is_loaded) { + for (const auto& [key, value] : graphics_bin) { + int offset = height * (key + 1); + int top_left_y = canvas.zero_point().y + 2; + if (key >= 1) { + top_left_y = canvas.zero_point().y + height * key; + } + canvas.GetDrawList()->AddImage( + (void*)value.texture(), + ImVec2(canvas.zero_point().x + 2, top_left_y), + ImVec2(canvas.zero_point().x + 0x100, + canvas.zero_point().y + offset)); + } + } + canvas.DrawTileSelector(tile_size); + canvas.DrawGrid(tile_size); + canvas.DrawOverlay(); + } + ImGui::EndChild(); +} + +void GraphicsManagerCanvasPipeline(int width, int height, int tile_size, + int num_sheets, int canvas_id, + bool is_loaded, + const gfx::BitmapManager& graphics_manager) { + gui::Canvas canvas; + + if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)canvas_id); + ImGui::BeginChild(child_id, ImGui::GetContentRegionAvail(), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + canvas.DrawBackground(ImVec2(width + 1, num_sheets * height + 1)); + canvas.DrawContextMenu(); + if (is_loaded) { + for (const auto& [key, value] : graphics_manager) { + int offset = height * (key + 1); + int top_left_y = canvas.zero_point().y + 2; + if (key >= 1) { + top_left_y = canvas.zero_point().y + height * key; + } + canvas.GetDrawList()->AddImage( + (void*)value->texture(), + ImVec2(canvas.zero_point().x + 2, top_left_y), + ImVec2(canvas.zero_point().x + 0x100, + canvas.zero_point().y + offset)); + } + } + canvas.DrawTileSelector(tile_size); + canvas.DrawGrid(tile_size); + canvas.DrawOverlay(); + } + ImGui::EndChild(); +} + +void ButtonPipe(absl::string_view button_text, std::function callback) { + if (ImGui::Button(button_text.data())) { + callback(); + } +} + +void BitmapCanvasPipeline(gui::Canvas& canvas, const gfx::Bitmap& bitmap, + int width, int height, int tile_size, bool is_loaded, + bool scrollbar, int canvas_id) { + auto draw_canvas = [](gui::Canvas& canvas, const gfx::Bitmap& bitmap, + int width, int height, int tile_size, bool is_loaded) { + canvas.DrawBackground(ImVec2(width + 1, height + 1)); + canvas.DrawContextMenu(); + canvas.DrawBitmap(bitmap, 2, is_loaded); + canvas.DrawTileSelector(tile_size); + canvas.DrawGrid(tile_size); + canvas.DrawOverlay(); + }; + + if (scrollbar) { + if (ImGuiID child_id = ImGui::GetID((void*)(intptr_t)canvas_id); + ImGui::BeginChild(child_id, ImGui::GetContentRegionAvail(), true, + ImGuiWindowFlags_AlwaysVerticalScrollbar)) { + draw_canvas(canvas, bitmap, width, height, tile_size, is_loaded); + } + ImGui::EndChild(); + } else { + draw_canvas(canvas, bitmap, width, height, tile_size, is_loaded); + } +} + +void BuildAndRenderBitmapPipeline(int width, int height, int depth, Bytes data, + ROM& z3_rom, gfx::Bitmap& bitmap, + gfx::SNESPalette& palette) { + bitmap.Create(width, height, depth, data); + bitmap.ApplyPalette(palette); + z3_rom.RenderBitmap(&bitmap); +} + +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(), "."); + } + + if (ImGuiFileDialog::Instance()->Display( + display_key.data(), ImGuiWindowFlags_NoCollapse, ImVec2(600, 400))) { + if (ImGuiFileDialog::Instance()->IsOk()) { + callback(); + } + ImGuiFileDialog::Instance()->Close(); + } +} + +} // namespace gui +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/gui/pipeline.h b/src/app/gui/pipeline.h new file mode 100644 index 00000000..d78e6d61 --- /dev/null +++ b/src/app/gui/pipeline.h @@ -0,0 +1,54 @@ +#ifndef YAZE_APP_CORE_PIPELINE_H +#define YAZE_APP_CORE_PIPELINE_H + +#include +#include +#include +#include + +#include +#include + +#include "absl/strings/string_view.h" +#include "app/core/constants.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_palette.h" +#include "app/gui/canvas.h" +#include "app/rom.h" + +namespace yaze { +namespace app { +namespace gui { + +void SelectablePalettePipeline(uint64_t& palette_id, bool& refresh_graphics, + gfx::SNESPalette& palette); + +void GraphicsBinCanvasPipeline(int width, int height, int tile_size, + int num_sheets_to_load, int canvas_id, + bool is_loaded, gfx::BitmapTable& graphics_bin); + +void ButtonPipe(absl::string_view button_text, std::function callback); + +void BitmapCanvasPipeline(gui::Canvas& canvas, const gfx::Bitmap& bitmap, + int width, int height, int tile_size, bool is_loaded, + bool scrollbar, int canvas_id); + +void GraphicsManagerCanvasPipeline(int width, int height, int tile_size, + int num_sheets, int canvas_id, + bool is_loaded, + const gfx::BitmapManager& graphics_manager); + +void BuildAndRenderBitmapPipeline(int width, int height, int depth, Bytes data, + ROM& z3_rom, gfx::Bitmap& bitmap, + gfx::SNESPalette& palette); + +void FileDialogPipeline(absl::string_view display_key, + absl::string_view file_extensions, + std::optional button_text, + std::function callback); + +} // namespace core +} // namespace app +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/app/gui/style.cc b/src/app/gui/style.cc new file mode 100644 index 00000000..c8d2819c --- /dev/null +++ b/src/app/gui/style.cc @@ -0,0 +1,478 @@ +#include "style.h" + +#include "imgui/imgui.h" +#include "imgui/imgui_internal.h" + +namespace yaze { +namespace app { + +namespace gui { + +void DrawDisplaySettings(ImGuiStyle* ref) { + // You can pass in a reference ImGuiStyle structure to compare to, revert to + // and save to (without a reference style pointer, we will use one compared + // locally as a reference) + ImGuiStyle& style = ImGui::GetStyle(); + static ImGuiStyle ref_saved_style; + + // Default to using internal storage as reference + static bool init = true; + if (init && ref == NULL) ref_saved_style = style; + init = false; + if (ref == NULL) ref = &ref_saved_style; + + ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.50f); + + if (ImGui::ShowStyleSelector("Colors##Selector")) ref_saved_style = style; + ImGui::ShowFontSelector("Fonts##Selector"); + + // Simplified Settings (expose floating-pointer border sizes as boolean + // representing 0.0f or 1.0f) + if (ImGui::SliderFloat("FrameRounding", &style.FrameRounding, 0.0f, 12.0f, + "%.0f")) + style.GrabRounding = style.FrameRounding; // Make GrabRounding always the + // same value as FrameRounding + { + bool border = (style.WindowBorderSize > 0.0f); + if (ImGui::Checkbox("WindowBorder", &border)) { + style.WindowBorderSize = border ? 1.0f : 0.0f; + } + } + ImGui::SameLine(); + { + bool border = (style.FrameBorderSize > 0.0f); + if (ImGui::Checkbox("FrameBorder", &border)) { + style.FrameBorderSize = border ? 1.0f : 0.0f; + } + } + ImGui::SameLine(); + { + bool border = (style.PopupBorderSize > 0.0f); + if (ImGui::Checkbox("PopupBorder", &border)) { + style.PopupBorderSize = border ? 1.0f : 0.0f; + } + } + + // Save/Revert button + if (ImGui::Button("Save Ref")) *ref = ref_saved_style = style; + ImGui::SameLine(); + if (ImGui::Button("Revert Ref")) style = *ref; + ImGui::SameLine(); + + ImGui::Separator(); + + if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_None)) { + if (ImGui::BeginTabItem("Sizes")) { + ImGui::SeparatorText("Main"); + ImGui::SliderFloat2("WindowPadding", (float*)&style.WindowPadding, 0.0f, + 20.0f, "%.0f"); + ImGui::SliderFloat2("FramePadding", (float*)&style.FramePadding, 0.0f, + 20.0f, "%.0f"); + ImGui::SliderFloat2("ItemSpacing", (float*)&style.ItemSpacing, 0.0f, + 20.0f, "%.0f"); + ImGui::SliderFloat2("ItemInnerSpacing", (float*)&style.ItemInnerSpacing, + 0.0f, 20.0f, "%.0f"); + ImGui::SliderFloat2("TouchExtraPadding", (float*)&style.TouchExtraPadding, + 0.0f, 10.0f, "%.0f"); + ImGui::SliderFloat("IndentSpacing", &style.IndentSpacing, 0.0f, 30.0f, + "%.0f"); + ImGui::SliderFloat("ScrollbarSize", &style.ScrollbarSize, 1.0f, 20.0f, + "%.0f"); + ImGui::SliderFloat("GrabMinSize", &style.GrabMinSize, 1.0f, 20.0f, + "%.0f"); + + ImGui::SeparatorText("Borders"); + ImGui::SliderFloat("WindowBorderSize", &style.WindowBorderSize, 0.0f, + 1.0f, "%.0f"); + ImGui::SliderFloat("ChildBorderSize", &style.ChildBorderSize, 0.0f, 1.0f, + "%.0f"); + ImGui::SliderFloat("PopupBorderSize", &style.PopupBorderSize, 0.0f, 1.0f, + "%.0f"); + ImGui::SliderFloat("FrameBorderSize", &style.FrameBorderSize, 0.0f, 1.0f, + "%.0f"); + ImGui::SliderFloat("TabBorderSize", &style.TabBorderSize, 0.0f, 1.0f, + "%.0f"); + ImGui::SliderFloat("TabBarBorderSize", &style.TabBarBorderSize, 0.0f, + 2.0f, "%.0f"); + + ImGui::SeparatorText("Rounding"); + ImGui::SliderFloat("WindowRounding", &style.WindowRounding, 0.0f, 12.0f, + "%.0f"); + ImGui::SliderFloat("ChildRounding", &style.ChildRounding, 0.0f, 12.0f, + "%.0f"); + ImGui::SliderFloat("FrameRounding", &style.FrameRounding, 0.0f, 12.0f, + "%.0f"); + ImGui::SliderFloat("PopupRounding", &style.PopupRounding, 0.0f, 12.0f, + "%.0f"); + ImGui::SliderFloat("ScrollbarRounding", &style.ScrollbarRounding, 0.0f, + 12.0f, "%.0f"); + ImGui::SliderFloat("GrabRounding", &style.GrabRounding, 0.0f, 12.0f, + "%.0f"); + ImGui::SliderFloat("TabRounding", &style.TabRounding, 0.0f, 12.0f, + "%.0f"); + + ImGui::SeparatorText("Tables"); + ImGui::SliderFloat2("CellPadding", (float*)&style.CellPadding, 0.0f, + 20.0f, "%.0f"); + ImGui::SliderAngle("TableAngledHeadersAngle", + &style.TableAngledHeadersAngle, -50.0f, +50.0f); + + ImGui::SeparatorText("Widgets"); + ImGui::SliderFloat2("WindowTitleAlign", (float*)&style.WindowTitleAlign, + 0.0f, 1.0f, "%.2f"); + int window_menu_button_position = style.WindowMenuButtonPosition + 1; + if (ImGui::Combo("WindowMenuButtonPosition", + (int*)&window_menu_button_position, + "None\0Left\0Right\0")) + style.WindowMenuButtonPosition = window_menu_button_position - 1; + ImGui::Combo("ColorButtonPosition", (int*)&style.ColorButtonPosition, + "Left\0Right\0"); + ImGui::SliderFloat2("ButtonTextAlign", (float*)&style.ButtonTextAlign, + 0.0f, 1.0f, "%.2f"); + ImGui::SameLine(); + + ImGui::SliderFloat2("SelectableTextAlign", + (float*)&style.SelectableTextAlign, 0.0f, 1.0f, + "%.2f"); + ImGui::SameLine(); + + ImGui::SliderFloat("SeparatorTextBorderSize", + &style.SeparatorTextBorderSize, 0.0f, 10.0f, "%.0f"); + ImGui::SliderFloat2("SeparatorTextAlign", + (float*)&style.SeparatorTextAlign, 0.0f, 1.0f, + "%.2f"); + ImGui::SliderFloat2("SeparatorTextPadding", + (float*)&style.SeparatorTextPadding, 0.0f, 40.0f, + "%.0f"); + ImGui::SliderFloat("LogSliderDeadzone", &style.LogSliderDeadzone, 0.0f, + 12.0f, "%.0f"); + + ImGui::SeparatorText("Tooltips"); + for (int n = 0; n < 2; n++) + if (ImGui::TreeNodeEx(n == 0 ? "HoverFlagsForTooltipMouse" + : "HoverFlagsForTooltipNav")) { + ImGuiHoveredFlags* p = (n == 0) ? &style.HoverFlagsForTooltipMouse + : &style.HoverFlagsForTooltipNav; + ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayNone", p, + ImGuiHoveredFlags_DelayNone); + ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayShort", p, + ImGuiHoveredFlags_DelayShort); + ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayNormal", p, + ImGuiHoveredFlags_DelayNormal); + ImGui::CheckboxFlags("ImGuiHoveredFlags_Stationary", p, + ImGuiHoveredFlags_Stationary); + ImGui::CheckboxFlags("ImGuiHoveredFlags_NoSharedDelay", p, + ImGuiHoveredFlags_NoSharedDelay); + ImGui::TreePop(); + } + + ImGui::SeparatorText("Misc"); + ImGui::SliderFloat2("DisplaySafeAreaPadding", + (float*)&style.DisplaySafeAreaPadding, 0.0f, 30.0f, + "%.0f"); + ImGui::SameLine(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Colors")) { + static int output_dest = 0; + static bool output_only_modified = true; + if (ImGui::Button("Export")) { + if (output_dest == 0) + ImGui::LogToClipboard(); + else + ImGui::LogToTTY(); + ImGui::LogText("ImVec4* colors = ImGui::GetStyle().Colors;" IM_NEWLINE); + for (int i = 0; i < ImGuiCol_COUNT; i++) { + const ImVec4& col = style.Colors[i]; + const char* name = ImGui::GetStyleColorName(i); + if (!output_only_modified || + memcmp(&col, &ref->Colors[i], sizeof(ImVec4)) != 0) + ImGui::LogText( + "colors[ImGuiCol_%s]%*s= ImVec4(%.2ff, %.2ff, %.2ff, " + "%.2ff);" IM_NEWLINE, + name, 23 - (int)strlen(name), "", col.x, col.y, col.z, col.w); + } + ImGui::LogFinish(); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(120); + ImGui::Combo("##output_type", &output_dest, "To Clipboard\0To TTY\0"); + ImGui::SameLine(); + ImGui::Checkbox("Only Modified Colors", &output_only_modified); + + static ImGuiTextFilter filter; + filter.Draw("Filter colors", ImGui::GetFontSize() * 16); + + static ImGuiColorEditFlags alpha_flags = 0; + if (ImGui::RadioButton("Opaque", + alpha_flags == ImGuiColorEditFlags_None)) { + alpha_flags = ImGuiColorEditFlags_None; + } + ImGui::SameLine(); + if (ImGui::RadioButton("Alpha", + alpha_flags == ImGuiColorEditFlags_AlphaPreview)) { + alpha_flags = ImGuiColorEditFlags_AlphaPreview; + } + ImGui::SameLine(); + if (ImGui::RadioButton( + "Both", alpha_flags == ImGuiColorEditFlags_AlphaPreviewHalf)) { + alpha_flags = ImGuiColorEditFlags_AlphaPreviewHalf; + } + ImGui::SameLine(); + + ImGui::SetNextWindowSizeConstraints( + ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 10), + ImVec2(FLT_MAX, FLT_MAX)); + ImGui::BeginChild("##colors", ImVec2(0, 0), ImGuiChildFlags_Border, + ImGuiWindowFlags_AlwaysVerticalScrollbar | + ImGuiWindowFlags_AlwaysHorizontalScrollbar | + ImGuiWindowFlags_NavFlattened); + ImGui::PushItemWidth(ImGui::GetFontSize() * -12); + for (int i = 0; i < ImGuiCol_COUNT; i++) { + const char* name = ImGui::GetStyleColorName(i); + if (!filter.PassFilter(name)) continue; + ImGui::PushID(i); + ImGui::ColorEdit4("##color", (float*)&style.Colors[i], + ImGuiColorEditFlags_AlphaBar | alpha_flags); + if (memcmp(&style.Colors[i], &ref->Colors[i], sizeof(ImVec4)) != 0) { + // Tips: in a real user application, you may want to merge and use + // an icon font into the main font, so instead of "Save"/"Revert" + // you'd use icons! Read the FAQ and docs/FONTS.md about using icon + // fonts. It's really easy and super convenient! + ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); + if (ImGui::Button("Save")) { + ref->Colors[i] = style.Colors[i]; + } + ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); + if (ImGui::Button("Revert")) { + style.Colors[i] = ref->Colors[i]; + } + } + ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); + ImGui::TextUnformatted(name); + ImGui::PopID(); + } + ImGui::PopItemWidth(); + ImGui::EndChild(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Fonts")) { + ImGuiIO& io = ImGui::GetIO(); + ImFontAtlas* atlas = io.Fonts; + ImGui::ShowFontAtlas(atlas); + + // Post-baking font scaling. Note that this is NOT the nice way of + // scaling fonts, read below. (we enforce hard clamping manually as by + // default DragFloat/SliderFloat allows CTRL+Click text to get out of + // bounds). + const float MIN_SCALE = 0.3f; + const float MAX_SCALE = 2.0f; + + static float window_scale = 1.0f; + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); + if (ImGui::DragFloat( + "window scale", &window_scale, 0.005f, MIN_SCALE, MAX_SCALE, + "%.2f", + ImGuiSliderFlags_AlwaysClamp)) // Scale only this window + ImGui::SetWindowFontScale(window_scale); + ImGui::DragFloat("global scale", &io.FontGlobalScale, 0.005f, MIN_SCALE, + MAX_SCALE, "%.2f", + ImGuiSliderFlags_AlwaysClamp); // Scale everything + ImGui::PopItemWidth(); + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Rendering")) { + ImGui::Checkbox("Anti-aliased lines", &style.AntiAliasedLines); + ImGui::SameLine(); + + ImGui::Checkbox("Anti-aliased lines use texture", + &style.AntiAliasedLinesUseTex); + ImGui::SameLine(); + + ImGui::Checkbox("Anti-aliased fill", &style.AntiAliasedFill); + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragFloat("Curve Tessellation Tolerance", + &style.CurveTessellationTol, 0.02f, 0.10f, 10.0f, + "%.2f"); + if (style.CurveTessellationTol < 0.10f) + style.CurveTessellationTol = 0.10f; + + // When editing the "Circle Segment Max Error" value, draw a preview of + // its effect on auto-tessellated circles. + ImGui::DragFloat("Circle Tessellation Max Error", + &style.CircleTessellationMaxError, 0.005f, 0.10f, 5.0f, + "%.2f", ImGuiSliderFlags_AlwaysClamp); + const bool show_samples = ImGui::IsItemActive(); + if (show_samples) ImGui::SetNextWindowPos(ImGui::GetCursorScreenPos()); + if (show_samples && ImGui::BeginTooltip()) { + ImGui::TextUnformatted("(R = radius, N = number of segments)"); + ImGui::Spacing(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + const float min_widget_width = ImGui::CalcTextSize("N: MMM\nR: MMM").x; + for (int n = 0; n < 8; n++) { + const float RAD_MIN = 5.0f; + const float RAD_MAX = 70.0f; + const float rad = + RAD_MIN + (RAD_MAX - RAD_MIN) * (float)n / (8.0f - 1.0f); + + ImGui::BeginGroup(); + + ImGui::Text("R: %.f\nN: %d", rad, + draw_list->_CalcCircleAutoSegmentCount(rad)); + + const float canvas_width = std::max(min_widget_width, rad * 2.0f); + const float offset_x = floorf(canvas_width * 0.5f); + const float offset_y = floorf(RAD_MAX); + + const ImVec2 p1 = ImGui::GetCursorScreenPos(); + draw_list->AddCircle(ImVec2(p1.x + offset_x, p1.y + offset_y), rad, + ImGui::GetColorU32(ImGuiCol_Text)); + ImGui::Dummy(ImVec2(canvas_width, RAD_MAX * 2)); + + /* + const ImVec2 p2 = ImGui::GetCursorScreenPos(); + draw_list->AddCircleFilled(ImVec2(p2.x + offset_x, p2.y + offset_y), + rad, ImGui::GetColorU32(ImGuiCol_Text)); + ImGui::Dummy(ImVec2(canvas_width, RAD_MAX * 2)); + */ + + ImGui::EndGroup(); + ImGui::SameLine(); + } + ImGui::EndTooltip(); + } + ImGui::SameLine(); + + ImGui::DragFloat("Global Alpha", &style.Alpha, 0.005f, 0.20f, 1.0f, + "%.2f"); // Not exposing zero here so user doesn't + // "lose" the UI (zero alpha clips all + // widgets). But application code could have a + // toggle to switch between zero and non-zero. + ImGui::DragFloat("Disabled Alpha", &style.DisabledAlpha, 0.005f, 0.0f, + 1.0f, "%.2f"); + ImGui::SameLine(); + + ImGui::PopItemWidth(); + + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + ImGui::PopItemWidth(); +} + +void TextWithSeparators(const absl::string_view& text) { + ImGui::Separator(); + ImGui::Text("%s", text.data()); + ImGui::Separator(); +} + +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); +} +} // namespace gui +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/gui/style.h b/src/app/gui/style.h new file mode 100644 index 00000000..6599a0a1 --- /dev/null +++ b/src/app/gui/style.h @@ -0,0 +1,22 @@ +#ifndef YAZE_APP_CORE_STYLE_H +#define YAZE_APP_CORE_STYLE_H + +#include + +#include "absl/strings/string_view.h" + +namespace yaze { +namespace app { +namespace gui { + +void DrawDisplaySettings(ImGuiStyle* ref = nullptr); + +void TextWithSeparators(const absl::string_view& text); + +void ColorsYaze(); + +} // namespace gui +} // namespace app +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/gui/widgets.cc b/src/app/gui/widgets.cc similarity index 55% rename from src/gui/widgets.cc rename to src/app/gui/widgets.cc index a3b54e3f..b269708c 100644 --- a/src/gui/widgets.cc +++ b/src/app/gui/widgets.cc @@ -1,19 +1,55 @@ #include "widgets.h" #include +#include #include "absl/status/status.h" #include "app/core/constants.h" namespace yaze { +namespace app { namespace gui { -namespace widgets { + +void RenderTabItem(const std::string &title, + const std::function &render_func) { + if (ImGui::BeginTabItem(title.c_str())) { + render_func(); + ImGui::EndTabItem(); + } +} + +// ============================================================================ +// 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 : app::core::kKeywords) language_65816.mKeywords.emplace(k); + for (auto &k : kKeywords) language_65816.mKeywords.emplace(k); - for (auto &k : app::core::kIdentifiers) { + for (auto &k : kIdentifiers) { TextEditor::Identifier id; id.mDeclaration = "Built-in function"; language_65816.mIdentifiers.insert(std::make_pair(std::string(k), id)); @@ -63,6 +99,6 @@ TextEditor::LanguageDefinition GetAssemblyLanguageDef() { return language_65816; } -} // namespace widgets } // namespace gui +} // namespace app } // namespace yaze diff --git a/src/app/gui/widgets.h b/src/app/gui/widgets.h new file mode 100644 index 00000000..e2e21d49 --- /dev/null +++ b/src/app/gui/widgets.h @@ -0,0 +1,79 @@ +#ifndef YAZE_GUI_WIDGETS_H +#define YAZE_GUI_WIDGETS_H + +#include +#include +#include + +#include +#include + +#include "absl/status/status.h" +#include "app/core/constants.h" +#include "app/gfx/bitmap.h" + +namespace yaze { +namespace app { +namespace gui { + +class DynamicLayout { + +}; + +TextEditor::LanguageDefinition GetAssemblyLanguageDef(); + +void RenderTabItem(const std::string& title, + const std::function& render_func); + +class BitmapViewer { + public: + BitmapViewer() : current_bitmap_index_(0) {} + + void Display(const std::vector& bitmaps) { + 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_index_ + 1, + bitmaps.size()); + + // Buttons to navigate through bitmaps. + if (ImGui::Button("<- Prev")) { + if (current_bitmap_index_ > 0) { + --current_bitmap_index_; + } + } + ImGui::SameLine(); + if (ImGui::Button("Next ->")) { + if (current_bitmap_index_ < bitmaps.size() - 1) { + ++current_bitmap_index_; + } + } + + // Display the current bitmap. + const gfx::Bitmap& current_bitmap = bitmaps[current_bitmap_index_]; + // Assuming Bitmap has a function to get its texture ID, and width and + // height. + ImTextureID tex_id = current_bitmap.texture(); + ImVec2 size(current_bitmap.width(), current_bitmap.height()); + 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(); + } + } + + private: + int current_bitmap_index_; +}; + +} // namespace gui +} // namespace app +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/app/rom.cc b/src/app/rom.cc index 8c6f6310..f1c33777 100644 --- a/src/app/rom.cc +++ b/src/app/rom.cc @@ -1,590 +1,200 @@ #include "rom.h" -#include -#include +#include // for remove +#include // for system_clock +#include // for size_t +#include // for uint32_t, uint8_t +#include // for memcpy +#include // for ctime +#include // for copy_options, copy_options... +#include // for string, fstream, ifstream +#include // for stack +#include // for hash, operator==, char_traits +#include // for unordered_map, operator!= +#include // for tuple_element<>::type +#include // for vector, vector<>::value_type -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" -#include "absl/strings/string_view.h" -#include "app/core/common.h" -#include "app/core/constants.h" -#include "app/gfx/bitmap.h" +#include "absl/container/flat_hash_map.h" // for flat_hash_map, BitMask +#include "absl/status/status.h" // for OkStatus, InternalError +#include "absl/status/statusor.h" // for StatusOr +#include "absl/strings/str_cat.h" // for StrCat +#include "absl/strings/string_view.h" // for string_view, operator== +#include "app/core/constants.h" // for Bytes, ASSIGN_OR_RETURN +#include "app/gfx/bitmap.h" // for Bitmap, BitmapTable +#include "app/gfx/compression.h" // for DecompressV2 +#include "app/gfx/snes_palette.h" // for PaletteGroup, SNESColor +#include "app/gfx/snes_tile.h" // for SnesTo8bppSheet namespace yaze { namespace app { -namespace lc_lz2 { - -void PrintCompressionPiece(const std::shared_ptr& piece) { - printf("Command: %d\n", piece->command); - printf("Command kength: %d\n", piece->length); - printf("Argument:"); - auto arg_size = piece->argument.size(); - for (int i = 0; i < arg_size; ++i) { - printf("%02X ", piece->argument.at(i)); - } - printf("\nArgument length: %d\n", piece->argument_length); -} - -void PrintCompressionChain( - const std::shared_ptr& compressed_chain_start) { - auto compressed_chain = compressed_chain_start->next; - while (compressed_chain != nullptr) { - printf("- Compression Piece -\n"); - PrintCompressionPiece(compressed_chain); - compressed_chain = compressed_chain->next; - } -} - -void CheckByteRepeat(const uchar* rom_data, DataSizeArray& data_size_taken, - CommandArgumentArray& cmd_args, uint& src_data_pos, - const uint last_pos) { - uint pos = src_data_pos; - char byte_to_repeat = rom_data[pos]; - while (pos <= last_pos && rom_data[pos] == byte_to_repeat) { - data_size_taken[kCommandByteFill]++; - pos++; - } - cmd_args[kCommandByteFill][0] = byte_to_repeat; -} - -void CheckWordRepeat(const uchar* rom_data, DataSizeArray& data_size_taken, - CommandArgumentArray& cmd_args, uint& src_data_pos, - const uint last_pos) { - if (src_data_pos + 2 <= last_pos && - rom_data[src_data_pos] != rom_data[src_data_pos + 1]) { - uint pos = src_data_pos; - char byte1 = rom_data[pos]; - char byte2 = rom_data[pos + 1]; - pos += 2; - data_size_taken[kCommandWordFill] = 2; - while (pos + 1 <= last_pos) { - if (rom_data[pos] == byte1 && rom_data[pos + 1] == byte2) - data_size_taken[kCommandWordFill] += 2; - else - break; - pos += 2; - } - cmd_args[kCommandWordFill][0] = byte1; - cmd_args[kCommandWordFill][1] = byte2; - } -} - -void CheckIncByte(const uchar* rom_data, DataSizeArray& data_size_taken, - CommandArgumentArray& cmd_args, uint& src_data_pos, - const uint last_pos) { - uint pos = src_data_pos; - char byte = rom_data[pos]; - pos++; - data_size_taken[kCommandIncreasingFill] = 1; - byte++; - while (pos <= last_pos && byte == rom_data[pos]) { - data_size_taken[kCommandIncreasingFill]++; - byte++; - pos++; - } - cmd_args[kCommandIncreasingFill][0] = rom_data[src_data_pos]; -} - -void CheckIntraCopy(const uchar* rom_data, DataSizeArray& data_size_taken, - CommandArgumentArray& cmd_args, uint& src_data_pos, - const uint last_pos, uint start) { - if (src_data_pos != start) { - uint searching_pos = start; - uint current_pos_u = src_data_pos; - uint copied_size = 0; - uint search_start = start; - - while (searching_pos < src_data_pos && current_pos_u <= last_pos) { - while (rom_data[current_pos_u] != rom_data[searching_pos] && - searching_pos < src_data_pos) - searching_pos++; - search_start = searching_pos; - while (current_pos_u <= last_pos && - rom_data[current_pos_u] == rom_data[searching_pos] && - searching_pos < src_data_pos) { - copied_size++; - current_pos_u++; - searching_pos++; - } - if (copied_size > data_size_taken[kCommandRepeatingBytes]) { - search_start -= start; - printf("- Found repeat of %d at %d\n", copied_size, search_start); - data_size_taken[kCommandRepeatingBytes] = copied_size; - cmd_args[kCommandRepeatingBytes][0] = search_start & kSnesByteMax; - cmd_args[kCommandRepeatingBytes][1] = search_start >> 8; - } - current_pos_u = src_data_pos; - copied_size = 0; - } - } -} - -// Check if a command managed to pick up `max_win` or more bytes -// Avoids being even with copy command, since it's possible to merge copy -void ValidateForByteGain(const DataSizeArray& data_size_taken, - const CommandSizeArray& cmd_size, uint& max_win, - uint& cmd_with_max) { - for (uint cmd_i = 1; cmd_i < 5; cmd_i++) { - uint cmd_size_taken = data_size_taken[cmd_i]; - // TODO(@scawful): Replace conditional with table of command sizes - // "Table that is even with copy but all other cmd are 2" - auto table_check = - !(cmd_i == kCommandRepeatingBytes && cmd_size_taken == 3); - if (cmd_size_taken > max_win && cmd_size_taken > cmd_size[cmd_i] && - table_check) { - printf("==> C:%d / S:%d\n", cmd_i, cmd_size_taken); - cmd_with_max = cmd_i; - max_win = cmd_size_taken; - } - } -} - -void CompressionCommandAlternative( - const uchar* rom_data, std::shared_ptr& compressed_chain, - const CommandSizeArray& cmd_size, const CommandArgumentArray& cmd_args, - uint& src_data_pos, uint& comp_accumulator, uint& cmd_with_max, - uint& max_win) { - printf("- Ok we get a gain from %d\n", cmd_with_max); - std::string buffer; - buffer.push_back(cmd_args[cmd_with_max][0]); - if (cmd_size[cmd_with_max] == 2) { - buffer.push_back(cmd_args[cmd_with_max][1]); - } - - auto new_comp_piece = std::make_shared( - cmd_with_max, max_win, buffer, cmd_size[cmd_with_max]); - PrintCompressionPiece(new_comp_piece); - // If we let non compressed stuff, we need to add a copy chunk before - if (comp_accumulator != 0) { - std::string copy_buff; - copy_buff.resize(comp_accumulator); - for (int i = 0; i < comp_accumulator; ++i) { - copy_buff[i] = rom_data[i + src_data_pos - comp_accumulator]; - } - auto copy_chunk = std::make_shared( - kCommandDirectCopy, comp_accumulator, copy_buff, comp_accumulator); - compressed_chain->next = copy_chunk; - compressed_chain = copy_chunk; - } else { - compressed_chain->next = new_comp_piece; - compressed_chain = new_comp_piece; - } - src_data_pos += max_win; - comp_accumulator = 0; -} - -absl::StatusOr> SplitCompressionPiece( - std::shared_ptr& piece, int mode) { - std::shared_ptr new_piece; - uint length_left = piece->length - kMaxLengthCompression; - piece->length = kMaxLengthCompression; - - switch (piece->command) { - case kCommandByteFill: - case kCommandWordFill: - new_piece = std::make_shared( - piece->command, length_left, piece->argument, piece->argument_length); - break; - case kCommandIncreasingFill: - new_piece = std::make_shared( - piece->command, length_left, piece->argument, piece->argument_length); - new_piece->argument[0] = - (char)(piece->argument[0] + kMaxLengthCompression); - break; - case kCommandDirectCopy: - piece->argument_length = kMaxLengthCompression; - new_piece = std::make_shared( - piece->command, length_left, nullptr, 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); - new_piece = std::make_shared( - piece->command, length_left, piece->argument, piece->argument_length); - if (mode == kNintendoMode2) { - new_piece->argument[0] = - (offset + kMaxLengthCompression) & kSnesByteMax; - new_piece->argument[1] = (offset + kMaxLengthCompression) >> 8; - } - if (mode == kNintendoMode1) { - new_piece->argument[1] = - (offset + kMaxLengthCompression) & kSnesByteMax; - new_piece->argument[0] = (offset + kMaxLengthCompression) >> 8; - } - } break; - default: { - return absl::InvalidArgumentError( - "SplitCompressionCommand: Invalid Command"); - } - } - return new_piece; -} - -Bytes CreateCompressionString(std::shared_ptr& start, - int mode) { - uint pos = 0; - auto piece = start; - Bytes output; - - while (piece != nullptr) { - if (piece->length <= kMaxLengthNormalHeader) { // Normal header - output.push_back(BUILD_HEADER(piece->command, piece->length)); - pos++; - } else { - if (piece->length <= kMaxLengthCompression) { - output.push_back(kCompressionStringMod | ((uchar)piece->command << 2) | - (((piece->length - 1) & 0xFF00) >> 8)); - pos++; - printf("Building extended header : cmd: %d, length: %d - %02X\n", - piece->command, piece->length, output[pos - 1]); - output.push_back(((piece->length - 1) & 0x00FF)); // (char) - pos++; - } else { - // We need to split the command - auto new_piece = SplitCompressionPiece(piece, mode); - if (!new_piece.ok()) { - std::cout << new_piece.status().ToString() << std::endl; - } - printf("New added piece\n"); - auto piece_data = new_piece.value(); - PrintCompressionPiece(piece_data); - piece_data->next = piece->next; - piece->next = piece_data; - continue; - } - } - - if (piece->command == kCommandRepeatingBytes) { - char tmp[2]; - tmp[0] = piece->argument[0]; - tmp[1] = piece->argument[1]; - if (mode == kNintendoMode1) { - tmp[0] = piece->argument[1]; - tmp[1] = piece->argument[0]; - } - for (const auto& each : tmp) { - output.push_back(each); - pos++; - } - } else { - for (int i = 0; i < piece->argument_length; ++i) { - output.push_back(piece->argument[i]); - pos++; - } - } - pos += piece->argument_length; - piece = piece->next; - } - output.push_back(kSnesByteMax); - return output; -} - -absl::Status ValidateCompressionResult( - CompressionPiecePointer& compressed_chain_start, int mode, int start, - int src_data_pos) { - if (compressed_chain_start->next != nullptr) { - ROM temp_rom; - RETURN_IF_ERROR(temp_rom.LoadFromBytes( - CreateCompressionString(compressed_chain_start->next, mode))) - ASSIGN_OR_RETURN(auto decomp_data, temp_rom.Decompress(0, temp_rom.size())) - if (!std::equal(decomp_data.begin() + start, decomp_data.end(), - temp_rom.begin())) { - return absl::InternalError(absl::StrFormat( - "Compressed data does not match uncompressed data at %d\n", - (uint)(src_data_pos - start))); - } +namespace { +absl::Status LoadOverworldMainPalettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 6; i++) { + RETURN_IF_ERROR(palette_groups["ow_main"].AddPalette( + gfx::ReadPaletteFromROM(core::overworldPaletteMain + (i * (35 * 2)), + /*num_colors*/ 35, data))) } return absl::OkStatus(); } -// Merge consecutive copy if possible -CompressionPiecePointer MergeCopy(CompressionPiecePointer& start) { - CompressionPiecePointer piece = start; - - while (piece != nullptr) { - if (piece->command == kCommandDirectCopy && piece->next != nullptr && - piece->next->command == kCommandDirectCopy && - piece->length + piece->next->length <= kMaxLengthCompression) { - uint previous_length = piece->length; - piece->length = piece->length + piece->next->length; - - for (int i = 0; i < piece->next->argument_length; ++i) { - piece->argument[i + previous_length] = piece->next->argument[i]; - } - piece->argument_length = piece->length; - PrintCompressionPiece(piece); - - auto p_next_next = piece->next->next; - piece->next = p_next_next; - continue; // Next could be another copy - } - piece = piece->next; +absl::Status LoadOverworldAuxiliaryPalettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 20; i++) { + RETURN_IF_ERROR(palette_groups["ow_aux"].AddPalette(gfx::ReadPaletteFromROM( + core::overworldPaletteAuxialiary + (i * (21 * 2)), + /*num_colors*/ 21, data))) } - return start; + return absl::OkStatus(); } -} // namespace lc_lz2 - -namespace { - -int GetGraphicsAddress(const uchar* data, uint8_t offset) { - auto part_one = data[kOverworldGraphicsPos1 + offset] << 16; - auto part_two = data[kOverworldGraphicsPos2 + offset] << 8; - auto part_three = data[kOverworldGraphicsPos3 + offset]; - auto snes_addr = (part_one | part_two | part_three); - return core::SnesToPc(snes_addr); +absl::Status LoadOverworldAnimatedPalettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 14; i++) { + RETURN_IF_ERROR( + palette_groups["ow_animated"].AddPalette(gfx::ReadPaletteFromROM( + core::overworldPaletteAnimated + (i * (7 * 2)), 7, data))) + } + return absl::OkStatus(); } -Bytes SnesTo8bppSheet(Bytes sheet, int bpp) { - int xx = 0; // positions where we are at on the sheet - int yy = 0; - int pos = 0; - int ypos = 0; - int num_tiles = 64; - int buffer_size = 0x1000; - if (bpp == 2) { - bpp = 16; - num_tiles = 128; - buffer_size = 0x2000; - } else if (bpp == 3) { - bpp = 24; +absl::Status LoadHUDPalettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 2; i++) { + RETURN_IF_ERROR(palette_groups["hud"].AddPalette( + gfx::ReadPaletteFromROM(core::hudPalettes + (i * 64), 32, data))) } - Bytes sheet_buffer_out(buffer_size); - - for (int i = 0; i < num_tiles; i++) { // for each tiles, 16 per line - for (int y = 0; y < 8; y++) { // for each line - for (int x = 0; x < 8; x++) { //[0] + [1] + [16] - auto b1 = (sheet[(y * 2) + (bpp * pos)] & (kGraphicsBitmap[x])); - auto b2 = (sheet[((y * 2) + (bpp * pos)) + 1] & (kGraphicsBitmap[x])); - auto b3 = (sheet[(16 + y) + (bpp * pos)] & (kGraphicsBitmap[x])); - unsigned char b = 0; - if (b1 != 0) { - b |= 1; - } - if (b2 != 0) { - b |= 2; - } - if (b3 != 0 && bpp != 16) { - b |= 4; - } - sheet_buffer_out[x + xx + (y * 128) + (yy * 1024)] = b; - } - } - pos++; - ypos++; - xx += 8; - if (ypos >= 16) { - yy++; - xx = 0; - ypos = 0; - } - } - return sheet_buffer_out; + return absl::OkStatus(); } +absl::Status LoadGlobalSpritePalettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + RETURN_IF_ERROR(palette_groups["global_sprites"].AddPalette( + gfx::ReadPaletteFromROM(core::globalSpritePalettesLW, 60, data))) + RETURN_IF_ERROR(palette_groups["global_sprites"].AddPalette( + gfx::ReadPaletteFromROM(core::globalSpritePalettesDW, 60, data))) + return absl::OkStatus(); +} + +absl::Status LoadArmorPalettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 5; i++) { + RETURN_IF_ERROR(palette_groups["armors"].AddPalette( + gfx::ReadPaletteFromROM(core::armorPalettes + (i * 30), 15, data))) + } + return absl::OkStatus(); +} + +absl::Status LoadSwordPalettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 4; i++) { + RETURN_IF_ERROR(palette_groups["swords"].AddPalette( + gfx::ReadPaletteFromROM(core::swordPalettes + (i * 6), 3, data))) + } + return absl::OkStatus(); +} + +absl::Status LoadShieldPalettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 3; i++) { + RETURN_IF_ERROR(palette_groups["shields"].AddPalette( + gfx::ReadPaletteFromROM(core::shieldPalettes + (i * 8), 4, data))) + } + return absl::OkStatus(); +} + +absl::Status LoadSpriteAux1Palettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 12; i++) { + RETURN_IF_ERROR(palette_groups["sprites_aux1"].AddPalette( + gfx::ReadPaletteFromROM(core::spritePalettesAux1 + (i * 14), 7, data))) + } + return absl::OkStatus(); +} + +absl::Status LoadSpriteAux2Palettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 11; i++) { + RETURN_IF_ERROR(palette_groups["sprites_aux2"].AddPalette( + gfx::ReadPaletteFromROM(core::spritePalettesAux2 + (i * 14), 7, data))) + } + return absl::OkStatus(); +} + +absl::Status LoadSpriteAux3Palettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 24; i++) { + RETURN_IF_ERROR(palette_groups["sprites_aux3"].AddPalette( + gfx::ReadPaletteFromROM(core::spritePalettesAux3 + (i * 14), 7, data))) + } + return absl::OkStatus(); +} + +absl::Status LoadDungeonMainPalettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 20; i++) { + RETURN_IF_ERROR( + palette_groups["dungeon_main"].AddPalette(gfx::ReadPaletteFromROM( + core::dungeonMainPalettes + (i * 180), 90, data))) + } + return absl::OkStatus(); +} + +absl::Status LoadGrassColors(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + RETURN_IF_ERROR(palette_groups["grass"].AddColor( + gfx::ReadColorFromROM(core::hardcodedGrassLW, rom_data.data()))) + RETURN_IF_ERROR(palette_groups["grass"].AddColor( + gfx::ReadColorFromROM(core::hardcodedGrassDW, rom_data.data()))) + RETURN_IF_ERROR(palette_groups["grass"].AddColor( + gfx::ReadColorFromROM(core::hardcodedGrassSpecial, rom_data.data()))) + return absl::OkStatus(); +} + +absl::Status Load3DObjectPalettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + RETURN_IF_ERROR(palette_groups["3d_object"].AddPalette( + gfx::ReadPaletteFromROM(core::triforcePalette, 8, data))) + RETURN_IF_ERROR(palette_groups["3d_object"].AddPalette( + gfx::ReadPaletteFromROM(core::crystalPalette, 8, data))) + return absl::OkStatus(); +} + +absl::Status LoadOverworldMiniMapPalettes(const Bytes& rom_data, + PaletteGroupMap& palette_groups) { + auto data = rom_data.data(); + for (int i = 0; i < 2; i++) { + RETURN_IF_ERROR( + palette_groups["ow_mini_map"].AddPalette(gfx::ReadPaletteFromROM( + core::overworldMiniMapPalettes + (i * 256), 128, data))) + } + return absl::OkStatus(); +} } // namespace -// TODO TEST compressed data border for each cmd -absl::StatusOr ROM::Compress(const int start, const int length, int mode, - bool check) { - // Worse case should be a copy of the string with extended header - auto compressed_chain = std::make_shared(1, 1, "aaa", 2); - auto compressed_chain_start = compressed_chain; - - CommandArgumentArray cmd_args = {{}}; - DataSizeArray data_size_taken = {0, 0, 0, 0, 0}; - CommandSizeArray cmd_size = {0, 1, 2, 1, 2}; - - uint src_data_pos = start; - uint last_pos = start + length - 1; - uint comp_accumulator = 0; // Used when skipping using copy - - while (true) { - data_size_taken.fill({}); - cmd_args.fill({{}}); - - lc_lz2::CheckByteRepeat(rom_data_.data(), data_size_taken, cmd_args, - src_data_pos, last_pos); - lc_lz2::CheckWordRepeat(rom_data_.data(), data_size_taken, cmd_args, - src_data_pos, last_pos); - lc_lz2::CheckIncByte(rom_data_.data(), data_size_taken, cmd_args, - src_data_pos, last_pos); - lc_lz2::CheckIntraCopy(rom_data_.data(), data_size_taken, cmd_args, - src_data_pos, last_pos, start); - - uint max_win = 2; - uint cmd_with_max = kCommandDirectCopy; - lc_lz2::ValidateForByteGain(data_size_taken, cmd_size, max_win, - cmd_with_max); - - if (cmd_with_max == kCommandDirectCopy) { - // This is the worst case scenario - // Progress through the next byte, in case there's a different - // compression command we can implement before we hit 32 bytes. - src_data_pos++; - comp_accumulator++; - - // Arbitrary choice to do a 32 bytes grouping for copy. - if (comp_accumulator == 32 || src_data_pos > last_pos) { - std::string buffer; - for (int i = 0; i < comp_accumulator; ++i) { - buffer.push_back(rom_data_[i + src_data_pos - comp_accumulator]); - } - auto new_comp_piece = std::make_shared( - kCommandDirectCopy, comp_accumulator, buffer, comp_accumulator); - compressed_chain->next = new_comp_piece; - compressed_chain = new_comp_piece; - comp_accumulator = 0; - } - } else { - lc_lz2::CompressionCommandAlternative( - rom_data_.data(), compressed_chain, cmd_size, cmd_args, src_data_pos, - comp_accumulator, cmd_with_max, max_win); - } - - if (src_data_pos > last_pos) { - printf("Breaking compression loop\n"); - break; - } - - if (check) { - RETURN_IF_ERROR(lc_lz2::ValidateCompressionResult( - compressed_chain_start, mode, start, src_data_pos)) - } - } - - // Skipping compression chain header - lc_lz2::MergeCopy(compressed_chain_start->next); - lc_lz2::PrintCompressionChain(compressed_chain_start); - return lc_lz2::CreateCompressionString(compressed_chain_start->next, mode); -} - -absl::StatusOr ROM::CompressGraphics(const int pos, const int length) { - return Compress(pos, length, kNintendoMode2); -} - -absl::StatusOr ROM::CompressOverworld(const int pos, const int length) { - return Compress(pos, length, kNintendoMode1); -} - -absl::StatusOr ROM::Decompress(int offset, int size, int mode) { - Bytes buffer(size, 0); - uint length = 0; - uint buffer_pos = 0; - uchar command = 0; - uchar header = rom_data_[offset]; - - while (header != kSnesByteMax) { - if ((header & kExpandedMod) == kExpandedMod) { - // Expanded Command - command = ((header >> 2) & kCommandMod); - length = (((header << 8) | rom_data_[offset + 1]) & kExpandedLengthMod); - offset += 2; // Advance 2 bytes in ROM - } else { - // Normal Command - command = ((header >> 5) & kCommandMod); - length = (header & kNormalLengthMod); - offset += 1; // Advance 1 byte in ROM - } - length += 1; // each commands is at least of size 1 even if index 00 - - switch (command) { - case kCommandDirectCopy: // Does not advance in the ROM - memcpy(buffer.data() + buffer_pos, rom_data_.data() + offset, length); - buffer_pos += length; - offset += length; - break; - case kCommandByteFill: - memset(buffer.data() + buffer_pos, (int)(rom_data_[offset]), length); - buffer_pos += length; - offset += 1; // Advances 1 byte in the ROM - break; - case kCommandWordFill: { - auto a = rom_data_[offset]; - auto b = rom_data_[offset + 1]; - for (int i = 0; i < length; i = i + 2) { - buffer[buffer_pos + i] = a; - if ((i + 1) < length) buffer[buffer_pos + i + 1] = b; - } - buffer_pos += length; - offset += 2; // Advance 2 byte in the ROM - } break; - case kCommandIncreasingFill: { - auto inc_byte = rom_data_[offset]; - for (int i = 0; i < length; i++) { - buffer[buffer_pos] = inc_byte++; - buffer_pos++; - } - offset += 1; // Advance 1 byte in the ROM - } break; - case kCommandRepeatingBytes: { - ushort s1 = ((rom_data_[offset + 1] & kSnesByteMax) << 8); - ushort s2 = ((rom_data_[offset] & kSnesByteMax)); - int addr = (s1 | s2); - if (mode == kNintendoMode1) { // Reversed byte order for overworld maps - addr = (rom_data_[offset + 1] & kSnesByteMax) | - ((rom_data_[offset] & kSnesByteMax) << 8); - } - if (addr > offset) { - return absl::InternalError(absl::StrFormat( - "Decompress: Offset for command copy exceeds current position " - "(Offset : %#04x | Pos : %#06x)\n", - addr, offset)); - } - if (buffer_pos + length >= size) { - size *= 2; - buffer.resize(size); - } - memcpy(buffer.data() + buffer_pos, buffer.data() + addr, length); - buffer_pos += length; - offset += 2; - } break; - default: { - std::cout << absl::StrFormat( - "Decompress: Invalid header (Offset : %#06x, Command: %#04x)\n", - offset, command); - } break; - } - // check next byte - header = rom_data_[offset]; - } - - return buffer; -} - -absl::StatusOr ROM::DecompressGraphics(int pos, int size) { - return Decompress(pos, size, kNintendoMode2); -} - -absl::StatusOr ROM::DecompressOverworld(int pos, int size) { - return Decompress(pos, size, kNintendoMode1); -} - -absl::StatusOr ROM::Load2bppGraphics() { +absl::StatusOr ROM::Load2BppGraphics() { Bytes sheet; const uint8_t sheets[] = {113, 114, 218, 219, 220, 221}; for (const auto& sheet_id : sheets) { - auto offset = GetGraphicsAddress(rom_data_.data(), sheet_id); - ASSIGN_OR_RETURN(auto decomp_sheet, Decompress(offset)) - auto converted_sheet = SnesTo8bppSheet(decomp_sheet, 2); + auto offset = GetGraphicsAddress(data(), sheet_id); + ASSIGN_OR_RETURN(auto decomp_sheet, + gfx::lc_lz2::DecompressV2(data(), offset)) + auto converted_sheet = gfx::SnesTo8bppSheet(decomp_sheet, 2); for (const auto& each_pixel : converted_sheet) { sheet.push_back(each_pixel); } @@ -592,19 +202,35 @@ absl::StatusOr ROM::Load2bppGraphics() { return sheet; } -// 0-112 -> compressed 3bpp bgr -> (decompressed each) 0x600 chars -// 113-114 -> compressed 2bpp -> (decompressed each) 0x800 chars -// 115-126 -> uncompressed 3bpp sprites -> (each) 0x600 chars -// 127-217 -> compressed 3bpp sprites -> (decompressed each) 0x600 chars -// 218-222 -> compressed 2bpp -> (decompressed each) 0x800 chars +// TODO: Load Links graphics from the ROM +absl::Status ROM::LoadLinkGraphics() { + const auto link_gfx_offset = 81920; // $10:8000 + const auto link_gfx_length = 0x800; + + // Load Links graphics from the ROM + for (int i = 0; i < 14; i++) { + ASSIGN_OR_RETURN( + auto link_sheet_data, + ReadByteVector(/*offset=*/link_gfx_offset + (i * link_gfx_length), + /*length=*/link_gfx_length)) + auto link_sheet_8bpp = gfx::SnesTo8bppSheet(link_sheet_data, /*bpp=*/4); + link_graphics_[i].Create(core::kTilesheetWidth, core::kTilesheetHeight, + core::kTilesheetDepth, link_sheet_8bpp); + link_graphics_[i].ApplyPalette(link_palette_); + RenderBitmap(&link_graphics_[i]); + } + + return absl::OkStatus(); +} + absl::Status ROM::LoadAllGraphicsData() { Bytes sheet; bool bpp3 = false; - for (int i = 0; i < core::NumberOfSheets; i++) { + for (int i = 0; i < kNumGfxSheets; i++) { if (i >= 115 && i <= 126) { // uncompressed sheets sheet.resize(core::Uncompressed3BPPSize); - auto offset = GetGraphicsAddress(rom_data_.data(), i); + auto offset = GetGraphicsAddress(data(), i); for (int j = 0; j < core::Uncompressed3BPPSize; j++) { sheet[j] = rom_data_[j + offset]; } @@ -612,23 +238,33 @@ absl::Status ROM::LoadAllGraphicsData() { } else if (i == 113 || i == 114 || i >= 218) { bpp3 = false; } else { - auto offset = GetGraphicsAddress(rom_data_.data(), i); - ASSIGN_OR_RETURN(sheet, Decompress(offset)) + auto offset = GetGraphicsAddress(data(), i); + ASSIGN_OR_RETURN(sheet, gfx::lc_lz2::DecompressV2(data(), offset)) bpp3 = true; } if (bpp3) { - auto converted_sheet = SnesTo8bppSheet(sheet, 3); + auto converted_sheet = gfx::SnesTo8bppSheet(sheet, 3); + if (flags()->kUseBitmapManager) { + graphics_manager_.LoadBitmap(i, converted_sheet, core::kTilesheetWidth, + core::kTilesheetHeight, + core::kTilesheetDepth); + graphics_manager_[i]->ApplyPaletteWithTransparent( + palette_groups_["dungeon_main"][0], 0); + graphics_manager_[i]->CreateTexture(renderer_); + } graphics_bin_[i] = gfx::Bitmap(core::kTilesheetWidth, core::kTilesheetHeight, - core::kTilesheetDepth, converted_sheet.data(), 0x1000); + core::kTilesheetDepth, converted_sheet); graphics_bin_.at(i).CreateTexture(renderer_); - for (int j = 0; j < graphics_bin_.at(i).GetSize(); ++j) { - graphics_buffer_.push_back(graphics_bin_.at(i).GetByte(j)); + if (flags()->kUseBitmapManager) { + for (int j = 0; j < graphics_manager_[i].get()->size(); ++j) { + graphics_buffer_.push_back(graphics_manager_[i]->at(j)); + } } } else { - for (int j = 0; j < graphics_bin_.at(0).GetSize(); ++j) { + for (int j = 0; j < graphics_bin_[0].size(); ++j) { graphics_buffer_.push_back(0xFF); } } @@ -636,27 +272,76 @@ absl::Status ROM::LoadAllGraphicsData() { return absl::OkStatus(); } -absl::Status ROM::LoadFromFile(const absl::string_view& filename) { +absl::Status ROM::LoadAllPalettes() { + RETURN_IF_ERROR(LoadOverworldMainPalettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadOverworldAuxiliaryPalettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadOverworldAnimatedPalettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadHUDPalettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadGlobalSpritePalettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadArmorPalettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadSwordPalettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadShieldPalettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadSpriteAux1Palettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadSpriteAux2Palettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadSpriteAux3Palettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadDungeonMainPalettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadGrassColors(rom_data_, palette_groups_)) + RETURN_IF_ERROR(Load3DObjectPalettes(rom_data_, palette_groups_)) + RETURN_IF_ERROR(LoadOverworldMiniMapPalettes(rom_data_, palette_groups_)) + return absl::OkStatus(); +} + +absl::Status ROM::LoadFromFile(const absl::string_view& filename, + bool z3_load) { + // Set filename filename_ = filename; + + // Open file std::ifstream file(filename.data(), std::ios::binary); if (!file.is_open()) { return absl::InternalError( absl::StrCat("Could not open ROM file: ", filename)); } + // Get file size and resize rom_data_ size_ = std::filesystem::file_size(filename); rom_data_.resize(size_); - for (auto i = 0; i < size_; ++i) { - char byte_to_read = ' '; - file.read(&byte_to_read, sizeof(char)); - rom_data_[i] = byte_to_read; + + // Read file into rom_data_ + file.read(reinterpret_cast(rom_data_.data()), size_); + + // Check if the sROM has a header + constexpr size_t baseROMSize = 1048576; // 1MB + constexpr size_t headerSize = 0x200; // 512 bytes + if (size_ % baseROMSize == headerSize) { + has_header_ = true; } - // copy ROM title - memcpy(title, rom_data_.data() + kTitleStringOffset, kTitleStringLength); + // Remove header if present + if (has_header_) { + auto header = + std::vector(rom_data_.begin(), rom_data_.begin() + 0x200); + rom_data_.erase(rom_data_.begin(), rom_data_.begin() + 0x200); + size_ -= 0x200; + } + // Close file file.close(); - LoadAllPalettes(); + + // Load Zelda 3 specific data if requested + if (z3_load) { + // Copy ROM title + memcpy(title_, rom_data_.data() + kTitleStringOffset, kTitleStringLength); + if (rom_data_[kTitleStringOffset + 0x19] == 0) { + version_ = Z3_Version::JP; + } else { + version_ = Z3_Version::US; + } + RETURN_IF_ERROR(LoadAllPalettes()) + LoadGfxGroups(); + } + + // Set is_loaded_ flag and return success is_loaded_ = true; return absl::OkStatus(); } @@ -677,175 +362,135 @@ absl::Status ROM::LoadFromBytes(const Bytes& data) { "Could not load ROM: parameter `data` is empty."); } rom_data_ = data; + size_ = data.size(); + is_loaded_ = true; return absl::OkStatus(); } -absl::Status ROM::SaveToFile() { - std::fstream file(filename_.data(), std::ios::binary | std::ios::out); - if (!file.is_open()) { - return absl::InternalError( - absl::StrCat("Could not open ROM file: ", filename_)); - } - for (auto i = 0; i < size_; ++i) { - file << rom_data_[i]; - } - return absl::OkStatus(); -} - -void ROM::RenderBitmap(gfx::Bitmap* bitmap) const { - bitmap->CreateTexture(renderer_); -} - -gfx::SNESColor ROM::ReadColor(int offset) { - short color = toint16(offset); - gfx::snes_color new_color; - new_color.red = (color & 0x1F) * 8; - new_color.green = ((color >> 5) & 0x1F) * 8; - new_color.blue = ((color >> 10) & 0x1F) * 8; - gfx::SNESColor snes_color(new_color); - return snes_color; -} - -gfx::SNESPalette ROM::ReadPalette(int offset, int num_colors) { - int color_offset = 0; - std::vector colors(num_colors); - - while (color_offset < num_colors) { - short color = toint16(offset); - gfx::snes_color new_color; - new_color.red = (color & 0x1F) * 8; - new_color.green = ((color >> 5) & 0x1F) * 8; - new_color.blue = ((color >> 10) & 0x1F) * 8; - colors[color_offset].setSNES(new_color); - color_offset++; - offset += 2; +absl::Status ROM::SaveToFile(bool backup, absl::string_view filename) { + if (rom_data_.empty()) { + return absl::InternalError("ROM data is empty."); } - gfx::SNESPalette palette(colors); - return palette; -} - -void ROM::LoadAllPalettes() { - // 35 colors each, 7x5 (0,2 on grid) - for (int i = 0; i < 6; i++) { - palette_groups_["ow_main"].AddPalette( - ReadPalette(core::overworldPaletteMain + (i * (35 * 2)), 35)); - } - // 21 colors each, 7x3 (8,2 and 8,5 on grid) - for (int i = 0; i < 20; i++) { - palette_groups_["ow_aux"].AddPalette( - ReadPalette(core::overworldPaletteAuxialiary + (i * (21 * 2)), 21)); - } - // 7 colors each 7x1 (0,7 on grid) - for (int i = 0; i < 14; i++) { - palette_groups_["ow_animated"].AddPalette( - ReadPalette(core::overworldPaletteAnimated + (i * (7 * 2)), 7)); - } - // 32 colors each 16x2 (0,0 on grid) - for (int i = 0; i < 2; i++) { - palette_groups_["hud"].AddPalette( - ReadPalette(core::hudPalettes + (i * 64), 32)); + // Check if filename is empty + if (filename == "") { + filename = filename_; } - palette_groups_["global_sprites"].AddPalette( - ReadPalette(core::globalSpritePalettesLW, 60)); - palette_groups_["global_sprites"].AddPalette( - ReadPalette(core::globalSpritePalettesDW, 60)); + // Check if backup is enabled + if (backup) { + // Create a backup file with timestamp in its name + auto now = std::chrono::system_clock::now(); + auto now_c = std::chrono::system_clock::to_time_t(now); + std::string backup_filename = + absl::StrCat(filename, "_backup_", std::ctime(&now_c)); - for (int i = 0; i < 5; i++) { - palette_groups_["armors"].AddPalette( - ReadPalette(core::armorPalettes + (i * 30), 15)); - } - for (int i = 0; i < 4; i++) { - palette_groups_["swords"].AddPalette( - ReadPalette(core::swordPalettes + (i * 6), 3)); - } - for (int i = 0; i < 3; i++) { - palette_groups_["shields"].AddPalette( - ReadPalette(core::shieldPalettes + (i * 8), 4)); - } - for (int i = 0; i < 12; i++) { - palette_groups_["sprites_aux1"].AddPalette( - ReadPalette(core::spritePalettesAux1 + (i * 14), 7)); - } - for (int i = 0; i < 11; i++) { - palette_groups_["sprites_aux2"].AddPalette( - ReadPalette(core::spritePalettesAux2 + (i * 14), 7)); - } - for (int i = 0; i < 24; i++) { - palette_groups_["sprites_aux3"].AddPalette( - ReadPalette(core::spritePalettesAux3 + (i * 14), 7)); - } - for (int i = 0; i < 20; i++) { - palette_groups_["dungeon_main"].AddPalette( - ReadPalette(core::dungeonMainPalettes + (i * 180), 90)); + // Remove newline character from ctime() + backup_filename.erase( + std::remove(backup_filename.begin(), backup_filename.end(), '\n'), + backup_filename.end()); + + // Replace spaces with underscores + std::replace(backup_filename.begin(), backup_filename.end(), ' ', '_'); + + // Now, copy the original file to the backup file + std::filesystem::copy(filename, backup_filename, + std::filesystem::copy_options::overwrite_existing); } - palette_groups_["grass"].AddColor(ReadColor(core::hardcodedGrassLW)); - palette_groups_["grass"].AddColor(ReadColor(core::hardcodedGrassDW)); - palette_groups_["grass"].AddColor(ReadColor(core::hardcodedGrassSpecial)); - - palette_groups_["3d_object"].AddPalette( - ReadPalette(core::triforcePalette, 8)); - palette_groups_["3d_object"].AddPalette(ReadPalette(core::crystalPalette, 8)); - - for (int i = 0; i < 2; i++) { - palette_groups_["ow_mini_map"].AddPalette( - ReadPalette(core::overworldMiniMapPalettes + (i * 256), 128)); + // Run the other save functions + if (flags()->kSaveAllPalettes) { + SaveAllPalettes(); } -} -absl::Status ROM::ApplyAssembly(const absl::string_view& filename, - size_t patch_size) { - int count = 0; - auto patch = filename.data(); - auto data = (char*)rom_data_.data(); - if (int size = size_; !asar_patch(patch, data, patch_size, &size)) { - auto asar_error = asar_geterrors(&count); - auto full_error = asar_error->fullerrdata; - return absl::InternalError(absl::StrCat("ASAR Error: ", full_error)); - } - return absl::OkStatus(); -} - -// TODO(scawful): Test me! -absl::Status ROM::PatchOverworldMosaic( - char mosaic_tiles[core::kNumOverworldMaps], int routine_offset) { - // Write the data for the mosaic tile array used by the assembly code. - for (int i = 0; i < core::kNumOverworldMaps; i++) { - if (mosaic_tiles[i]) { - rom_data_[core::overworldCustomMosaicArray + i] = 0x01; - } else { - rom_data_[core::overworldCustomMosaicArray + i] = 0x00; + if (flags()->kSaveWithChangeQueue) { + while (!changes_.empty()) { + auto change = changes_.top(); + change(); + changes_.pop(); } } - std::string filename = "assets/asm/mosaic_change.asm"; - std::fstream file(filename, std::ios::out | std::ios::in); - if (!file.is_open()) { - return absl::InvalidArgumentError( - "Unable to open mosaic change assembly source"); - } - - std::stringstream assembly; - assembly << file.rdbuf(); - file.close(); - auto assembly_string = assembly.str(); - - if (!core::StringReplace(assembly_string, "", kMosaicChangeOffset)) { + // Open the file that we know exists for writing + std::ofstream file(filename.data(), std::ios::binary); + if (!file) { return absl::InternalError( - "Mosaic template did not have proper `` to replace."); + absl::StrCat("Could not open ROM file: ", filename)); } - if (!core::StringReplace( - assembly_string, "", - absl::StrFormat("$%x", routine_offset + kSNESToPCOffset))) { + // Save the data to the file + try { + file.write( + static_cast(static_cast(rom_data_.data())), + rom_data_.size()); + } catch (const std::ofstream::failure& e) { + return absl::InternalError(absl::StrCat( + "Error while writing to ROM file: ", filename, " - ", e.what())); + } + + // Check for write errors + if (!file) { return absl::InternalError( - "Mosaic template did not have proper `` to replace."); + absl::StrCat("Error while writing to ROM file: ", filename)); } - return ApplyAssembly(filename, assembly_string.size()); + return absl::OkStatus(); } +void ROM::SavePalette(int index, const std::string& group_name, + gfx::SNESPalette& palette) { + // Iterate through all colors in the palette + for (size_t j = 0; j < palette.size(); ++j) { + gfx::SNESColor color = palette[j]; + // If the color is modified, save the color to the ROM + if (color.IsModified()) { + WriteColor(gfx::GetPaletteAddress(group_name, index, j), color); + color.SetModified(false); // Reset the modified flag after saving + } + } +} + +void ROM::SaveAllPalettes() { + // Iterate through all palette_groups_ + for (auto& [group_name, palettes] : palette_groups_) { + // Iterate through all palettes in the group + for (size_t i = 0; i < palettes.size(); ++i) { + auto palette = palettes[i]; + SavePalette(i, group_name, palette); + } + } +} + +absl::Status ROM::UpdatePaletteColor(const std::string& groupName, + size_t paletteIndex, size_t colorIndex, + const gfx::SNESColor& newColor) { + // Check if the groupName exists in the palette_groups_ map + if (palette_groups_.find(groupName) != palette_groups_.end()) { + // Check if the paletteIndex is within the range of available palettes in + // the group + if (paletteIndex < palette_groups_[groupName].size()) { + // Check if the colorIndex is within the range of available colors in the + // palette + if (colorIndex < palette_groups_[groupName][paletteIndex].size()) { + // Update the color value in the palette + palette_groups_[groupName][paletteIndex][colorIndex] = newColor; + palette_groups_[groupName][paletteIndex][colorIndex].SetModified(true); + } else { + return absl::AbortedError( + "Error: Invalid color index in UpdatePaletteColor."); + } + } else { + return absl::AbortedError( + "Error: Invalid palette index in UpdatePaletteColor."); + } + } else { + return absl::AbortedError( + "Error: Invalid group name in UpdatePaletteColor"); + } + return absl::OkStatus(); +} + +std::shared_ptr SharedROM::shared_rom_ = nullptr; + } // namespace app } // namespace yaze \ No newline at end of file diff --git a/src/app/rom.h b/src/app/rom.h index 5e2a59b6..c9b951c3 100644 --- a/src/app/rom.h +++ b/src/app/rom.h @@ -4,129 +4,445 @@ #include #include -#include +#include +#include +#include // for size_t +#include // for uint32_t, uint8_t, uint16_t #include +#include #include #include -#include -#include -#include -#include -#include +#include // for function +#include // for string, operator<<, basic_... +#include // for map +#include // for shared_ptr, make_shared +#include // for stack +#include // for hash, operator== +#include // for unordered_map +#include +#include // for vector -#include "absl/status/status.h" -#include "absl/status/statusor.h" +#include "SDL_render.h" // for SDL_Renderer +#include "absl/container/flat_hash_map.h" // for flat_hash_map +#include "absl/status/status.h" // for Status +#include "absl/status/statusor.h" // for StatusOr #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" -#include "absl/strings/string_view.h" +#include "absl/strings/string_view.h" // for string_view #include "app/core/common.h" -#include "app/core/constants.h" -#include "app/gfx/bitmap.h" - -#define BUILD_HEADER(command, length) (command << 5) + (length - 1) +#include "app/core/constants.h" // for Bytes, uchar, armorPalettes +#include "app/gfx/bitmap.h" // for Bitmap, BitmapTable +#include "app/gfx/compression.h" +#include "app/gfx/snes_palette.h" // for PaletteGroup, SNESColor +#include "app/gfx/snes_tile.h" namespace yaze { namespace app { -constexpr int kCommandDirectCopy = 0; -constexpr int kCommandByteFill = 1; -constexpr int kCommandWordFill = 2; -constexpr int kCommandIncreasingFill = 3; -constexpr int kCommandRepeatingBytes = 4; -constexpr int kCommandLongLength = 7; -constexpr int kMaxLengthNormalHeader = 32; -constexpr int kMaxLengthCompression = 1024; -constexpr int kNintendoMode1 = 0; -constexpr int kNintendoMode2 = 1; -constexpr int kTile32Num = 4432; -constexpr int kTitleStringOffset = 0x7FC0; -constexpr int kTitleStringLength = 20; -constexpr int kOverworldGraphicsPos1 = 0x4F80; -constexpr int kOverworldGraphicsPos2 = 0x505F; -constexpr int kOverworldGraphicsPos3 = 0x513E; -constexpr int kSnesByteMax = 0xFF; -constexpr int kCommandMod = 0x07; -constexpr int kExpandedMod = 0xE0; -constexpr int kExpandedLengthMod = 0x3FF; -constexpr int kNormalLengthMod = 0x1F; -constexpr int kCompressionStringMod = 7 << 5; -constexpr uchar kGraphicsBitmap[8] = {0x80, 0x40, 0x20, 0x10, - 0x08, 0x04, 0x02, 0x01}; +using PaletteGroupMap = std::unordered_map; -const std::string kMosaicChangeOffset = "$02AADB"; -constexpr int kSNESToPCOffset = 0x138000; - -using CommandArgumentArray = std::array, 5>; -using CommandSizeArray = std::array; -using DataSizeArray = std::array; -struct CompressionPiece { - char command; - int length; - int argument_length; - std::string argument; - std::shared_ptr next = nullptr; - CompressionPiece() = default; - CompressionPiece(int cmd, int len, std::string args, int arg_len) - : command(cmd), length(len), argument_length(arg_len), argument(args) {} +// Define an enum class for the different versions of the game +enum class Z3_Version { + US = 1, + JP = 2, + SD = 3, + RANDO = 4, }; -using CompressionPiece = struct CompressionPiece; -using CompressionPiecePointer = std::shared_ptr; -class ROM { +// Define a struct to hold the version-specific constants +struct VersionConstants { + uint32_t kGfxAnimatedPointer; + uint32_t kOverworldGfxGroups1; + uint32_t kOverworldGfxGroups2; + uint32_t kCompressedAllMap32PointersHigh; + uint32_t kCompressedAllMap32PointersLow; + uint32_t overworldMapPaletteGroup; + uint32_t overlayPointers; + uint32_t overlayPointersBank; + uint32_t overworldTilesType; + uint32_t kOverworldGfxPtr1; + uint32_t kOverworldGfxPtr2; + uint32_t kOverworldGfxPtr3; + uint32_t kMap32TileTL; + uint32_t kMap32TileTR; + uint32_t kMap32TileBL; + uint32_t kMap32TileBR; + uint32_t kSpriteBlocksetPointer; + uint32_t kDungeonPalettesGroups; +}; + +// Define a map to hold the version constants for each version +static const std::map kVersionConstantsMap = { + {Z3_Version::US, + { + 0x10275, // kGfxAnimatedPointer + 0x5D97, // kOverworldGfxGroups1 + 0x6073, // kOverworldGfxGroups2 + 0x1794D, // kCompressedAllMap32PointersHigh + 0x17B2D, // kCompressedAllMap32PointersLow + 0x75504, // overworldMapPaletteGroup + 0x77664, // overlayPointers + 0x0E, // overlayPointersBank + 0x71459, // overworldTilesType + 0x4F80, // kOverworldGfxPtr1 + 0x505F, // kOverworldGfxPtr2 + 0x513E, // kOverworldGfxPtr3 + 0x18000, // kMap32TileTL + 0x1B400, // kMap32TileTR + 0x20000, // kMap32TileBL + 0x23400, // kMap32TileBR + 0x5B57, // kSpriteBlocksetPointer + 0x75460, // kDungeonPalettesGroups + }}, + {Z3_Version::JP, + { + 0x10624, // kGfxAnimatedPointer + 0x5DD7, // kOverworldGfxGroups1 + 0x60B3, // kOverworldGfxGroups2 + 0x176B1, // kCompressedAllMap32PointersHigh + 0x17891, // kCompressedAllMap32PointersLow + 0x67E74, // overworldMapPaletteGroup + 0x3FAF4, // overlayPointers + 0x07, // overlayPointersBank + 0x7FD94, // overworldTilesType + 0x4FC0, // kOverworldGfxPtr1 + 0x509F, // kOverworldGfxPtr2 + 0x517E, // kOverworldGfxPtr3 + 0x18000, // kMap32TileTL + 0x1B3C0, // kMap32TileTR + 0x20000, // kMap32TileBL + 0x233C0, // kMap32TileBR + 0x5B97, // kSpriteBlocksetPointer + 0x67DD0, // kDungeonPalettesGroups + }}}; + +// Define some constants used throughout the ROM class +constexpr uint32_t kOverworldGraphicsPos1 = 0x4F80; +constexpr uint32_t kOverworldGraphicsPos2 = 0x505F; +constexpr uint32_t kOverworldGraphicsPos3 = 0x513E; +constexpr uint32_t kTile32Num = 4432; +constexpr uint32_t kTitleStringOffset = 0x7FC0; +constexpr uint32_t kTitleStringLength = 20; +constexpr uint32_t kNumGfxSheets = 223; +constexpr uint32_t kNormalGfxSpaceStart = 0x87000; +constexpr uint32_t kNormalGfxSpaceEnd = 0xC4200; +constexpr uint32_t kLinkSpriteLocation = 0x80000; +constexpr uint32_t kFontSpriteLocation = 0x70000; +constexpr uint32_t gfx_groups_pointer = 0x6237; + +struct WriteAction { + int address; + std::variant, gfx::SNESColor> + value; +}; + +class ROM : public core::ExperimentFlags { public: - absl::StatusOr Compress(const int start, const int length, - int mode = 1, bool check = false); - absl::StatusOr CompressGraphics(const int pos, const int length); - absl::StatusOr CompressOverworld(const int pos, const int length); + template + absl::Status RunTransaction(Args... args) { + absl::Status status; + // Fold expression to apply the Write function on each argument + ((status = WriteHelper(args)), ...); + return status; + } - absl::StatusOr Decompress(int offset, int size = 0x800, int mode = 1); - absl::StatusOr DecompressGraphics(int pos, int size); - absl::StatusOr DecompressOverworld(int pos, int size); + absl::Status WriteHelper(const WriteAction& action) { + if (std::holds_alternative(action.value) || + std::holds_alternative(action.value)) { + return Write(action.address, std::get(action.value)); + } else if (std::holds_alternative(action.value)) { + return WriteShort(action.address, std::get(action.value)); + } else if (std::holds_alternative>(action.value)) { + return WriteVector(action.address, + std::get>(action.value)); + } else if (std::holds_alternative(action.value)) { + return WriteColor(action.address, std::get(action.value)); + } + return absl::InvalidArgumentError("Invalid write argument type"); + } - absl::StatusOr Load2bppGraphics(); + /** + * 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(); + absl::Status LoadLinkGraphics(); + + /** + * This function iterates over all graphics sheets in the ROM and loads them + * into memory. Depending on the sheet's index, it may be uncompressed or + * compressed using the LC-LZ2 algorithm. The uncompressed sheets are 3 bits + * per pixel (BPP), while the compressed sheets are 4 BPP. The loaded graphics + * data is converted to 8 BPP and stored in a bitmap. + * + * The graphics sheets are divided into the following ranges: + * 0-112 -> compressed 3bpp bgr -> (decompressed each) 0x600 chars + * 113-114 -> compressed 2bpp -> (decompressed each) 0x800 chars + * 115-126 -> uncompressed 3bpp sprites -> (each) 0x600 chars + * 127-217 -> compressed 3bpp sprites -> (decompressed each) 0x600 chars + * 218-222 -> compressed 2bpp -> (decompressed each) 0x800 chars + * + */ absl::Status LoadAllGraphicsData(); - absl::Status LoadFromFile(const absl::string_view& filename); + + /** + * @brief Loads all the palettes for the game. + * + * This function loads all the palettes for the game, including overworld, + * HUD, armor, swords, shields, sprites, dungeon, grass, and 3D object + * palettes. It also adds the loaded palettes to their respective palette + * groups. + * + */ + absl::Status LoadAllPalettes(); + + /** + * Load ROM data from a file. + * + * @param filename The name of the file to load. + * @param z3_load Whether to load data specific to Zelda 3. + * + */ + absl::Status LoadFromFile(const absl::string_view& filename, + bool z3_load = true); absl::Status LoadFromPointer(uchar* data, size_t length); absl::Status LoadFromBytes(const Bytes& data); - void LoadAllPalettes(); - absl::Status SaveToFile(); + /** + * @brief Saves the ROM data to a file + * + * @param backup If true, creates a backup file with timestamp in its name + * @param filename The name of the file to save the ROM data to + * + * @return absl::Status Returns an OK status if the save was successful, + * otherwise returns an error status + */ + absl::Status SaveToFile(bool backup, absl::string_view filename = ""); - gfx::SNESColor ReadColor(int offset); - gfx::SNESPalette ReadPalette(int offset, int num_colors); + /** + * Saves the given palette to the ROM if any of its colors have been modified. + * + * @param index The index of the palette to save. + * @param group_name The name of the group containing the palette. + * @param palette The palette to save. + */ + void SavePalette(int index, const std::string& group_name, + gfx::SNESPalette& palette); - void RenderBitmap(gfx::Bitmap* bitmap) const; + /** + * @brief Saves all palettes in the ROM. + * + * This function iterates through all palette groups and all palettes in each + * group, and saves each palette using the SavePalette() function. + */ + void SaveAllPalettes(); - absl::Status ApplyAssembly(const absl::string_view& filename, - size_t patch_size); - absl::Status PatchOverworldMosaic(char mosaic_tiles[core::kNumOverworldMaps], - int routine_offset); + /** + * @brief Updates a color in a specified palette group. + * + * This function updates the color at the specified `colorIndex` in the + * palette at `palette_index` within the palette group with the given + * `group_name`. If the group, palette, or color indices are invalid, an error + * is returned. + * + * @param group_name The name of the palette group to update. + * @param palette_index The index of the palette within the group to update. + * @param colorIndex The index of the color within the palette to update. + * @param newColor The new color value to set. + * + * @return An `absl::Status` indicating whether the update was successful. + * Returns `absl::OkStatus()` if successful, or an error status if the + * group, palette, or color indices are invalid. + */ + absl::Status UpdatePaletteColor(const std::string& group_name, + size_t palette_index, size_t colorIndex, + const gfx::SNESColor& newColor); - auto GetTitle() const { return title; } - auto GetGraphicsBin() const { return graphics_bin_; } - auto GetGraphicsBuffer() const { return graphics_buffer_; } - auto GetPaletteGroup(std::string group) { return palette_groups_[group]; } - void SetupRenderer(std::shared_ptr renderer) { - renderer_ = renderer; + // Read functions + absl::StatusOr ReadByte(int offset) { + if (offset >= rom_data_.size()) { + return absl::InvalidArgumentError("Offset out of range"); + } + return rom_data_[offset]; } - auto isLoaded() const { return is_loaded_; } + + absl::StatusOr ReadWord(int offset) { + if (offset + 1 >= rom_data_.size()) { + return absl::InvalidArgumentError("Offset out of range"); + } + auto result = (uint16_t)(rom_data_[offset] | (rom_data_[offset + 1] << 8)); + return result; + } + + absl::StatusOr ReadLong(int offset) { + if (offset + 2 >= rom_data_.size()) { + return absl::InvalidArgumentError("Offset out of range"); + } + auto result = (uint32_t)(rom_data_[offset] | (rom_data_[offset + 1] << 8) | + (rom_data_[offset + 2] << 16)); + return result; + } + + absl::StatusOr> ReadByteVector(uint32_t offset, + uint32_t length) { + if (offset + length > rom_data_.size()) { + return absl::InvalidArgumentError("Offset and length out of range"); + } + std::vector result; + for (int i = offset; i < length; i++) { + result.push_back(rom_data_[i]); + } + return result; + } + + absl::StatusOr ReadTile16(uint32_t tile16_id) { + // Skip 8 bytes per tile. + auto tpos = 0x78000 + (tile16_id * 0x08); + gfx::Tile16 tile16; + ASSIGN_OR_RETURN(auto new_tile0, ReadWord(tpos)) + tile16.tile0_ = gfx::WordToTileInfo(new_tile0); + tpos += 2; + ASSIGN_OR_RETURN(auto new_tile1, ReadWord(tpos)) + tile16.tile1_ = gfx::WordToTileInfo(new_tile1); + tpos += 2; + ASSIGN_OR_RETURN(auto new_tile2, ReadWord(tpos)) + tile16.tile2_ = gfx::WordToTileInfo(new_tile2); + tpos += 2; + ASSIGN_OR_RETURN(auto new_tile3, ReadWord(tpos)) + tile16.tile3_ = gfx::WordToTileInfo(new_tile3); + return tile16; + } + + absl::Status WriteTile16(int tile16_id, const gfx::Tile16& tile) { + // Skip 8 bytes per tile. + auto tpos = 0x78000 + (tile16_id * 0x08); + RETURN_IF_ERROR(WriteShort(tpos, gfx::TileInfoToWord(tile.tile0_))); + tpos += 2; + RETURN_IF_ERROR(WriteShort(tpos, gfx::TileInfoToWord(tile.tile1_))); + tpos += 2; + RETURN_IF_ERROR(WriteShort(tpos, gfx::TileInfoToWord(tile.tile2_))); + tpos += 2; + RETURN_IF_ERROR(WriteShort(tpos, gfx::TileInfoToWord(tile.tile3_))); + return absl::OkStatus(); + } + + // Write functions + absl::Status Write(int addr, int value) { + if (addr >= rom_data_.size()) { + return absl::InvalidArgumentError("Address out of range"); + } + rom_data_[addr] = value; + return absl::OkStatus(); + } + + absl::Status WriteShort(uint32_t addr, uint16_t value) { + if (addr + 1 >= rom_data_.size()) { + return absl::InvalidArgumentError("Address out of range"); + } + rom_data_[addr] = (uint8_t)(value & 0xFF); + rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF); + return absl::OkStatus(); + } + + absl::Status WriteVector(int addr, std::vector data) { + if (addr + data.size() > rom_data_.size()) { + return absl::InvalidArgumentError("Address and data size out of range"); + } + for (int i = 0; i < data.size(); i++) { + rom_data_[addr + i] = data[i]; + } + return absl::OkStatus(); + } + + absl::Status WriteColor(uint32_t address, const gfx::SNESColor& color) { + uint16_t bgr = ((color.GetSNES() >> 10) & 0x1F) | + ((color.GetSNES() & 0x1F) << 10) | + (color.GetSNES() & 0x7C00); + + // Write the 16-bit color value to the ROM at the specified address + return WriteShort(address, bgr); + } + + void Expand(int size) { + rom_data_.resize(size); + size_ = size; + } + + absl::Status Reload() { + if (filename_.empty()) { + return absl::InvalidArgumentError("No filename specified"); + } + return LoadFromFile(filename_); + } + + absl::Status Close() { + rom_data_.clear(); + size_ = 0; + is_loaded_ = false; + return absl::OkStatus(); + } + + void QueueChanges(std::function const& function) { + changes_.push(function); + } + + VersionConstants version_constants() const { + return kVersionConstantsMap.at(version_); + } + + int GetGraphicsAddress(const uchar* data, uint8_t addr) const { + auto part_one = data[version_constants().kOverworldGfxPtr1 + addr] << 16; + auto part_two = data[version_constants().kOverworldGfxPtr2 + addr] << 8; + auto part_three = data[version_constants().kOverworldGfxPtr3 + addr]; + auto snes_addr = (part_one | part_two | part_three); + return core::SnesToPc(snes_addr); + } + + gfx::PaletteGroup palette_group(const std::string& group) { + return palette_groups_[group]; + } + auto mutable_palette_group(const std::string& group) { + return &palette_groups_[group]; + } + + Bytes graphics_buffer() const { return graphics_buffer_; } + + gfx::BitmapTable graphics_bin() const { return graphics_bin_; } + + gfx::Bitmap* mutable_graphics_sheet(int index) { + return &graphics_bin_.at(index); + } + auto bitmap_manager() { return graphics_manager_; } + auto mutable_bitmap_manager() { return &graphics_manager_; } + + auto title() const { return title_; } + auto size() const { return size_; } auto begin() { return rom_data_.begin(); } auto end() { return rom_data_.end(); } auto data() { return rom_data_.data(); } - auto char_data() { return reinterpret_cast(rom_data_.data()); } - auto size() const { return size_; } + auto push_back(uint8_t byte) { rom_data_.push_back(byte); } + auto vector() const { return rom_data_; } + auto filename() const { return filename_; } + auto isLoaded() const { return is_loaded_; } + auto version() const { return version_; } uchar& operator[](int i) { if (i > size_) { - std::cout << "ROM: Index out of bounds" << std::endl; + std::cout << "ROM: Index " << i << " out of bounds, size: " << size_ + << std::endl; return rom_data_[0]; } return rom_data_[i]; } uchar& operator+(int i) { if (i > size_) { - std::cout << "ROM: Index out of bounds" << std::endl; + std::cout << "ROM: Index " << i << " out of bounds, size: " << size_ + << std::endl; return rom_data_[0]; } return rom_data_[i]; @@ -137,18 +453,147 @@ class ROM { return (ushort)((rom_data_[offset + 1]) << 8) | rom_data_[offset]; } + void SetupRenderer(std::shared_ptr renderer) { + renderer_ = renderer; + } + + void RenderBitmap(gfx::Bitmap* bitmap) { + if (flags()->kLoadTexturesAsStreaming) { + bitmap->CreateTexture(renderer_.get()); + } else { + bitmap->CreateTexture(renderer_); + } + } + + void UpdateBitmap(gfx::Bitmap* bitmap) { + if (flags()->kLoadTexturesAsStreaming) { + bitmap->UpdateTexture(renderer_.get()); + } else { + bitmap->UpdateTexture(renderer_); + } + } + + std::vector> main_blockset_ids; + std::vector> room_blockset_ids; + std::vector> spriteset_ids; + std::vector> paletteset_ids; + + void LoadGfxGroups() { + main_blockset_ids.resize(37, std::vector(8)); + room_blockset_ids.resize(82, std::vector(4)); + spriteset_ids.resize(144, std::vector(4)); + paletteset_ids.resize(72, std::vector(4)); + + int gfxPointer = (rom_data_[gfx_groups_pointer + 1] << 8) + + rom_data_[gfx_groups_pointer]; + gfxPointer = core::SnesToPc(gfxPointer); + + for (int i = 0; i < 37; i++) { + for (int j = 0; j < 8; j++) { + main_blockset_ids[i][j] = rom_data_[gfxPointer + (i * 8) + j]; + } + } + + for (int i = 0; i < 82; i++) { + for (int j = 0; j < 4; j++) { + room_blockset_ids[i][j] = + rom_data_[core::entrance_gfx_group + (i * 4) + j]; + } + } + + for (int i = 0; i < 144; i++) { + for (int j = 0; j < 4; j++) { + spriteset_ids[i][j] = + rom_data_[version_constants().kSpriteBlocksetPointer + (i * 4) + j]; + } + } + + for (int i = 0; i < 72; i++) { + for (int j = 0; j < 4; j++) { + paletteset_ids[i][j] = + rom_data_[version_constants().kDungeonPalettesGroups + (i * 4) + j]; + } + } + } + + bool SaveGroupsToROM() { + int gfxPointer = (rom_data_[gfx_groups_pointer + 1] << 8) + + rom_data_[gfx_groups_pointer]; + gfxPointer = core::SnesToPc(gfxPointer); + + for (int i = 0; i < 37; i++) { + for (int j = 0; j < 8; j++) { + rom_data_[gfxPointer + (i * 8) + j] = main_blockset_ids[i][j]; + } + } + + for (int i = 0; i < 82; i++) { + for (int j = 0; j < 4; j++) { + rom_data_[core::entrance_gfx_group + (i * 4) + j] = + room_blockset_ids[i][j]; + } + } + + for (int i = 0; i < 144; i++) { + for (int j = 0; j < 4; j++) { + rom_data_[version_constants().kSpriteBlocksetPointer + (i * 4) + j] = + spriteset_ids[i][j]; + } + } + + for (int i = 0; i < 72; i++) { + for (int j = 0; j < 4; j++) { + rom_data_[version_constants().kDungeonPalettesGroups + (i * 4) + j] = + paletteset_ids[i][j]; + } + } + + return false; + } + private: long size_ = 0; - uchar title[21] = "ROM Not Loaded"; bool is_loaded_ = false; - bool isbpp3[223]; + bool has_header_ = false; + uchar title_[21] = "ROM Not Loaded"; std::string filename_; Bytes rom_data_; Bytes graphics_buffer_; + + Z3_Version version_ = Z3_Version::US; + gfx::BitmapTable graphics_bin_; + gfx::BitmapManager graphics_manager_; + gfx::BitmapTable link_graphics_; + gfx::SNESPalette link_palette_; + PaletteGroupMap palette_groups_; + + std::stack> changes_; std::shared_ptr renderer_; - std::unordered_map graphics_bin_; - std::unordered_map palette_groups_; +}; + +class SharedROM { + public: + SharedROM() = default; + virtual ~SharedROM() = default; + + std::shared_ptr shared_rom() { + if (!shared_rom_) { + shared_rom_ = std::make_shared(); + } + return shared_rom_; + } + + auto rom() { + if (!shared_rom_) { + shared_rom_ = std::make_shared(); + } + ROM* rom = shared_rom_.get(); + return rom; + } + + // private: + static std::shared_ptr shared_rom_; }; } // namespace app diff --git a/src/app/yaze.cc b/src/app/yaze.cc index a6b7e20b..c41ca6f7 100644 --- a/src/app/yaze.cc +++ b/src/app/yaze.cc @@ -1,5 +1,7 @@ #if defined(_WIN32) #define main SDL_main +#elif __APPLE__ +#include "app/core/platform/app_delegate.h" #endif #include "absl/debugging/failure_signal_handler.h" @@ -10,22 +12,22 @@ int main(int argc, char** argv) { absl::InitializeSymbolizer(argv[0]); absl::FailureSignalHandlerOptions options; + options.symbolize_stacktrace = true; + options.alarm_on_failure_secs = true; absl::InstallFailureSignalHandler(options); yaze::app::core::Controller controller; + EXIT_IF_ERROR(controller.OnEntry()) - auto entry_status = controller.onEntry(); - if (!entry_status.ok()) { - // TODO(@scawful): log the specific error - return EXIT_FAILURE; +#ifdef __APPLE__ + InitializeCocoa(); +#endif + + while (controller.IsActive()) { + controller.OnInput(); + controller.OnLoad(); + controller.DoRender(); } - - while (controller.isActive()) { - controller.onInput(); - controller.onLoad(); - controller.doRender(); - } - controller.onExit(); - + controller.OnExit(); return EXIT_SUCCESS; } \ No newline at end of file diff --git a/src/app/zelda3/dungeon/object_names.h b/src/app/zelda3/dungeon/object_names.h new file mode 100644 index 00000000..15a731b3 --- /dev/null +++ b/src/app/zelda3/dungeon/object_names.h @@ -0,0 +1,465 @@ +#ifndef YAZE_APP_ZELDA3_DUNGEON_OBJECT_NAMES_H +#define YAZE_APP_ZELDA3_DUNGEON_OBJECT_NAMES_H + +#include "absl/strings/string_view.h" + +namespace yaze { +namespace app { +namespace zelda3 { +namespace dungeon { + +static const absl::string_view Type1RoomObjectNames[] = { + "Ceiling ↔", + "Wall (top, north) ↔", + "Wall (top, south) ↔", + "Wall (bottom, north) ↔", + "Wall (bottom, south) ↔", + "Wall columns (north) ↔", + "Wall columns (south) ↔", + "Deep wall (north) ↔", + "Deep wall (south) ↔", + "Diagonal wall A ◤ (top) ↔", + "Diagonal wall A ◣ (top) ↔", + "Diagonal wall A ◥ (top) ↔", + "Diagonal wall A ◢ (top) ↔", + "Diagonal wall B ◤ (top) ↔", + "Diagonal wall B ◣ (top) ↔", + "Diagonal wall B ◥ (top) ↔", + "Diagonal wall B ◢ (top) ↔", + "Diagonal wall C ◤ (top) ↔", + "Diagonal wall C ◣ (top) ↔", + "Diagonal wall C ◥ (top) ↔", + "Diagonal wall C ◢ (top) ↔", + "Diagonal wall A ◤ (bottom) ↔", + "Diagonal wall A ◣ (bottom) ↔", + "Diagonal wall A ◥ (bottom) ↔", + "Diagonal wall A ◢ (bottom) ↔", + "Diagonal wall B ◤ (bottom) ↔", + "Diagonal wall B ◣ (bottom) ↔", + "Diagonal wall B ◥ (bottom) ↔", + "Diagonal wall B ◢ (bottom) ↔", + "Diagonal wall C ◤ (bottom) ↔", + "Diagonal wall C ◣ (bottom) ↔", + "Diagonal wall C ◥ (bottom) ↔", + "Diagonal wall C ◢ (bottom) ↔", + "Platform stairs ↔", + "Rail ↔", + "Pit edge ┏━┓ A (north) ↔", + "Pit edge ┏━┓ B (north) ↔", + "Pit edge ┏━┓ C (north) ↔", + "Pit edge ┏━┓ D (north) ↔", + "Pit edge ┏━┓ E (north) ↔", + "Pit edge ┗━┛ (south) ↔", + "Pit edge ━━━ (south) ↔", + "Pit edge ━━━ (north) ↔", + "Pit edge ━━┛ (south) ↔", + "Pit edge ┗━━ (south) ↔", + "Pit edge ━━┓ (north) ↔", + "Pit edge ┏━━ (north) ↔", + "Rail wall (north) ↔", + "Rail wall (south) ↔", + "Nothing", + "Nothing", + "Carpet ↔", + "Carpet trim ↔", + "Weird door", // TODO: WEIRD DOOR OBJECT NEEDS INVESTIGATION + "Drapes (north) ↔", + "Drapes (west, odd) ↔", + "Statues ↔", + "Columns ↔", + "Wall decors (north) ↔", + "Wall decors (south) ↔", + "Chairs in pairs ↔", + "Tall torches ↔", + "Supports (north) ↔", + "Water edge ┏━┓ (concave) ↔", + "Water edge ┗━┛ (concave) ↔", + "Water edge ┏━┓ (convex) ↔", + "Water edge ┗━┛ (convex) ↔", + "Water edge ┏━┛ (concave) ↔", + "Water edge ┗━┓ (concave) ↔", + "Water edge ┗━┓ (convex) ↔", + "Water edge ┏━┛ (convex) ↔", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Supports (south) ↔", + "Bar ↔", + "Shelf A ↔", + "Shelf B ↔", + "Shelf C ↔", + "Somaria path ↔", + "Cannon hole A (north) ↔", + "Cannon hole A (south) ↔", + "Pipe path ↔", + "Nothing", + "Wall torches (north) ↔", + "Wall torches (south) ↔", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Cannon hole B (north) ↔", + "Cannon hole B (south) ↔", + "Thick rail ↔", + "Blocks ↔", + "Long rail ↔", + "Ceiling ↕", + "Wall (top, west) ↕", + "Wall (top, east) ↕", + "Wall (bottom, west) ↕", + "Wall (bottom, east) ↕", + "Wall columns (west) ↕", + "Wall columns (east) ↕", + "Deep wall (west) ↕", + "Deep wall (east) ↕", + "Rail ↕", + "Pit edge (west) ↕", + "Pit edge (east) ↕", + "Rail wall (west) ↕", + "Rail wall (east) ↕", + "Nothing", + "Nothing", + "Carpet ↕", + "Carpet trim ↕", + "Nothing", + "Drapes (west) ↕", + "Drapes (east) ↕", + "Columns ↕", + "Wall decors (west) ↕", + "Wall decors (east) ↕", + "Supports (west) ↕", + "Water edge (west) ↕", + "Water edge (east) ↕", + "Supports (east) ↕", + "Somaria path ↕", + "Pipe path ↕", + "Nothing", + "Wall torches (west) ↕", + "Wall torches (east) ↕", + "Wall decors tight A (west) ↕", + "Wall decors tight A (east) ↕", + "Wall decors tight B (west) ↕", + "Wall decors tight B (east) ↕", + "Cannon hole (west) ↕", + "Cannon hole (east) ↕", + "Tall torches ↕", + "Thick rail ↕", + "Blocks ↕", + "Long rail ↕", + "Jump ledge (west) ↕", + "Jump ledge (east) ↕", + "Rug trim (west) ↕", + "Rug trim (east) ↕", + "Bar ↕", + "Wall flair (west) ↕", + "Wall flair (east) ↕", + "Blue pegs ↕", + "Orange pegs ↕", + "Invisible floor ↕", + "Fake pots ↕", + "Hammer pegs ↕", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Diagonal ceiling A ◤", + "Diagonal ceiling A ◣", + "Diagonal ceiling A ◥", + "Diagonal ceiling A ◢", + "Pit ⇲", + "Diagonal layer 2 mask A ◤", + "Diagonal layer 2 mask A ◣", + "Diagonal layer 2 mask A ◥", + "Diagonal layer 2 mask A ◢", + "Diagonal layer 2 mask B ◤", // TODO: VERIFY + "Diagonal layer 2 mask B ◣", // TODO: VERIFY + "Diagonal layer 2 mask B ◥", // TODO: VERIFY + "Diagonal layer 2 mask B ◢", // TODO: VERIFY + "Nothing", + "Nothing", + "Nothing", + "Jump ledge (north) ↔", + "Jump ledge (south) ↔", + "Rug ↔", + "Rug trim (north) ↔", + "Rug trim (south) ↔", + "Archery game curtains ↔", + "Wall flair (north) ↔", + "Wall flair (south) ↔", + "Blue pegs ↔", + "Orange pegs ↔", + "Invisible floor ↔", + "Fake pressure plates ↔", + "Fake pots ↔", + "Hammer pegs ↔", + "Nothing", + "Nothing", + "Ceiling (large) ⇲", + "Chest platform (tall) ⇲", + "Layer 2 pit mask (large) ⇲", + "Layer 2 pit mask (medium) ⇲", + "Floor 1 ⇲", + "Floor 3 ⇲", + "Layer 2 mask (large) ⇲", + "Floor 4 ⇲", + "Water floor ⇲ ", + "Flood water (medium) ⇲ ", + "Conveyor floor ⇲ ", + "Nothing", + "Nothing", + "Moving wall (west) ⇲", + "Moving wall (east) ⇲", + "Nothing", + "Nothing", + "Icy floor A ⇲", + "Icy floor B ⇲", + "Moving wall flag", // TODO: WTF IS THIS? + "Moving wall flag", // TODO: WTF IS THIS? + "Moving wall flag", // TODO: WTF IS THIS? + "Moving wall flag", // TODO: WTF IS THIS? + "Layer 2 mask (medium) ⇲", + "Flood water (large) ⇲", + "Layer 2 swim mask ⇲", + "Flood water B (large) ⇲", + "Floor 2 ⇲", + "Chest platform (short) ⇲", + "Table / rock ⇲", + "Spike blocks ⇲", + "Spiked floor ⇲", + "Floor 7 ⇲", + "Tiled floor ⇲", + "Rupee floor ⇲", + "Conveyor upwards ⇲", + "Conveyor downwards ⇲", + "Conveyor leftwards ⇲", + "Conveyor rightwards ⇲", + "Heavy current water ⇲", + "Floor 10 ⇲", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", + "Nothing", +}; + +static const absl::string_view Type2RoomObjectNames[] = { + "Corner (top, concave) ▛", + "Corner (top, concave) ▙", + "Corner (top, concave) ▜", + "Corner (top, concave) ▟", + "Corner (top, convex) ▟", + "Corner (top, convex) ▜", + "Corner (top, convex) ▙", + "Corner (top, convex) ▛", + "Corner (bottom, concave) ▛", + "Corner (bottom, concave) ▙", + "Corner (bottom, concave) ▜", + "Corner (bottom, concave) ▟", + "Corner (bottom, convex) ▟", + "Corner (bottom, convex) ▜", + "Corner (bottom, convex) ▙", + "Corner (bottom, convex) ▛", + "Kinked corner north (bottom) ▜", + "Kinked corner south (bottom) ▟", + "Kinked corner north (bottom) ▛", + "Kinked corner south (bottom) ▙", + "Kinked corner west (bottom) ▙", + "Kinked corner west (bottom) ▛", + "Kinked corner east (bottom) ▟", + "Kinked corner east (bottom) ▜", + "Deep corner (concave) ▛", + "Deep corner (concave) ▙", + "Deep corner (concave) ▜", + "Deep corner (concave) ▟", + "Large brazier", + "Statue", + "Star tile (disabled)", + "Star tile (enabled)", + "Small torch (lit)", + "Barrel", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Table", + "Fairy statue", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Chair", + "Bed", + "Fireplace", + "Mario portrait", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Interroom stairs (up)", + "Interroom stairs (down)", + "Interroom stairs B (down)", + "Intraroom stairs north B", // TODO: VERIFY LAYER HANDLING + "Intraroom stairs north (separate layers)", + "Intraroom stairs north (merged layers)", + "Intraroom stairs north (swim layer)", + "Block", + "Water ladder (north)", + "Water ladder (south)", // TODO: NEEDS IN GAME VERIFICATION + "Dam floodgate", + "Interroom spiral stairs up (top)", + "Interroom spiral stairs down (top)", + "Interroom spiral stairs up (bottom)", + "Interroom spiral stairs down (bottom)", + "Sanctuary wall (north)", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Pew", + "Magic bat altar", +}; + +static const absl::string_view Type3RoomObjectNames[] = { + "Waterfall face (empty)", + "Waterfall face (short)", + "Waterfall face (long)", + "Somaria path endpoint", + "Somaria path intersection ╋", + "Somaria path corner ┏", + "Somaria path corner ┗", + "Somaria path corner ┓", + "Somaria path corner ┛", + "Somaria path intersection ┳", + "Somaria path intersection ┻", + "Somaria path intersection ┣", + "Somaria path intersection ┫", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Somaria path 2-way endpoint", + "Somaria path crossover", + "Babasu hole (north)", + "Babasu hole (south)", + "9 blue rupees", + "Telepathy tile", + "Warp door", // TODO: NEEDS IN GAME VERIFICATION THAT THIS IS USELESS + "Kholdstare's shell", + "Hammer peg", + "Prison cell", + "Big key lock", + "Chest", + "Chest (open)", + "Intraroom stairs south", // TODO: VERIFY LAYER HANDLING + "Intraroom stairs south (separate layers)", + "Intraroom stairs south (merged layers)", + "Interroom straight stairs up (north, top)", + "Interroom straight stairs down (north, top)", + "Interroom straight stairs up (south, top)", + "Interroom straight stairs down (south, top)", + "Deep corner (convex) ▟", + "Deep corner (convex) ▜", + "Deep corner (convex) ▙", + "Deep corner (convex) ▛", + "Interroom straight stairs up (north, bottom)", + "Interroom straight stairs down (north, bottom)", + "Interroom straight stairs up (south, bottom)", + "Interroom straight stairs down (south, bottom)", + "Lamp cones", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Liftable large block", + "Agahnim's altar", + "Agahnim's boss room", + "Pot", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Big chest", + "Big chest (open)", + "Intraroom stairs south (swim layer)", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Pipe end (south)", + "Pipe end (north)", + "Pipe end (east)", + "Pipe end (west)", + "Pipe corner ▛", + "Pipe corner ▙", + "Pipe corner ▜", + "Pipe corner ▟", + "Pipe-rock intersection ⯊", + "Pipe-rock intersection ⯋", + "Pipe-rock intersection ◖", + "Pipe-rock intersection ◗", + "Pipe crossover", + "Bombable floor", + "Fake bombable floor", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Warp tile", + "Tool rack", + "Furnace", + "Tub (wide)", + "Anvil", + "Warp tile (disabled)", + "Pressure plate", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Blue peg", + "Orange peg", + "Fortune teller room", + "Unknown", // TODO: NEEDS IN GAME CHECKING + "Bar corner ▛", + "Bar corner ▙", + "Bar corner ▜", + "Bar corner ▟", + "Decorative bowl", + "Tub (tall)", + "Bookcase", + "Range", + "Suitcase", + "Bar bottles", + "Arrow game hole (west)", + "Arrow game hole (east)", + "Vitreous goo gfx", + "Fake pressure plate", + "Medusa head", + "4-way shooter block", + "Pit", + "Wall crack (north)", + "Wall crack (south)", + "Wall crack (west)", + "Wall crack (east)", + "Large decor", + "Water grate (north)", + "Water grate (south)", + "Water grate (west)", + "Water grate (east)", + "Window sunlight", + "Floor sunlight", + "Trinexx's shell", + "Layer 2 mask (full)", + "Boss entrance", + "Minigame chest", + "Ganon door", + "Triforce wall ornament", + "Triforce floor tiles", + "Freezor hole", + "Pile of bones", + "Vitreous goo damage", + "Arrow tile ↑", + "Arrow tile ↓", + "Arrow tile →", + "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.h b/src/app/zelda3/dungeon/object_renderer.h new file mode 100644 index 00000000..4da8cd89 --- /dev/null +++ b/src/app/zelda3/dungeon/object_renderer.h @@ -0,0 +1,210 @@ +#include +#include +#include +#include +#include + +#include "app/emu/cpu/cpu.h" +#include "app/emu/memory/memory.h" +#include "app/emu/video/ppu.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_palette.h" +#include "app/gfx/snes_tile.h" +#include "app/rom.h" +#include "app/zelda3/dungeon/object_names.h" + +namespace yaze { +namespace app { +namespace zelda3 { +namespace dungeon { + +class DungeonObjectRenderer : public SharedROM { + public: + struct PseudoVram { + std::array sheets; + std::vector palettes; + }; + + DungeonObjectRenderer() = default; + + void LoadObject(uint16_t objectId, std::array& sheet_ids) { + vram_.sheets = sheet_ids; + + rom_data_ = rom()->vector(); + // Prepare the CPU and memory environment + memory_.Initialize(rom_data_); + + // Fetch the subtype pointers for the given object ID + auto subtypeInfo = FetchSubtypeInfo(objectId); + + // Configure the object based on the fetched information + ConfigureObject(subtypeInfo); + + // Run the CPU emulation for the object's draw routines + RenderObject(subtypeInfo); + } + + gfx::Bitmap* bitmap() { return &bitmap_; } + auto memory() { return memory_; } + auto* memory_ptr() { return &memory_; } + auto mutable_memory() { return memory_.data(); } + + private: + struct SubtypeInfo { + uint32_t subtype_ptr; + uint32_t routine_ptr; + }; + + SubtypeInfo FetchSubtypeInfo(uint16_t object_id) { + SubtypeInfo info; + + // Determine the subtype based on objectId + uint8_t subtype = 1; + + // Based on the subtype, fetch the correct pointers + switch (subtype) { + case 1: // Subtype 1 + info.subtype_ptr = core::subtype1_tiles + (object_id & 0xFF) * 2; + info.routine_ptr = + core::subtype1_tiles + 0x200 + (object_id & 0xFF) * 2; + std::cout << "Subtype 1 " << std::hex << info.subtype_ptr << std::endl; + std::cout << "Subtype 1 " << std::hex << info.routine_ptr << std::endl; + break; + case 2: // Subtype 2 + info.subtype_ptr = core::subtype2_tiles + (object_id & 0x7F) * 2; + info.routine_ptr = core::subtype2_tiles + 0x80 + (object_id & 0x7F) * 2; + break; + case 3: // Subtype 3 + info.subtype_ptr = core::subtype3_tiles + (object_id & 0xFF) * 2; + info.routine_ptr = + core::subtype3_tiles + 0x100 + (object_id & 0xFF) * 2; + break; + default: + // Handle unknown subtype + throw std::runtime_error("Unknown subtype for object ID: " + + std::to_string(object_id)); + } + + // Find the RTS of the subtype routine + while (true) { + uint8_t opcode = memory_.ReadByte(info.routine_ptr); + if (opcode == 0x60) { + break; + } + info.routine_ptr++; + } + + return info; + } + + void ConfigureObject(const SubtypeInfo& info) { + cpu.A = 0x03D8; + cpu.X = 0x03D8; + cpu.DB = 0x7E; + // VRAM target destinations + cpu.WriteLong(0xBF, 0x7E2000); + cpu.WriteLong(0xCB, 0x7E2080); + cpu.WriteLong(0xC2, 0x7E2002); + cpu.WriteLong(0xCE, 0x7E2082); + cpu.SetAccumulatorSize(false); + cpu.SetIndexSize(false); + } + + /** + * Example: + * the STA $BF, $CD, $C2, $CE are the location of the object in the room + * $B2 is used for size loop + * so if object size is setted on 07 that draw code will be repeated 7 times + * and since Y is increasing by 4 it makes the object draw from left to right + + RoomDraw_Rightwards2x2_1to15or32: + #_018B89: JSR RoomDraw_GetSize_1to15or32 + .next + #_018B8C: JSR RoomDraw_Rightwards2x2 + #_018B8F: DEC.b $B2 + #_018B91: BNE .next + #_018B93: RTS + + RoomDraw_Rightwards2x2: + #_019895: LDA.w RoomDrawObjectData+0,X + #_019898: STA.b [$BF],Y + #_01989A: LDA.w RoomDrawObjectData+2,X + #_01989D: STA.b [$CB],Y + #_01989F: LDA.w RoomDrawObjectData+4,X + #_0198A2: STA.b [$C2],Y + #_0198A4: LDA.w RoomDrawObjectData+6,X + #_0198A7: STA.b [$CE],Y + #_0198A9: INY #4 + #_0198AD: RTS + */ + + void RenderObject(const SubtypeInfo& info) { + cpu.PB = 0x01; + cpu.PC = cpu.ReadWord(0x01 << 16 | info.routine_ptr); + + int i = 0; + while (true) { + uint8_t opcode = cpu.ReadByte(cpu.PB << 16 | cpu.PC); + cpu.ExecuteInstruction(opcode); + cpu.HandleInterrupts(); + + if (i > 50) { + break; + } + i++; + } + + UpdateObjectBitmap(); + } + + // In the underworld, this holds a copy of the entire BG tilemap for + // Layer 1 (BG2) in TILEMAPA + // Layer 2 (BG1) in TILEMAPB + // + // In the overworld, this holds the entire map16 space, using both blocks as a + // single array TILEMAPA = $7E2000 TILEMAPB = $7E4000 + void UpdateObjectBitmap() { + tilemap_.reserve(0x2000); + for (int i = 0; i < 0x2000; ++i) { + tilemap_.push_back(0); + } + int tilemap_offset = 0; + + // Iterate over tilemap in memory to read tile IDs + for (int tile_index = 0; tile_index < 512; tile_index++) { + // Read the tile ID from memory + int tile_id = memory_.ReadWord(0x7E4000 + tile_index); + + int sheet_number = tile_id / 32; + int local_id = tile_id % 32; + + int row = local_id / 8; + int column = local_id % 8; + + int x = column * 8; + int y = row * 8; + + auto sheet = rom()->mutable_graphics_sheet(vram_.sheets[sheet_number]); + + // Copy the tile from VRAM using the read tile_id + sheet->Get8x8Tile(tile_id, x, y, tilemap_, tilemap_offset); + } + + bitmap_.Create(256, 256, 8, tilemap_); + } + + std::vector tilemap_; + uint16_t pc_with_rts_; + std::vector rom_data_; + emu::MemoryImpl memory_; + emu::ClockImpl clock_; + emu::CPU cpu{memory_, clock_}; + emu::Ppu ppu{memory_, clock_}; + gfx::Bitmap bitmap_; + PseudoVram vram_; +}; + +} // 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 new file mode 100644 index 00000000..507154ff --- /dev/null +++ b/src/app/zelda3/dungeon/room.cc @@ -0,0 +1,426 @@ +#include "room.h" + +#include +#include + +#include "app/core/common.h" +#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 { + +void Room::LoadHeader() { + // Address of the room header + int header_pointer = (rom()->data()[kRoomHeaderPointer + 2] << 16) + + (rom()->data()[kRoomHeaderPointer + 1] << 8) + + (rom()->data()[kRoomHeaderPointer]); + header_pointer = core::SnesToPc(header_pointer); + + int address = (rom()->data()[kRoomHeaderPointerBank] << 16) + + (rom()->data()[(header_pointer + 1) + (room_id_ * 2)] << 8) + + rom()->data()[(header_pointer) + (room_id_ * 2)]; + + auto header_location = core::SnesToPc(address); + + bg2 = (Background2)((rom()->data()[header_location] >> 5) & 0x07); + // collision = (CollisionKey)((rom()->data()[header_location] >> 2) & 0x07); + light = ((rom()->data()[header_location]) & 0x01) == 1; + + if (light) { + bg2 = Background2::DarkRoom; + } + + palette = ((rom()->data()[header_location + 1] & 0x3F)); + blockset = (rom()->data()[header_location + 2]); + spriteset = (rom()->data()[header_location + 3]); + // effect = (EffectKey)((rom()->data()[header_location + 4])); + // tag1 = (TagKey)((rom()->data()[header_location + 5])); + // tag2 = (TagKey)((rom()->data()[header_location + 6])); + + staircase_plane[0] = ((rom()->data()[header_location + 7] >> 2) & 0x03); + staircase_plane[1] = ((rom()->data()[header_location + 7] >> 4) & 0x03); + staircase_plane[2] = ((rom()->data()[header_location + 7] >> 6) & 0x03); + staircase_plane[3] = ((rom()->data()[header_location + 8]) & 0x03); + + holewarp = (rom()->data()[header_location + 9]); + staircase_rooms[0] = (rom()->data()[header_location + 10]); + staircase_rooms[1] = (rom()->data()[header_location + 11]); + staircase_rooms[2] = (rom()->data()[header_location + 12]); + staircase_rooms[3] = (rom()->data()[header_location + 13]); +} + +void Room::LoadRoomGraphics(uchar entrance_blockset) { + const auto& mainGfx = rom()->main_blockset_ids; + const auto& roomGfx = rom()->room_blockset_ids; + const auto& spriteGfx = rom()->spriteset_ids; + current_gfx16_.reserve(0x4000); + + for (int i = 0; i < 8; i++) { + blocks_[i] = mainGfx[blockset][i]; + if (i >= 6 && i <= 6) { + // 3-6 + if (entrance_blockset != 0xFF) { + if (roomGfx[entrance_blockset][i - 3] != 0) { + blocks_[i] = roomGfx[entrance_blockset][i - 3]; + } + } + } + } + + blocks_[8] = 115 + 0; // Static Sprites Blocksets (fairy,pot,ect...) + blocks_[9] = 115 + 10; + blocks_[10] = 115 + 6; + blocks_[11] = 115 + 7; + for (int i = 0; i < 4; i++) { + blocks_[12 + i] = (uchar)(spriteGfx[spriteset + 64][i] + 115); + } // 12-16 sprites +} + +constexpr int kGfxBufferOffset = 92 * 2048; +constexpr int kGfxBufferStride = 512; +constexpr int kGfxBufferAnimatedFrameOffset = 7 * 2048; +constexpr int kGfxBufferAnimatedFrameStride = 512; +constexpr int kGfxBufferRoomOffset = 2048; +constexpr int kGfxBufferRoomSpriteOffset = 512; +constexpr int kGfxBufferRoomSpriteStride = 2048; +constexpr int kGfxBufferRoomSpriteLastLineOffset = 0x88; + +void Room::CopyRoomGraphicsToBuffer() { + auto gfx_buffer_data = rom()->graphics_buffer(); + + // Copy room graphics to buffer + int sheet_pos = 0; + for (int i = 0; i < 16; i++) { + int data = 0; + int block_offset = blocks_[i] * kGfxBufferRoomOffset; + while (data < kGfxBufferRoomOffset) { + uchar map_byte = gfx_buffer_data[data + block_offset]; + if (i < 4) { + map_byte += kGfxBufferRoomSpriteLastLineOffset; + } + + current_gfx16_[data + sheet_pos] = map_byte; + data++; + } + + sheet_pos += kGfxBufferRoomOffset; + } + + LoadAnimatedGraphics(); +} + +void Room::LoadAnimatedGraphics() { + int gfx_ptr = core::SnesToPc(rom()->version_constants().kGfxAnimatedPointer); + + auto gfx_buffer_data = rom()->graphics_buffer(); + auto rom_data = rom()->vector(); + int data = 0; + while (data < 512) { + uchar mapByte = + gfx_buffer_data[data + (92 * 2048) + (512 * animated_frame)]; + current_gfx16_[data + (7 * 2048)] = mapByte; + + mapByte = + gfx_buffer_data[data + (rom_data[gfx_ptr + BackgroundTileset] * 2048) + + (512 * animated_frame)]; + current_gfx16_[data + (7 * 2048) - 512] = mapByte; + data++; + } +} + +void Room::LoadSprites() { + auto rom_data = rom()->vector(); + int spritePointer = (0x04 << 16) + (rom_data[rooms_sprite_pointer + 1] << 8) + + (rom_data[rooms_sprite_pointer]); + int sprite_address_snes = + (0x09 << 16) + (rom_data[spritePointer + (room_id_ * 2) + 1] << 8) + + rom_data[spritePointer + (room_id_ * 2)]; + + int sprite_address = core::SnesToPc(sprite_address_snes); + bool sortsprites = rom_data[sprite_address] == 1; + sprite_address += 1; + + while (true) { + uint8_t b1 = rom_data[sprite_address]; + uint8_t b2 = rom_data[sprite_address + 1]; + uint8_t b3 = rom_data[sprite_address + 2]; + + if (b1 == 0xFF) { + break; + } + + // sprites_.emplace_back(this, b3, (b2 & 0x1F), (b1 & 0x1F), + // ((b2 & 0xE0) >> 5) + ((b1 & 0x60) >> 2), + // (b1 & 0x80) >> 7); + + if (sprites_.size() > 1) { + Sprite& spr = sprites_.back(); + Sprite& prevSprite = sprites_[sprites_.size() - 2]; + + if (spr.id() == 0xE4 && spr.x() == 0x00 && spr.y() == 0x1E && + spr.layer() == 1 && spr.subtype() == 0x18) { + // prevSprite.keyDrop() = 1; + sprites_.pop_back(); + } + + if (spr.id() == 0xE4 && spr.x() == 0x00 && spr.y() == 0x1D && + spr.layer() == 1 && spr.subtype() == 0x18) { + // prevSprite.keyDrop() = 2; + sprites_.pop_back(); + } + } + + sprite_address += 3; + } +} + +void Room::LoadChests() { + auto rom_data = rom()->vector(); + int cpos = (rom_data[chests_data_pointer1 + 2] << 16) + + (rom_data[chests_data_pointer1 + 1] << 8) + + (rom_data[chests_data_pointer1]); + cpos = core::SnesToPc(cpos); + int clength = (rom_data[chests_length_pointer + 1] << 8) + + (rom_data[chests_length_pointer]); + + for (int i = 0; i < clength; i++) { + if ((((rom_data[cpos + (i * 3) + 1] << 8) + (rom_data[cpos + (i * 3)])) & + 0x7FFF) == room_id_) { + // There's a chest in that room ! + bool big = false; + if ((((rom_data[cpos + (i * 3) + 1] << 8) + (rom_data[cpos + (i * 3)])) & + 0x8000) == 0x8000) // ????? + { + big = true; + } + + chests_in_room.emplace_back(ChestData(rom_data[cpos + (i * 3) + 2], big)); + } + } +} + +void Room::LoadObjects() { + auto rom_data = rom()->vector(); + int objectPointer = (rom_data[room_object_pointer + 2] << 16) + + (rom_data[room_object_pointer + 1] << 8) + + (rom_data[room_object_pointer]); + objectPointer = core::SnesToPc(objectPointer); + int room_address = objectPointer + (room_id_ * 3); + + int tile_address = (rom_data[room_address + 2] << 16) + + (rom_data[room_address + 1] << 8) + rom_data[room_address]; + + int objects_location = core::SnesToPc(tile_address); + + if (objects_location == 0x52CA2) { + std::cout << "Room ID : " << room_id_ << std::endl; + } + + if (floor) { + floor1 = static_cast(rom_data[objects_location] & 0x0F); + floor2 = static_cast((rom_data[objects_location] >> 4) & 0x0F); + } + + layout = static_cast((rom_data[objects_location + 1] >> 2) & 0x07); + + LoadChests(); + + staircaseRooms.clear(); + int nbr_of_staircase = 0; + + int pos = objects_location + 2; + uint8_t b1 = 0; + uint8_t b2 = 0; + uint8_t b3 = 0; + uint8_t posX = 0; + uint8_t posY = 0; + uint8_t sizeX = 0; + uint8_t sizeY = 0; + uint8_t sizeXY = 0; + short oid = 0; + int layer = 0; + bool door = false; + bool end_read = false; + while (!end_read) { + b1 = rom_data[pos]; + b2 = rom_data[pos + 1]; + + if (b1 == 0xFF && b2 == 0xFF) { + pos += 2; // We jump to layer2 + layer++; + door = false; + if (layer == 3) { + break; + } + continue; + } + + if (b1 == 0xF0 && b2 == 0xFF) { + pos += 2; // We jump to layer2 + door = true; + continue; + } + + b3 = rom_data[pos + 2]; + if (door) { + pos += 2; + } else { + pos += 3; + } + + if (!door) { + if (b3 >= 0xF8) { + oid = static_cast((b3 << 4) | + 0x80 + (((b2 & 0x03) << 2) + ((b1 & 0x03)))); + posX = static_cast((b1 & 0xFC) >> 2); + posY = static_cast((b2 & 0xFC) >> 2); + sizeXY = static_cast((((b1 & 0x03) << 2) + (b2 & 0x03))); + } else { + oid = b3; + posX = static_cast((b1 & 0xFC) >> 2); + posY = static_cast((b2 & 0xFC) >> 2); + sizeX = static_cast((b1 & 0x03)); + sizeY = static_cast((b2 & 0x03)); + sizeXY = static_cast(((sizeX << 2) + sizeY)); + } + + if (b1 >= 0xFC) { + oid = static_cast((b3 & 0x3F) + 0x100); + posX = static_cast(((b2 & 0xF0) >> 4) + ((b1 & 0x3) << 4)); + posY = static_cast(((b2 & 0x0F) << 2) + ((b3 & 0xC0) >> 6)); + sizeXY = 0; + } + + RoomObject r = + AddObject(oid, posX, posY, sizeXY, static_cast(layer)); + + /** + if (r != nullptr) { + tilesObjects.push_back(r); + } + + + for (short stair : stairsObjects) { + if (stair == oid) { + if (nbr_of_staircase < 4) { + tilesObjects.back().options |= ObjectOption::Stairs; + staircaseRooms.push_back(StaircaseRoom( + posX, posY, "To " + staircase_rooms[nbr_of_staircase])); + nbr_of_staircase++; + } else { + tilesObjects.back().options |= ObjectOption::Stairs; + staircaseRooms.push_back(StaircaseRoom(posX, posY, "To ???")); + } + } + } + + if (oid == 0xF99) { + if (chests_in_room.size() > 0) { + tilesObjects.back().options |= ObjectOption::Chest; + chest_list.push_back( + Chest(posX, posY, chests_in_room.front().itemIn, false)); + chests_in_room.erase(chests_in_room.begin()); + } + } else if (oid == 0xFB1) { + if (chests_in_room.size() > 0) { + tilesObjects.back().options |= ObjectOption::Chest; + chest_list.push_back( + Chest(posX + 1, posY, chests_in_room.front().itemIn, true)); + chests_in_room.erase(chests_in_room.begin()); + } + } + } else { + tilesObjects.push_back(object_door(static_cast((b2 << 8) + b1), 0, + 0, 0, static_cast(layer))); + } + + **/ + } + } +} + +void Room::LoadRoomFromROM() { + // Load dungeon header + auto rom_data = rom()->vector(); + int header_pointer = core::SnesToPc(kRoomHeaderPointer); + + message_id_ = messages_id_dungeon + (room_id_ * 2); + + int address = (rom()->data()[kRoomHeaderPointerBank] << 16) + + (rom()->data()[(header_pointer + 1) + (room_id_ * 2)] << 8) + + rom()->data()[(header_pointer) + (room_id_ * 2)]; + + int hpos = core::SnesToPc(address); + hpos++; + uint8_t b = rom_data[hpos]; + + Layer2Mode = (b >> 5); + // TODO(@scawful): Make LayerMerging object. + // LayerMerging = LayerMergeType.ListOf[(b & 0x0C) >> 2]; + + IsDark = (b & 0x01) == 0x01; + hpos++; + + Palette = rom_data[hpos]; + hpos++; + + BackgroundTileset = rom_data[hpos]; + hpos++; + + SpriteTileset = rom_data[hpos]; + hpos++; + + Layer2Behavior = rom_data[hpos]; + hpos++; + + Tag1 = rom_data[hpos]; + hpos++; + + Tag2 = rom_data[hpos]; + hpos++; + + b = rom_data[hpos]; + + Pits.TargetLayer = (uchar)(b & 0x03); + Stair1.TargetLayer = (uchar)((b >> 2) & 0x03); + Stair2.TargetLayer = (uchar)((b >> 4) & 0x03); + Stair3.TargetLayer = (uchar)((b >> 6) & 0x03); + hpos++; + Stair4.TargetLayer = (uchar)(rom_data[hpos] & 0x03); + hpos++; + + Pits.Target = rom_data[hpos]; + hpos++; + Stair1.Target = rom_data[hpos]; + hpos++; + Stair2.Target = rom_data[hpos]; + hpos++; + Stair3.Target = rom_data[hpos]; + hpos++; + Stair4.Target = rom_data[hpos]; + hpos++; + + // Load room objects + // int objectPointer = core::SnesToPc(room_object_pointer); + // int room_address = objectPointer + (room_id_ * 3); + // int objects_location = core::SnesToPc(room_address); + + // Load sprites + // int spr_ptr = 0x040000 | rooms_sprite_pointer; + // int sprite_address = + // core::SnesToPc(dungeon_spr_ptrs | spr_ptr + (room_id_ * 2)); +} + +} // namespace dungeon +} // namespace zelda3 +} // namespace app +} // namespace yaze diff --git a/src/app/zelda3/dungeon/room.h b/src/app/zelda3/dungeon/room.h new file mode 100644 index 00000000..8bf13986 --- /dev/null +++ b/src/app/zelda3/dungeon/room.h @@ -0,0 +1,206 @@ +#ifndef YAZE_APP_ZELDA3_DUNGEON_ROOM_H +#define YAZE_APP_ZELDA3_DUNGEON_ROOM_H + +#include +#include + +#include "app/core/common.h" +#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_names.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 +// 0x882D -> readlong() -> 2FEF04 (04EF2F -> toPC->026F2F) -> + +// 47EF04 ; layout00 ptr +// AFEF04 ; layout01 ptr +// F0EF04 ; layout02 ptr +// 4CF004 ; layout03 ptr +// A8F004 ; layout04 ptr +// ECF004 ; layout05 ptr +// 48F104 ; layout06 ptr +// A4F104 ; layout07 ptr +// also they are not exactly the same as rooms +// the object array is terminated by a 0xFFFF there's no layers +// in normal room when you encounter a 0xFFFF it goes to the next layer + +constexpr int room_object_layout_pointer = 0x882D; +constexpr int room_object_pointer = 0x874C; // Long pointer + +constexpr int entrance_gfx_group = 0x5D97; +constexpr int dungeons_main_bg_palette_pointers = 0xDEC4B; // JP Same +constexpr int dungeons_palettes = 0xDD734; +constexpr int room_items_pointers = 0xDB69; // JP 0xDB67 +constexpr int rooms_sprite_pointer = 0x4C298; // JP Same //2byte bank 09D62E +constexpr int kRoomHeaderPointer = 0xB5DD; // LONG +constexpr int kRoomHeaderPointerBank = 0xB5E7; // JP Same +constexpr int gfx_groups_pointer = 0x6237; +constexpr int chests_length_pointer = 0xEBF6; +constexpr int chests_data_pointer1 = 0xEBFB; + +constexpr int messages_id_dungeon = 0x3F61D; + +constexpr int blocks_length = 0x8896; // Word value +constexpr int blocks_pointer1 = 0x15AFA; +constexpr int blocks_pointer2 = 0x15B01; +constexpr int blocks_pointer3 = 0x15B08; +constexpr int blocks_pointer4 = 0x15B0F; +constexpr int torch_data = 0x2736A; // JP 0x2704A +constexpr int torches_length_pointer = 0x88C1; +constexpr int sprite_blockset_pointer = 0x5B57; + +constexpr int sprites_data = 0x4D8B0; +constexpr int sprites_data_empty_room = 0x4D8AE; +constexpr int sprites_end_data = 0x4EC9E; +constexpr int pit_pointer = 0x394AB; +constexpr int pit_count = 0x394A6; +constexpr int doorPointers = 0xF83C0; + +// doors +constexpr int door_gfx_up = 0x4D9E; +constexpr int door_gfx_down = 0x4E06; +constexpr int door_gfx_cavexit_down = 0x4E06; +constexpr int door_gfx_left = 0x4E66; +constexpr int door_gfx_right = 0x4EC6; +constexpr int door_pos_up = 0x197E; +constexpr int door_pos_down = 0x1996; +constexpr int door_pos_left = 0x19AE; +constexpr int door_pos_right = 0x19C6; + +constexpr int dungeon_spr_ptrs = 0x090000; + +constexpr ushort stairsObjects[] = {0x139, 0x138, 0x13B, 0x12E, 0x12D}; + +class DungeonDestination { + public: + DungeonDestination() = default; + ~DungeonDestination() = default; + DungeonDestination(uint8_t i) : Index(i) {} + + uint8_t Index; + uint8_t Target = 0; + uint8_t TargetLayer = 0; + // RoomObject* AssociatedObject = nullptr; +}; + +struct object_door { + object_door() = default; + object_door(short id, uint8_t x, uint8_t y, uint8_t size, uint8_t layer) + : id_(id), x_(x), y_(y), size_(size), layer_(layer) {} + + short id_; + uint8_t x_; + uint8_t y_; + uint8_t size_; + uint8_t type_; + uint8_t layer_; +}; + +struct ChestData { + ChestData() = default; + ChestData(uchar i, bool s) : id_(i), size_(s){}; + + uchar id_; + bool size_; +}; + +struct StaircaseRooms {}; + +class Room : public SharedROM { + public: + Room() = default; + Room(int room_id) : room_id_(room_id) {} + ~Room() = default; + void LoadHeader(); + void LoadRoomGraphics(uchar entrance_blockset = 0xFF); + void CopyRoomGraphicsToBuffer(); + void LoadAnimatedGraphics(); + + void LoadSprites(); + void LoadChests(); + void LoadObjects(); + + void LoadRoomFromROM(); + + auto blocks() const { return blocks_; } + auto& mutable_blocks() { return blocks_; } + + 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 floor1 = 0; + uint8_t floor2 = 0; + uint8_t blockset = 0; + uint8_t spriteset = 0; + uint8_t palette = 0; + uint8_t layout = 0; + uint8_t holewarp = 0; + + uint16_t message_id_ = 0; + + gfx::Bitmap current_graphics_; + std::vector bg1_buffer_; + std::vector bg2_buffer_; + std::vector current_gfx16_; + + private: + bool light = false; + bool is_loaded_ = false; + bool IsDark = false; + bool floor = false; + + int room_id_ = 0; + int animated_frame = 0; + + uchar Tag1; + uchar Tag2; + + uint8_t staircase_plane[4]; + uint8_t staircase_rooms[4]; + + uint8_t BackgroundTileset; + uint8_t SpriteTileset; + uint8_t Layer2Behavior; + uint8_t Palette; + uint8_t Floor1Graphics; + uint8_t Floor2Graphics; + uint8_t Layer2Mode; + + std::array blocks_; + std::array ChestList; + + std::array background_bmps_; + std::vector sprites_; + std::vector staircaseRooms; + + Background2 bg2; + DungeonDestination Pits; + DungeonDestination Stair1; + DungeonDestination Stair2; + DungeonDestination Stair3; + DungeonDestination Stair4; + + std::vector chests_in_room; + std::vector tilesObjects; +}; + +} // namespace dungeon +} // namespace zelda3 +} // namespace app +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/app/zelda3/dungeon/room_names.h b/src/app/zelda3/dungeon/room_names.h new file mode 100644 index 00000000..a971a351 --- /dev/null +++ b/src/app/zelda3/dungeon/room_names.h @@ -0,0 +1,352 @@ +#ifndef YAZE_APP_ZELDA3_DUNGEON_ROOM_NAMES_H +#define YAZE_APP_ZELDA3_DUNGEON_ROOM_NAMES_H + +#include +#include +#include + +namespace yaze { +namespace app { +namespace zelda3 { +namespace dungeon { + +constexpr std::string_view kRoomNames[] = { + "Ganon", + "Hyrule Castle (North Corridor)", + "Behind Sanctuary (Switch)", + "Houlihan", + "Turtle Rock (Crysta-Roller)", + "Empty", + "Swamp Palace (Arrghus[Boss])", + "Tower of Hera (Moldorm[Boss])", + "Cave (Healing Fairy)", + "Palace of Darkness", + "Palace of Darkness (Stalfos Trap)", + "Palace of Darkness (Turtle)", + "Ganon's Tower (Entrance)", + "Ganon's Tower (Agahnim2[Boss])", + "Ice Palace (Entrance )", + "Empty Clone ", + "Ganon Evacuation Route", + "Hyrule Castle (Bombable Stock )", + "Sanctuary", + "Turtle Rock (Hokku-Bokku Key 2)", + "Turtle Rock (Big Key )", + "Turtle Rock", + "Swamp Palace (Swimming Treadmill)", + "Tower of Hera (Moldorm Fall )", + "Cave", + "Palace of Darkness (Dark Maze)", + "Palace of Darkness (Big Chest )", + "Palace of Darkness (Mimics / Moving Wall )", + "Ganon's Tower (Ice Armos)", + "Ganon's Tower (Final Hallway)", + "Ice Palace (Bomb Floor / Bari )", + "Ice Palace (Pengator / Big Key )", + "Agahnim's Tower (Agahnim[Boss])", + "Hyrule Castle (Key-rat )", + "Hyrule Castle (Sewer Text Trigger )", + "Turtle Rock (West Exit to Balcony)", + "Turtle Rock (Double Hokku-Bokku / Big chest )", + "Empty Clone ", + "Swamp Palace (Statue )", + "Tower of Hera (Big Chest)", + "Swamp Palace (Entrance )", + "Skull Woods (Mothula[Boss])", + "Palace of Darkness (Big Hub )", + "Palace of Darkness (Map Chest / Fairy )", + "Cave", + "Empty Clone ", + "Ice Palace (Compass )", + "Cave (Kakariko Well HP)", + "Agahnim's Tower (Maiden Sacrifice Chamber)", + "Tower of Hera (Hardhat Beetles )", + "Hyrule Castle (Sewer Key Chest )", + "Desert Palace (Lanmolas[Boss])", + "Swamp Palace (Push Block Puzzle / Pre-Big Key )", + "Swamp Palace (Big Key / BS )", + "Swamp Palace (Big Chest )", + "Swamp Palace (Map Chest / Water Fill )", + "Swamp Palace (Key Pot )", + "Skull Woods (Gibdo Key / Mothula Hole )", + "Palace of Darkness (Bombable Floor )", + "Palace of Darkness (Spike Block / Conveyor )", + "Cave", + "Ganon's Tower (Torch 2)", + "Ice Palace (Stalfos Knights / Conveyor Hellway)", + "Ice Palace (Map Chest )", + "Agahnim's Tower (Final Bridge )", + "Hyrule Castle (First Dark )", + "Hyrule Castle (6 Ropes )", + "Desert Palace (Torch Puzzle / Moving Wall )", + "Thieves Town (Big Chest )", + "Thieves Town (Jail Cells )", + "Swamp Palace (Compass Chest )", + "Empty Clone ", + "Empty Clone ", + "Skull Woods (Gibdo Torch Puzzle )", + "Palace of Darkness (Entrance )", + "Palace of Darkness (Warps / South Mimics )", + "Ganon's Tower (Mini-Helmasaur Conveyor )", + "Ganon's Tower (Moldorm )", + "Ice Palace (Bomb-Jump )", + "Ice Palace Clone (Fairy )", + "Hyrule Castle (West Corridor)", + "Hyrule Castle (Throne )", + "Hyrule Castle (East Corridor)", + "Desert Palace (Popos 2 / Beamos Hellway )", + "Swamp Palace (Upstairs Pits )", + "Castle Secret Entrance / Uncle Death ", + "Skull Woods (Key Pot / Trap )", + "Skull Woods (Big Key )", + "Skull Woods (Big Chest )", + "Skull Woods (Final Section Entrance )", + "Palace of Darkness (Helmasaur King[Boss])", + "Ganon's Tower (Spike Pit )", + "Ganon's Tower (Ganon-Ball Z)", + "Ganon's Tower (Gauntlet 1/2/3)", + "Ice Palace (Lonely Firebar)", + "Ice Palace (Hidden Chest / Spike Floor )", + "Hyrule Castle (West Entrance )", + "Hyrule Castle (Main Entrance )", + "Hyrule Castle (East Entrance )", + "Desert Palace (Final Section Entrance )", + "Thieves Town (West Attic )", + "Thieves Town (East Attic )", + "Swamp Palace (Hidden Chest / Hidden Door )", + "Skull Woods (Compass Chest )", + "Skull Woods (Key Chest / Trap )", + "Empty Clone ", + "Palace of Darkness (Rupee )", + "Ganon's Tower (Mimics s)", + "Ganon's Tower (Lanmolas )", + "Ganon's Tower (Gauntlet 4/5)", + "Ice Palace (Pengators )", + "Empty Clone ", + "Hyrule Castle (Small Corridor to Jail Cells)", + "Hyrule Castle (Boomerang Chest )", + "Hyrule Castle (Map Chest )", + "Desert Palace (Big Chest )", + "Desert Palace (Map Chest )", + "Desert Palace (Big Key Chest )", + "Swamp Palace (Water Drain )", + "Tower of Hera (Entrance )", + "Empty Clone ", + "Empty Clone ", + "Empty Clone ", + "Ganon's Tower", + "Ganon's Tower (East Side Collapsing Bridge / Exploding Wall )", + "Ganon's Tower (Winder / Warp Maze )", + "Ice Palace (Hidden Chest / Bombable Floor )", + "Ice Palace ( Big Spike Traps )", + "Hyrule Castle (Jail Cell )", + "Hyrule Castle", + "Hyrule Castle (Basement Chasm )", + "Desert Palace (West Entrance )", + "Desert Palace (Main Entrance )", + "Desert Palace (East Entrance )", + "Empty Clone ", + "Tower of Hera (Tile )", + "Empty Clone ", + "Eastern Palace (Fairy )", + "Empty Clone ", + "Ganon's Tower (Block Puzzle / Spike Skip / Map Chest )", + "Ganon's Tower (East and West Downstairs / Big Chest )", + "Ganon's Tower (Tile / Torch Puzzle )", + "Ice Palace", + "Empty Clone ", + "Misery Mire (Vitreous[Boss])", + "Misery Mire (Final Switch )", + "Misery Mire (Dark Bomb Wall / Switches )", + "Misery Mire (Dark Cane Floor Switch Puzzle )", + "Empty Clone ", + "Ganon's Tower (Final Collapsing Bridge )", + "Ganon's Tower (Torches 1 )", + "Misery Mire (Torch Puzzle / Moving Wall )", + "Misery Mire (Entrance )", + "Eastern Palace (Eyegore Key )", + "Empty Clone ", + "Ganon's Tower (Many Spikes / Warp Maze )", + "Ganon's Tower (Invisible Floor Maze )", + "Ganon's Tower (Compass Chest / Invisible Floor )", + "Ice Palace (Big Chest )", + "Ice Palace", + "Misery Mire (Pre-Vitreous )", + "Misery Mire (Fish )", + "Misery Mire (Bridge Key Chest )", + "Misery Mire", + "Turtle Rock (Trinexx[Boss])", + "Ganon's Tower (Wizzrobes s)", + "Ganon's Tower (Moldorm Fall )", + "Tower of Hera (Fairy )", + "Eastern Palace (Stalfos Spawn )", + "Eastern Palace (Big Chest )", + "Eastern Palace (Map Chest )", + "Thieves Town (Moving Spikes / Key Pot )", + "Thieves Town (Blind The Thief[Boss])", + "Empty Clone ", + "Ice Palace", + "Ice Palace (Ice Bridge )", + "Agahnim's Tower (Circle of Pots)", + "Misery Mire (Hourglass )", + "Misery Mire (Slug )", + "Misery Mire (Spike Key Chest )", + "Turtle Rock (Pre-Trinexx )", + "Turtle Rock (Dark Maze)", + "Turtle Rock (Chain Chomps )", + "Turtle Rock (Map Chest / Key Chest / Roller )", + "Eastern Palace (Big Key )", + "Eastern Palace (Lobby Cannonballs )", + "Eastern Palace (Dark Antifairy / Key Pot )", + "Thieves Town (Hellway)", + "Thieves Town (Conveyor Toilet)", + "Empty Clone ", + "Ice Palace (Block Puzzle )", + "Ice Palace Clone (Switch )", + "Agahnim's Tower (Dark Bridge )", + "Misery Mire (Compass Chest / Tile )", + "Misery Mire (Big Hub )", + "Misery Mire (Big Chest )", + "Turtle Rock (Final Crystal Switch Puzzle )", + "Turtle Rock (Laser Bridge)", + "Turtle Rock", + "Turtle Rock (Torch Puzzle)", + "Eastern Palace (Armos Knights[Boss])", + "Eastern Palace (Entrance )", + "??", + "Thieves Town (North West Entrance )", + "Thieves Town (North East Entrance )", + "Empty Clone ", + "Ice Palace (Hole to Kholdstare )", + "Empty Clone ", + "Agahnim's Tower (Dark Maze)", + "Misery Mire (Conveyor Slug / Big Key )", + "Misery Mire (Mire02 / Wizzrobes )", + "Empty Clone ", + "Empty Clone ", + "Turtle Rock (Laser Key )", + "Turtle Rock (Entrance )", + "Empty Clone ", + "Eastern Palace (Zeldagamer / Pre-Armos Knights )", + "Eastern Palace (Canonball ", + "Eastern Palace", + "Thieves Town (Main (South West) Entrance )", + "Thieves Town (South East Entrance )", + "Empty Clone ", + "Ice Palace (Kholdstare[Boss])", + "Cave", + "Agahnim's Tower (Entrance )", + "Cave (Lost Woods HP)", + "Cave (Lumberjack's Tree HP)", + "Cave (1/2 Magic)", + "Cave (Lost Old Man Final Cave)", + "Cave (Lost Old Man Final Cave)", + "Cave", + "Cave", + "Cave", + "Empty Clone ", + "Cave (Spectacle Rock HP)", + "Cave", + "Empty Clone ", + "Cave", + "Cave (Spiral Cave)", + "Cave (Crystal Switch / 5 Chests )", + "Cave (Lost Old Man Starting Cave)", + "Cave (Lost Old Man Starting Cave)", + "House", + "House (Old Woman (Sahasrahla's Wife?))", + "House (Angry Brothers)", + "House (Angry Brothers)", + "Empty Clone ", + "Empty Clone ", + "Cave", + "Cave", + "Cave", + "Cave", + "Empty Clone ", + "Cave", + "Cave", + "Cave", + + "Chest Minigame", + "Houses", + "Sick Boy house", + "Tavern", + "Link's House", + "Sarashrala Hut", + "Chest Minigame", + "Library", + "Chicken House", + "Witch Shop", + "A Aginah's Cave", + "Dam", + "Mimic Cave", + "Mire Shed", + "Cave", + "Shop", + "Shop", + "Archery Minigame", + "DW Church/Shop", + "Grave Cave", + "Fairy Fountain", + "Fairy Upgrade", + "Pyramid Fairy", + "Spike Cave", + "Chest Minigame", + "Blind Hut", + "Bonzai Cave", + "Circle of bush Cave", + "Big Bomb Shop, C-House", + "Blind Hut 2", + "Hype Cave", + "Shop", + "Ice Cave", + "Smith", + "Fortune Teller", + "MiniMoldorm Cave", + "Under Rock Caves", + "Smith", + "Cave", + "Mazeblock Cave", + "Smith Peg Cave"}; + +class Room_Name { + public: + static std::vector room_name; + + static void loadFromFile(const std::string& file = "DefaultNames.txt") { + std::ifstream ifs(file); + std::string line; + int l = 0; + bool found = false; + + while (getline(ifs, line)) { + if (line == "[Rooms Names]") { + l = 0; + found = true; + continue; + } + + if (found) { + if (line.length() > 0) { + if (line[0] == '/' && line[1] == '/') { + continue; + } + if (l >= 0x4B) { + break; + } + + room_name[l] = line; + l++; + } + } + } + } +}; + +} // namespace dungeon +} // namespace zelda3 +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_ZELDA3_DUNGEON_ROOM_NAMES_H \ No newline at end of file diff --git a/src/app/zelda3/dungeon/room_object.cc b/src/app/zelda3/dungeon/room_object.cc new file mode 100644 index 00000000..ed68a02f --- /dev/null +++ b/src/app/zelda3/dungeon/room_object.cc @@ -0,0 +1,92 @@ +#include "room_object.h" + +namespace yaze { +namespace app { +namespace zelda3 { +namespace dungeon { + +void RoomObject::DrawTile(Tile t, int xx, int yy, + std::vector& current_gfx16, + std::vector& tiles_bg1_buffer, + std::vector& tiles_bg2_buffer, + ushort tileUnder) { + bool preview = false; + if (width_ < xx + 8) { + width_ = xx + 8; + } + if (height_ < yy + 8) { + height_ = yy + 8; + } + if (preview) { + if (xx < 0x39 && yy < 0x39 && xx >= 0 && yy >= 0) { + gfx::TileInfo ti; // t.GetTileInfo(); + for (auto yl = 0; yl < 8; yl++) { + for (auto xl = 0; xl < 4; xl++) { + int mx = xl; + int my = yl; + uint8_t r = 0; + + if (ti.horizontal_mirror_) { + mx = 3 - xl; + r = 1; + } + if (ti.vertical_mirror_) { + my = 7 - yl; + } + + // Formula information to get tile index position in the array. + //((ID / nbrofXtiles) * (imgwidth/2) + (ID - ((ID/16)*16) )) + int tx = ((ti.id_ / 0x10) * 0x200) + + ((ti.id_ - ((ti.id_ / 0x10) * 0x10)) * 4); + auto pixel = current_gfx16[tx + (yl * 0x40) + xl]; + // nx,ny = object position, xx,yy = tile position, xl,yl = pixel + // position + + int index = + ((xx / 8) * 8) + ((yy / 8) * 0x200) + ((mx * 2) + (my * 0x40)); + preview_object_data_[index + r ^ 1] = + (uint8_t)((pixel & 0x0F) + ti.palette_ * 0x10); + preview_object_data_[index + r] = + (uint8_t)(((pixel >> 4) & 0x0F) + ti.palette_ * 0x10); + } + } + } + } else { + if (((xx / 8) + nx_ + offset_x_) + ((ny_ + offset_y_ + (yy / 8)) * 0x40) < + 0x1000 && + ((xx / 8) + nx_ + offset_x_) + ((ny_ + offset_y_ + (yy / 8)) * 0x40) >= + 0) { + ushort td = 0; // gfx::GetTilesInfo(); // TODO t.GetTileInfo() + + // collisionPoint.Add( + // new Point(xx + ((nx + offsetX) * 8), yy + ((ny + +offsetY) * 8))); + + if (layer_ == 0 || (uint8_t)layer_ == 2 || all_bgs_) { + if (tileUnder == + tiles_bg1_buffer[((xx / 8) + offset_x_ + nx_) + + ((ny_ + offset_y_ + (yy / 8)) * 0x40)]) { + return; + } + + tiles_bg1_buffer[((xx / 8) + offset_x_ + nx_) + + ((ny_ + offset_y_ + (yy / 8)) * 0x40)] = td; + } + + if ((uint8_t)layer_ == 1 || all_bgs_) { + if (tileUnder == + tiles_bg2_buffer[((xx / 8) + nx_ + offset_x_) + + ((ny_ + offset_y_ + (yy / 8)) * 0x40)]) { + return; + } + + tiles_bg2_buffer[((xx / 8) + nx_ + offset_x_) + + ((ny_ + offset_y_ + (yy / 8)) * 0x40)] = td; + } + } + } +} + +} // 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 new file mode 100644 index 00000000..12ca9c25 --- /dev/null +++ b/src/app/zelda3/dungeon/room_object.h @@ -0,0 +1,253 @@ +#ifndef YAZE_APP_ZELDA3_DUNGEON_ROOM_OBJECT_H +#define YAZE_APP_ZELDA3_DUNGEON_ROOM_OBJECT_H + +#include +#include +#include +#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 Tile {}; + +enum class SpecialObjectType { Chest, BigChest, InterroomStairs }; + +enum Background2 { + Off, + Parallax, + Dark, + OnTop, + Translucent, + Addition, + Normal, + Transparent, + DarkRoom // TODO: Determine if DarkRoom will stay there or not +}; + +enum Sorting { + All = 0, + Wall = 1, + Horizontal = 2, + Vertical = 4, + NonScalable = 8, + Dungeons = 16, + Floors = 32, + SortStairs = 64 +}; + +enum ObjectOption { + Nothing = 0, + Door = 1, + Chest = 2, + Block = 4, + Torch = 8, + Bgr = 16, + Stairs = 32 +}; + +class RoomObject : public SharedROM { + public: + enum LayerType { BG1 = 0, BG2 = 1, BG3 = 2 }; + + RoomObject(int16_t id, uint8_t x, uint8_t y, uint8_t size, uint8_t layer = 0) + : id_(id), + x_(x), + y_(y), + size_(size), + layer_(static_cast(layer)), + nx_(x), + ny_(y), + ox_(x), + oy_(y), + width_(16), + height_(16), + unique_id_(0) {} + + virtual void Draw() { + // ... Draw function implementation here + } + + void GetObjectSize() { + previous_size_ = size_; + size_ = 1; + // Draw(); + GetBaseSize(); + UpdateSize(); + size_ = 2; + // Draw(); + GetSizeSized(); + UpdateSize(); + size_ = previous_size_; + } + + void GetBaseSize() { + base_width_ = width_; + base_height_ = height_; + } + + void GetSizeSized() { + size_height_ = height_ - base_height_; + size_width_ = width_ - base_width_; + } + + void UpdateSize() { + width_ = 8; + height_ = 8; + } + + void AddTiles(int nbr, int pos) { + auto rom_data = rom()->data(); + for (int i = 0; i < nbr; i++) { + ASSIGN_OR_LOG_ERROR(auto tile, rom()->ReadTile16(pos + (i * 2))); + tiles_.push_back(tile); + } + } + + void DrawTile(Tile t, int xx, int yy, std::vector& current_gfx16, + std::vector& tiles_bg1_buffer, + std::vector& tiles_bg2_buffer, + ushort tile_under = 0xFFFF); + + protected: + bool all_bgs_ = false; + bool lit_ = false; + bool deleted_ = false; + bool show_rectangle_ = false; + bool diagonal_fix_ = false; + bool selected_ = false; + + int16_t id_; + uint8_t x_; + uint8_t y_; + uint8_t size_; + uint8_t nx_; + uint8_t ny_; + uint8_t ox_; + uint8_t oy_; + uint8_t z_ = 0; + uint8_t previous_size_ = 0; + + int width_; + int height_; + int base_width_; + int base_height_; + int size_width_; + int size_height_; + int tile_index_ = 0; + int offset_x_ = 0; + int offset_y_ = 0; + int preview_id_ = 0; + int unique_id_ = 0; + + std::string name_; + + LayerType layer_; + ObjectOption options_ = ObjectOption::Nothing; + std::vector tiles_; + std::vector preview_object_data_; +}; + +class Subtype1 : public RoomObject { + public: + std::vector tiles; + std::string name; + bool allBgs; + Sorting sort; + int tile_count_; + + Subtype1(int16_t id, uint8_t x, uint8_t y, uint8_t size, uint8_t layer, + int tileCount) + : RoomObject(id, x, y, size, layer), tile_count_(tileCount) { + auto rom_data = rom()->data(); + name = Type1RoomObjectNames[id & 0xFF]; + int pos = + core::tile_address + + static_cast( + (rom_data[core::subtype1_tiles + ((id & 0xFF) * 2) + 1] << 8) + + rom_data[core::subtype1_tiles + ((id & 0xFF) * 2)]); + AddTiles(tile_count_, pos); + sort = (Sorting)(Sorting::Horizontal | Sorting::Wall); + } + + void Draw() override { + for (int s = 0; s < size_ + (tile_count_ == 8 ? 1 : 0); s++) { + for (int i = 0; i < tile_count_; i++) { + // DrawTile(tiles[i], ((s * 2)) * 8, (i / 2) * 8); + } + } + } +}; + +class Subtype2 : public RoomObject { + public: + std::vector tiles; + std::string name; + bool allBgs; + Sorting sort; + + Subtype2(int16_t id, uint8_t x, uint8_t y, uint8_t size, uint8_t layer) + : RoomObject(id, x, y, size, layer) { + auto rom_data = rom()->data(); + name = Type2RoomObjectNames[id & 0x7F]; + int pos = + core::tile_address + + static_cast( + (rom_data[core::subtype2_tiles + ((id & 0x7F) * 2) + 1] << 8) + + rom_data[core::subtype2_tiles + ((id & 0x7F) * 2)]); + AddTiles(8, pos); + sort = (Sorting)(Sorting::Horizontal | Sorting::Wall); + } + + void Draw() override { + for (int i = 0; i < 8; i++) { + // DrawTile(tiles[i], x_ * 8, (y_ + i) * 8); + } + } +}; + +class Subtype3 : public RoomObject { + public: + std::vector tiles; + std::string name; + bool allBgs; + Sorting sort; + + Subtype3(int16_t id, uint8_t x, uint8_t y, uint8_t size, uint8_t layer) + : RoomObject(id, x, y, size, layer) { + auto rom_data = rom()->data(); + name = Type3RoomObjectNames[id & 0xFF]; + int pos = + core::tile_address + + static_cast( + (rom_data[core::subtype3_tiles + ((id & 0xFF) * 2) + 1] << 8) + + rom_data[core::subtype3_tiles + ((id & 0xFF) * 2)]); + AddTiles(8, pos); + sort = (Sorting)(Sorting::Horizontal | Sorting::Wall); + } + + void Draw() override { + for (int i = 0; i < 8; i++) { + // DrawTile(tiles[i], x_ * 8, (y_ + i) * 8); + } + } +}; + +} // namespace dungeon +} // namespace zelda3 +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_ZELDA3_DUNGEON_ROOM_OBJECT_H \ No newline at end of file diff --git a/src/app/spc700/spc700.def b/src/app/zelda3/music/spc700.def similarity index 100% rename from src/app/spc700/spc700.def rename to src/app/zelda3/music/spc700.def diff --git a/src/app/zelda3/music/tracker.cc b/src/app/zelda3/music/tracker.cc new file mode 100644 index 00000000..c729c16e --- /dev/null +++ b/src/app/zelda3/music/tracker.cc @@ -0,0 +1,1344 @@ +/** + * @file tracker.cc + * + * @brief Legacy code from Hyrule Magic TrackerLogic.c + * + * @details Attemtping to extract the song banks and convert them into SPC + * format for the snes_spc library. + * + */ +#include "tracker.h" + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "app/core/constants.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_tile.h" +#include "app/rom.h" +#include "snes_spc/snes_spc/spc.h" + +namespace yaze { +namespace app { +namespace zelda3 { + +namespace { + +void AddSPCReloc(SongSPCBlock *sbl, short addr) { + sbl->relocs[sbl->relnum++] = addr; + if (sbl->relnum == sbl->relsz) { + sbl->relsz += 16; + sbl->relocs = (unsigned short *)realloc(sbl->relocs, sbl->relsz << 1); + } +} + +} // namespace + +// ============================================================================= + +SongSPCBlock *Tracker::AllocSPCBlock(int len, int bank) { + SongSPCBlock *sbl; + if (!len) { + printf("warning zero length block allocated"); + } + if (ss_num == ss_size) { + ss_size += 512; + ssblt = (SongSPCBlock **)realloc(ssblt, ss_size << 2); + } + ssblt[ss_num] = sbl = (SongSPCBlock *)malloc(sizeof(SongSPCBlock)); + ss_num++; + sbl->start = ss_next; + sbl->len = len; + sbl->buf = (uchar *)malloc(len); + sbl->relocs = (ushort *)malloc(32); + sbl->relsz = 16; + sbl->relnum = 0; + sbl->bank = bank & 7; + sbl->flag = bank >> 3; + ss_next += len; + return sbl; +} + +// ============================================================================= + +unsigned char *Tracker::GetSPCAddr(ROM &rom, unsigned short addr, short bank) { + unsigned char *rom_ptr; + unsigned short a; + unsigned short b; + spcbank = bank + 1; + +again: + rom_ptr = rom.data() + sbank_ofs[spcbank]; + + for (;;) { + a = *(unsigned short *)rom_ptr; + + if (!a) { + if (spcbank) { + spcbank = 0; + + goto again; + } else + return nullptr; + } + + b = *(unsigned short *)(rom_ptr + 2); + rom_ptr += 4; + + if (addr >= b && addr - b < a) { + spclen = a; + + return rom_ptr + addr - b; + } + + rom_ptr += a; + } +} + +// ============================================================================= + +short Tracker::AllocSPCCommand() { + int i = m_free; + int j; + int k; + SPCCommand *spc_command; + if (i == -1) { + j = m_size; + m_size += 1024; + spc_command = current_spc_command_ = (SPCCommand *)realloc( + current_spc_command_, m_size * sizeof(SPCCommand)); + k = 1023; + while (k--) spc_command[j].next = j + 1, j++; + spc_command[j].next = -1; + k = 1023; + while (k--) spc_command[j].prev = j - 1, j--; + spc_command[j].prev = -1; + i = j; + } else + spc_command = current_spc_command_; + m_free = spc_command[m_free].next; + if (m_free != -1) spc_command[m_free].prev = -1; + return i; +} + +// ============================================================================= + +short Tracker::GetBlockTime(ROM &rom, short num, short prevtime) { + SPCCommand *spc_command = current_spc_command_; + SPCCommand *spc_command2; + + int i = -1; + int j = 0; + int k = 0; + int l; + int m = 0; + int n = prevtime; + l = num; + + if (l == -1) return 0; + + for (;;) { + if (spc_command[l].flag & 4) { + j = spc_command[l].tim; + m = spc_command[l].tim2; + k = 1; + } + + if (!k) i = l; + + if (spc_command[l].flag & 1) n = spc_command[l].b1; + + l = spc_command[l].next; + + if (l == -1) { + if (!k) { + m = 0; + j = 0; + } + + break; + } + } + + if (i != -1) + for (;;) { + if (i == -1) { + printf("Error"); + m_modf = 1; + return 0; + } + spc_command2 = spc_command + i; + if (spc_command2->cmd == 0xef) { + k = *(short *)&(spc_command2->p1); + if (k >= m_size) { + printf("Invalid music address\n"); + m_modf = 1; + return 0; + } + if (spc_command2->flag & 1) { + j += GetBlockTime(rom, k, 0) * spc_command2->p3; + if (ss_lasttime) { + j += ss_lasttime * m; + j += spc_command[k].tim2 * spc_command2->b1; + j += (spc_command2->p3 - 1) * spc_command[k].tim2 * ss_lasttime; + } else { + j += + spc_command2->b1 * (m + spc_command[k].tim2 * spc_command2->p3); + } + m = 0; + } else { + j += GetBlockTime(rom, k, 0) * spc_command2->p3; + j += ss_lasttime * m; + if (ss_lasttime) + j += (spc_command2->p3 - 1) * ss_lasttime * spc_command[k].tim2, + m = spc_command[k].tim2; + else + m += spc_command[k].tim2 * spc_command2->p3; + } + } else { + if (spc_command2->cmd < 0xe0) m++; + if (spc_command2->flag & 1) { + j += m * spc_command[i].b1; + m = 0; + } + } + spc_command2->tim = j; + spc_command2->tim2 = m; + spc_command2->flag |= 4; + + if (i == num) break; + + i = spc_command2->prev; + } + + ss_lasttime = n; + return spc_command[num].tim + prevtime * spc_command[num].tim2; +} + +// ============================================================================= + +short Tracker::LoadSPCCommand(ROM &rom, unsigned short addr, short bank, + int t) { + int b = 0; + int c = 0; + int d = 0; + int e = 0; + int f = 0; + int g = 0; + int h = 0; + int i = 0; + int l = 0; + int m = 0; + int n = 0; + int o = 0; + + unsigned char j = 0; + unsigned char k = 0; + + unsigned char *a = nullptr; + + SongRange *sr; + SPCCommand *spc_command = current_spc_command_; + SPCCommand *spc_command2; + if (!addr) return -1; + + a = GetSPCAddr(rom, addr, bank); + d = spcbank; + if (!a) { + printf("Address not found when loading track"); + return -1; + } + sr = song_range_; + e = srnum; + f = 0x10000; + + for (c = 0; c < e; c++) { + if (sr[c].bank == d) + if (sr[c].start > addr) { + if (sr[c].start < f) f = sr[c].start; + n = c; + } else if (sr[c].end > addr) { + for (f = sr[c].first; f != -1; f = spc_command[f].next) { + if (spc_command[f].flag & 4) + m = spc_command[f].tim, o = spc_command[f].tim2; + if (spc_command[f].addr == addr) { + spc_command[f].tim = m; + spc_command[f].tim2 = o; + lastsr = c; + return f; + } + if (spc_command[f].flag & 1) k = spc_command[f].b1; + if (spc_command[f].cmd < 0xca) + if (k) + m -= k; + else + o--; + } + printf("Misaligned music pointer"); + return -1; + } + } + + c = n; + i = h = m_free; + a -= addr; + m = 0; + k = 0; + o = 0; + + for (g = addr; g < f;) { + spc_command2 = spc_command + i; + if (spc_command2->next == -1) { + l = m_size; + spc_command = current_spc_command_ = (SPCCommand *)realloc( + spc_command, sizeof(SPCCommand) * (m_size += 1024)); + spc_command2 = spc_command + i; + n = l + 1023; + while (l < n) spc_command[l].next = l + 1, l++; + spc_command[l].next = -1; + n -= 1023; + while (l > n) spc_command[l].prev = l - 1, l--; + spc_command[l].prev = i; + spc_command2->next = l; + } + spc_command2->addr = g; + b = a[g]; + if (!b) break; + if (m >= t && b != 0xf9) break; + g++; + j = 0; + if (b < 128) { + j = 1; + k = spc_command2->b1 = b; + b = a[g++]; + if (b < 128) j = 3, spc_command2->b2 = b, b = a[g++]; + } + if (b < 0xe0) + if (k) + m += k; + else + o++; + spc_command2->cmd = b; + spc_command2->flag = j; + if (b >= 0xe0) { + b -= 0xe0; + if (op_len[b]) spc_command2->p1 = a[g++]; + if (op_len[b] > 1) spc_command2->p2 = a[g++]; + if (op_len[b] > 2) spc_command2->p3 = a[g++]; + if (b == 15) { + m_free = spc_command2->next; + spc_command[spc_command2->next].prev = -1; + l = LoadSPCCommand(rom, *(short *)(&(spc_command2->p1)), bank, t - m); + spc_command = current_spc_command_; + spc_command2 = spc_command + i; + *(short *)(&(spc_command2->p1)) = l; + spc_command2->next = m_free; + spc_command[spc_command2->next].prev = i; + GetBlockTime(rom, l, 0); + if (spc_command[l].flag & 4) + m += + (spc_command[l].tim + spc_command[l].tim2 * k) * spc_command2->p3; + else { + i = spc_command2->next; + break; + } + if (song_range_[lastsr].endtime) k = song_range_[lastsr].endtime; + } + } + i = spc_command2->next; + } + + spc_command[h].tim = m; + spc_command[h].tim2 = o; + spc_command[h].flag |= 4; + + if (f == g && m < t) { + l = spc_command[i].prev; + lastsr = c; + spc_command[sr[lastsr].first].prev = l; + l = spc_command[l].next = sr[lastsr].first; + if (spc_command[l].flag & 4) + spc_command[h].tim = spc_command[l].tim + m + spc_command[l].tim2 * k, + spc_command[h].flag |= 4; + sr[lastsr].first = h; + sr[lastsr].start = addr; + sr[lastsr].inst++; + } else { + if (srsize == srnum) + song_range_ = + (SongRange *)realloc(song_range_, (srsize += 16) * sizeof(SongRange)); + + lastsr = srnum; + sr = song_range_ + (srnum++); + sr->start = addr; + sr->end = g; + sr->first = h; + sr->endtime = k; + sr->inst = 1; + sr->editor = 0; + sr->bank = d; + spc_command[spc_command[i].prev].next = -1; + } + + spc_command[i].prev = -1; + m_free = i; + + return h; +} + +// ============================================================================= + +void Tracker::LoadSongs(ROM &rom) { + unsigned char *b; + unsigned char *c; + unsigned char *d; + short *e; + + Song song; + Song song2; + SongPart *sp; + SPCCommand *spc_command; + ZeldaWave *zelda_wave; + + int i; + int j; + int k; + int l = 0; + int m; + int n; + int o; + int p; + int q; + int r; + int t; + int u; + int range; + int filter; + + spc_command = current_spc_command_ = + (SPCCommand *)malloc(1024 * sizeof(SPCCommand)); + m_free = 0; + m_size = 1024; + srnum = 0; + srsize = 0; + song_range_ = 0; + sp_mark = 0; + b = rom.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]; + + for (i = 0; i < 1024; i++) { + spc_command[i].next = i + 1; + spc_command[i].prev = i - 1; + } + + // Init blank songs. + for (i = 0; i < 128; i++) { + Song new_song; + songs.emplace_back(new_song); + } + + spc_command[1023].next = -1; + for (i = 0; i < 3; i++) { + // Extract the song banks. + b = GetSPCAddr(rom, 0xd000, i); + for (j = 0;; j++) { + if ((r = ((unsigned short *)b)[j]) >= 0xd000) { + r = (r - 0xd000) >> 1; + break; + } + } + + numsong[i] = r; + for (j = 0; j < r; j++) { + k = ((unsigned short *)b)[j]; + if (!k) + songs[l].in_use = false; + else { + c = GetSPCAddr(rom, k, i); + + // Init the bank index we are current loading. + if (!spcbank) + m = 0; + else + m = l - j; + + for (; m < l; m++) + if (songs[m].in_use && songs[m].addr == k) { + (songs[l] = songs[m]).inst++; + + break; + } + + if (m == l) { + // create a new song (Song *)malloc(sizeof(Song)); + songs[l] = song; + song.inst = 1; + song.addr = k; + song.flag = !spcbank; + + for (m = 0;; m++) + if ((n = ((unsigned short *)c)[m]) < 256) break; + + if (n > 0) { + song.flag |= 2; + song.lopst = (((unsigned short *)c)[m + 1] - k) >> 1; + } + + song.numparts = m; + song.tbl = (SongPart **)malloc(4 * m); + + for (m = 0; m < song.numparts; m++) { + k = ((unsigned short *)c)[m]; + d = GetSPCAddr(rom, k, i); + if (!spcbank) + n = 0; + else + n = l - j; + + for (; n < l; n++) { + song2 = songs[n]; + if (song2.in_use) + for (o = 0; o < song2.numparts; o++) + if (song2.tbl[o]->addr == k) { + (song.tbl[m] = song2.tbl[o])->inst++; + goto foundpart; + } + } + + for (o = 0; o < m; o++) + if (song.tbl[o]->addr == k) { + (song.tbl[m] = song.tbl[o])->inst++; + goto foundpart; + } + + sp = song.tbl[m] = (SongPart *)malloc(sizeof(SongPart)); + sp->flag = !spcbank; + sp->inst = 1; + sp->addr = k; + p = 50000; + for (o = 0; o < 8; o++) { + q = sp->tbl[o] = + LoadSPCCommand(rom, ((unsigned short *)d)[o], i, p); + spc_command = current_spc_command_ + q; + if ((spc_command->flag & 4) && spc_command->tim < p) + p = spc_command->tim; + } + foundpart:; + } + } + } + l++; + } + } + + b = GetSPCAddr(rom, 0x800, 0); + snddat1 = (char *)malloc(spclen); + sndlen1 = spclen; + memcpy(snddat1, b, spclen); + + b = GetSPCAddr(rom, 0x17c0, 0); + snddat2 = (char *)malloc(spclen); + sndlen2 = spclen; + memcpy(snddat2, b, spclen); + + b = GetSPCAddr(rom, 0x3d00, 0); + insts = (ZeldaInstrument *)malloc(spclen); + memcpy(insts, b, spclen); + numinst = spclen / 6; + + b = GetSPCAddr(rom, 0x3e00, 0); + m_ofs = b - rom.data() + spclen; + sndinsts = (ZeldaSfxInstrument *)malloc(spclen); + memcpy(sndinsts, b, spclen); + numsndinst = spclen / 9; + + b = GetSPCAddr(rom, 0x3c00, 0); + zelda_wave = waves = (ZeldaWave *)malloc(sizeof(ZeldaWave) * (spclen >> 2)); + p = spclen >> 1; + + for (i = 0; i < p; i += 2) { + j = ((unsigned short *)b)[i]; + + if (j == 65535) break; + + for (k = 0; k < i; k += 2) { + if (((unsigned short *)b)[k] == j) { + zelda_wave->copy = (short)(k >> 1); + goto foundwave; + } + } + + zelda_wave->copy = -1; + + foundwave: + + d = GetSPCAddr(rom, j, 0); + e = (short *)malloc(2048); + + k = 0; + l = 1024; + u = t = 0; + + for (;;) { + m = *(d++); + + range = (m >> 4) + 8; + filter = (m & 12) >> 2; + + for (n = 0; n < 8; n++) { + o = (*d) >> 4; + + if (o > 7) o -= 16; + + o <<= range; + + if (filter) + o += (t * fil1[filter] >> fil2[filter]) - + ((u & -256) * fil3[filter] >> 4); + + if (o > 0x7fffff) o = 0x7fffff; + + if (o < -0x800000) o = -0x800000; + + u = o; + + // \code if(t>0x7fffff) t=0x7fffff; + // \code if(t < -0x800000) t=-0x800000; + + e[k++] = o >> 8; + + o = *(d++) & 15; + + if (o > 7) o -= 16; + + o <<= range; + + if (filter) + o += (u * fil1[filter] >> fil2[filter]) - + ((t & -256) * fil3[filter] >> 4); + + if (o > 0x7fffff) o = 0x7fffff; + + if (o < -0x800000) o = -0x800000; + + t = o; + // \code if(u>0x7fffff) u=0x7fffff; + // \code if(u < -0x800000) u= -0x800000; + e[k++] = o >> 8; + } + + if (m & 1) { + zelda_wave->lflag = (m & 2) >> 1; + break; + } + if (k == l) { + l += 1024; + e = (short *)realloc(e, l << 1); + } + } + + e = zelda_wave->buf = (short *)realloc(e, (k + 1) << 1); + + zelda_wave->lopst = (((unsigned short *)b)[i + 1] - j) * 16 / 9; + + if (zelda_wave->lflag) + e[k] = e[zelda_wave->lopst]; + else + e[k] = 0; + + zelda_wave->end = k; + + zelda_wave++; + } + + numwave = i >> 1; + m_loaded = 1; + w_modf = 0; +} + +short Tracker::SaveSPCCommand(ROM &rom, short num, short songtime, + short endtr) { + SPCCommand *spc_command = current_spc_command_; + SPCCommand *spc_command2; + SongRange *sr = song_range_; + SongSPCBlock *sbl; + + text_buf_ty buf; + + unsigned char *b; + int i = num; + int j = 0; + int k = 0; + int l = 0; + int m = 0; + int n = 0; + int o = 0; + int p = 0; + + if (i == -1) return 0; + + if (i >= m_size) { + printf("Error.\n"); + m_modf = 1; + return 0; + } + + if (spc_command[i].flag & 8) return spc_command[i].addr; + + for (;;) { + j = spc_command[i].prev; + if (j == -1) break; + i = j; + } + + for (j = 0; j < srnum; j++) { + if (sr[j].first == i) { + l = GetBlockTime(rom, i, 0); + m = i; + for (;;) { + if (m == -1) break; + k++; + spc_command2 = spc_command + m; + if (spc_command2->flag & 1) k++, n = spc_command2->b1; + if (spc_command2->flag & 2) k++; + if (spc_command2->cmd >= 0xe0) k += op_len[spc_command2->cmd - 0xe0]; + m = spc_command2->next; + } + songtime -= l; + + if (songtime > 0) { + l = (songtime + 126) / 127; + if (songtime % l) l += 2; + l++; + if (n && !songtime % n) { + p = songtime / n; + if (p < l) l = p; + } else + p = -1; + k += l; + } + k++; + sbl = AllocSPCBlock(k, sr[j].bank | ((!endtr) << 3) | 16); + b = sbl->buf; + + for (;;) { + if (i == -1) break; + spc_command2 = spc_command + i; + spc_command2->addr = b - sbl->buf + sbl->start; + spc_command2->flag |= 8; + if (spc_command2->flag & 1) *(b++) = spc_command2->b1; + if (spc_command2->flag & 2) *(b++) = spc_command2->b2; + *(b++) = spc_command2->cmd; + if (spc_command2->cmd >= 0xe0) { + o = op_len[spc_command2->cmd - 0xe0]; + if (spc_command2->cmd == 0xef) { + *(short *)b = + SaveSPCCommand(rom, *(short *)&(spc_command2->p1), 0, 1); + if (b) AddSPCReloc(sbl, b - sbl->buf); + b[2] = spc_command2->p3; + b += 3; + } else { + if (o) *(b++) = spc_command2->p1; + if (o > 1) *(b++) = spc_command2->p2; + if (o > 2) *(b++) = spc_command2->p3; + } + } + i = spc_command2->next; + } + + if (songtime > 0) { + if (l != p) { + l = (songtime + 126) / 127; + if (songtime % l) + n = 127; + else + n = songtime / l; + *(b++) = n; + } + + for (; songtime >= n; songtime -= n) *(b++) = 0xc9; + + if (songtime) { + *(b++) = (uint8_t)songtime; + *(b++) = 0xc9; + } + } + *(b++) = 0; + return spc_command[num].addr; + } + } + + printf("Address %04X not found", num); + + printf("Error"); + + m_modf = 1; + + return 0; +} + +// ============================================================================= + +int Tracker::WriteSPCData(ROM &rom, void *buf, int len, int addr, int spc, + int limit) { + unsigned char *rom_data = rom.data(); + + if (!len) return addr; + + if (((addr + len + 4) & 0x7fff) > 0x7ffb) { + if (addr + 5 > limit) goto error; + *(int *)(rom_data + addr) = 0x00010140; + rom_data[addr + 4] = 0xff; + addr += 5; + } + + if (addr + len + 4 > limit) { + error: + printf("Not enough space for sound data"); + m_modf = 1; + return 0xc8000; + } + + *(short *)(rom_data + addr) = len; + *(short *)(rom_data + addr + 2) = spc; + + memcpy(rom_data + addr + 4, buf, len); + + return addr + len + 4; +} + +// ============================================================================= + +void Tracker::SaveSongs(ROM &rom) { + int i; + int j; + int k; + int l = 0; + int m; + int n; + int o; + int p; + int q; + int r; + int t; + int u; + int v; + int w; + int a; + int e; + int f; + int g; + unsigned short bank_next[4]; + unsigned short bank_lwr[4]; + short *c; + short *d; + unsigned char *rom_data; + unsigned char *b; + + Song song; + + SPCCommand *spc_command; + + SongPart *sp; + + SongSPCBlock *stbl; + SongSPCBlock *sptbl; + SongSPCBlock *trtbl; + SongSPCBlock *pstbl; + + ZeldaWave *zelda_wave; + ZeldaWave *zelda_wave2; + + ZeldaInstrument *zi; + + SampleEdit *sed; + + short wtbl[128]; + short x[16]; + short y[18]; + unsigned char z[64]; + + ss_num = 0; + ss_size = 512; + ss_next = 0; + + // if the music has not been modified, return. + if (!(m_modf)) return; + + ssblt = (SongSPCBlock **)malloc(512 * sizeof(SongSPCBlock)); + + // set it so the music has not been modified. (reset the status) + m_modf = 0; + rom_data = rom.data(); + + // SetCursor(wait_cursor); + + for (i = 0; i < 3; i++) { + k = numsong[i]; + + for (j = 0; j < k; j++) { + song = songs[l++]; + + if (!song.in_use) continue; + + song.flag &= -5; + + for (m = 0; m < song.numparts; m++) { + sp = song.tbl[m]; + sp->flag &= -3; + } + } + } + + j = m_size; + spc_command = current_spc_command_; + + for (i = 0; i < j; i++) { + spc_command->flag &= -13; + spc_command++; + } + + l = 0; + + for (i = 0; i < 3; i++) { + k = numsong[i]; + stbl = AllocSPCBlock(k << 1, i + 1); + + for (j = 0; j < k; j++) { + song = songs[l++]; + + if (!song.in_use) { + ((short *)(stbl->buf))[j] = 0; + + continue; + } + + if (song.flag & 4) goto alreadysaved; + + sptbl = AllocSPCBlock(((song.numparts + 1) << 1) + (song.flag & 2), + (song.flag & 1) ? 0 : (i + 1)); + + for (m = 0; m < song.numparts; m++) { + sp = song.tbl[m]; + + if (sp->flag & 2) goto spsaved; + + trtbl = AllocSPCBlock(16, (sp->flag & 1) ? 0 : (i + 1)); + + p = 0; + + for (n = 0; n < 8; n++) { + o = GetBlockTime(rom, sp->tbl[n], 0); + + if (o > p) p = o; + } + + q = 1; + + for (n = 0; n < 8; n++) { + core::stle16b_i(trtbl->buf, n, SaveSPCCommand(rom, sp->tbl[n], p, q)); + + if (core::ldle16b_i(trtbl->buf, n)) AddSPCReloc(trtbl, n << 1), q = 0; + } + + sp->addr = trtbl->start; + sp->flag |= 2; + spsaved: + ((short *)(sptbl->buf))[m] = sp->addr; + + AddSPCReloc(sptbl, m << 1); + } + + if (song.flag & 2) { + ((short *)(sptbl->buf))[m++] = 255; + ((short *)(sptbl->buf))[m] = sptbl->start + (song.lopst << 1); + + AddSPCReloc(sptbl, m << 1); + } else + ((short *)(sptbl->buf))[m++] = 0; + + song.addr = sptbl->start; + song.flag |= 4; + alreadysaved: + ((short *)(stbl->buf))[j] = song.addr; + + AddSPCReloc(stbl, j << 1); + } + } + + if (w_modf) { + b = (uint8_t *)malloc(0xc000); + j = 0; + + zelda_wave = waves; + + // if (mbanks[3]) + // sed = (SampleEdit *)GetWindowLongPtr(mbanks[3], GWLP_USERDATA); + // else + // sed = 0; + + for (i = 0; i < numwave; i++, zelda_wave++) { + if (zelda_wave->copy != -1) continue; + + wtbl[i << 1] = j + 0x4000; + + if (zelda_wave->lflag) { + l = zelda_wave->end - zelda_wave->lopst; + + if (l & 15) { + k = (l << 15) / ((l + 15) & -16); + p = (zelda_wave->end << 15) / k; + c = (short *)malloc(p << 1); + n = 0; + d = zelda_wave->buf; + + for (m = 0;;) { + c[n++] = (d[m >> 15] * ((m & 32767) ^ 32767) + + d[(m >> 15) + 1] * (m & 32767)) / + 32767; + + m += k; + + if (n >= p) break; + } + + zelda_wave->lopst = (zelda_wave->lopst << 15) / k; + zelda_wave->end = p; + zelda_wave->buf = + (short *)realloc(zelda_wave->buf, (zelda_wave->end + 1) << 1); + memcpy(zelda_wave->buf, c, zelda_wave->end << 1); + free(c); + zelda_wave->buf[zelda_wave->end] = zelda_wave->buf[zelda_wave->lopst]; + zelda_wave2 = waves; + + for (m = 0; m < numwave; m++, zelda_wave2++) + if (zelda_wave2->copy == i) + zelda_wave2->lopst = zelda_wave2->lopst << 15 / k; + + zi = insts; + + for (m = 0; m < numinst; m++) { + n = zi->samp; + + if (n >= numwave) continue; + + if (n == i || waves[n].copy == i) { + o = (zi->multhi << 8) + zi->multlo; + o = (o << 15) / k; + zi->multlo = o; + zi->multhi = o >> 8; + + if (sed && sed->editinst == m) { + sed->init = 1; + // SetDlgItemInt(sed->dlg, 3014, o, 0); + sed->init = 0; + } + } + + zi++; + } + + // Modifywaves(rom, i); + } + } + + k = (-zelda_wave->end) & 15; + d = zelda_wave->buf; + n = 0; + wtbl[(i << 1) + 1] = ((zelda_wave->lopst + k) >> 4) * 9 + wtbl[i << 1]; + y[0] = y[1] = 0; + u = 4; + + for (;;) { + for (o = 0; o < 16; o++) { + if (k) + k--, x[o] = 0; + else + x[o] = d[n++]; + } + p = 0x7fffffff; + a = 0; + + for (t = 0; t < 4; t++) { + r = 0; + for (o = 0; o < 16; o++) { + l = x[o]; + y[o + 2] = l; + l += (y[o] * fil3[t] >> 4) - (y[o + 1] * fil1[t] >> fil2[t]); + if (l > r) + r = l; + else if (-l > r) + r = -l; + } + r <<= 1; + if (t) + m = 14; + else + m = 15; + for (q = 0; q < 12; q++, m += m) + if (m >= r) break; + tryagain: + if (q && (q < 12 || m == r)) + v = (1 << (q - 1)) - 1; + else + v = 0; + m = 0; + for (o = 0; o < 16; o++) { + l = (y[o + 1] * fil1[t] >> fil2[t]) - (y[o] * fil3[t] >> 4); + w = x[o]; + r = (w - l + v) >> q; + if ((r + 8) & 0xfff0) { + q++; + a -= o; + goto tryagain; + } + z[a++] = r; + l = (r << q) + l; + y[o + 2] = l; + l -= w; + m += l * l; + } + if (u == 4) { + u = 0, e = q, f = y[16], g = y[17]; + break; + } + if (m < p) p = m, u = t, e = q, f = y[16], g = y[17]; + } + m = (e << 4) | (u << 2); + if (n == zelda_wave->end) m |= 1; + if (zelda_wave->lflag) m |= 2; + b[j++] = m; + m = 0; + a = u << 4; + for (o = 0; o < 16; o++) { + m |= z[a++] & 15; + if (o & 1) + b[j++] = m, m = 0; + else + m <<= 4; + } + if (n == zelda_wave->end) break; + y[0] = f; + y[1] = g; + } + } + + // if (sed) { + // SetDlgItemInt(sed->dlg, ID_Samp_SampleLengthEdit, sed->zelda_wave->end, + // 0); SetDlgItemInt(sed->dlg, ID_Samp_LoopPointEdit, + // sed->zelda_wave->lopst, 0); + + // InvalidateRect(GetDlgItem(sed->dlg, ID_Samp_Display), 0, 1); + // } + + zelda_wave = waves; + + for (i = 0; i < numwave; i++, zelda_wave++) { + if (zelda_wave->copy != -1) { + wtbl[i << 1] = wtbl[zelda_wave->copy << 1]; + wtbl[(i << 1) + 1] = (zelda_wave->lopst >> 4) * 9 + wtbl[i << 1]; + } + } + + m = WriteSPCData(rom, wtbl, numwave << 2, 0xc8000, 0x3c00, 0xd74fc); + m = WriteSPCData(rom, b, j, m, 0x4000, 0xd74fc); + + free(b); + m = WriteSPCData(rom, insts, numinst * 6, m, 0x3d00, 0xd74fc); + m = WriteSPCData(rom, snddat1, sndlen1, m, 0x800, 0xd74fc); + m = WriteSPCData(rom, snddat2, sndlen2, m, 0x17c0, 0xd74fc); + m = WriteSPCData(rom, sndinsts, numsndinst * 9, m, 0x3e00, 0xd74fc); + m_ofs = m; + } else { + m = m_ofs; + } + + bank_next[0] = 0x2880; + bank_next[1] = 0xd000; + bank_next[2] = 0xd000; + bank_next[3] = 0xd000; + bank_lwr[0] = 0x2880; + + for (k = 0; k < 4; k++) { + pstbl = 0; + for (i = 0; i < ss_num; i++) { + stbl = ssblt[i]; + if (stbl->bank != k) continue; + j = bank_next[k]; + if (j + stbl->len > 0xffc0) { + if (k == 3) + j = 0x2880; + else + j = bank_next[0]; + bank_lwr[k] = j; + pstbl = 0; + } + if (j + stbl->len > 0x3c00 && j < 0xd000) { + printf("Not enough space for music bank %d", k); + m_modf = 1; + return; + } + if (pstbl && (pstbl->flag & 1) && (stbl->flag & 2)) j--, pstbl->len--; + stbl->addr = j; + pstbl = stbl; + bank_next[k] = j + stbl->len; + } + } + for (i = 0; i < ss_num; i++) { + stbl = ssblt[i]; + for (j = stbl->relnum - 1; j >= 0; j--) { + k = *(unsigned short *)(stbl->buf + stbl->relocs[j]); + for (l = 0; l < ss_num; l++) { + sptbl = ssblt[l]; + if (sptbl->start <= k && sptbl->len > k - sptbl->start) goto noerror; + } + printf("Internal error"); + m_modf = 1; + return; + noerror: + + if (((!sptbl->bank) && stbl->bank < 3) || (sptbl->bank == stbl->bank)) { + *(unsigned short *)(stbl->buf + stbl->relocs[j]) = + sptbl->addr + k - sptbl->start; + } else { + printf("An address outside the bank was referenced.\n"); + m_modf = 1; + return; + } + } + } + l = m; + for (k = 0; k < 4; k++) { + switch (k) { + case 1: + rom[0x914] = l; + rom[0x918] = (l >> 8) | 128; + rom[0x91c] = l >> 15; + break; + case 2: + l = 0xd8000; + break; + case 3: + l = m; + rom[0x932] = l; + rom[0x936] = (l >> 8) | 128; + rom[0x93a] = l >> 15; + break; + } + for (o = 0; o < 2; o++) { + n = l + 4; + for (i = 0; i < ss_num; i++) { + stbl = ssblt[i]; + if (!stbl) continue; + if ((stbl->addr < 0xd000) ^ o) continue; + if (stbl->bank != k) continue; + if (n + stbl->len > ((k == 2) ? 0xdb7fc : 0xd74fc)) { + printf("Not enough space for music"); + m_modf = 1; + return; + } + memcpy(rom.data() + n, stbl->buf, stbl->len); + n += stbl->len; + free(stbl->relocs); + free(stbl->buf); + free(stbl); + ssblt[i] = 0; + } + if (n > l + 4) { + *(short *)(rom + l) = n - l - 4; + *(short *)(rom + l + 2) = o ? bank_lwr[k] : 0xd000; + l = n; + } + } + *(short *)(rom + l) = 0; + *(short *)(rom + l + 2) = 0x800; + if (k == 1) m = l + 4; + } + free(ssblt); +} + +// ============================================================================= + +void Tracker::EditTrack(ROM &rom, short i) { + int j, k, l; + SongRange *sr = song_range_; + SPCCommand *spc_command; + + text_buf_ty buf; + + // ----------------------------- + + k = srnum; + + spc_command = current_spc_command_; + + if (i == -1) return; + + if (i >= m_size) { + printf("Invalid address: %04X", i); + goto error; + } + + for (;;) { + if ((j = spc_command[i].prev) != -1) + i = j; + else + break; + } + + for (l = 0; l < k; l++) + if (sr->first == i) + break; + else + sr++; + + if (l == k) { + printf("Not found: %04X", i); + error: + printf("Error"); + return; + } + + // if (sr->editor) + // HM_MDI_ActivateChild(clientwnd, sr->editor); + // else + // Editwin(rom, "TRACKEDIT", "Song editor", l + (i << 16), + // sizeof(TRACKEDIT)); +} + +// CRITICAL_SECTION cs_song; +// ============================================================================= + +void Tracker::NewSR(ROM &rom, int bank) { + SPCCommand *spc_command; + SongRange *sr; + + if (srnum == srsize) { + srsize += 16; + song_range_ = (SongRange *)realloc(song_range_, srsize * sizeof(SongRange)); + } + + sr = song_range_ + srnum; + srnum++; + sr->first = AllocSPCCommand(); + sr->bank = bank; + sr->editor = 0; + spc_command = current_spc_command_ + sr->first; + spc_command->prev = -1; + spc_command->next = -1; + spc_command->cmd = 128; + spc_command->flag = 0; + EditTrack(rom, sr->first); +} + +// ============================================================================= + +} // namespace zelda3 +} // namespace app +} // namespace yaze diff --git a/src/app/zelda3/music/tracker.h b/src/app/zelda3/music/tracker.h new file mode 100644 index 00000000..e9fc4083 --- /dev/null +++ b/src/app/zelda3/music/tracker.h @@ -0,0 +1,259 @@ +#ifndef YAZE_APP_ZELDA3_TRACKER_H +#define YAZE_APP_ZELDA3_TRACKER_H + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "app/core/common.h" +#include "app/core/constants.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_tile.h" +#include "app/rom.h" +#include "snes_spc/snes_spc/spc.h" + +namespace yaze { +namespace app { +namespace zelda3 { + +// bank 19, 1A, 1B +// iirc 1A is OW, 1B is dungeon +// 19 is general spc stuff like samples, ects +constexpr char op_len[32] = {1, 1, 2, 3, 0, 1, 2, 1, 2, 1, 1, 3, 0, 1, 2, 3, + 1, 3, 3, 0, 1, 3, 0, 3, 3, 3, 1, 2, 0, 0, 0, 0}; + +// ============================================================================= + +static int sbank_ofs[] = {0xc8000, 0, 0xd8000, 0}; + +constexpr char fil1[4] = {0, 15, 61, 115}; +constexpr char fil2[4] = {0, 4, 5, 6}; +constexpr char fil3[4] = {0, 0, 15, 13}; + +constexpr int kOverworldMusicBank = 0x0D0000; +constexpr int kDungeonMusicBank = 0x0D8000; + +using text_buf_ty = char[512]; +// ============================================================================ + +using SongSPCBlock = struct { + unsigned short start; + unsigned short len; + unsigned short relnum; + unsigned short relsz; + unsigned short *relocs; + unsigned short bank; + unsigned short addr; + unsigned char *buf; + int flag; +}; + +// ============================================================================= + +using SongRange = struct { + unsigned short start; + unsigned short end; + + short first; + short inst; + short bank; + + unsigned char endtime; + unsigned char filler; + + int editor; +}; + +// ============================================================================= + +using SongPart = struct { + uchar flag; + uchar inst; + short tbl[8]; + unsigned short addr; +}; + +// ============================================================================= + +using Song = struct { + unsigned char flag; + unsigned char inst; + SongPart **tbl; + short numparts; + short lopst; + unsigned short addr; + bool in_use; // true +}; +// ============================================================================= + +using ZeldaWave = struct { + int lopst; + int end; + short lflag; + short copy; + short *buf; +}; + +// ============================================================================ + +using SampleEdit = struct { + unsigned short flag; + unsigned short init; + unsigned short editsamp; + int width; + int height; + int pageh; + int pagev; + int zoom; + int scroll; + int page; + + /// Left hand sample selection point + int sell; + + /// Right hand sample selection point + int selr; + + int editinst; + + ZeldaWave *zw; +}; + +// ============================================================================= + +using ZeldaInstrument = struct { + unsigned char samp; + unsigned char ad; + unsigned char sr; + unsigned char gain; + unsigned char multhi; + unsigned char multlo; +}; + +// ============================================================================= + +using ZeldaSfxInstrument = struct { + unsigned char voll; + unsigned char volr; + short freq; + unsigned char samp; + unsigned char ad; + unsigned char sr; + unsigned char gain; + unsigned char multhi; +}; + +// ============================================================================= + +using SPCCommand = struct { + unsigned short addr; + short next; + short prev; + unsigned char flag; + unsigned char cmd; + unsigned char p1; + unsigned char p2; + unsigned char p3; + unsigned char b1; + unsigned char b2; + unsigned char tim2; + unsigned short tim; +}; + +// ============================================================================= + +class Tracker { + public: + SongSPCBlock *AllocSPCBlock(int len, int bank); + + unsigned char *GetSPCAddr(ROM &rom, unsigned short addr, short bank); + + short AllocSPCCommand(); + + short GetBlockTime(ROM &rom, short num, short prevtime); + + short SaveSPCCommand(ROM &rom, short num, short songtime, short endtr); + short LoadSPCCommand(ROM &rom, unsigned short addr, short bank, int t); + + void SaveSongs(ROM &rom); + + void LoadSongs(ROM &rom); + + int WriteSPCData(ROM &rom, void *buf, int len, int addr, int spc, int limit); + + void EditTrack(ROM &rom, short i); + + void NewSR(ROM &rom, int bank); + + private: + // A "modified" flag + int modf; + + int mark_sr; + + int mark_start; + int mark_end; + int mark_first; + int mark_last; + + int numwave; + int numinst; + int numsndinst; + + int sndinit = 0; + + int sndlen1; + int sndlen2; + int m_ofs; + int w_modf; + + int ss_num; + int ss_size; + + char op_len[32]; + + char *snddat1; + char *snddat2; // more music stuff. + + unsigned short ss_next = 0; + unsigned short spclen; + unsigned short numseg; + + short spcbank; + short lastsr; + short ss_lasttime; + short srnum; + short srsize; + short numsong[3]; // ditto + short m_size; + short m_free; + short m_modf; // ??? + short m_loaded; + + short t_loaded; + short t_modf; + short withhead; + + size_t t_number; + + std::vector songs; + SongPart *sp_mark; + SongRange *song_range_; + SPCCommand *current_spc_command_; + + SongSPCBlock **ssblt; + + ZeldaWave *waves; + ZeldaInstrument *insts; + ZeldaSfxInstrument *sndinsts; +}; + +// ============================================================================= + +} // namespace zelda3 +} // namespace app +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/app/zelda3/overworld.cc b/src/app/zelda3/overworld.cc index 352e14c2..71e15f94 100644 --- a/src/app/zelda3/overworld.cc +++ b/src/app/zelda3/overworld.cc @@ -1,7 +1,23 @@ #include "overworld.h" +#include + +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "app/core/constants.h" +#include "app/core/common.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_map.h" +#include "app/zelda3/sprite/sprite.h" namespace yaze { namespace app { @@ -9,22 +25,107 @@ namespace zelda3 { namespace { -uint GetOwMapGfxHighPtr(const uchar *rom, int index) { - int map_high_ptr = core::compressedAllMap32PointersHigh; - int p1 = (rom[(map_high_ptr) + 2 + (3 * index)] << 16) + - (rom[(map_high_ptr) + 1 + (3 * index)] << 8) + - (rom[(map_high_ptr + (3 * index))]); +uint GetOwMapGfxHighPtr(const uchar *rom, int index, uint32_t map_high_ptr) { + int p1 = (rom[map_high_ptr + 2 + (3 * index)] << 16) + + (rom[map_high_ptr + 1 + (3 * index)] << 8) + + (rom[map_high_ptr + (3 * index)]); return core::SnesToPc(p1); } -uint GetOwMapGfxLowPtr(const uchar *rom, int index) { - int map_low_ptr = core::compressedAllMap32PointersLow; - int p2 = (rom[(map_low_ptr) + 2 + (3 * index)] << 16) + - (rom[(map_low_ptr) + 1 + (3 * index)] << 8) + - (rom[(map_low_ptr + (3 * index))]); +uint GetOwMapGfxLowPtr(const uchar *rom, int index, uint32_t map_low_ptr) { + int p2 = (rom[map_low_ptr + 2 + (3 * index)] << 16) + + (rom[map_low_ptr + 1 + (3 * index)] << 8) + + (rom[map_low_ptr + (3 * index)]); return core::SnesToPc(p2); } +std::vector GetAllTile16(OWBlockset &tiles_used) { + std::vector all_tile_16; // Ensure it's 64 bits + + int sx = 0; + int sy = 0; + int c = 0; + for (int i = 0; i < kNumOverworldMaps; i++) { + for (int y = 0; y < 32; y += 2) { + for (int x = 0; x < 32; x += 2) { + gfx::Tile32 current_tile( + tiles_used[x + (sx * 32)][y + (sy * 32)], + tiles_used[x + 1 + (sx * 32)][y + (sy * 32)], + tiles_used[x + (sx * 32)][y + 1 + (sy * 32)], + tiles_used[x + 1 + (sx * 32)][y + 1 + (sy * 32)]); + + all_tile_16.push_back(current_tile.GetPackedValue()); + } + } + + sx++; + if (sx >= 8) { + sy++; + sx = 0; + } + + c++; + if (c >= 64) { + sx = 0; + sy = 0; + c = 0; + } + } + + return all_tile_16; +} + +absl::flat_hash_map parseFile(const std::string &filename) { + absl::flat_hash_map resultMap; + + std::ifstream file(filename); + if (!file.is_open()) { + std::cerr << "Failed to open file: " << filename << std::endl; + return resultMap; + } + + std::string line; + int currentKey; + bool isHigh = true; + + while (getline(file, line)) { + // Skip empty or whitespace-only lines + if (line.find_first_not_of(" \t\r\n") == std::string::npos) { + continue; + } + + // If the line starts with "MAPDTH" or "MAPDTL", extract the ID. + if (line.find("MAPDTH") == 0) { + auto num_str = line.substr(6); // Extract ID after "MAPDTH" + currentKey = std::stoi(num_str); + isHigh = true; + } else if (line.find("MAPDTL") == 0) { + auto num_str = line.substr(6); // Extract ID after "MAPDTH" + currentKey = std::stoi(num_str); + isHigh = false; + } else { + // Check if the currentKey is already in the map. If not, initialize it. + if (resultMap.find(currentKey) == resultMap.end()) { + resultMap[currentKey] = MapData(); + } + + // Split the line by commas and convert to uint8_t. + std::stringstream ss(line); + std::string valueStr; + while (getline(ss, valueStr, ',')) { + uint8_t value = std::stoi(valueStr, nullptr, 16); + if (isHigh) { + resultMap[currentKey].highData.push_back(value); + } else { + resultMap[currentKey].lowData.push_back(value); + } + } + } + } + + return resultMap; +} + } // namespace absl::Status Overworld::Load(ROM &rom) { @@ -34,49 +135,533 @@ absl::Status Overworld::Load(ROM &rom) { AssembleMap16Tiles(); RETURN_IF_ERROR(DecompressAllMapTiles()) - for (int map_index = 0; map_index < core::kNumOverworldMaps; ++map_index) + for (int map_index = 0; map_index < kNumOverworldMaps; ++map_index) overworld_maps_.emplace_back(map_index, rom_, tiles16); FetchLargeMaps(); LoadEntrances(); - LoadSprites(); - - auto size = tiles16.size(); - for (int i = 0; i < core::kNumOverworldMaps; ++i) { - if (i < 64) { - RETURN_IF_ERROR(overworld_maps_[i].BuildMap( - size, game_state_, 0, map_parent_, map_tiles_.light_world)) - } else if (i < 0x80 && i >= 0x40) { - RETURN_IF_ERROR(overworld_maps_[i].BuildMap( - size, game_state_, 1, map_parent_, map_tiles_.dark_world)) - } else { - RETURN_IF_ERROR(overworld_maps_[i].BuildMap( - size, game_state_, 2, map_parent_, map_tiles_.special_world)) - } + RETURN_IF_ERROR(LoadOverworldMaps()) + if (flags()->kDrawOverworldSprites) { + LoadSprites(); } is_loaded_ = true; return absl::OkStatus(); } -ushort Overworld::GenerateTile32(int i, int k, int dimension) { - return (ushort)(rom_[map32address[dimension] + k + (i)] + - (((rom_[map32address[dimension] + (i) + (k <= 1 ? 4 : 5)] >> - (k % 2 == 0 ? 4 : 0)) & +OWBlockset &Overworld::GetMapTiles(int world_type) { + switch (world_type) { + case 0: + return map_tiles_.light_world; + case 1: + return map_tiles_.dark_world; + case 2: + return map_tiles_.special_world; + default: + return map_tiles_.light_world; + } +} + +absl::Status Overworld::LoadOverworldMaps() { + auto size = tiles16.size(); + std::vector> futures; + for (int i = 0; i < kNumOverworldMaps; ++i) { + int world_type = 0; + if (i >= 64 && i < 0x80) { + world_type = 1; + } else if (i >= 0x80) { + world_type = 2; + } + futures.push_back(std::async(std::launch::async, [this, i, size, + world_type]() { + return overworld_maps_[i].BuildMap(size, game_state_, world_type, + map_parent_, GetMapTiles(world_type)); + })); + } + + // 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 absl::OkStatus(); +} + +absl::Status Overworld::SaveOverworldMaps() { + // Initialize map pointers + std::fill(map_pointers1_id.begin(), map_pointers1_id.end(), -1); + std::fill(map_pointers1_id.begin(), map_pointers1_id.end(), -1); + + // Compress and save each map + int pos = 0x058000; + for (int i = 0; i < 160; i++) { + std::vector single_map_1(512); + std::vector single_map_2(512); + + // Copy tiles32 data to single_map_1 and single_map_2 + int npos = 0; + for (int y = 0; y < 16; y++) { + for (int x = 0; x < 16; x++) { + auto packed1 = tiles32[npos + (i * 256)].GetPackedValue(); + auto packed2 = tiles32[npos + (i * 256) + 16].GetPackedValue(); + single_map_1[npos] = static_cast(packed1 & 0xFF); + single_map_2[npos] = static_cast(packed2 & 0xFF); + npos++; + } + } + + // Compress single_map_1 and single_map_2 + ASSIGN_OR_RETURN( + auto a, gfx::lc_lz2::CompressOverworld(single_map_1.data(), 0, 256)) + ASSIGN_OR_RETURN( + auto b, gfx::lc_lz2::CompressOverworld(single_map_2.data(), 0, 256)) + if (a.empty() || b.empty()) { + return absl::AbortedError("Error compressing map gfx."); + } + + // Save compressed data and pointers + map_data_p1[i] = a; + map_data_p2[i] = b; + + if (map_pointers1_id[i] == -1) { + // Save compressed data and pointer for map1 + std::copy(a.begin(), a.end(), map_data_p1[i].begin()); + int snes_pos = core::PcToSnes(pos); + map_pointers1[i] = snes_pos; + + RETURN_IF_ERROR(rom()->RunTransaction( + WriteAction{kCompressedAllMap32PointersLow + 0 + 3 * i, + uint8_t(snes_pos & 0xFF)}, + WriteAction{kCompressedAllMap32PointersLow + 1 + 3 * i, + uint8_t((snes_pos >> 8) & 0xFF)}, + WriteAction{kCompressedAllMap32PointersLow + 2 + 3 * i, + uint8_t((snes_pos >> 16) & 0xFF)}, + WriteAction{pos, std::vector(a)})) + + pos += a.size(); + } else { + // Save pointer for map1 + int snes_pos = map_pointers1[map_pointers1_id[i]]; + RETURN_IF_ERROR(rom()->RunTransaction( + WriteAction{kCompressedAllMap32PointersLow + 0 + 3 * i, + uint8_t(snes_pos & 0xFF)}, + WriteAction{kCompressedAllMap32PointersLow + 1 + 3 * i, + uint8_t((snes_pos >> 8) & 0xFF)}, + WriteAction{kCompressedAllMap32PointersLow + 2 + 3 * i, + uint8_t((snes_pos >> 16) & 0xFF)})) + } + + if (map_pointers2_id[i] == -1) { + // Save compressed data and pointer for map2 + std::copy(b.begin(), b.end(), map_data_p2[i].begin()); + int snes_pos = core::PcToSnes(pos); + map_pointers2[i] = snes_pos; + RETURN_IF_ERROR(rom()->RunTransaction( + WriteAction{kCompressedAllMap32PointersHigh + 0 + 3 * i, + static_cast(snes_pos & 0xFF)}, + WriteAction{kCompressedAllMap32PointersHigh + 1 + 3 * i, + static_cast((snes_pos >> 8) & 0xFF)}, + WriteAction{kCompressedAllMap32PointersHigh + 2 + 3 * i, + static_cast((snes_pos >> 16) & 0xFF)}, + WriteAction{pos, std::vector(b)})) + pos += b.size(); + } else { + // Save pointer for map2 + int snes_pos = map_pointers2[map_pointers2_id[i]]; + RETURN_IF_ERROR(rom()->RunTransaction( + WriteAction{kCompressedAllMap32PointersHigh + 0 + 3 * i, + static_cast(snes_pos & 0xFF)}, + WriteAction{kCompressedAllMap32PointersHigh + 1 + 3 * i, + static_cast((snes_pos >> 8) & 0xFF)}, + WriteAction{kCompressedAllMap32PointersHigh + 2 + 3 * i, + static_cast((snes_pos >> 16) & 0xFF)})) + } + } + + // Check if too many maps data + if (pos > 0x137FFF) { + std::cerr << "Too many maps data " << std::hex << pos << std::endl; + return absl::AbortedError("Too many maps data"); + } + + // Save large maps + RETURN_IF_ERROR(SaveLargeMaps()) + + return absl::OkStatus(); +} + +absl::Status Overworld::SaveLargeMaps() { + for (int i = 0; i < 0x40; i++) { + int yPos = i / 8; + int xPos = i % 8; + int parentyPos = overworld_maps_[i].Parent() / 8; + int parentxPos = overworld_maps_[i].Parent() % 8; + + std::unordered_map checkedMap; + + // Always write the map parent since it should not matter + RETURN_IF_ERROR( + rom()->Write(overworldMapParentId + i, overworld_maps_[i].Parent())) + + if (checkedMap.count(overworld_maps_[i].Parent()) > 0) { + continue; + } + + // If it's large then save parent pos * + // 0x200 otherwise pos * 0x200 + if (overworld_maps_[i].IsLargeMap()) { + RETURN_IF_ERROR(rom()->RunTransaction( + // Check 1 + WriteAction{overworldMapSize + i, 0x20}, + WriteAction{overworldMapSize + i + 1, 0x20}, + WriteAction{overworldMapSize + i + 8, 0x20}, + WriteAction{overworldMapSize + i + 9, 0x20}, + + // Check 2 + WriteAction{overworldMapSizeHighByte + i, 0x03}, + WriteAction{overworldMapSizeHighByte + i + 1, 0x03}, + WriteAction{overworldMapSizeHighByte + i + 8, 0x03}, + WriteAction{overworldMapSizeHighByte + i + 9, 0x03}, + + // Check 3 + WriteAction{overworldScreenSize + i, 0x00}, + WriteAction{overworldScreenSize + i + 64, 0x00}, + + WriteAction{overworldScreenSize + i + 1, 0x00}, + WriteAction{overworldScreenSize + i + 1 + 64, 0x00}, + + WriteAction{overworldScreenSize + i + 8, 0x00}, + WriteAction{overworldScreenSize + i + 8 + 64, 0x00}, + + WriteAction{overworldScreenSize + i + 9, 0x00}, + WriteAction{overworldScreenSize + i + 9 + 64, 0x00}, + + // Check 4 + WriteAction{OverworldScreenSizeForLoading + i, 0x04}, + WriteAction{OverworldScreenSizeForLoading + i + 64, 0x04}, + WriteAction{OverworldScreenSizeForLoading + i + 128, 0x04}, + + WriteAction{OverworldScreenSizeForLoading + i + 1, 0x04}, + WriteAction{OverworldScreenSizeForLoading + i + 1 + 64, 0x04}, + WriteAction{OverworldScreenSizeForLoading + i + 1 + 128, 0x04}, + + WriteAction{OverworldScreenSizeForLoading + i + 8, 0x04}, + WriteAction{OverworldScreenSizeForLoading + i + 8 + 64, 0x04}, + WriteAction{OverworldScreenSizeForLoading + i + 8 + 128, 0x04}, + + WriteAction{OverworldScreenSizeForLoading + i + 9, 0x04}, + WriteAction{OverworldScreenSizeForLoading + i + 9 + 64, 0x04}, + WriteAction{OverworldScreenSizeForLoading + i + 9 + 128, 0x04}, + + // Check 5 and 6 + WriteAction{transition_target_north + (i * 2) + 2, + (short)((parentyPos * 0x200) - + 0xE0)}, // (short) is placed to reduce the int to + // 2 bytes. + WriteAction{transition_target_west + (i * 2) + 2, + (short)((parentxPos * 0x200) - 0x100)}, + + // (short) is placed to reduce the int to 2 bytes. + WriteAction{transition_target_north + (i * 2) + 16, + (short)((parentyPos * 0x200) - 0xE0)}, + WriteAction{transition_target_west + (i * 2) + 16, + (short)((parentxPos * 0x200) - 0x100)}, + + // (short) is placed to reduce the int to 2 bytes. + WriteAction{transition_target_north + (i * 2) + 18, + (short)((parentyPos * 0x200) - 0xE0)}, + WriteAction{transition_target_west + (i * 2) + 18, + (short)((parentxPos * 0x200) - 0x100)}, + + // Check 7 and 8 + WriteAction{overworldTransitionPositionX + (i * 2), + (parentxPos * 0x200)}, + WriteAction{overworldTransitionPositionY + (i * 2), + (parentyPos * 0x200)}, + + WriteAction{overworldTransitionPositionX + (i * 2) + 2, + (parentxPos * 0x200)}, + WriteAction{overworldTransitionPositionY + (i * 2) + 2, + (parentyPos * 0x200)}, + + WriteAction{overworldTransitionPositionX + (i * 2) + 16, + (parentxPos * 0x200)}, + WriteAction{overworldTransitionPositionY + (i * 2) + 16, + (parentyPos * 0x200)}, + + WriteAction{overworldTransitionPositionX + (i * 2) + 18, + (parentxPos * 0x200)}, + WriteAction{overworldTransitionPositionY + (i * 2) + 18, + (parentyPos * 0x200)}, + + // Check 9 + // Always 0x0060 + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2), 0x0060}, + // Always 0x0060 + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 2, + 0x0060})) + + uint16_t lowerSubmaps; + // If parentX == 0 then lower submaps == 0x0060 too + if (parentxPos == 0) { + lowerSubmaps = 0x0060; + } else { + // Otherwise lower submaps == 0x1060 + lowerSubmaps = 0x1060; + } + + RETURN_IF_ERROR(rom()->RunTransaction( + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 16, + uint16_t(lowerSubmaps)}, + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 18, + uint16_t(lowerSubmaps)}, + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 128, + uint16_t(0x0080)}, // Always 0x0080 + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 2 + 128, + uint16_t(0x0080)}, // Always 0x0080 + // Lower are always 8010 + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 16 + 128, + uint16_t(0x1080)}, // Always 0x1080 + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 18 + 128, + uint16_t(0x1080)}, // Always 0x1080 + + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 256, + uint16_t(0x1800)}, // Always 0x1800 + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 16 + 256, + uint16_t(0x1800)}, // Always 0x1800 + // Right side is always 1840 + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 2 + 256, + uint16_t(0x1840)}, // Always 0x1840 + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 18 + 256, + uint16_t(0x1840)}, // Always 0x1840 + + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 384, + uint16_t(0x2000)}, // Always 0x2000 + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 16 + 384, + uint16_t(0x2000)}, // Always 0x2000 + // Right side is always 0x2040 + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 2 + 384, + uint16_t(0x2040)}, // Always 0x2000 + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 18 + 384, + uint16_t(0x2040)})) // Always 0x2000 + + checkedMap.emplace(i, 1); + checkedMap.emplace((i + 1), 1); + checkedMap.emplace((i + 8), 1); + checkedMap.emplace((i + 9), 1); + + } else { + RETURN_IF_ERROR(rom()->RunTransaction( + WriteAction{overworldMapSize + i, 0x00}, + WriteAction{overworldMapSizeHighByte + i, 0x01}, + WriteAction{overworldScreenSize + i, 0x01}, + WriteAction{overworldScreenSize + i + 64, 0x01}, + WriteAction{OverworldScreenSizeForLoading + i, 0x02}, + WriteAction{OverworldScreenSizeForLoading + i + 64, 0x02}, + WriteAction{OverworldScreenSizeForLoading + i + 128, 0x02}, + + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2), + uint16_t(0x0060)}, + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 128, + uint16_t(0x0040)}, + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 256, + uint16_t(0x1800)}, + WriteAction{OverworldScreenTileMapChangeByScreen + (i * 2) + 384, + (0x1000)}, + WriteAction{transition_target_north + (i * 2), + uint16_t((yPos * 0x200) - 0xE0)}, + WriteAction{transition_target_west + (i * 2), + uint16_t((xPos * 0x200) - 0x100)}, + WriteAction{overworldTransitionPositionX + (i * 2), + uint16_t(xPos * 0x200)}, + WriteAction{overworldTransitionPositionY + (i * 2), + uint16_t(yPos * 0x200)})) + + checkedMap.emplace(i, 1); + } + } + return absl::OkStatus(); +} + +bool Overworld::CreateTile32Tilemap(bool only_show) { + tiles32_unique_.clear(); + tiles32.clear(); + + OWBlockset *tiles_used; + for (int i = 0; i < kNumOverworldMaps; i++) { + if (i < 64) { + tiles_used = &map_tiles_.light_world; + } else if (i < 128 && i >= 64) { + tiles_used = &map_tiles_.dark_world; + } else { + tiles_used = &map_tiles_.special_world; + } + + std::vector all_tile_16 = GetAllTile16(*tiles_used); + + std::vector unique_tiles(all_tile_16); // Ensure it's 64 bits + std::sort(unique_tiles.begin(), unique_tiles.end()); + unique_tiles.erase(std::unique(unique_tiles.begin(), unique_tiles.end()), + unique_tiles.end()); + + // Ensure it's 64 bits + std::unordered_map all_tiles_indexed; + for (size_t j = 0; j < unique_tiles.size(); j++) { + all_tiles_indexed.insert({unique_tiles[j], static_cast(j)}); + } + + for (int j = 0; j < NumberOfMap32; j++) { + tiles32.push_back(all_tiles_indexed[all_tile_16[j]]); + } + + for (const auto &tile : unique_tiles) { + tiles32_unique_.push_back(static_cast(tile)); + } + + while (tiles32_unique_.size() % 4 != 0) { + gfx::Tile32 padding_tile(420, 420, 420, 420); + tiles32_unique_.push_back(padding_tile.GetPackedValue()); + } + } + + if (only_show) { + std::cout << "Number of unique Tiles32: " << tiles32_unique_.size() + << " Out of: " << LimitOfMap32 << std::endl; + } else if (tiles32_unique_.size() > LimitOfMap32) { + std::cerr << "Number of unique Tiles32: " << tiles32_unique_.size() + << " Out of: " << LimitOfMap32 + << "\nUnique Tile32 count exceed the limit" + << "\nThe ROM Has not been saved" + << "\nYou can fill maps with grass tiles to free some space" + << "\nOr use the option Clear DW Tiles in the Overworld Menu" + << std::endl; + return true; + } + + std::cout << "Number of unique Tiles32: " << tiles32_unique_.size() + << " Saved:" << tiles32_unique_.size() + << " Out of: " << LimitOfMap32 << std::endl; + + int v = tiles32_unique_.size(); + for (int i = v; i < LimitOfMap32; i++) { + gfx::Tile32 padding_tile(420, 420, 420, 420); + tiles32_unique_.push_back(padding_tile.GetPackedValue()); + } + + return false; +} + +absl::Status Overworld::SaveMap16Tiles() { + int tpos = kMap16Tiles; + // 3760 + for (int i = 0; i < NumberOfMap16; i += 1) { + RETURN_IF_ERROR(rom()->WriteShort(tpos, TileInfoToShort(tiles16[i].tile0_))) + tpos += 2; + RETURN_IF_ERROR(rom()->WriteShort(tpos, TileInfoToShort(tiles16[i].tile1_))) + tpos += 2; + RETURN_IF_ERROR(rom()->WriteShort(tpos, TileInfoToShort(tiles16[i].tile2_))) + tpos += 2; + RETURN_IF_ERROR(rom()->WriteShort(tpos, TileInfoToShort(tiles16[i].tile3_))) + tpos += 2; + } + return absl::OkStatus(); +} + +absl::Status Overworld::SaveMap32Tiles() { + constexpr int kMaxUniqueTiles = 0x4540; + constexpr int kTilesPer32x32Tile = 6; + constexpr int kQuadrantsPer32x32Tile = 4; + + if (tiles32_unique_.size() % kTilesPer32x32Tile != 0) { + return absl::InvalidArgumentError("Invalid number of unique tiles."); + } + + int unique_tile_index = 0; + int num_unique_tiles = tiles32_unique_.size(); + int num_32x32_tiles = num_unique_tiles / kTilesPer32x32Tile; + + if (num_32x32_tiles > kMaxUniqueTiles / kQuadrantsPer32x32Tile) { + return absl::AbortedError("Too many unique tile32 definitions."); + } + + for (int i = 0; i < num_32x32_tiles; ++i) { + int base_addr = + rom()->version_constants().kMap32TileTL + i * kQuadrantsPer32x32Tile; + + auto write_quadrant_to_rom = [&](int quadrant, + auto get_tile) -> absl::Status { + for (int j = 0; j < kQuadrantsPer32x32Tile; ++j) { + int tile_index = unique_tile_index + j; + const gfx::Tile32 &tile = tiles32_unique_[tile_index]; + RETURN_IF_ERROR( + rom()->Write(base_addr + quadrant + j, get_tile(tile) & 0xFF)); + } + + int tile0 = get_tile(tiles32_unique_[unique_tile_index]); + int tile1 = get_tile(tiles32_unique_[unique_tile_index + 1]); + int tile2 = get_tile(tiles32_unique_[unique_tile_index + 2]); + int tile3 = get_tile(tiles32_unique_[unique_tile_index + 3]); + + RETURN_IF_ERROR( + rom()->Write(base_addr + quadrant + 4, + ((tile0 >> 4) & 0xF0) | ((tile1 >> 8) & 0x0F))); + RETURN_IF_ERROR( + rom()->Write(base_addr + quadrant + 5, + ((tile2 >> 4) & 0xF0) | ((tile3 >> 8) & 0x0F))); + return absl::OkStatus(); + }; + + RETURN_IF_ERROR(write_quadrant_to_rom( + 0, [](const gfx::Tile32 &t) { return t.tile0_; })); + RETURN_IF_ERROR(write_quadrant_to_rom( + 1, [](const gfx::Tile32 &t) { return t.tile1_; })); + RETURN_IF_ERROR(write_quadrant_to_rom( + 2, [](const gfx::Tile32 &t) { return t.tile2_; })); + RETURN_IF_ERROR(write_quadrant_to_rom( + 3, [](const gfx::Tile32 &t) { return t.tile3_; })); + + unique_tile_index += kTilesPer32x32Tile; + } + + return absl::OkStatus(); +} + +uint16_t Overworld::GenerateTile32(int index, int quadrant, int dimension) { + // The addresses of the four 32x32 pixel tiles in the ROM. + const uint32_t map32address[4] = {rom()->version_constants().kMap32TileTL, + rom()->version_constants().kMap32TileTR, + rom()->version_constants().kMap32TileBL, + rom()->version_constants().kMap32TileBR}; + + return (ushort)(rom_[map32address[dimension] + quadrant + (index)] + + (((rom_[map32address[dimension] + (index) + + (quadrant <= 1 ? 4 : 5)] >> + (quadrant % 2 == 0 ? 4 : 0)) & 0x0F) * 256)); } void Overworld::AssembleMap32Tiles() { + // Loop through each 32x32 pixel tile in the ROM. for (int i = 0; i < 0x33F0; i += 6) { + // Loop through each quadrant of the 32x32 pixel tile. for (int k = 0; k < 4; k++) { - tiles32.push_back(gfx::Tile32( - /*top-left=*/GenerateTile32(i, k, (int)Dimension::map32TilesTL), - /*top-right=*/GenerateTile32(i, k, (int)Dimension::map32TilesTR), - /*bottom-left=*/GenerateTile32(i, k, (int)Dimension::map32TilesBL), - /*bottom-right=*/GenerateTile32(i, k, (int)Dimension::map32TilesBR))); + // Generate the 16-bit tile for the current quadrant of the current + // 32x32 pixel tile. + uint16_t tl = GenerateTile32(i, k, (int)Dimension::map32TilesTL); + uint16_t tr = GenerateTile32(i, k, (int)Dimension::map32TilesTR); + uint16_t bl = GenerateTile32(i, k, (int)Dimension::map32TilesBL); + uint16_t br = GenerateTile32(i, k, (int)Dimension::map32TilesBR); + + // Add the generated 16-bit tiles to the tiles32 vector. + tiles32.push_back(gfx::Tile32(tl, tr, bl, br)); } } + + // Initialize the light_world, dark_world, and special_world vectors with + // the appropriate number of tiles. map_tiles_.light_world.resize(kTile32Num); map_tiles_.dark_world.resize(kTile32Num); map_tiles_.special_world.resize(kTile32Num); @@ -88,15 +673,15 @@ void Overworld::AssembleMap32Tiles() { } void Overworld::AssembleMap16Tiles() { - int tpos = core::map16Tiles; + int tpos = kMap16Tiles; for (int i = 0; i < 4096; i += 1) { - auto t0 = gfx::GetTilesInfo((rom_.toint16(tpos))); + auto t0 = gfx::GetTilesInfo(rom()->toint16(tpos)); tpos += 2; - auto t1 = gfx::GetTilesInfo((rom_.toint16(tpos))); + auto t1 = gfx::GetTilesInfo(rom()->toint16(tpos)); tpos += 2; - auto t2 = gfx::GetTilesInfo((rom_.toint16(tpos))); + auto t2 = gfx::GetTilesInfo(rom()->toint16(tpos)); tpos += 2; - auto t3 = gfx::GetTilesInfo((rom_.toint16(tpos))); + auto t3 = gfx::GetTilesInfo(rom()->toint16(tpos)); tpos += 2; tiles16.emplace_back(t0, t1, t2, t3); } @@ -119,8 +704,7 @@ void Overworld::OrganizeMapTiles(Bytes &bytes, Bytes &bytes2, int i, int sx, for (int y = 0; y < 16; y++) { for (int x = 0; x < 16; x++) { auto tidD = (ushort)((bytes2[ttpos] << 8) + bytes[ttpos]); - int tpos = tidD; - if (tpos < tiles32.size()) { + if (int tpos = tidD; tpos < tiles32.size()) { if (i < 64) { AssignWorldTiles(x, y, sx, sy, tpos, map_tiles_.light_world); } else if (i < 128 && i >= 64) { @@ -141,8 +725,12 @@ absl::Status Overworld::DecompressAllMapTiles() { int sy = 0; int c = 0; for (int i = 0; i < 160; i++) { - auto p1 = GetOwMapGfxHighPtr(rom_.data(), i); - auto p2 = GetOwMapGfxLowPtr(rom_.data(), i); + auto p1 = GetOwMapGfxHighPtr( + rom()->data(), i, + rom()->version_constants().kCompressedAllMap32PointersHigh); + auto p2 = GetOwMapGfxLowPtr( + rom()->data(), i, + rom()->version_constants().kCompressedAllMap32PointersLow); int ttpos = 0; if (p1 >= highest) { @@ -159,8 +747,42 @@ absl::Status Overworld::DecompressAllMapTiles() { lowest = p2; } - ASSIGN_OR_RETURN(auto bytes, rom_.DecompressOverworld(p2, 1000)) - ASSIGN_OR_RETURN(auto bytes2, rom_.DecompressOverworld(p1, 1000)) + ASSIGN_OR_RETURN(auto bytes, + gfx::lc_lz2::DecompressOverworld(rom()->data(), p2, 1000)) + ASSIGN_OR_RETURN(auto bytes2, + gfx::lc_lz2::DecompressOverworld(rom()->data(), p1, 1000)) + OrganizeMapTiles(bytes, bytes2, i, sx, sy, ttpos); + + sx++; + if (sx >= 8) { + sy++; + sx = 0; + } + + c++; + if (c >= 64) { + sx = 0; + sy = 0; + c = 0; + } + } + return absl::OkStatus(); +} + +absl::Status Overworld::DecompressProtoMapTiles(const std::string &filename) { + proto_map_data_ = parseFile(filename); + int sx = 0; + int sy = 0; + int c = 0; + for (int i = 0; i < proto_map_data_.size(); i++) { + int ttpos = 0; + + ASSIGN_OR_RETURN(auto bytes, gfx::lc_lz2::DecompressOverworld( + proto_map_data_[i].lowData, 0, + proto_map_data_[i].lowData.size())) + ASSIGN_OR_RETURN(auto bytes2, gfx::lc_lz2::DecompressOverworld( + proto_map_data_[i].highData, 0, + proto_map_data_[i].highData.size())) OrganizeMapTiles(bytes, bytes2, i, sx, sy, ttpos); sx++; @@ -177,8 +799,6 @@ absl::Status Overworld::DecompressAllMapTiles() { } } - std::cout << "MapPointers(lowest) : " << lowest << std::endl; - std::cout << "MapPointers(highest) : " << highest << std::endl; return absl::OkStatus(); } @@ -195,15 +815,15 @@ void Overworld::FetchLargeMaps() { map_parent_[136] = 136; overworld_maps_[136].SetLargeMap(false); - bool mapChecked[64]; + std::vector mapChecked; + mapChecked.reserve(0x40); for (int i = 0; i < 64; i++) { mapChecked[i] = false; } int xx = 0; int yy = 0; while (true) { - int i = xx + (yy * 8); - if (mapChecked[i] == false) { + if (int i = xx + (yy * 8); mapChecked[i] == false) { if (overworld_maps_[i].IsLargeMap() == true) { mapChecked[i] = true; map_parent_[i] = (uchar)i; @@ -241,9 +861,9 @@ void Overworld::FetchLargeMaps() { void Overworld::LoadEntrances() { for (int i = 0; i < 129; i++) { - short mapId = rom_.toint16(core::OWEntranceMap + (i * 2)); - ushort mapPos = rom_.toint16(core::OWEntrancePos + (i * 2)); - uchar entranceId = (rom_[core::OWEntranceEntranceId + i]); + short mapId = rom()->toint16(OWEntranceMap + (i * 2)); + ushort mapPos = rom()->toint16(OWEntrancePos + (i * 2)); + uchar entranceId = (rom_[OWEntranceEntranceId + i]); int p = mapPos >> 1; int x = (p % 64); int y = (p >> 6); @@ -258,11 +878,11 @@ void Overworld::LoadEntrances() { } for (int i = 0; i < 0x13; i++) { - auto mapId = (short)((rom_[core::OWHoleArea + (i * 2) + 1] << 8) + - (rom_[core::OWHoleArea + (i * 2)])); - auto mapPos = (short)((rom_[core::OWHolePos + (i * 2) + 1] << 8) + - (rom_[core::OWHolePos + (i * 2)])); - uchar entranceId = (rom_[core::OWHoleEntrance + i]); + auto mapId = (short)((rom_[OWHoleArea + (i * 2) + 1] << 8) + + (rom_[OWHoleArea + (i * 2)])); + auto mapPos = (short)((rom_[OWHolePos + (i * 2) + 1] << 8) + + (rom_[OWHolePos + (i * 2)])); + uchar entranceId = (rom_[OWHoleEntrance + i]); int p = (mapPos + 0x400) >> 1; int x = (p % 64); int y = (p >> 6); @@ -274,111 +894,107 @@ void Overworld::LoadEntrances() { } void Overworld::LoadSprites() { - // LW[0] = RainState 0 to 63 there's no data for DW - // LW[1] = ZeldaState 0 to 128 ; Contains LW and DW <128 or 144 wtf - // LW[2] = AgahState 0 to ?? ;Contains data for LW and DW - for (int i = 0; i < 3; i++) { - all_sprites_.emplace_back(std::vector()); + all_sprites_.emplace_back(); } for (int i = 0; i < 64; i++) { - if (map_parent_[i] == i) { - // Beginning Sprites - int ptrPos = core::overworldSpritesBegining + (i * 2); - int spriteAddress = core::SnesToPc((0x09 << 0x10) + rom_.toint16(ptrPos)); - while (true) { - uchar b1 = rom_[spriteAddress]; - uchar b2 = rom_[spriteAddress + 1]; - uchar b3 = rom_[spriteAddress + 2]; - if (b1 == 0xFF) { - break; - } - - int mapY = (i / 8); - int mapX = (i % 8); - - int realX = ((b2 & 0x3F) * 16) + mapX * 512; - int realY = ((b1 & 0x3F) * 16) + mapY * 512; - - all_sprites_[0].emplace_back(overworld_maps_[i].AreaGraphics(), - (uchar)i, b3, (uchar)(b2 & 0x3F), - (uchar)(b1 & 0x3F), realX, realY); - - spriteAddress += 3; - } - } + all_sprites_[0].emplace_back(); } for (int i = 0; i < 144; i++) { - if (map_parent_[i] == i) { - // Zelda Saved Sprites - int ptrPos = core::overworldSpritesZelda + (i * 2); - int spriteAddress = core::SnesToPc((0x09 << 0x10) + rom_.toint16(ptrPos)); - while (true) { - uchar b1 = rom_[spriteAddress]; - uchar b2 = rom_[spriteAddress + 1]; - uchar b3 = rom_[spriteAddress + 2]; - if (b1 == 0xFF) { - break; - } + all_sprites_[1].emplace_back(); + } - int editorMapIndex = i; - if (editorMapIndex >= 128) { - editorMapIndex = i - 128; - } else if (editorMapIndex >= 64) { - editorMapIndex = i - 64; - } + for (int i = 0; i < 144; i++) { + all_sprites_[2].emplace_back(); + } - int mapY = (editorMapIndex / 8); - int mapX = (editorMapIndex % 8); + LoadSpritesFromMap(overworldSpritesBegining, 64, 0); + LoadSpritesFromMap(overworldSpritesZelda, 144, 1); + LoadSpritesFromMap(overworldSpritesAgahnim, 144, 2); +} - int realX = ((b2 & 0x3F) * 16) + mapX * 512; - int realY = ((b1 & 0x3F) * 16) + mapY * 512; +void Overworld::LoadSpritesFromMap(int spriteStart, int spriteCount, + int spriteIndex) { + for (int i = 0; i < spriteCount; i++) { + if (map_parent_[i] != i) continue; - all_sprites_[1].emplace_back(overworld_maps_[i].AreaGraphics(), - (uchar)i, b3, (uchar)(b2 & 0x3F), - (uchar)(b1 & 0x3F), realX, realY); + int ptrPos = spriteStart + (i * 2); + int spriteAddress = core::SnesToPc((0x09 << 0x10) + rom()->toint16(ptrPos)); + while (true) { + uchar b1 = rom_[spriteAddress]; + uchar b2 = rom_[spriteAddress + 1]; + uchar b3 = rom_[spriteAddress + 2]; + if (b1 == 0xFF) break; - spriteAddress += 3; - } - } + int editorMapIndex = i; + if (editorMapIndex >= 128) + editorMapIndex -= 128; + else if (editorMapIndex >= 64) + editorMapIndex -= 64; - // Agahnim Dead Sprites - if (map_parent_[i] == i) { - int ptrPos = core::overworldSpritesAgahnim + (i * 2); - int spriteAddress = core::SnesToPc((0x09 << 0x10) + rom_.toint16(ptrPos)); - while (true) { - uchar b1 = rom_[spriteAddress]; - uchar b2 = rom_[spriteAddress + 1]; - uchar b3 = rom_[spriteAddress + 2]; - if (b1 == 0xFF) { - break; - } + int mapY = (editorMapIndex / 8); + int mapX = (editorMapIndex % 8); - int editorMapIndex = i; - if (editorMapIndex >= 128) { - editorMapIndex = i - 128; - } else if (editorMapIndex >= 64) { - editorMapIndex = i - 64; - } + int realX = ((b2 & 0x3F) * 16) + mapX * 512; + int realY = ((b1 & 0x3F) * 16) + mapY * 512; + auto graphics_bytes = overworld_maps_[i].AreaGraphics(); + all_sprites_[spriteIndex][i].InitSprite(graphics_bytes, (uchar)i, b3, + (uchar)(b2 & 0x3F), + (uchar)(b1 & 0x3F), realX, realY); + all_sprites_[spriteIndex][i].Draw(); - int mapY = (editorMapIndex / 8); - int mapX = (editorMapIndex % 8); - - int realX = ((b2 & 0x3F) * 16) + mapX * 512; - int realY = ((b1 & 0x3F) * 16) + mapY * 512; - - all_sprites_[2].emplace_back(overworld_maps_[i].AreaGraphics(), - (uchar)i, b3, (uchar)(b2 & 0x3F), - (uchar)(b1 & 0x3F), realX, realY); - - spriteAddress += 3; - } + spriteAddress += 3; } } } +absl::Status Overworld::LoadPrototype(ROM &rom, + const std::string &tilemap_filename) { + rom_ = rom; + + AssembleMap32Tiles(); + AssembleMap16Tiles(); + RETURN_IF_ERROR(DecompressProtoMapTiles(tilemap_filename)) + + for (int map_index = 0; map_index < kNumOverworldMaps; ++map_index) + overworld_maps_.emplace_back(map_index, rom_, tiles16); + + FetchLargeMaps(); + LoadEntrances(); + + auto size = tiles16.size(); + std::vector> futures; + for (int i = 0; i < kNumOverworldMaps; ++i) { + futures.push_back(std::async(std::launch::async, [this, i, size]() { + if (i < 64) { + return overworld_maps_[i].BuildMap(size, game_state_, 0, map_parent_, + map_tiles_.light_world); + } else if (i < 0x80 && i >= 0x40) { + return overworld_maps_[i].BuildMap(size, game_state_, 1, map_parent_, + map_tiles_.dark_world); + } else { + return overworld_maps_[i].BuildMap(size, game_state_, 2, map_parent_, + map_tiles_.special_world); + } + })); + } + + // Wait for all tasks to complete and check their results + for (auto &future : futures) { + absl::Status status = future.get(); + if (!status.ok()) { + return status; + } + } + + // LoadSprites(); + + is_loaded_ = true; + return absl::OkStatus(); +} + } // namespace zelda3 } // namespace app } // namespace yaze \ No newline at end of file diff --git a/src/app/zelda3/overworld.h b/src/app/zelda3/overworld.h index 0606378e..c3ac3613 100644 --- a/src/app/zelda3/overworld.h +++ b/src/app/zelda3/overworld.h @@ -3,21 +3,63 @@ #include +#include #include #include +#include "absl/container/flat_hash_map.h" #include "absl/status/status.h" +#include "app/core/common.h" #include "app/core/constants.h" #include "app/gfx/bitmap.h" #include "app/gfx/snes_tile.h" #include "app/rom.h" #include "app/zelda3/overworld_map.h" -#include "app/zelda3/sprite.h" +#include "app/zelda3/sprite/sprite.h" namespace yaze { namespace app { 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 OWEntranceMap = 0xDB96F; +constexpr int OWEntrancePos = 0xDBA71; +constexpr int OWEntranceEntranceId = 0xDBB73; +constexpr int OWHolePos = 0xDB800; //(0x13 entries, 2 bytes each) modified(less + // 0x400) map16 coordinates for each hole +constexpr int OWHoleArea = + 0xDB826; //(0x13 entries, 2 bytes each) corresponding + // area numbers for each hole +constexpr int OWHoleEntrance = + 0xDB84C; //(0x13 entries, 1 byte each) corresponding entrance numbers + +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 OverworldEntrance { public: int x_; @@ -67,31 +109,115 @@ class OverworldEntrance { } }; -class Overworld { +constexpr int kCompressedAllMap32PointersHigh = 0x1794D; +constexpr int kCompressedAllMap32PointersLow = 0x17B2D; +constexpr int overworldgfxGroups = 0x05D97; +constexpr int overworldPalGroup1 = 0xDE6C8; +constexpr int overworldPalGroup2 = 0xDE86C; +constexpr int overworldPalGroup3 = 0xDE604; +constexpr int overworldMapPalette = 0x7D1C; +constexpr int overworldSpritePalette = 0x7B41; +constexpr int overworldMapPaletteGroup = 0x75504; +constexpr int overworldSpritePaletteGroup = 0x75580; +constexpr int overworldSpriteset = 0x7A41; +constexpr int overworldSpecialGFXGroup = 0x16821; +constexpr int OverworldMapDataOverflow = 0x130000; +constexpr int overworldSpecialPALGroup = 0x16831; +constexpr int overworldSpritesBegining = 0x4C881; +constexpr int overworldSpritesAgahnim = 0x4CA21; +constexpr int overworldSpritesZelda = 0x4C901; +constexpr int overworldItemsPointers = 0xDC2F9; +constexpr int overworldItemsAddress = 0xDC8B9; // 1BC2F9 +constexpr int overworldItemsBank = 0xDC8BF; +constexpr int overworldItemsEndData = 0xDC89C; // 0DC89E +constexpr int mapGfx = 0x7C9C; +constexpr int overlayPointers = 0x77664; +constexpr int overlayPointersBank = 0x0E; +constexpr int overworldTilesType = 0x71459; +constexpr int overworldMessages = 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; + +// 0x00 = small maps, 0x20 = large maps +constexpr int overworldMapSize = 0x12844; + +// 0x01 = small maps, 0x03 = large maps +constexpr int overworldMapSizeHighByte = 0x12884; + +// relative to the WORLD + 0x200 per map +// large map that are not == parent id = same position as their parent! +// eg for X position small maps : +// 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 OverworldScreenTileMapChangeByScreen = 0x12634; +constexpr int transition_target_north = 0x13ee2; +constexpr int transition_target_west = 0x13f62; +constexpr int overworldCustomMosaicASM = 0x1301D0; +constexpr int overworldCustomMosaicArray = 0x1301F0; + +constexpr int kMap16Tiles = 0x78000; +constexpr int kNumOverworldMaps = 160; +constexpr int Map32PerScreen = 256; +constexpr int NumberOfMap16 = 3752; // 4096 +constexpr int LimitOfMap32 = 8864; +constexpr int NumberOfOWSprites = 352; +constexpr int NumberOfMap32 = Map32PerScreen * kNumOverworldMaps; + +struct MapData { + std::vector highData; + std::vector lowData; +}; + +class Overworld : public SharedROM, public core::ExperimentFlags { public: absl::Status Load(ROM &rom); - auto GetTiles16() const { return tiles16; } - auto GetOverworldMap(uint index) { return overworld_maps_[index]; } - auto GetOverworldMaps() const { return overworld_maps_; } - auto Sprites() const { return all_sprites_[game_state_]; } + OWBlockset &GetMapTiles(int world_type); + absl::Status LoadOverworldMaps(); + absl::Status SaveOverworldMaps(); + absl::Status SaveLargeMaps(); + + bool CreateTile32Tilemap(bool onlyShow = false); + absl::Status SaveMap16Tiles(); + absl::Status SaveMap32Tiles(); + + auto overworld_map(int i) const { return overworld_maps_[i]; } + auto mutable_overworld_map(int i) { return &overworld_maps_[i]; } + + auto Sprites(int state) const { return all_sprites_[state]; } auto AreaGraphics() const { return overworld_maps_[current_map_].AreaGraphics(); } - auto Entrances() const { return all_entrances_; } + auto &Entrances() { return all_entrances_; } auto AreaPalette() const { return overworld_maps_[current_map_].AreaPalette(); } + auto AreaPaletteById(int id) const { + return overworld_maps_[id].AreaPalette(); + } auto BitmapData() const { return overworld_maps_[current_map_].BitmapData(); } auto Tile16Blockset() const { return overworld_maps_[current_map_].Tile16Blockset(); } - auto GameState() const { return game_state_; } auto isLoaded() const { return is_loaded_; } void SetCurrentMap(int i) { current_map_ = i; } + auto MapTiles() const { return map_tiles_; } + auto mutable_map_tiles() { return &map_tiles_; } + + absl::Status LoadPrototype(ROM &rom_, const std::string &tilemap_filename); + private: - const int map32address[4] = {core::map32TilesTL, core::map32TilesTR, - core::map32TilesBL, core::map32TilesBR}; enum Dimension { map32TilesTL = 0, map32TilesTR = 1, @@ -99,7 +225,7 @@ class Overworld { map32TilesBR = 3 }; - ushort GenerateTile32(int i, int k, int dimension); + uint16_t GenerateTile32(int index, int quadrant, int dimension); void AssembleMap32Tiles(); void AssembleMap16Tiles(); void AssignWorldTiles(int x, int y, int sx, int sy, int tpos, @@ -107,11 +233,11 @@ class Overworld { void OrganizeMapTiles(Bytes &bytes, Bytes &bytes2, int i, int sx, int sy, int &ttpos); absl::Status DecompressAllMapTiles(); + absl::Status DecompressProtoMapTiles(const std::string &filename); void FetchLargeMaps(); void LoadEntrances(); void LoadSprites(); - - void LoadOverworldMap(); + void LoadSpritesFromMap(int spriteStart, int spriteCount, int spriteIndex); int game_state_ = 0; int current_map_ = 0; @@ -123,10 +249,24 @@ class Overworld { std::vector tiles16; std::vector tiles32; + std::vector tiles32_unique_; std::vector overworld_maps_; std::vector all_entrances_; std::vector all_holes_; std::vector> all_sprites_; + + absl::flat_hash_map proto_map_data_; + + std::vector> map_data_p1 = + std::vector>(kNumOverworldMaps); + std::vector> map_data_p2 = + std::vector>(kNumOverworldMaps); + + 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); }; } // namespace zelda3 diff --git a/src/app/zelda3/overworld_map.cc b/src/app/zelda3/overworld_map.cc index 35ce1d22..0fd009e3 100644 --- a/src/app/zelda3/overworld_map.cc +++ b/src/app/zelda3/overworld_map.cc @@ -12,6 +12,7 @@ #include "app/gfx/bitmap.h" #include "app/gfx/snes_tile.h" #include "app/rom.h" +#include "app/zelda3/overworld.h" namespace yaze { namespace app { @@ -91,7 +92,7 @@ void SetColorsPalette(ROM& rom, int index, gfx::SNESPalette& current, k = 0; for (int y = 8; y < 9; y++) { for (int x = 1; x < 8; x++) { - new_palette[x + (16 * y)] = rom.GetPaletteGroup("sprites_aux1")[1][k]; + new_palette[x + (16 * y)] = rom.palette_group("sprites_aux1")[1][k]; k++; } } @@ -100,7 +101,7 @@ void SetColorsPalette(ROM& rom, int index, gfx::SNESPalette& current, k = 0; for (int y = 8; y < 9; y++) { for (int x = 9; x < 16; x++) { - new_palette[x + (16 * y)] = rom.GetPaletteGroup("sprites_aux3")[0][k]; + new_palette[x + (16 * y)] = rom.palette_group("sprites_aux3")[0][k]; k++; } } @@ -109,7 +110,7 @@ void SetColorsPalette(ROM& rom, int index, gfx::SNESPalette& current, k = 0; for (int y = 9; y < 13; y++) { for (int x = 1; x < 16; x++) { - new_palette[x + (16 * y)] = rom.GetPaletteGroup("global_sprites")[0][k]; + new_palette[x + (16 * y)] = rom.palette_group("global_sprites")[0][k]; k++; } } @@ -136,14 +137,14 @@ void SetColorsPalette(ROM& rom, int index, gfx::SNESPalette& current, k = 0; for (int y = 15; y < 16; y++) { for (int x = 1; x < 16; x++) { - new_palette[x + (16 * y)] = rom.GetPaletteGroup("armors")[0][k]; + new_palette[x + (16 * y)] = rom.palette_group("armors")[0][k]; k++; } } current.Create(new_palette); for (int i = 0; i < 256; i++) { - current[(i / 16) * 16].setTransparent(true); + current[(i / 16) * 16].SetTransparent(true); } } @@ -164,15 +165,14 @@ absl::Status OverworldMap::BuildMap(int count, int game_state, int world, parent_ = map_parent[index_]; if (parent_ != index_ && !initialized_) { if (index_ >= 0x80 && index_ <= 0x8A && index_ != 0x88) { - area_graphics_ = - rom_[core::overworldSpecialGFXGroup + (parent_ - 0x80)]; - area_palette_ = rom_[core::overworldSpecialPALGroup + 1]; + area_graphics_ = rom_[overworldSpecialGFXGroup + (parent_ - 0x80)]; + area_palette_ = rom_[overworldSpecialPALGroup + 1]; } else if (index_ == 0x88) { area_graphics_ = 0x51; area_palette_ = 0x00; } else { - area_graphics_ = rom_[core::mapGfx + parent_]; - area_palette_ = rom_[core::overworldMapPalette + parent_]; + area_graphics_ = rom_[mapGfx + parent_]; + area_palette_ = rom_[overworldMapPalette + parent_]; } initialized_ = true; @@ -190,37 +190,37 @@ absl::Status OverworldMap::BuildMap(int count, int game_state, int world, void OverworldMap::LoadAreaInfo() { if (index_ != 0x80 && index_ <= 150 && - rom_[core::overworldMapSize + (index_ & 0x3F)] != 0) { + rom_[overworldMapSize + (index_ & 0x3F)] != 0) { large_map_ = true; } if (index_ < 64) { - area_graphics_ = rom_[core::mapGfx + parent_]; - area_palette_ = rom_[core::overworldMapPalette + parent_]; + area_graphics_ = rom_[mapGfx + parent_]; + area_palette_ = rom_[overworldMapPalette + parent_]; - area_music_[0] = rom_[core::overworldMusicBegining + parent_]; - area_music_[1] = rom_[core::overworldMusicZelda + parent_]; - area_music_[2] = rom_[core::overworldMusicMasterSword + parent_]; - area_music_[3] = rom_[core::overworldMusicAgahim + 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_]; - sprite_graphics_[0] = rom_[core::overworldSpriteset + parent_]; - sprite_graphics_[1] = rom_[core::overworldSpriteset + parent_ + 0x40]; - sprite_graphics_[2] = rom_[core::overworldSpriteset + parent_ + 0x80]; + sprite_graphics_[0] = rom_[overworldSpriteset + parent_]; + sprite_graphics_[1] = rom_[overworldSpriteset + parent_ + 0x40]; + sprite_graphics_[2] = rom_[overworldSpriteset + parent_ + 0x80]; - sprite_palette_[0] = rom_[core::overworldSpritePalette + parent_]; - sprite_palette_[1] = rom_[core::overworldSpritePalette + parent_ + 0x40]; - sprite_palette_[2] = rom_[core::overworldSpritePalette + parent_ + 0x80]; + sprite_palette_[0] = rom_[overworldSpritePalette + parent_]; + sprite_palette_[1] = rom_[overworldSpritePalette + parent_ + 0x40]; + sprite_palette_[2] = rom_[overworldSpritePalette + parent_ + 0x80]; } else if (index_ < 0x80) { - area_graphics_ = rom_[core::mapGfx + parent_]; - area_palette_ = rom_[core::overworldMapPalette + parent_]; - area_music_[0] = rom_[core::overworldMusicDW + (parent_ - 64)]; + area_graphics_ = rom_[mapGfx + parent_]; + area_palette_ = rom_[overworldMapPalette + parent_]; + area_music_[0] = rom_[overworldMusicDW + (parent_ - 64)]; - sprite_graphics_[0] = rom_[core::overworldSpriteset + parent_ + 0x80]; - sprite_graphics_[1] = rom_[core::overworldSpriteset + parent_ + 0x80]; - sprite_graphics_[2] = rom_[core::overworldSpriteset + parent_ + 0x80]; + sprite_graphics_[0] = rom_[overworldSpriteset + parent_ + 0x80]; + sprite_graphics_[1] = rom_[overworldSpriteset + parent_ + 0x80]; + sprite_graphics_[2] = rom_[overworldSpriteset + parent_ + 0x80]; - sprite_palette_[0] = rom_[core::overworldSpritePalette + parent_ + 0x80]; - sprite_palette_[1] = rom_[core::overworldSpritePalette + parent_ + 0x80]; - sprite_palette_[2] = rom_[core::overworldSpritePalette + parent_ + 0x80]; + sprite_palette_[0] = rom_[overworldSpritePalette + parent_ + 0x80]; + sprite_palette_[1] = rom_[overworldSpritePalette + parent_ + 0x80]; + sprite_palette_[2] = rom_[overworldSpritePalette + parent_ + 0x80]; } else { if (index_ == 0x94) { parent_ = 0x80; @@ -242,235 +242,187 @@ void OverworldMap::LoadAreaInfo() { parent_ = 0x88; } - area_palette_ = rom_[core::overworldSpecialPALGroup + parent_ - 0x80]; + area_palette_ = rom_[overworldSpecialPALGroup + parent_ - 0x80]; if (index_ >= 0x80 && index_ <= 0x8A && index_ != 0x88) { - area_graphics_ = rom_[core::overworldSpecialGFXGroup + (parent_ - 0x80)]; - area_palette_ = rom_[core::overworldSpecialPALGroup + 1]; + area_graphics_ = rom_[overworldSpecialGFXGroup + (parent_ - 0x80)]; + area_palette_ = rom_[overworldSpecialPALGroup + 1]; } else if (index_ == 0x88) { area_graphics_ = 0x51; area_palette_ = 0x00; } else { // pyramid bg use 0x5B map - area_graphics_ = rom_[core::mapGfx + parent_]; - area_palette_ = rom_[core::overworldMapPalette + parent_]; + area_graphics_ = rom_[mapGfx + parent_]; + area_palette_ = rom_[overworldMapPalette + parent_]; } - message_id_ = rom_[core::overworldMessages + parent_]; + message_id_ = rom_[overworldMessages + parent_]; - sprite_graphics_[0] = rom_[core::overworldSpriteset + parent_ + 0x80]; - sprite_graphics_[1] = rom_[core::overworldSpriteset + parent_ + 0x80]; - sprite_graphics_[2] = rom_[core::overworldSpriteset + parent_ + 0x80]; + sprite_graphics_[0] = rom_[overworldSpriteset + parent_ + 0x80]; + sprite_graphics_[1] = rom_[overworldSpriteset + parent_ + 0x80]; + sprite_graphics_[2] = rom_[overworldSpriteset + parent_ + 0x80]; - sprite_palette_[0] = rom_[core::overworldSpritePalette + parent_ + 0x80]; - sprite_palette_[1] = rom_[core::overworldSpritePalette + parent_ + 0x80]; - sprite_palette_[2] = rom_[core::overworldSpritePalette + parent_ + 0x80]; + sprite_palette_[0] = rom_[overworldSpritePalette + parent_ + 0x80]; + sprite_palette_[1] = rom_[overworldSpritePalette + parent_ + 0x80]; + sprite_palette_[2] = rom_[overworldSpritePalette + parent_ + 0x80]; } } -void OverworldMap::LoadAreaGraphics() { - int world_index = 0x20; +void OverworldMap::LoadWorldIndex() { if (parent_ < 0x40) { - world_index = 0x20; + world_index_ = 0x20; } else if (parent_ >= 0x40 && parent_ < 0x80) { - world_index = 0x21; + world_index_ = 0x21; } else if (parent_ == 0x88) { - world_index = 0x24; + world_index_ = 0x24; } +} + +void OverworldMap::LoadSpritesBlocksets() { + int static_graphics_base = 0x73; + static_graphics_[8] = static_graphics_base + 0x00; + static_graphics_[9] = static_graphics_base + 0x01; + static_graphics_[10] = static_graphics_base + 0x06; + static_graphics_[11] = static_graphics_base + 0x07; - // Sprites Blocksets - static_graphics_[8] = 0x73 + 0x00; - static_graphics_[9] = 0x73 + 0x01; - static_graphics_[10] = 0x73 + 0x06; - static_graphics_[11] = 0x73 + 0x07; for (int i = 0; i < 4; i++) { - static_graphics_[12 + i] = (rom_[core::kSpriteBlocksetPointer + - (sprite_graphics_[game_state_] * 4) + i] + - 0x73); + static_graphics_[12 + i] = + (rom_[rom_.version_constants().kSpriteBlocksetPointer + + (sprite_graphics_[game_state_] * 4) + i] + + static_graphics_base); } +} - // Main Blocksets +void OverworldMap::LoadMainBlocksets() { for (int i = 0; i < 8; i++) { - static_graphics_[i] = - rom_[core::overworldgfxGroups2 + (world_index * 8) + i]; + static_graphics_[i] = rom_[rom_.version_constants().kOverworldGfxGroups2 + + (world_index_ * 8) + i]; } +} - if (rom_[core::overworldgfxGroups + (area_graphics_ * 4)] != 0) { - static_graphics_[3] = rom_[core::overworldgfxGroups + (area_graphics_ * 4)]; - } - if (rom_[core::overworldgfxGroups + (area_graphics_ * 4) + 1] != 0) { - static_graphics_[4] = - rom_[core::overworldgfxGroups + (area_graphics_ * 4) + 1]; - } - if (rom_[core::overworldgfxGroups + (area_graphics_ * 4) + 2] != 0) { - static_graphics_[5] = - rom_[core::overworldgfxGroups + (area_graphics_ * 4) + 2]; - } - if (rom_[core::overworldgfxGroups + (area_graphics_ * 4) + 3] != 0) { - static_graphics_[6] = - rom_[core::overworldgfxGroups + (area_graphics_ * 4) + 3]; +void OverworldMap::LoadAreaGraphicsBlocksets() { + for (int i = 0; i < 4; i++) { + uchar value = rom_[rom_.version_constants().kOverworldGfxGroups1 + + (area_graphics_ * 4) + i]; + if (value != 0) { + static_graphics_[3 + i] = value; + } } +} - // Hardcoded overworld GFX Values, for death mountain - if ((parent_ >= 0x03 && parent_ <= 0x07) || - (parent_ >= 0x0B && parent_ <= 0x0E)) { - static_graphics_[7] = 0x59; - } else if ((parent_ >= 0x43 && parent_ <= 0x47) || - (parent_ >= 0x4B && parent_ <= 0x4E)) { - static_graphics_[7] = 0x59; +void OverworldMap::LoadDeathMountainGFX() { + static_graphics_[7] = (((parent_ >= 0x03 && parent_ <= 0x07) || + (parent_ >= 0x0B && parent_ <= 0x0E)) || + ((parent_ >= 0x43 && parent_ <= 0x47) || + (parent_ >= 0x4B && parent_ <= 0x4E))) + ? 0x59 + : 0x5B; +} + +void OverworldMap::LoadAreaGraphics() { + LoadWorldIndex(); + LoadSpritesBlocksets(); + LoadMainBlocksets(); + LoadAreaGraphicsBlocksets(); + LoadDeathMountainGFX(); +} + +// New helper function to get a palette from the ROM. +gfx::SNESPalette OverworldMap::GetPalette(const std::string& group, int index, + int previousIndex, int limit) { + if (index == 255) { + index = rom_[rom_.version_constants().overworldMapPaletteGroup + + (previousIndex * 4)]; + } + if (index != 255) { + if (index >= limit) { + index = limit - 1; + } + return rom_.palette_group(group)[index]; } else { - static_graphics_[7] = 0x5B; + return rom_.palette_group(group)[0]; } } void OverworldMap::LoadPalette() { - int previousPalId = 0; - int previousSprPalId = 0; - if (index_ > 0) { - previousPalId = rom_[core::overworldMapPalette + parent_ - 1]; - previousSprPalId = rom_[core::overworldSpritePalette + parent_ - 1]; - } + int previousPalId = index_ > 0 ? rom_[overworldMapPalette + parent_ - 1] : 0; + int previousSprPalId = + index_ > 0 ? rom_[overworldSpritePalette + parent_ - 1] : 0; - if (area_palette_ >= 0xA3) { - area_palette_ = 0xA3; - } + area_palette_ = std::min((int)area_palette_, 0xA3); uchar pal0 = 0; + uchar pal1 = rom_[rom_.version_constants().overworldMapPaletteGroup + + (area_palette_ * 4)]; + uchar pal2 = rom_[rom_.version_constants().overworldMapPaletteGroup + + (area_palette_ * 4) + 1]; + uchar pal3 = rom_[rom_.version_constants().overworldMapPaletteGroup + + (area_palette_ * 4) + 2]; + uchar pal4 = + rom_[overworldSpritePaletteGroup + (sprite_palette_[game_state_] * 2)]; + uchar pal5 = rom_[overworldSpritePaletteGroup + + (sprite_palette_[game_state_] * 2) + 1]; - uchar pal1 = rom_[core::overworldMapPaletteGroup + (area_palette_ * 4)]; - uchar pal2 = - rom_[core::overworldMapPaletteGroup + (area_palette_ * 4) + 1]; // aux2 - uchar pal3 = rom_[core::overworldMapPaletteGroup + (area_palette_ * 4) + - 2]; // animated + gfx::SNESColor bgr = rom_.palette_group("grass")[0].GetColor(0); - uchar pal4 = rom_[core::overworldSpritePaletteGroup + - (sprite_palette_[game_state_] * 2)]; // spr3 - uchar pal5 = rom_[core::overworldSpritePaletteGroup + - (sprite_palette_[game_state_] * 2) + 1]; // spr4 - - gfx::SNESPalette aux1; - gfx::SNESPalette aux2; - gfx::SNESPalette main; - gfx::SNESPalette animated; - gfx::SNESPalette hud; - gfx::SNESPalette spr; - gfx::SNESPalette spr2; - gfx::SNESColor bgr = rom_.GetPaletteGroup("grass")[0].GetColor(0); - - if (pal1 == 255) { - pal1 = rom_[core::overworldMapPaletteGroup + (previousPalId * 4)]; - } - if (pal1 != 255) { - if (pal1 >= 20) { - pal1 = 19; - } - - aux1 = rom_.GetPaletteGroup("ow_aux")[pal1]; - } else { - aux1 = rom_.GetPaletteGroup("ow_aux")[0]; - } - - if (pal2 == 255) { - pal2 = rom_[core::overworldMapPaletteGroup + (previousPalId * 4) + 1]; - } - if (pal2 != 255) { - if (pal2 >= 20) { - pal2 = 19; - } - - aux2 = rom_.GetPaletteGroup("ow_aux")[pal2]; - } else { - aux2 = rom_.GetPaletteGroup("ow_aux")[0]; - } + gfx::SNESPalette aux1 = GetPalette("ow_aux", pal1, previousPalId, 20); + gfx::SNESPalette aux2 = GetPalette("ow_aux", pal2, previousPalId, 20); + // Additional handling of `pal3` and `parent_` if (pal3 == 255) { - pal3 = rom_[core::overworldMapPaletteGroup + (previousPalId * 4) + 2]; + pal3 = rom_[rom_.version_constants().overworldMapPaletteGroup + + (previousPalId * 4) + 2]; } - if (parent_ < 0x40) { - // Default LW Palette - pal0 = 0; - bgr = rom_.GetPaletteGroup("grass")[0].GetColor(0); - if (parent_ == 0x03 || parent_ == 0x05 || parent_ == 0x07) { - pal0 = 2; - } + pal0 = parent_ == 0x03 || parent_ == 0x05 || parent_ == 0x07 ? 2 : 0; + bgr = rom_.palette_group("grass")[0].GetColor(0); } else if (parent_ >= 0x40 && parent_ < 0x80) { - // Default DW Palette - pal0 = 1; - bgr = rom_.GetPaletteGroup("grass")[0].GetColor(1); - if (parent_ == 0x43 || parent_ == 0x45 || parent_ == 0x47) { - pal0 = 3; - } - } else if (parent_ >= 128 && parent_ < core::kNumOverworldMaps) { - // Default SP Palette + pal0 = parent_ == 0x43 || parent_ == 0x45 || parent_ == 0x47 ? 3 : 1; + bgr = rom_.palette_group("grass")[0].GetColor(1); + } else if (parent_ >= 128 && parent_ < kNumOverworldMaps) { pal0 = 0; - bgr = rom_.GetPaletteGroup("grass")[0].GetColor(2); + bgr = rom_.palette_group("grass")[0].GetColor(2); } - if (parent_ == 0x88) { pal0 = 4; } + gfx::SNESPalette main = GetPalette("ow_main", pal0, previousPalId, 255); + gfx::SNESPalette animated = + GetPalette("ow_animated", std::min((int)pal3, 13), previousPalId, 14); + gfx::SNESPalette hud = rom_.palette_group("hud")[0]; - if (pal0 != 255) { - main = rom_.GetPaletteGroup("ow_main")[pal0]; - } else { - main = rom_.GetPaletteGroup("ow_main")[0]; - } - - if (pal3 >= 14) { - pal3 = 13; - } - animated = rom_.GetPaletteGroup("ow_animated")[(pal3)]; - - hud = rom_.GetPaletteGroup("hud")[0]; - if (pal4 == 255) { - pal4 = rom_[core::overworldSpritePaletteGroup + - (previousSprPalId * 2)]; // spr3 - } - if (pal4 == 255) { - pal4 = 0; - } - if (pal4 >= 24) { - pal4 = 23; - } - spr = rom_.GetPaletteGroup("sprites_aux3")[pal4]; - - if (pal5 == 255) { - pal5 = rom_[core::overworldSpritePaletteGroup + (previousSprPalId * 2) + - 1]; // spr3 - } - if (pal5 == 255) { - pal5 = 0; - } - if (pal5 >= 24) { - pal5 = 23; - } - spr2 = rom_.GetPaletteGroup("sprites_aux3")[pal5]; + gfx::SNESPalette spr = GetPalette("sprites_aux3", pal4, previousSprPalId, 24); + gfx::SNESPalette spr2 = + GetPalette("sprites_aux3", pal5, previousSprPalId, 24); SetColorsPalette(rom_, parent_, current_palette_, main, animated, aux1, aux2, hud, bgr, spr, spr2); } -absl::Status OverworldMap::BuildTileset() { - all_gfx_ = rom_.GetGraphicsBuffer(); - current_gfx_.reserve(0x10000); - for (int i = 0; i < 0x10000; i++) { - current_gfx_.push_back(0x00); +// New helper function to process graphics buffer. +void OverworldMap::ProcessGraphicsBuffer(int index, int static_graphics_offset, + int size) { + for (int i = 0; i < size; i++) { + auto byte = all_gfx_[i + (static_graphics_offset * size)]; + switch (index) { + case 0: + case 3: + case 4: + case 5: + byte += 0x88; + break; + } + current_gfx_[(index * size) + i] = byte; } +} + +absl::Status OverworldMap::BuildTileset() { + all_gfx_ = rom_.graphics_buffer(); + current_gfx_.resize(0x10000, 0x00); for (int i = 0; i < 0x10; i++) { - for (int j = 0; j < 0x1000; j++) { - auto byte = all_gfx_[j + (static_graphics_[i] * 0x1000)]; - switch (i) { - case 0: - case 3: - case 4: - case 5: - byte += 0x88; - break; - } - current_gfx_[(i * 0x1000) + j] = byte; - } + ProcessGraphicsBuffer(i, static_graphics_[i], 0x1000); } + return absl::OkStatus(); } diff --git a/src/app/zelda3/overworld_map.h b/src/app/zelda3/overworld_map.h index 51d35902..d830d561 100644 --- a/src/app/zelda3/overworld_map.h +++ b/src/app/zelda3/overworld_map.h @@ -12,6 +12,7 @@ #include "absl/status/status.h" #include "app/core/common.h" #include "app/gfx/bitmap.h" +#include "app/gfx/snes_palette.h" #include "app/gfx/snes_tile.h" #include "app/rom.h" @@ -23,6 +24,7 @@ static constexpr int kTileOffsets[] = {0, 8, 4096, 4104}; class OverworldMap { public: + OverworldMap() = default; OverworldMap(int index, ROM& rom, std::vector& tiles16); absl::Status BuildMap(int count, int game_state, int world, uchar* map_parent, @@ -35,12 +37,30 @@ class OverworldMap { auto SetLargeMap(bool is_set) { large_map_ = is_set; } auto IsLargeMap() const { return large_map_; } auto IsInitialized() const { return initialized_; } + auto Parent() const { return parent_; } + + auto mutable_area_graphics() { return &area_graphics_; } + auto mutable_area_palette() { return &area_palette_; } + auto mutable_sprite_graphics(int i) { return &sprite_graphics_[i]; } + auto mutable_sprite_palette(int i) { return &sprite_palette_[i]; } + auto mutable_message_id() { return &message_id_; } private: void LoadAreaInfo(); + + void LoadWorldIndex(); + void LoadSpritesBlocksets(); + void LoadMainBlocksets(); + void LoadAreaGraphicsBlocksets(); + void LoadDeathMountainGFX(); void LoadAreaGraphics(); + void LoadPalette(); + void ProcessGraphicsBuffer(int index, int static_graphics_offset, int size); + gfx::SNESPalette GetPalette(const std::string& group, int index, + int previousIndex, int limit); + absl::Status BuildTileset(); absl::Status BuildTiles16Gfx(int count); absl::Status BuildBitmap(OWBlockset& world_blockset); @@ -48,11 +68,13 @@ class OverworldMap { int parent_ = 0; int index_ = 0; int world_ = 0; - int message_id_ = 0; - int area_graphics_ = 0; - int area_palette_ = 0; + uint8_t message_id_ = 0; + uint8_t area_graphics_ = 0; + uint8_t area_palette_ = 0; int game_state_ = 0; + int world_index_ = 0; + uchar sprite_graphics_[3]; uchar sprite_palette_[3]; uchar area_music_[4]; @@ -70,6 +92,7 @@ class OverworldMap { OWMapTiles map_tiles_; gfx::SNESPalette current_palette_; + // std::vector sprite_graphics_; std::vector tiles16_; }; diff --git a/src/app/zelda3/inventory.cc b/src/app/zelda3/screen/inventory.cc similarity index 73% rename from src/app/zelda3/inventory.cc rename to src/app/zelda3/screen/inventory.cc index 89198cc0..afe41d31 100644 --- a/src/app/zelda3/inventory.cc +++ b/src/app/zelda3/screen/inventory.cc @@ -2,8 +2,8 @@ #include "app/gfx/bitmap.h" #include "app/gfx/snes_tile.h" +#include "app/gui/canvas.h" #include "app/rom.h" -#include "gui/canvas.h" namespace yaze { namespace app { @@ -16,10 +16,10 @@ void Inventory::Create() { } PRINT_IF_ERROR(BuildTileset()) for (int i = 0; i < 0x500; i += 0x08) { - tiles_.push_back(gfx::GetTilesInfo(rom_.toint16(i + kBowItemPos))); - tiles_.push_back(gfx::GetTilesInfo(rom_.toint16(i + kBowItemPos + 0x02))); - tiles_.push_back(gfx::GetTilesInfo(rom_.toint16(i + kBowItemPos + 0x04))); - tiles_.push_back(gfx::GetTilesInfo(rom_.toint16(i + kBowItemPos + 0x08))); + tiles_.push_back(gfx::GetTilesInfo(rom()->toint16(i + kBowItemPos))); + tiles_.push_back(gfx::GetTilesInfo(rom()->toint16(i + kBowItemPos + 0x02))); + tiles_.push_back(gfx::GetTilesInfo(rom()->toint16(i + kBowItemPos + 0x04))); + tiles_.push_back(gfx::GetTilesInfo(rom()->toint16(i + kBowItemPos + 0x08))); } const int offsets[] = {0x00, 0x08, 0x800, 0x808}; auto xx = 0; @@ -61,15 +61,16 @@ void Inventory::Create() { i++; } } - bitmap_.Create(256, 256, 128, data_); + + bitmap_.Create(256, 256, 8, data_); bitmap_.ApplyPalette(palette_); - rom_.RenderBitmap(&bitmap_); + rom()->RenderBitmap(&bitmap_); } 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_, rom()->Load2BppGraphics()) Bytes test; for (int i = 0; i < 0x4000; i++) { test_.push_back(tilesheets_[i]); @@ -78,9 +79,9 @@ absl::Status Inventory::BuildTileset() { test_.push_back(tilesheets_[i]); } tilesheets_bmp_.Create(128, 0x130, 64, test_); - palette_ = rom_.GetPaletteGroup("hud")[0]; + palette_ = rom()->palette_group("hud")[0]; tilesheets_bmp_.ApplyPalette(palette_); - rom_.RenderBitmap(&tilesheets_bmp_); + rom()->RenderBitmap(&tilesheets_bmp_); return absl::OkStatus(); } diff --git a/src/app/zelda3/inventory.h b/src/app/zelda3/screen/inventory.h similarity index 84% rename from src/app/zelda3/inventory.h rename to src/app/zelda3/screen/inventory.h index 4b714f8a..9a50c9e9 100644 --- a/src/app/zelda3/inventory.h +++ b/src/app/zelda3/screen/inventory.h @@ -5,7 +5,7 @@ #include "app/gfx/snes_tile.h" #include "app/gfx/snes_palette.h" #include "app/rom.h" -#include "gui/canvas.h" +#include "app/gui/canvas.h" namespace yaze { namespace app { @@ -14,9 +14,8 @@ namespace zelda3 { constexpr int kInventoryStart = 0x6564A; constexpr int kBowItemPos = 0x6F631; -class Inventory { +class Inventory : public SharedROM { public: - void SetupROM(ROM& rom) { rom_ = rom; } auto Bitmap() const { return bitmap_; } auto Tilesheet() const { return tilesheets_bmp_; } auto Palette() const { return palette_; } @@ -26,8 +25,6 @@ class Inventory { private: absl::Status BuildTileset(); - ROM rom_; - Bytes data_; gfx::Bitmap bitmap_; diff --git a/src/app/zelda3/title_screen.cc b/src/app/zelda3/screen/title_screen.cc similarity index 93% rename from src/app/zelda3/title_screen.cc rename to src/app/zelda3/screen/title_screen.cc index 7553ca77..cdb647b0 100644 --- a/src/app/zelda3/title_screen.cc +++ b/src/app/zelda3/screen/title_screen.cc @@ -42,7 +42,7 @@ void TitleScreen::BuildTileset() { staticgfx[15] = 112; // Loaded gfx for the current screen (empty at this point) - uchar* currentmapgfx8Data = tiles8Bitmap.GetData(); + uchar* currentmapgfx8Data = tiles8Bitmap.mutable_data().data(); // All gfx of the game pack of 2048 bytes (4bpp) uchar* allgfxData = nullptr; // rom_.GetMasterGraphicsBin(); diff --git a/src/app/zelda3/title_screen.h b/src/app/zelda3/screen/title_screen.h similarity index 100% rename from src/app/zelda3/title_screen.h rename to src/app/zelda3/screen/title_screen.h diff --git a/src/app/zelda3/sprite.h b/src/app/zelda3/sprite.h deleted file mode 100644 index aeb3308e..00000000 --- a/src/app/zelda3/sprite.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef YAZE_APP_ZELDA3_SPRITE_H -#define YAZE_APP_ZELDA3_SPRITE_H - -#include - -#include -#include -#include -#include - -#include "absl/status/status.h" -#include "app/core/constants.h" -#include "app/gfx/bitmap.h" -#include "app/gfx/snes_tile.h" -#include "app/rom.h" - -namespace yaze { -namespace app { -namespace zelda3 { - -class Sprite { - public: - uchar x_, y_, id_; - uchar nx_, ny_; - uchar layer_ = 0; - uchar subtype_ = 0; - uchar overlord_ = 0; - std::string name_; - uchar keyDrop_ = 0; - int sizeMap_ = 512; - bool overworld_ = false; - bool preview_ = false; - uchar map_id_ = 0; - int map_x_ = 0; - int map_y_ = 0; - short room_id_ = 0; - bool picker_ = false; - bool selected_ = false; - SDL_Rect bounding_box_; - - Bytes current_gfx_; - Bytes preview_gfx_; - - int lowerX_ = 32; - int lowerY_ = 32; - int higherX_ = 0; - int higherY_ = 0; - int width_ = 16; - int height_ = 16; - - Sprite(Bytes src, uchar mapid, uchar id, uchar x, uchar y, int map_x, - int map_y); - void updateBBox(); - - void Draw(bool picker = false); - - void DrawSpriteTile(int x, int y, int srcx, int srcy, int pal, - bool mirror_x = false, bool mirror_y = false, - int sizex = 2, int sizey = 2, bool iskey = false); - - auto PreviewGraphics() { return preview_gfx_; } -}; - -} // namespace zelda3 -} // namespace app -} // namespace yaze - -#endif \ No newline at end of file diff --git a/src/app/zelda3/sprite.cc b/src/app/zelda3/sprite/sprite.cc similarity index 95% rename from src/app/zelda3/sprite.cc rename to src/app/zelda3/sprite/sprite.cc index 52774d58..ba0562aa 100644 --- a/src/app/zelda3/sprite.cc +++ b/src/app/zelda3/sprite/sprite.cc @@ -4,8 +4,15 @@ namespace yaze { namespace app { namespace zelda3 { -Sprite::Sprite(Bytes src, uchar mapid, uchar id, uchar x, uchar y, int map_x, - int map_y) { +Sprite::Sprite() { + preview_gfx_.reserve(64 * 64); + for (int i = 0; i < 64 * 64; i++) { + preview_gfx_.push_back(0xFF); + } +} + +void Sprite::InitSprite(const Bytes& src, uchar mapid, uchar id, uchar x, + uchar y, int map_x, int map_y) { current_gfx_ = src; overworld_ = true; map_id_ = mapid; @@ -17,12 +24,34 @@ Sprite::Sprite(Bytes src, uchar mapid, uchar id, uchar x, uchar y, int map_x, name_ = core::kSpriteDefaultNames[id]; map_x_ = map_x; map_y_ = map_y; +} + +Sprite::Sprite(Bytes src, uchar mapid, uchar id, uchar x, uchar y, int map_x, + int map_y) + : current_gfx_(src), + map_id_(mapid), + id_(id), + x_(x), + y_(y), + nx_(x), + ny_(y), + map_x_(map_x), + map_y_(map_y) { + current_gfx_ = src; + overworld_ = true; + + name_ = core::kSpriteDefaultNames[id]; preview_gfx_.reserve(64 * 64); for (int i = 0; i < 64 * 64; i++) { preview_gfx_.push_back(0xFF); } } +void Sprite::updateCoordinates(int map_x, int map_y) { + map_x_ = map_x; + map_y_ = map_y; +} + void Sprite::updateBBox() { lowerX_ = 1; lowerY_ = 1; @@ -30,10 +59,9 @@ void Sprite::updateBBox() { higherY_ = 15; } -void Sprite::Draw(bool picker) { +void Sprite::Draw() { uchar x = nx_; uchar y = ny_; - picker_ = picker; if (overlord_ == 0x07) { if (id_ == 0x1A) { @@ -288,11 +316,11 @@ void Sprite::Draw(bool picker) { DrawSpriteTile((x * 16), (y * 16), 14, 22, 10); } /* -else if (id_== 0x33) // Pull for rupees -{ + else if (id_== 0x33) // Pull for rupees + { -} -*/ + } + */ else if (id_ == 0x34) // Npcs { DrawSpriteTile((x * 16), (y * 16), 14, 22, 10); @@ -304,11 +332,11 @@ else if (id_== 0x33) // Pull for rupees DrawSpriteTile((x * 16), (y * 16), 14, 22, 10); } /* -else if (id_== 0x37) // Waterfall -{ -DrawSpriteTile((x*16), (y *16), 14, 6, 10); -} -*/ + else if (id_== 0x37) // Waterfall + { + DrawSpriteTile((x*16), (y *16), 14, 6, 10); + } + */ else if (id_ == 0x38) // Arrowtarget { DrawSpriteTile((x * 16), (y * 16), 14, 22, 10); @@ -602,9 +630,6 @@ DrawSpriteTile((x*16), (y *16), 14, 6, 10); } else if (id_ == 0x79) // Bee { DrawSpriteTile((x * 16), (y * 16), 4, 14, 11, false, false, 1, 1); - } else if (id_ == 0x7A) { - DrawSpriteTile((x * 16), (y * 16) - 16, 2, 24, 12, false, false, 2, 4); - DrawSpriteTile((x * 16) + 16, (y * 16) - 16, 2, 24, 12, true, false, 2, 4); } else if (id_ == 0x7C) // Skull head { DrawSpriteTile((x * 16), (y * 16), 0, 16, 10); @@ -808,7 +833,7 @@ DrawSpriteTile((x*16), (y *16), 14, 6, 10); DrawSpriteTile((x * 16), (y * 16), 12, 10, 10); } else if (id_ == 0xBA) { DrawSpriteTile((x * 16), (y * 16), 14, 14, 6); - } else if (id_ == 0xC1) { + } else if (id_ == 0xC1 || id_ == 0x7A) { DrawSpriteTile((x * 16), (y * 16) - 16, 2, 24, 12, false, false, 2, 4); DrawSpriteTile((x * 16) + 16, (y * 16) - 16, 2, 24, 12, true, false, 2, 4); } else if (id_ == 0xC3) { @@ -871,7 +896,6 @@ DrawSpriteTile((x*16), (y *16), 14, 6, 10); } else if (id_ == 0xF4) { DrawSpriteTile((x * 16), (y * 16), 12, 28, 5, false, false, 4, 4); } else { - // stringtodraw.Add(new SpriteName(x, (y *16), sprites_name.name[id])); DrawSpriteTile((x * 16), (y * 16), 4, 4, 5); } @@ -882,8 +906,13 @@ DrawSpriteTile((x*16), (y *16), 14, 6, 10); } void Sprite::DrawSpriteTile(int x, int y, int srcx, int srcy, int pal, - bool mirror_x, bool mirror_y, int sizex, int sizey, - bool iskey) { + bool mirror_x, bool mirror_y, int sizex, + int sizey) { + if (current_gfx_.empty()) { + std::cout << "No gfx loaded" << std::endl; + return; + } + x += 16; y += 16; int drawid_ = (srcx + (srcy * 16)) + 512; @@ -896,7 +925,7 @@ void Sprite::DrawSpriteTile(int x, int y, int srcx, int srcy, int pal, mx = (((sizex * 8) / 2) - 1) - xl; } if (mirror_y) { - my = (((sizey * 8)) - 1) - yl; + my = ((sizey * 8) - 1) - yl; } // Formula information to get tile index position in the array diff --git a/src/app/zelda3/sprite/sprite.h b/src/app/zelda3/sprite/sprite.h new file mode 100644 index 00000000..81b876ad --- /dev/null +++ b/src/app/zelda3/sprite/sprite.h @@ -0,0 +1,89 @@ +#ifndef YAZE_APP_ZELDA3_SPRITE_H +#define YAZE_APP_ZELDA3_SPRITE_H + +#include + +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "app/core/constants.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_tile.h" +#include "app/rom.h" + +namespace yaze { +namespace app { +namespace zelda3 { + +class Sprite { + public: + Sprite(); + Sprite(Bytes src, uchar mapid, uchar id, uchar x, uchar y, int map_x, + int map_y); + void InitSprite(const Bytes& src, uchar mapid, uchar id, uchar x, uchar y, + int map_x, int map_y); + void updateBBox(); + + void Draw(); + void DrawSpriteTile(int x, int y, int srcx, int srcy, int pal, + bool mirror_x = false, bool mirror_y = false, + int sizex = 2, int sizey = 2); + + // New methods + void updateCoordinates(int map_x, int map_y); + + auto PreviewGraphics() const { return preview_gfx_; } + auto GetRealX() const { return bounding_box_.x; } + auto GetRealY() const { return bounding_box_.y; } + auto id() const { return id_; } + auto x() const { return x_; } + auto y() const { return y_; } + auto nx() const { return nx_; } + auto ny() const { return ny_; } + auto layer() const { return layer_; } + auto subtype() const { return subtype_; } + auto& keyDrop() const { return key_drop_; } + + auto Width() const { return bounding_box_.w; } + auto Height() const { return bounding_box_.h; } + std::string Name() const { return name_; } + + private: + Bytes current_gfx_; + bool overworld_; + + uchar map_id_; + uchar id_; + uchar x_; + uchar y_; + uchar nx_; + uchar ny_; + uchar overlord_ = 0; + std::string name_; + + int subtype_; + int layer_; + + int map_x_; + int map_y_; + Bytes preview_gfx_; + uchar lowerX_; + uchar lowerY_; + uchar higherX_; + uchar higherY_; + SDL_Rect bounding_box_; + + int width_ = 16; + int height_ = 16; + + int key_drop_; +}; + +} // namespace zelda3 +} // namespace app +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt new file mode 100644 index 00000000..15f43dfc --- /dev/null +++ b/src/cli/CMakeLists.txt @@ -0,0 +1,37 @@ +add_executable( + z3ed + cli/z3ed.cc + cli/patch.cc + cli/command_handler.cc + app/rom.cc + app/core/common.cc + app/gui/pipeline.cc + ${YAZE_APP_EMU_SRC} + ${YAZE_APP_GFX_SRC} + ${YAZE_APP_ZELDA3_SRC} + ${YAZE_GUI_SRC} + ${IMGUI_SRC} +) + +target_include_directories( + z3ed PUBLIC + lib/ + app/ + lib/SDL_mixer/include/ + ${CMAKE_SOURCE_DIR}/src/ + ${PNG_INCLUDE_DIRS} + ${SDL2_INCLUDE_DIR} + ${GLEW_INCLUDE_DIRS} +) + +target_link_libraries( + z3ed PUBLIC + ${ABSL_TARGETS} + ${SDL_TARGETS} + ${PNG_LIBRARIES} + ${GLEW_LIBRARIES} + ${OPENGL_LIBRARIES} + ${CMAKE_DL_LIBS} + SDL2_mixer + ImGui +) \ No newline at end of file diff --git a/src/cli/command_handler.cc b/src/cli/command_handler.cc new file mode 100644 index 00000000..7d5a27d0 --- /dev/null +++ b/src/cli/command_handler.cc @@ -0,0 +1,86 @@ +#include "cli/command_handler.h" + +#include // for basic_string, char_traits, stoi +#include // for vector, vector<>::value_type + +#include "absl/status/status.h" // for OkStatus, Status +#include "app/core/common.h" // for app +#include "app/core/constants.h" // for RETURN_IF_ERROR +#include "app/rom.h" // for ROM + +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])) + + // Load the destination rom + ROM dest_rom; + RETURN_IF_ERROR(dest_rom.LoadFromFile(arg_vec[1])) + + std::vector tileIDs; + + // Parse the CSV list of tile16 IDs. + std::stringstream ss(arg_vec[2].data()); + for (std::string tileID; std::getline(ss, tileID, ',');) { + if (tileID == "*") { + // for (uint32_t i = 0; i <= rom_.GetMaxTileID(); ++i) { + // tileIDs.push_back(i); + // } + break; // No need to continue parsing if * is used + } else if (tileID.find('-') != std::string::npos) { + // Handle range: split by hyphen and add all tile IDs in the range. + std::stringstream rangeSS(tileID); + std::string start; + std::string end; + std::getline(rangeSS, start, '-'); + std::getline(rangeSS, end); + uint32_t startID = std::stoi(start, nullptr, 16); + uint32_t endID = std::stoi(end, nullptr, 16); + for (uint32_t i = startID; i <= endID; ++i) { + tileIDs.push_back(i); + } + } else { + // Handle single tile ID + uint32_t tileID_int = std::stoi(tileID, nullptr, 16); + tileIDs.push_back(tileID_int); + } + } + + for (const auto& tile16_id_int : tileIDs) { + // Compare the tile16 data between source and destination ROMs. + // auto source_tile16_data = rom_.ReadTile16(tile16_id_int); + // auto dest_tile16_data = dest_rom.ReadTile16(tile16_id_int); + ASSIGN_OR_RETURN(auto source_tile16_data, rom_.ReadTile16(tile16_id_int)) + ASSIGN_OR_RETURN(auto dest_tile16_data, dest_rom.ReadTile16(tile16_id_int)) + if (source_tile16_data != dest_tile16_data) { + // Notify user of difference + std::cout << "Difference detected in tile16 ID " << tile16_id_int + << ". Do you want to transfer it to dest rom? (y/n): "; + char userChoice; + std::cin >> userChoice; + + // Transfer if user confirms + if (userChoice == 'y' || userChoice == 'Y') { + dest_rom.WriteTile16(tile16_id_int, source_tile16_data); + std::cout << "Transferred tile16 ID " << tile16_id_int + << " to dest rom." << std::endl; + } else { + std::cout << "Skipped transferring tile16 ID " << tile16_id_int << "." + << std::endl; + } + } + } + + RETURN_IF_ERROR(dest_rom.SaveToFile(/*backup=*/true, arg_vec[1])) + + std::cout << "Successfully transferred tile16" << std::endl; + + return absl::OkStatus(); +} + +} // namespace cli +} // namespace yaze \ No newline at end of file diff --git a/src/cli/command_handler.h b/src/cli/command_handler.h new file mode 100644 index 00000000..f4cf7b15 --- /dev/null +++ b/src/cli/command_handler.h @@ -0,0 +1,344 @@ +#ifndef YAZE_CLI_COMMAND_HANDLER_H +#define YAZE_CLI_COMMAND_HANDLER_H + +#include // for uint8_t, uint32_t +#include // for operator<<, string, ostream, basic_... +#include // for make_shared, shared_ptr +#include +#include // for char_traits, basic_string, hash +#include +#include // for unordered_map +#include // for vector, vector<>::value_type + +#include "absl/status/status.h" // for OkStatus, Status +#include "absl/strings/str_cat.h" +#include "app/core/common.h" // for PcToSnes, SnesToPc +#include "app/core/constants.h" // for RETURN_IF_ERROR +#include "app/emu/snes.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/compression.h" +#include "app/gfx/snes_palette.h" +#include "app/gfx/snes_tile.h" +#include "app/gui/canvas.h" +#include "app/gui/pipeline.h" +#include "app/rom.h" // for ROM +#include "app/zelda3/overworld.h" +#include "cli/patch.h" // for ApplyBpsPatch, CreateBpsPatch + +namespace yaze { +namespace cli { + +namespace Color { +enum Code { + FG_RED = 31, + FG_GREEN = 32, + FG_YELLOW = 33, + FG_BLUE = 36, + FG_MAGENTA = 35, + FG_DEFAULT = 39, + FG_RESET = 0, + FG_UNDERLINE = 4, + BG_RED = 41, + BG_GREEN = 42, + BG_BLUE = 44, + BG_DEFAULT = 49 +}; +class Modifier { + Code code; + + public: + explicit Modifier(Code pCode) : code(pCode) {} + friend std::ostream& operator<<(std::ostream& os, const Modifier& mod) { + return os << "\033[" << mod.code << "m"; + } +}; +} // namespace Color + +class CommandHandler { + public: + CommandHandler() = default; + virtual ~CommandHandler() = default; + virtual absl::Status handle(const std::vector& arg_vec) = 0; + + app::ROM rom_; +}; + +class ApplyPatch : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override { + std::string rom_filename = arg_vec[1]; + std::string patch_filename = arg_vec[2]; + RETURN_IF_ERROR(rom_.LoadFromFile(rom_filename)) + auto source = rom_.vector(); + std::ifstream patch_file(patch_filename, std::ios::binary); + std::vector patch; + patch.resize(rom_.size()); + patch_file.read((char*)patch.data(), patch.size()); + + // Apply patch + std::vector patched; + ApplyBpsPatch(source, patch, patched); + + // Save patched file + std::ofstream patched_rom("patched.sfc", std::ios::binary); + patched_rom.write((char*)patched.data(), patched.size()); + patched_rom.close(); + return absl::OkStatus(); + } +}; + +class CreatePatch : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override { + std::vector source; + std::vector target; + std::vector patch; + // Create patch + CreateBpsPatch(source, target, patch); + + // Save patch to file + // std::ofstream patchFile("patch.bps", ios::binary); + // patchFile.write(reinterpret_cast(patch.data()), + // patch.size()); patchFile.close(); + return absl::OkStatus(); + } +}; + +class Open : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override { + Color::Modifier green(Color::FG_GREEN); + Color::Modifier blue(Color::FG_BLUE); + Color::Modifier reset(Color::FG_RESET); + auto const& arg = arg_vec[0]; + RETURN_IF_ERROR(rom_.LoadFromFile(arg)) + std::cout << "Title: " << green << rom_.title() << std::endl; + std::cout << reset << "Size: " << blue << "0x" << std::hex << rom_.size() + << reset << std::endl; + return absl::OkStatus(); + } +}; + +// Backup ROM +class Backup : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override { + RETURN_IF_ERROR(rom_.LoadFromFile(arg_vec[0])) + if (arg_vec.size() == 2) { + // Optional filename added + RETURN_IF_ERROR(rom_.SaveToFile(/*backup=*/true, arg_vec[1])) + } else { + RETURN_IF_ERROR(rom_.SaveToFile(/*backup=*/true)) + } + return absl::OkStatus(); + } +}; + +// Compress Graphics +class Compress : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override { + std::cout << "Compress selected with argument: " << arg_vec[0] << std::endl; + return absl::OkStatus(); + } +}; + +// Decompress (Export) Graphics +// +// -e --mode= +// +// mode: +class Decompress : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override { + Color::Modifier underline(Color::FG_UNDERLINE); + Color::Modifier reset(Color::FG_RESET); + std::cout << "Please specify the tilesheets you want to export\n"; + std::cout << "You can input an individual sheet, a range X-Y, or comma " + "separate values.\n\n"; + std::cout << underline << "Tilesheets\n" << reset; + std::cout << "0-112 -> compressed 3bpp bgr \n"; + std::cout << "113-114 -> compressed 2bpp\n"; + std::cout << "115-126 -> uncompressed 3bpp sprites\n"; + std::cout << "127-217 -> compressed 3bpp sprites\n"; + std::cout << "218-222 -> compressed 2bpp\n"; + + std::cout << "Enter tilesheets: "; + std::string sheet_input; + std::cin >> sheet_input; + + // Batch Mode + // if (arg_vec.size() == 1) { + // auto rom_filename = arg_vec[1]; + // RETURN_IF_ERROR(rom_.LoadFromFile(arg, true)) + // RETURN_IF_ERROR(rom_.LoadAllGraphicsData()) + // for (auto& graphic_sheet : rom_.graphics_bin()) { + // const auto filename = + // absl::StrCat(rom_.filename(), graphic_sheet.first); + // graphic_sheet.second.SaveSurfaceToFile(filename); + // } + // } + + std::cout << "Decompress selected with argument: " << arg_vec[0] + << std::endl; + return absl::OkStatus(); + } +}; + +// SnesToPc Conversion +// -s
+class SnesToPc : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override { + auto arg = arg_vec[0]; + std::stringstream ss(arg.data()); + uint32_t snes_address; + ss >> std::hex >> snes_address; + uint32_t pc_address = app::core::SnesToPc(snes_address); + std::cout << std::hex << pc_address << std::endl; + return absl::OkStatus(); + } +}; + +class PcToSnes : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override { + auto arg = arg_vec[0]; + std::stringstream ss(arg.data()); + uint32_t pc_address; + ss >> std::hex >> pc_address; + uint32_t snes_address = app::core::PcToSnes(pc_address); + Color::Modifier blue(Color::FG_BLUE); + std::cout << "SNES LoROM Address: "; + std::cout << blue << "$" << std::uppercase << std::hex << snes_address + << "\n"; + return absl::OkStatus(); + } +}; + +// -r
+class ReadFromRom : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override { + RETURN_IF_ERROR(rom_.LoadFromFile(arg_vec[0])) + + std::stringstream ss(arg_vec[1].data()); + uint32_t offset; + ss >> std::hex >> offset; + uint32_t length = 0x01; + if (!arg_vec[2].empty()) { + length = std::stoi(arg_vec[2]); + } + + if (length > 1) { + auto returned_bytes_status = rom_.ReadByteVector(offset, length); + if (!returned_bytes_status.ok()) { + return returned_bytes_status.status(); + } + auto returned_bytes = returned_bytes_status.value(); + for (const auto& each : returned_bytes) { + std::cout << each; + } + std::cout << std::endl; + } else { + auto byte = rom_.ReadByte(offset); + std::cout << std::hex << byte.value() << std::endl; + } + + return absl::OkStatus(); + } +}; + +// Transfer tile 16 data from one rom to another +// -t "" +class Tile16Transfer : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override; +}; + +class Expand : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override { + RETURN_IF_ERROR(rom_.LoadFromFile(arg_vec[0])) + + std::stringstream ss(arg_vec[1].data()); + uint32_t size; + ss >> std::hex >> size; + + rom_.Expand(size); + + std::cout << "Successfully expanded ROM to " << std::hex << size + << std::endl; + + return absl::OkStatus(); + } +}; + +// Start Emulator on a SNES rom file +// -emu +class Emulator : public CommandHandler { + public: + absl::Status handle(const std::vector& arg_vec) override { + std::string filename = arg_vec[0]; + RETURN_IF_ERROR(rom_.LoadFromFile(filename)) + + bool step = false; + if (arg_vec[1].empty()) { + snes.SetCpuMode(0); + } else { + snes.SetCpuMode(1); + step = true; + } + + snes.SetupMemory(rom_); + snes.Init(rom_); + + if (!step) { + int i = 0; + while (i < 80000) { + snes.Run(); + i++; + } + } else { + // This loop should take in input from the keyboard, such as pressing + // space to step through the loop and pressing x to end the execution. + bool stepping = true; + std::cout << "Press space to step, x to exit" << std::endl; + while (stepping) { + char input; + std::cin.get(input); + if (input == 'x') { + break; + } else { + snes.StepRun(); + } + } + } + + return absl::OkStatus(); + } + + app::emu::SNES snes; +}; + +struct Commands { + std::unordered_map> handlers = { + {"-emu", std::make_shared()}, + {"-a", std::make_shared()}, + {"-c", std::make_shared()}, + {"-o", std::make_shared()}, + {"-b", std::make_shared()}, + {"-x", std::make_shared()}, + {"-i", std::make_shared()}, // Import + {"-e", std::make_shared()}, // Export + {"-s", std::make_shared()}, + {"-p", std::make_shared()}, + {"-t", std::make_shared()}, + {"-r", std::make_shared()} // Read from ROM + }; +}; + +} // namespace cli +} // namespace yaze + +#endif \ No newline at end of file diff --git a/src/cli/patch.cc b/src/cli/patch.cc new file mode 100644 index 00000000..1b6945c8 --- /dev/null +++ b/src/cli/patch.cc @@ -0,0 +1,205 @@ +#include "cli/patch.h" + +#include + +#include +#include +#include +#include +#include + +namespace yaze { +namespace cli { + +void encode(uint64_t data, std::vector& output) { + while (true) { + uint8_t x = data & 0x7f; + data >>= 7; + if (data == 0) { + output.push_back(0x80 | x); + break; + } + output.push_back(x); + data--; + } +} + +uint64_t decode(const std::vector& input, size_t& offset) { + uint64_t data = 0; + uint64_t shift = 1; + while (true) { + uint8_t x = input[offset++]; + data += (x & 0x7f) * shift; + if (x & 0x80) break; + shift <<= 7; + data += shift; + } + return data; +} + +uint32_t crc32(const std::vector& data) { + uint32_t crc = ::crc32(0L, Z_NULL, 0); + return ::crc32(crc, data.data(), data.size()); +} + +void CreateBpsPatch(const std::vector& source, + const std::vector& target, + std::vector& patch) { + patch.clear(); + patch.insert(patch.end(), {'B', 'P', 'S', '1'}); + + encode(source.size(), patch); + encode(target.size(), patch); + encode(0, patch); // No metadata + + size_t sourceOffset = 0; + size_t targetOffset = 0; + int64_t sourceRelOffset = 0; + int64_t targetRelOffset = 0; + + while (targetOffset < target.size()) { + if (sourceOffset < source.size() && + source[sourceOffset] == target[targetOffset]) { + size_t length = 0; + while (sourceOffset + length < source.size() && + targetOffset + length < target.size() && + source[sourceOffset + length] == target[targetOffset + length]) { + length++; + } + encode((length - 1) << 2 | 0, patch); // SourceRead + sourceOffset += length; + targetOffset += length; + } else { + size_t length = 0; + while (targetOffset + length < target.size() && + (sourceOffset + length >= source.size() || + source[sourceOffset + length] != target[targetOffset + length])) { + length++; + } + if (length > 0) { + encode((length - 1) << 2 | 1, patch); // TargetRead + for (size_t i = 0; i < length; i++) { + patch.push_back(target[targetOffset + i]); + } + targetOffset += length; + } + } + + // SourceCopy + if (sourceOffset < source.size()) { + size_t length = 0; + int64_t offset = sourceOffset - sourceRelOffset; + while (sourceOffset + length < source.size() && + targetOffset + length < target.size() && + source[sourceOffset + length] == target[targetOffset + length]) { + length++; + } + if (length > 0) { + encode((length - 1) << 2 | 2, patch); + encode((offset < 0 ? 1 : 0) | (abs(offset) << 1), patch); + sourceOffset += length; + targetOffset += length; + sourceRelOffset = sourceOffset; + } + } + + // TargetCopy + if (targetOffset > 0) { + size_t length = 0; + int64_t offset = targetOffset - targetRelOffset; + while (targetOffset + length < target.size() && + target[targetOffset - 1] == target[targetOffset + length]) { + length++; + } + if (length > 0) { + encode((length - 1) << 2 | 3, patch); + encode((offset < 0 ? 1 : 0) | (abs(offset) << 1), patch); + targetOffset += length; + targetRelOffset = targetOffset; + } + } + } + + patch.resize(patch.size() + 12); // Make space for the checksums + uint32_t sourceChecksum = crc32(source); + uint32_t targetChecksum = crc32(target); + uint32_t patchChecksum = crc32(patch); + + memcpy(patch.data() + patch.size() - 12, &sourceChecksum, sizeof(uint32_t)); + memcpy(patch.data() + patch.size() - 8, &targetChecksum, sizeof(uint32_t)); + memcpy(patch.data() + patch.size() - 4, &patchChecksum, sizeof(uint32_t)); +} + +void ApplyBpsPatch(const std::vector& source, + const std::vector& patch, + std::vector& target) { + if (patch.size() < 4 || patch[0] != 'B' || patch[1] != 'P' || + patch[2] != 'S' || patch[3] != '1') { + throw std::runtime_error("Invalid patch format"); + } + + size_t patchOffset = 4; + uint64_t sourceSize = decode(patch, patchOffset); + uint64_t targetSize = decode(patch, patchOffset); + uint64_t metadataSize = decode(patch, patchOffset); + patchOffset += metadataSize; + + target.resize(targetSize); + size_t sourceOffset = 0; + size_t targetOffset = 0; + int64_t sourceRelOffset = 0; + int64_t targetRelOffset = 0; + + while (patchOffset < patch.size() - 12) { + uint64_t data = decode(patch, patchOffset); + uint64_t command = data & 3; + uint64_t length = (data >> 2) + 1; + + switch (command) { + case 0: // SourceRead + while (length--) { + target[targetOffset++] = source[sourceOffset++]; + } + break; + case 1: // TargetRead + while (length--) { + target[targetOffset++] = patch[patchOffset++]; + } + break; + case 2: // SourceCopy + { + int64_t offsetData = decode(patch, patchOffset); + sourceRelOffset += (offsetData & 1 ? -1 : +1) * (offsetData >> 1); + while (length--) { + target[targetOffset++] = source[sourceRelOffset++]; + } + } break; + case 3: // TargetCopy + { + uint64_t offsetData = decode(patch, patchOffset); + targetRelOffset += (offsetData & 1 ? -1 : +1) * (offsetData >> 1); + while (length--) { + target[targetOffset++] = target[targetRelOffset++]; + } + } + default: + throw std::runtime_error("Invalid patch command"); + } + } + + uint32_t sourceChecksum; + uint32_t targetChecksum; + uint32_t patchChecksum; + memcpy(&sourceChecksum, patch.data() + patch.size() - 12, sizeof(uint32_t)); + memcpy(&targetChecksum, patch.data() + patch.size() - 8, sizeof(uint32_t)); + memcpy(&patchChecksum, patch.data() + patch.size() - 4, sizeof(uint32_t)); + + if (sourceChecksum != crc32(source) || targetChecksum != crc32(target) || + patchChecksum != + crc32(std::vector(patch.begin(), patch.end() - 4))) { + throw std::runtime_error("Checksum mismatch"); + } +} + +} // namespace cli +} // namespace yaze \ No newline at end of file diff --git a/src/cli/patch.h b/src/cli/patch.h new file mode 100644 index 00000000..19892605 --- /dev/null +++ b/src/cli/patch.h @@ -0,0 +1,29 @@ +#ifndef YAZE_CLI_PATCH_H +#define YAZE_CLI_PATCH_H + +#include + +#include +#include +#include +#include +#include +namespace yaze { +namespace cli { +void encode(uint64_t data, std::vector& output); + +uint64_t decode(const std::vector& input, size_t& offset); + +uint32_t crc32(const std::vector& data); + +void CreateBpsPatch(const std::vector& source, + const std::vector& target, + std::vector& patch); + +void ApplyBpsPatch(const std::vector& source, + const std::vector& patch, + std::vector& target); + +} // namespace cli +} // namespace yaze +#endif \ No newline at end of file diff --git a/src/cli/z3ed.cc b/src/cli/z3ed.cc new file mode 100644 index 00000000..1201b33f --- /dev/null +++ b/src/cli/z3ed.cc @@ -0,0 +1,86 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "app/core/common.h" +#include "app/core/constants.h" +#include "app/rom.h" +#include "cli/command_handler.h" +#include "cli/patch.h" + +namespace yaze { +namespace cli { +namespace { + +void HelpCommand() { + Color::Modifier ylw(Color::FG_YELLOW); + Color::Modifier mag(Color::FG_MAGENTA); + Color::Modifier red(Color::FG_RED); + Color::Modifier reset(Color::FG_RESET); + Color::Modifier underline(Color::FG_UNDERLINE); + std::cout << "\n"; + std::cout << ylw << " ▲ " << reset << " z3ed\n"; + std::cout << ylw << "▲ ▲ " << reset << " by " << mag << "scawful\n\n" + << reset; + std::cout << "The Legend of " << red << "Zelda" << reset + << ": A Link to the Past Hacking Tool\n\n"; + std::cout << underline; + std::cout << "Command" << reset << " " << underline << "Arg" + << reset << " " << underline << "Params\n" + << reset; + + std::cout << "Apply BPS Patch -a \n"; + std::cout << "Create BPS Patch -c " + "\n\n"; + + std::cout << "Open ROM -o \n"; + std::cout << "Backup ROM -b \n"; + std::cout << "Expand ROM -x \n\n"; + + std::cout << "Transfer Tile16 -t " + "\n\n"; + + std::cout << "Export Graphics -e \n"; + std::cout << "Import Graphics -i \n\n"; + + std::cout << "SNES to PC Address -s
\n"; + std::cout << "PC to SNES Address -p
\n"; + std::cout << "\n"; +} + +int RunCommandHandler(int argc, char* argv[]) { + if (argv[1] == "-h" || argc == 1) { + HelpCommand(); + return EXIT_SUCCESS; + } + + std::vector arguments; + for (int i = 2; i < argc; i++) { // Skip the arg mode (argv[1]) + std::cout << "argv[" << i << "] = " << argv[i] << std::endl; + arguments.emplace_back(argv[i]); + } + + Commands commands; + std::string mode = argv[1]; + if (commands.handlers.find(mode) != commands.handlers.end()) { + PRINT_IF_ERROR(commands.handlers[mode]->handle(arguments)) + } else { + std::cerr << "Invalid mode specified: " << mode << std::endl; + } + return EXIT_SUCCESS; +} + +} // namespace +} // namespace cli +} // namespace yaze + +int main(int argc, char* argv[]) { + return yaze::cli::RunCommandHandler(argc, argv); +} \ No newline at end of file diff --git a/src/gui/canvas.cc b/src/gui/canvas.cc deleted file mode 100644 index 14b4ec86..00000000 --- a/src/gui/canvas.cc +++ /dev/null @@ -1,199 +0,0 @@ -#include "canvas.h" - -#include - -#include -#include - -#include "app/gfx/bitmap.h" -#include "app/rom.h" - -namespace yaze { -namespace gui { - -void Canvas::DrawBackground(ImVec2 canvas_size) { - canvas_p0_ = ImGui::GetCursorScreenPos(); - if (!custom_canvas_size_) canvas_sz_ = ImGui::GetContentRegionAvail(); - if (canvas_size.x != 0) canvas_sz_ = canvas_size; - - canvas_p1_ = ImVec2(canvas_p0_.x + canvas_sz_.x, canvas_p0_.y + canvas_sz_.y); - - // Draw border and background color - draw_list_ = ImGui::GetWindowDrawList(); - draw_list_->AddRectFilled(canvas_p0_, canvas_p1_, IM_COL32(32, 32, 32, 255)); - draw_list_->AddRect(canvas_p0_, canvas_p1_, IM_COL32(255, 255, 255, 255)); -} - -void Canvas::DrawContextMenu() { - // This will catch our interactions - const ImGuiIO &io = ImGui::GetIO(); - ImGui::InvisibleButton( - "canvas", canvas_sz_, - ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); - const bool is_hovered = ImGui::IsItemHovered(); // Hovered - const bool is_active = ImGui::IsItemActive(); // Held - const ImVec2 origin(canvas_p0_.x + scrolling_.x, - canvas_p0_.y + scrolling_.y); // Lock scrolled origin - const ImVec2 mouse_pos_in_canvas(io.MousePos.x - origin.x, - io.MousePos.y - origin.y); - - // Pan (we use a zero mouse threshold when there's no context menu) - const float mouse_threshold_for_pan = enable_context_menu_ ? -1.0f : 0.0f; - if (is_active && - ImGui::IsMouseDragging(ImGuiMouseButton_Right, mouse_threshold_for_pan)) { - scrolling_.x += io.MouseDelta.x; - scrolling_.y += io.MouseDelta.y; - } - - // Context menu (under default mouse threshold) - ImVec2 drag_delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Right); - if (enable_context_menu_ && drag_delta.x == 0.0f && drag_delta.y == 0.0f) - ImGui::OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); - - if (ImGui::BeginPopup("context")) { - ImGui::MenuItem("Show Grid", nullptr, &enable_grid_); - if (ImGui::MenuItem("Reset Position", nullptr, false)) { - scrolling_.x = 0; - scrolling_.y = 0; - } - if (ImGui::MenuItem("Remove all", nullptr, false, points_.Size > 0)) { - points_.clear(); - } - ImGui::EndPopup(); - } -} - -void Canvas::DrawTilePainter(const Bitmap &bitmap, int size) { - const ImGuiIO &io = ImGui::GetIO(); - const bool is_hovered = ImGui::IsItemHovered(); // Hovered - const ImVec2 origin(canvas_p0_.x + scrolling_.x, - canvas_p0_.y + scrolling_.y); // Lock scrolled origin - const ImVec2 mouse_pos_in_canvas(io.MousePos.x - origin.x, - io.MousePos.y - origin.y); - - if (is_hovered) { - if (!points_.empty()) { - points_.clear(); - } - ImVec2 draw_tile_outline_pos; - draw_tile_outline_pos.x = - std::floor((double)mouse_pos_in_canvas.x / size) * size; - draw_tile_outline_pos.y = - std::floor((double)mouse_pos_in_canvas.y / size) * size; - - points_.push_back(draw_tile_outline_pos); - points_.push_back( - ImVec2(draw_tile_outline_pos.x + size, draw_tile_outline_pos.y + size)); - - if (bitmap.IsActive()) { - draw_list_->AddImage( - (void *)bitmap.GetTexture(), - ImVec2(origin.x + draw_tile_outline_pos.x, - origin.y + draw_tile_outline_pos.y), - ImVec2(origin.x + draw_tile_outline_pos.x + bitmap.GetWidth(), - origin.y + draw_tile_outline_pos.y + bitmap.GetHeight())); - } - - } else { - points_.clear(); - } -} - -void Canvas::DrawTileSelector(int size) { - const ImGuiIO &io = ImGui::GetIO(); - const bool is_hovered = ImGui::IsItemHovered(); // Hovered - const ImVec2 origin(canvas_p0_.x + scrolling_.x, - canvas_p0_.y + scrolling_.y); // Lock scrolled origin - const ImVec2 mouse_pos_in_canvas(io.MousePos.x - origin.x, - io.MousePos.y - origin.y); - - if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - if (!points_.empty()) { - points_.clear(); - } - ImVec2 draw_tile_outline_pos; - draw_tile_outline_pos.x = - std::floor((double)mouse_pos_in_canvas.x / size) * size; - draw_tile_outline_pos.y = - std::floor((double)mouse_pos_in_canvas.y / size) * size; - - points_.push_back(draw_tile_outline_pos); - points_.push_back( - ImVec2(draw_tile_outline_pos.x + size, draw_tile_outline_pos.y + size)); - } -} - -void Canvas::DrawBitmap(const Bitmap &bitmap, int border_offset, bool ready) { - if (ready) { - draw_list_->AddImage( - (void *)bitmap.GetTexture(), - ImVec2(canvas_p0_.x + border_offset, canvas_p0_.y + border_offset), - ImVec2(canvas_p0_.x + (bitmap.GetWidth() * 2), - canvas_p0_.y + (bitmap.GetHeight() * 2))); - } -} - -void Canvas::DrawBitmap(const Bitmap &bitmap, int x_offset, int y_offset) { - draw_list_->AddImage( - (void *)bitmap.GetTexture(), - ImVec2(canvas_p0_.x + x_offset + scrolling_.x, - canvas_p0_.y + y_offset + scrolling_.y), - ImVec2(canvas_p0_.x + x_offset + scrolling_.x + (bitmap.GetWidth()), - canvas_p0_.y + y_offset + scrolling_.y + (bitmap.GetHeight()))); -} - -void Canvas::DrawOutline(int x, int y, int w, int h) { - ImVec2 origin(canvas_p0_.x + scrolling_.x + x, - canvas_p0_.y + scrolling_.y + y); - ImVec2 size(canvas_p0_.x + scrolling_.x + x + w, - canvas_p0_.y + scrolling_.y + y + h); - draw_list_->AddRect(origin, size, IM_COL32(255, 255, 255, 255)); -} - -void Canvas::DrawRect(int x, int y, int w, int h, ImVec4 color) { - ImVec2 origin(canvas_p0_.x + scrolling_.x + x, - canvas_p0_.y + scrolling_.y + y); - ImVec2 size(canvas_p0_.x + scrolling_.x + x + w, - canvas_p0_.y + scrolling_.y + y + h); - draw_list_->AddRectFilled(origin, size, - IM_COL32(color.x, color.y, color.z, color.w)); -} - -void Canvas::DrawText(std::string text, int x, int y) { - draw_list_->AddText( - ImVec2(canvas_p0_.x + scrolling_.x + x, canvas_p0_.y + scrolling_.y + y), - IM_COL32(255, 255, 255, 255), text.data()); -} - -void Canvas::DrawGrid(float grid_step) { - // Draw grid + all lines in the canvas - draw_list_->PushClipRect(canvas_p0_, canvas_p1_, true); - if (enable_grid_) { - for (float x = fmodf(scrolling_.x, grid_step); x < canvas_sz_.x; - x += grid_step) - draw_list_->AddLine(ImVec2(canvas_p0_.x + x, canvas_p0_.y), - ImVec2(canvas_p0_.x + x, canvas_p1_.y), - IM_COL32(200, 200, 200, 40)); - for (float y = fmodf(scrolling_.y, grid_step); y < canvas_sz_.y; - y += grid_step) - draw_list_->AddLine(ImVec2(canvas_p0_.x, canvas_p0_.y + y), - ImVec2(canvas_p1_.x, canvas_p0_.y + y), - IM_COL32(200, 200, 200, 40)); - } -} - -void Canvas::DrawOverlay() { - const ImVec2 origin(canvas_p0_.x + scrolling_.x, - canvas_p0_.y + scrolling_.y); // Lock scrolled origin - for (int n = 0; n < points_.Size; n += 2) { - draw_list_->AddRect( - ImVec2(origin.x + points_[n].x, origin.y + points_[n].y), - ImVec2(origin.x + points_[n + 1].x, origin.y + points_[n + 1].y), - IM_COL32(255, 255, 255, 255), 1.0f); - } - - draw_list_->PopClipRect(); -} - -} // namespace gui -} // namespace yaze \ No newline at end of file diff --git a/src/gui/canvas.h b/src/gui/canvas.h deleted file mode 100644 index e0a6eb78..00000000 --- a/src/gui/canvas.h +++ /dev/null @@ -1,67 +0,0 @@ -#ifndef YAZE_GUI_CANVAS_H -#define YAZE_GUI_CANVAS_H - -#include - -#include -#include - -#include "app/gfx/bitmap.h" -#include "app/rom.h" - -namespace yaze { -namespace gui { - -using app::gfx::Bitmap; - -class Canvas { - public: - Canvas() = default; - explicit Canvas(ImVec2 canvas_size) - : custom_canvas_size_(true), canvas_sz_(canvas_size) {} - - void DrawBackground(ImVec2 canvas_size = ImVec2(0, 0)); - void DrawContextMenu(); - void DrawTilePainter(const Bitmap& bitmap, int size); - void DrawTileSelector(int size); - void DrawBitmap(const Bitmap& bitmap, int border_offset = 0, - bool ready = true); - void DrawBitmap(const Bitmap& bitmap, int x_offset, int y_offset); - void DrawOutline(int x, int y, int w, int h); - void DrawRect(int x, int y, int w, int h, ImVec4 color); - void DrawText(std::string text, int x, int y); - void DrawGrid(float grid_step = 64.0f); - void DrawOverlay(); // last - - auto Points() const { return points_; } - auto GetDrawList() const { return draw_list_; } - auto GetZeroPoint() const { return canvas_p0_; } - void SetCanvasSize(ImVec2 canvas_size) { - canvas_sz_ = canvas_size; - custom_canvas_size_ = true; - } - - private: - bool enable_grid_ = true; - bool enable_context_menu_ = true; - bool custom_canvas_size_ = false; - bool is_hovered_ = false; - - ImDrawList* draw_list_; - ImVector points_; - ImVec2 scrolling_; - ImVec2 canvas_sz_; - ImVec2 canvas_p0_; - ImVec2 canvas_p1_; - ImVec2 mouse_pos_in_canvas_; - - std::vector changed_tiles_; - app::gfx::Bitmap current_tile_; - - std::string title_; -}; - -} // namespace gui -} // namespace yaze - -#endif \ No newline at end of file diff --git a/src/gui/color.h b/src/gui/color.h deleted file mode 100644 index fc9c5bc4..00000000 --- a/src/gui/color.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef YAZE_GUI_COLOR_H -#define YAZE_GUI_COLOR_H - -#include - -#include -#include - -#include "app/gfx/bitmap.h" -#include "app/gfx/snes_palette.h" - -namespace yaze { -namespace gui { - -void DisplayPalette(app::gfx::SNESPalette& palette, bool loaded); - -} // namespace gui -} // namespace yaze - -#endif \ No newline at end of file diff --git a/src/gui/input.cc b/src/gui/input.cc deleted file mode 100644 index add939c9..00000000 --- a/src/gui/input.cc +++ /dev/null @@ -1,70 +0,0 @@ -#include "input.h" - -#include -#include - -#include "absl/strings/string_view.h" - -namespace yaze { -namespace gui { - -const int kStepOneHex = 0x01; -const int kStepFastHex = 0x0F; - -bool InputHex(const char* label, int* data) { - return ImGui::InputScalar(label, ImGuiDataType_U64, data, &kStepOneHex, - &kStepFastHex, "%06X", - ImGuiInputTextFlags_CharsHexadecimal); -} - -bool InputHexShort(const char* label, int* data) { - return ImGui::InputScalar(label, ImGuiDataType_U32, data, &kStepOneHex, - &kStepFastHex, "%06X", - ImGuiInputTextFlags_CharsHexadecimal); -} - -void ItemLabel(absl::string_view title, ItemLabelFlags flags) { - ImGuiWindow* window = ImGui::GetCurrentWindow(); - const ImVec2 lineStart = ImGui::GetCursorScreenPos(); - const ImGuiStyle& style = ImGui::GetStyle(); - float fullWidth = ImGui::GetContentRegionAvail().x; - float itemWidth = ImGui::CalcItemWidth() + style.ItemSpacing.x; - ImVec2 textSize = ImGui::CalcTextSize(title.begin(), title.end()); - ImRect textRect; - textRect.Min = ImGui::GetCursorScreenPos(); - if (flags & ItemLabelFlag::Right) textRect.Min.x = textRect.Min.x + itemWidth; - textRect.Max = textRect.Min; - textRect.Max.x += fullWidth - itemWidth; - textRect.Max.y += textSize.y; - - ImGui::SetCursorScreenPos(textRect.Min); - - ImGui::AlignTextToFramePadding(); - // Adjust text rect manually because we render it directly into a drawlist - // instead of using public functions. - textRect.Min.y += window->DC.CurrLineTextBaseOffset; - textRect.Max.y += window->DC.CurrLineTextBaseOffset; - - ImGui::ItemSize(textRect); - if (ImGui::ItemAdd( - textRect, window->GetID(title.data(), title.data() + title.size()))) { - ImGui::RenderTextEllipsis( - ImGui::GetWindowDrawList(), textRect.Min, textRect.Max, textRect.Max.x, - textRect.Max.x, title.data(), title.data() + title.size(), &textSize); - - if (textRect.GetWidth() < textSize.x && ImGui::IsItemHovered()) - ImGui::SetTooltip("%.*s", (int)title.size(), title.data()); - } - if (flags & ItemLabelFlag::Left) { - ImVec2 result; - auto other = ImVec2{0, textSize.y + window->DC.CurrLineTextBaseOffset}; - result.x = textRect.Max.x - other.x; - result.y = textRect.Max.y - other.y; - ImGui::SetCursorScreenPos(result); - ImGui::SameLine(); - } else if (flags & ItemLabelFlag::Right) - ImGui::SetCursorScreenPos(lineStart); -} - -} // namespace gui -} // namespace yaze diff --git a/src/gui/input.h b/src/gui/input.h deleted file mode 100644 index 11be3f2f..00000000 --- a/src/gui/input.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef YAZE_APP_CORE_INPUT_H -#define YAZE_APP_CORE_INPUT_H - -#include -#include - -#include -#include - -#include "absl/strings/string_view.h" - -namespace yaze { -namespace gui { - -IMGUI_API bool InputHex(const char* label, int* data); -IMGUI_API bool InputHexShort(const char* label, int* data); - -using ItemLabelFlags = enum ItemLabelFlag { - Left = 1u << 0u, - Right = 1u << 1u, - Default = Left, -}; - -IMGUI_API void ItemLabel(absl::string_view title, ItemLabelFlags flags); - -} // namespace gui -} // namespace yaze - -#endif \ No newline at end of file diff --git a/src/gui/style.cc b/src/gui/style.cc deleted file mode 100644 index 2e22b55a..00000000 --- a/src/gui/style.cc +++ /dev/null @@ -1,108 +0,0 @@ -#include "style.h" - -#include "imgui/imgui.h" -#include "imgui/imgui_internal.h" - -namespace yaze { -namespace gui { - -void ColorsYaze() { - ImGuiStyle *style = &ImGui::GetStyle(); - ImVec4 *colors = style->Colors; - - style->WindowPadding = ImVec2(10.f, 10.f); - style->FramePadding = ImVec2(10.f, 3.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); -} -} // namespace gui -} // namespace yaze \ No newline at end of file diff --git a/src/gui/style.h b/src/gui/style.h deleted file mode 100644 index 7f64b582..00000000 --- a/src/gui/style.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef YAZE_APP_CORE_STYLE_H -#define YAZE_APP_CORE_STYLE_H - -#include -#include - -namespace yaze { -namespace gui { - -void ColorsYaze(); - -} // namespace gui -} // namespace yaze - -#endif \ No newline at end of file diff --git a/src/gui/widgets.h b/src/gui/widgets.h deleted file mode 100644 index 69bf15b9..00000000 --- a/src/gui/widgets.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef YAZE_GUI_WIDGETS_H -#define YAZE_GUI_WIDGETS_H - -#include - -#include "absl/status/status.h" -#include "app/core/constants.h" - -namespace yaze { -namespace gui { -namespace widgets { - -TextEditor::LanguageDefinition GetAssemblyLanguageDef(); - -} // namespace widgets -} // namespace gui -} // namespace yaze - -#endif \ No newline at end of file diff --git a/src/lib/ImGuiFileDialog b/src/lib/ImGuiFileDialog index af4800e8..2917cd9e 160000 --- a/src/lib/ImGuiFileDialog +++ b/src/lib/ImGuiFileDialog @@ -1 +1 @@ -Subproject commit af4800e85a79154c8c08cf28d4cfee82bdc0cd03 +Subproject commit 2917cd9ec120bce7b4297e7f3afb660071707e05 diff --git a/src/lib/SDL b/src/lib/SDL index 5b904a10..ac13ca9a 160000 --- a/src/lib/SDL +++ b/src/lib/SDL @@ -1 +1 @@ -Subproject commit 5b904a103ad3bd58c13c82b11e32e97a91bafcfb +Subproject commit ac13ca9ab691e13e8eebe9684740ddcb0d716203 diff --git a/src/lib/SDL_mixer b/src/lib/SDL_mixer index 7f73f724..5ec6ceff 160000 --- a/src/lib/SDL_mixer +++ b/src/lib/SDL_mixer @@ -1 +1 @@ -Subproject commit 7f73f724f2097e457cd7cfe3323a876670e79daf +Subproject commit 5ec6ceff78e0762296e785cc9cc3bbdc8da2aa80 diff --git a/src/lib/asar b/src/lib/asar index 634d6baf..4d04c897 160000 --- a/src/lib/asar +++ b/src/lib/asar @@ -1 +1 @@ -Subproject commit 634d6baf7ad073ef01055c9f0ce923636738d2a7 +Subproject commit 4d04c897b94357ae4251f49513f5b8b4f85c4b3b diff --git a/src/lib/imgui b/src/lib/imgui index e57871bb..3733b506 160000 --- a/src/lib/imgui +++ b/src/lib/imgui @@ -1 +1 @@ -Subproject commit e57871bb95faec757e51214bc0e1ae29b13258ab +Subproject commit 3733b5064e1820dc8517fedba2b34bfbdef569ab diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 06f9870a..3664e35f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,4 @@ -# GoogleTest ------------------------------------------------------------------------------------ +# GoogleTest ------------------------------------------------------------------ include(FetchContent) FetchContent_Declare( googletest @@ -13,30 +13,52 @@ enable_testing() add_executable( yaze_test yaze_test.cc - rom_test.cc + emu/cpu_test.cc + emu/spc700_test.cc + emu/ppu_test.cc + compression_test.cc + snes_palette_test.cc + room_object_test.cc + ../src/cli/patch.cc + ../src/cli/command_handler.cc ../src/app/rom.cc + ../src/app/emu/cpu/cpu.cc + ../src/app/emu/cpu/internal/instructions.cc + ../src/app/emu/cpu/internal/addressing.cc + ../src/app/emu/audio/internal/addressing.cc + ../src/app/emu/audio/internal/instructions.cc + ../src/app/emu/audio/apu.cc + ../src/app/emu/video/ppu.cc + ../src/app/emu/audio/dsp.cc + ../src/app/emu/audio/spc700.cc ../src/app/gfx/bitmap.cc ../src/app/gfx/snes_tile.cc ../src/app/gfx/snes_palette.cc + ../src/app/gfx/compression.cc ../src/app/core/common.cc - ${ASAR_STATIC_SRC} + # ${ASAR_STATIC_SRC} ) target_include_directories( yaze_test PUBLIC ../src/ ../src/lib/ - ../src/lib/asar/src/asar/ - ${SDL_INCLUDE_DIRS} + # ../src/lib/asar/src/asar/ + ${SDL2_INCLUDE_DIR} + ${PNG_INCLUDE_DIRS} ) target_link_libraries( yaze_test - SDL2 ${ABSL_TARGETS} + SDL2::SDL2 + ${PNG_LIBRARIES} + ${GLEW_LIBRARIES} ${OPENGL_LIBRARIES} ${CMAKE_DL_LIBS} - asar-static + # asar-static + # snes_spc + ImGui gmock_main gmock gtest_main diff --git a/test/asm_test.cc b/test/asm_test.cc deleted file mode 100644 index 9308895a..00000000 --- a/test/asm_test.cc +++ /dev/null @@ -1,67 +0,0 @@ -#include - -#include -#include - -#include -#include -#include -#include -#include - -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/string_view.h" -#include "app/asm/script.h" -#include "app/core/constants.h" -#include "app/rom.h" - -namespace yaze_test { -namespace asm_test { - -using yaze::app::ROM; -using yaze::app::snes_asm::Script; - -using ::testing::_; -using ::testing::ElementsAreArray; -using ::testing::Eq; -using ::testing::Return; - -class MockScript : public Script { - public: - MOCK_METHOD(absl::Status, ApplyPatchToROM, (ROM & rom)); - MOCK_METHOD(absl::Status, PatchOverworldMosaic, - (ROM & rom, char mosaic_tiles[yaze::app::core::kNumOverworldMaps], - int routine_offset, int hook_offset)); -}; - -TEST(ASMTest, ApplyMosaicChangePatchOk) { - ROM rom; - MockScript script; - char mosaic_tiles[yaze::app::core::kNumOverworldMaps]; - - EXPECT_CALL(script, PatchOverworldMosaic(_, Eq(mosaic_tiles), - Eq(0x1301D0 + 0x138000), 0)) - .WillOnce(Return(absl::OkStatus())); - - EXPECT_CALL(script, ApplyPatchToROM(_)).WillOnce(Return(absl::OkStatus())); - - EXPECT_THAT( - script.PatchOverworldMosaic(rom, mosaic_tiles, 0x1301D0 + 0x138000, 0), - absl::OkStatus()); - EXPECT_THAT(script.ApplyPatchToROM(rom), absl::OkStatus()); -} - -TEST(ASMTest, NoPatchLoadedError) { - ROM rom; - MockScript script; - EXPECT_CALL(script, ApplyPatchToROM(_)) - .WillOnce(Return(absl::InvalidArgumentError("No patch loaded!"))); - - EXPECT_THAT(script.ApplyPatchToROM(rom), - absl::InvalidArgumentError("No patch loaded!")); -} - -} // namespace asm_test -} // namespace yaze_test \ No newline at end of file diff --git a/test/compression_test.cc b/test/compression_test.cc new file mode 100644 index 00000000..50a2cfa3 --- /dev/null +++ b/test/compression_test.cc @@ -0,0 +1,433 @@ +#include "app/gfx/compression.h" + +#include +#include + +#include + +#include "absl/status/statusor.h" +#include "app/rom.h" + +#define BUILD_HEADER(command, length) (command << 5) + (length - 1) + +namespace yaze_test { +namespace gfx_test { + +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 ::testing::ElementsAre; +using ::testing::ElementsAreArray; +using ::testing::TypedEq; + +namespace { + +Bytes ExpectCompressOk(ROM& rom, uchar* in, int in_size) { + auto load_status = rom.LoadFromPointer(in, in_size); + EXPECT_TRUE(load_status.ok()); + auto compression_status = CompressV3(rom.vector(), 0, in_size); + EXPECT_TRUE(compression_status.ok()); + auto compressed_bytes = std::move(*compression_status); + return compressed_bytes; +} + +Bytes ExpectDecompressBytesOk(ROM& rom, Bytes& in) { + auto load_status = rom.LoadFromBytes(in); + EXPECT_TRUE(load_status.ok()); + auto decompression_status = DecompressV2(rom.data(), 0, in.size()); + EXPECT_TRUE(decompression_status.ok()); + auto decompressed_bytes = std::move(*decompression_status); + return decompressed_bytes; +} + +Bytes ExpectDecompressOk(ROM& rom, uchar* in, int in_size) { + auto load_status = rom.LoadFromPointer(in, in_size); + EXPECT_TRUE(load_status.ok()); + auto decompression_status = DecompressV2(rom.data(), 0, in_size); + EXPECT_TRUE(decompression_status.ok()); + auto decompressed_bytes = std::move(*decompression_status); + return decompressed_bytes; +} + +std::shared_ptr ExpectNewCompressionPieceOk( + const char command, const int length, const std::string args, + const int argument_length) { + auto new_piece = std::make_shared(command, length, args, + argument_length); + EXPECT_TRUE(new_piece != nullptr); + return new_piece; +} + +// Helper function to assert compression quality. +void AssertCompressionQuality( + const std::vector& uncompressed_data, + const std::vector& expected_compressed_data) { + absl::StatusOr result = + CompressV3(uncompressed_data, 0, uncompressed_data.size(), 0, false); + ASSERT_TRUE(result.ok()); + auto compressed_data = std::move(*result); + EXPECT_THAT(compressed_data, ElementsAreArray(expected_compressed_data)); +} + +Bytes 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()); + auto compressed_data = std::move(*result); + return compressed_data; +} + +std::vector CreateRepeatedBetweenUncompressable( + int leftUncompressedSize, int repeatedByteSize, int rightUncompressedSize) { + std::vector result( + leftUncompressedSize + repeatedByteSize + rightUncompressedSize, 0); + std::fill_n(result.begin() + leftUncompressedSize, repeatedByteSize, 0x00); + return result; +} + +} // namespace + +TEST(LC_LZ2_CompressionTest, TrivialRepeatedBytes) { + AssertCompressionQuality({0x00, 0x00, 0x00}, {0x22, 0x00, 0xFF}); +} + +TEST(LC_LZ2_CompressionTest, RepeatedBytesBetweenUncompressable) { + AssertCompressionQuality({0x01, 0x00, 0x00, 0x00, 0x10}, + {0x04, 0x01, 0x00, 0x00, 0x00, 0x10, 0xFF}); +} + +TEST(LC_LZ2_CompressionTest, RepeatedBytesBeforeUncompressable) { + AssertCompressionQuality({0x00, 0x00, 0x00, 0x10}, + {0x22, 0x00, 0x00, 0x10, 0xFF}); +} + +TEST(LC_LZ2_CompressionTest, RepeatedBytesAfterUncompressable) { + AssertCompressionQuality({0x01, 0x00, 0x00, 0x00}, + {0x00, 0x01, 0x22, 0x00, 0xFF}); +} + +TEST(LC_LZ2_CompressionTest, RepeatedBytesAfterUncompressableRepeated) { + AssertCompressionQuality( + {0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}, + {0x22, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x02, 0xFF}); +} + +TEST(LC_LZ2_CompressionTest, RepeatedBytesBeforeUncompressableRepeated) { + AssertCompressionQuality( + {0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00}, + {0x04, 0x01, 0x00, 0x00, 0x00, 0x02, 0x22, 0x00, 0xFF}); +} + +TEST(LC_LZ2_CompressionTest, CompressionDecompressionEmptyData) { + ROM rom; + uchar empty_input[0] = {}; + auto comp_result = ExpectCompressOk(rom, empty_input, 0); + EXPECT_EQ(0, comp_result.size()); + + auto decomp_result = ExpectDecompressOk(rom, empty_input, 0); + EXPECT_EQ(0, decomp_result.size()); +} + +TEST(LC_LZ2_CompressionTest, NewDecompressionPieceOk) { + char command = 1; + int length = 1; + char args[] = "aaa"; + int argument_length = 0x02; + CompressionPiece old_piece; + old_piece.command = command; + old_piece.length = length; + old_piece.argument = args; + old_piece.argument_length = argument_length; + old_piece.next = nullptr; + + auto new_piece = ExpectNewCompressionPieceOk(0x01, 0x01, "aaa", 0x02); + + EXPECT_EQ(old_piece.command, new_piece->command); + EXPECT_EQ(old_piece.length, new_piece->length); + ASSERT_EQ(old_piece.argument_length, new_piece->argument_length); + for (int i = 0; i < old_piece.argument_length; ++i) { + EXPECT_EQ(old_piece.argument[i], new_piece->argument[i]); + } +} + +// TODO: Check why header built is off by one +// 0x25 instead of 0x24 +TEST(LC_LZ2_CompressionTest, CompressionSingleSet) { + ROM rom; + uchar single_set[5] = {0x2A, 0x2A, 0x2A, 0x2A, 0x2A}; + uchar single_set_expected[3] = {BUILD_HEADER(1, 5), 0x2A, 0xFF}; + + auto comp_result = ExpectCompressOk(rom, single_set, 5); + EXPECT_THAT(single_set_expected, ElementsAreArray(comp_result.data(), 3)); +} + +TEST(LC_LZ2_CompressionTest, CompressionSingleWord) { + ROM rom; + uchar single_word[6] = {0x2A, 0x01, 0x2A, 0x01, 0x2A, 0x01}; + uchar single_word_expected[4] = {BUILD_HEADER(0x02, 0x06), 0x2A, 0x01, 0xFF}; + + auto comp_result = ExpectCompressOk(rom, single_word, 6); + EXPECT_THAT(single_word_expected, ElementsAreArray(comp_result.data(), 4)); +} + +TEST(LC_LZ2_CompressionTest, CompressionSingleIncrement) { + ROM rom; + uchar single_inc[3] = {0x01, 0x02, 0x03}; + uchar single_inc_expected[3] = {BUILD_HEADER(0x03, 0x03), 0x01, 0xFF}; + auto comp_result = ExpectCompressOk(rom, single_inc, 3); + EXPECT_THAT(single_inc_expected, ElementsAreArray(comp_result.data(), 3)); +} + +TEST(LC_LZ2_CompressionTest, CompressionSingleCopy) { + ROM rom; + uchar single_copy[4] = {0x03, 0x0A, 0x07, 0x14}; + uchar single_copy_expected[6] = { + BUILD_HEADER(0x00, 0x04), 0x03, 0x0A, 0x07, 0x14, 0xFF}; + auto comp_result = ExpectCompressOk(rom, single_copy, 4); + EXPECT_THAT(single_copy_expected, ElementsAreArray(comp_result.data(), 6)); +} + +TEST(LC_LZ2_CompressionTest, CompressionSingleOverflowIncrement) { + AssertCompressionQuality({0xFE, 0xFF, 0x00, 0x01}, + {BUILD_HEADER(0x03, 0x04), 0xFE, 0xFF}); +} +/** + +TEST(LC_LZ2_CompressionTest, CompressionSingleCopyRepeat) { + std::vector single_copy_expected = {0x03, 0x0A, 0x07, 0x14, + 0x03, 0x0A, 0x07, 0x14}; + + auto comp_result = ExpectCompressV3Ok( + single_copy_expected, {BUILD_HEADER(0x00, 0x04), 0x03, 0x0A, 0x07, 0x14, + BUILD_HEADER(0x04, 0x04), 0x00, 0x00, 0xFF}); + EXPECT_THAT(single_copy_expected, ElementsAreArray(comp_result.data(), 6)); +} + +TEST(LC_LZ2_CompressionTest, CompressionMixedRepeatIncrement) { + AssertCompressionQuality( + {0x05, 0x05, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, + 0x05, 0x02, 0x05, 0x02, 0x05, 0x02, 0x0A, 0x0B, 0x05, 0x02, + 0x05, 0x02, 0x05, 0x02, 0x08, 0x0A, 0x00, 0x05}, + {BUILD_HEADER(0x01, 0x04), 0x05, BUILD_HEADER(0x03, 0x06), 0x06, + BUILD_HEADER(0x00, 0x01), 0x05, 0xFF}); +} + +TEST(LC_LZ2_CompressionTest, CompressionMixedIncrementIntraCopyOffset) { + // "Mixing, inc, alternate, intra copy" + // compress start: 3, length: 21 + // compressed length: 9 + AssertCompressionQuality( + {0x05, 0x05, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, + 0x05, 0x02, 0x05, 0x02, 0x05, 0x02, 0x0A, 0x0B, 0x05, 0x02, + 0x05, 0x02, 0x05, 0x02, 0x08, 0x0A, 0x00, 0x05}, + {BUILD_HEADER(0x03, 0x07), 0x05, BUILD_HEADER(0x02, 0x06), 0x05, 0x02, + BUILD_HEADER(0x04, 0x08), 0x05, 0x00, 0xFF}); +} + +TEST(LC_LZ2_CompressionTest, CompressionMixedIncrementIntraCopySource) { + // "Mixing, inc, alternate, intra copy" + // 0, 28 + // 16 + AssertCompressionQuality( + {0x05, 0x05, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, + 0x05, 0x02, 0x05, 0x02, 0x05, 0x02, 0x0A, 0x0B, 0x05, 0x02, + 0x05, 0x02, 0x05, 0x02, 0x08, 0x0A, 0x00, 0x05}, + {BUILD_HEADER(0x01, 0x04), 0x05, BUILD_HEADER(0x03, 0x06), 0x06, + BUILD_HEADER(0x02, 0x06), 0x05, 0x02, BUILD_HEADER(0x04, 0x08), 0x08, + 0x00, BUILD_HEADER(0x00, 0x04), 0x08, 0x0A, 0x00, 0x05, 0xFF}); +} + +// Extended Header +// 111CCCLL LLLLLLLL +// CCC: Real command +// LLLLLLLLLL: Length + +// Normally you have 5 bits for the length, so the maximum value you can +// represent is 31 (which outputs 32 bytes). With the long length, you get 5 +// more bits for the length, so the maximum value you can represent becomes +// 1023, outputting 1024 bytes at a time. + +void build_extended_header(uint8_t command, uint8_t length, uint8_t& byte1, + uint8_t& byte2) { + byte1 = command << 3; + byte1 += (length - 1); + byte1 += 0b11100000; + byte2 = length >> 3; +} + +std::vector CreateRepeatedBetweenUncompressable( + int leftUncompressedSize, int repeatedByteSize, int rightUncompressedSize) { + std::vector result( + leftUncompressedSize + repeatedByteSize + rightUncompressedSize, 0); + std::fill_n(result.begin() + leftUncompressedSize, repeatedByteSize, 0x00); + return result; +} + +TEST(LC_LZ2_CompressionTest, LengthBorderCompression) { + // "Length border compression" + std::vector result(42, 0); + std::fill_n(result.begin(), 42, 0x05); + AssertCompressionQuality(result, {BUILD_HEADER(0x04, 42), 0x05, 0x05, 0xFF}); + + // "Extended length, 400 repeat of 5" + std::vector result2(400, 0); + std::fill_n(result2.begin(), 400, 0x05); + uint8_t byte1; + uint8_t byte2; + build_extended_header(0x01, 42, byte1, byte2); + AssertCompressionQuality(result2, {byte1, byte2, 0x05, 0x05, 0xFF}); + + // "Extended length, 1050 repeat of 5" + std::vector result3(1050, 0); + std::fill_n(result3.begin(), 1050, 0x05); + uint8_t byte3; + uint8_t byte4; + build_extended_header(0x04, 1050, byte3, byte4); + AssertCompressionQuality(result3, {byte3, byte4, 0x05, 0x05, 0xFF}); + + // // "Extended length, 2050 repeat of 5" + std::vector result4(2050, 0); + std::fill_n(result4.begin(), 2050, 0x05); + uint8_t byte5; + uint8_t byte6; + build_extended_header(0x04, 2050, byte5, byte6); + AssertCompressionQuality(result4, {byte5, byte6, 0x05, 0x05, 0xFF}); +} + +TEST(LC_LZ2_CompressionTest, CompressionExtendedWordCopy) { + // ROM rom; + // uchar buffer[3000]; + // for (unsigned int i = 0; i < 3000; i += 2) { + // buffer[i] = 0x05; + // buffer[i + 1] = 0x06; + // } + // uchar hightlength_word_1050[] = { + // 0b11101011, 0xFF, 0x05, 0x06, BUILD_HEADER(0x02, 0x1A), 0x05, 0x06, + // 0xFF}; + + // // "Extended word copy" + // auto comp_result = ExpectCompressOk(rom, buffer, 1050); + // EXPECT_THAT(hightlength_word_1050, ElementsAreArray(comp_result.data(), + // 8)); + + std::vector buffer(3000, 0); + std::fill_n(buffer.begin(), 3000, 0x05); + for (unsigned int i = 0; i < 3000; i += 2) { + buffer[i] = 0x05; + buffer[i + 1] = 0x06; + } + + uint8_t byte1; + uint8_t byte2; + build_extended_header(0x02, 0x1A, byte1, byte2); + AssertCompressionQuality( + buffer, {0b11101011, 0xFF, 0x05, 0x06, byte1, byte2, 0x05, 0x06, 0xFF}); +} + +TEST(LC_LZ2_CompressionTest, CompressionMixedPatterns) { + AssertCompressionQuality( + {0x05, 0x05, 0x05, 0x06, 0x07, 0x06, 0x07, 0x08, 0x09, 0x0A}, + {BUILD_HEADER(0x01, 0x03), 0x05, BUILD_HEADER(0x02, 0x04), 0x06, 0x07, + BUILD_HEADER(0x03, 0x03), 0x08, 0xFF}); +} + +TEST(LC_LZ2_CompressionTest, CompressionLongIntraCopy) { + ROM rom; + uchar long_data[15] = {0x05, 0x06, 0x07, 0x08, 0x05, 0x06, 0x07, 0x08, + 0x05, 0x06, 0x07, 0x08, 0x05, 0x06, 0x07}; + uchar long_expected[] = {BUILD_HEADER(0x00, 0x04), 0x05, 0x06, 0x07, 0x08, + BUILD_HEADER(0x04, 0x0C), 0x00, 0x00, 0xFF}; + + auto comp_result = ExpectCompressOk(rom, long_data, 15); + EXPECT_THAT(long_expected, + ElementsAreArray(comp_result.data(), sizeof(long_expected))); +} + +*/ +// Tests for HandleDirectCopy + +TEST(HandleDirectCopyTest, NotDirectCopyWithAccumulatedBytes) { + CompressionContext context({0x01, 0x02, 0x03}, 0, 3); + context.cmd_with_max = kCommandByteFill; + context.comp_accumulator = 2; + HandleDirectCopy(context); + EXPECT_EQ(context.compressed_data.size(), 3); +} + +TEST(HandleDirectCopyTest, NotDirectCopyWithoutAccumulatedBytes) { + CompressionContext context({0x01, 0x02, 0x03}, 0, 3); + context.cmd_with_max = kCommandByteFill; + HandleDirectCopy(context); + EXPECT_EQ(context.compressed_data.size(), 2); // Header + 1 byte +} + +TEST(HandleDirectCopyTest, AccumulateBytesWithoutMax) { + CompressionContext context({0x01, 0x02, 0x03}, 0, 3); + context.cmd_with_max = kCommandDirectCopy; + HandleDirectCopy(context); + EXPECT_EQ(context.comp_accumulator, 1); + EXPECT_EQ(context.compressed_data.size(), 0); // No data added yet +} + +// Tests for CheckIncByteV3 + +TEST(CheckIncByteV3Test, IncreasingSequence) { + CompressionContext context({0x01, 0x02, 0x03}, 0, 3); + CheckIncByteV3(context); + EXPECT_EQ(context.current_cmd.data_size[kCommandIncreasingFill], 3); +} + +TEST(CheckIncByteV3Test, IncreasingSequenceSurroundedByIdenticalBytes) { + CompressionContext context({0x01, 0x02, 0x03, 0x04, 0x01}, 1, + 3); // Start from index 1 + CheckIncByteV3(context); + EXPECT_EQ(context.current_cmd.data_size[kCommandIncreasingFill], + 0); // Reset to prioritize direct copy +} + +TEST(CheckIncByteV3Test, NotAnIncreasingSequence) { + CompressionContext context({0x01, 0x01, 0x03}, 0, 3); + CheckIncByteV3(context); + EXPECT_EQ(context.current_cmd.data_size[kCommandIncreasingFill], + 1); // Only one byte is detected +} + +TEST(LC_LZ2_CompressionTest, DecompressionValidCommand) { + ROM rom; + Bytes 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)); +} + +TEST(LC_LZ2_CompressionTest, DecompressionMixingCommand) { + ROM rom; + uchar random1_i[11] = {BUILD_HEADER(0x01, 0x03), + 0x2A, + BUILD_HEADER(0x00, 0x04), + 0x01, + 0x02, + 0x03, + 0x04, + BUILD_HEADER(0x02, 0x02), + 0x0B, + 0x16, + 0xFF}; + uchar random1_o[9] = {42, 42, 42, 1, 2, 3, 4, 11, 22}; + auto decomp_result = ExpectDecompressOk(rom, random1_i, 11); + EXPECT_THAT(random1_o, ElementsAreArray(decomp_result.data(), 9)); +} + +} // namespace gfx_test +} // namespace yaze_test \ No newline at end of file diff --git a/test/delta_test.cc b/test/delta_test.cc deleted file mode 100644 index a4db2f78..00000000 --- a/test/delta_test.cc +++ /dev/null @@ -1,74 +0,0 @@ -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/string_view.h" -#include "app/core/constants.h" -#include "app/delta/client.h" -#include "app/delta/service.h" -#include "app/rom.h" -#include "src/app/delta/delta.grpc.pb.h" -#include "src/app/delta/delta.pb.h" - -namespace yaze_test { -namespace delta_test { - -TEST(DeltaTest, InitRepoAndPushOk) { - yaze::app::delta::DeltaService service; - yaze::app::ROM rom; - Bytes test_bytes; - test_bytes.push_back(0x40); - EXPECT_TRUE(rom.LoadFromBytes(test_bytes).ok()); - grpc::ServerContext* context; - InitRequest init_request; - auto repo = init_request.mutable_repo(); - repo->set_project_name("test_repo"); - Branch branch; - branch.set_branch_name("test_branch"); - auto new_mutable_commits = branch.mutable_commits(); - new_mutable_commits->Reserve(5); - for (int i = 0; i < 5; ++i) { - auto new_commit = new Commit(); - new_commit->set_commit_id(i); - new_mutable_commits->Add(); - new_mutable_commits->at(i) = *new_commit; - } - auto mutable_tree = repo->mutable_tree(); - mutable_tree->Add(); - mutable_tree->at(0) = branch; - InitResponse init_response; - auto init_status = service.Init(context, &init_request, &init_response); - EXPECT_TRUE(init_status.ok()); - - PushRequest request; - request.set_branch_name("test_branch"); - request.set_repository_name("test_repo"); - auto mutable_commits = request.mutable_commits(); - mutable_commits->Reserve(5); - for (int i = 0; i < 5; ++i) { - auto new_commit = new Commit(); - new_commit->set_commit_id(i * 2); - mutable_commits->Add(); - mutable_commits->at(i) = *new_commit; - } - PushResponse reply; - auto status = service.Push(context, &request, &reply); - EXPECT_TRUE(status.ok()); - - auto repos = service.Repos(); - auto result_branch = repos.at(0).tree(); - std::cerr << result_branch.at(0).DebugString() << std::endl; -} - -} // namespace delta_test -} // namespace yaze_test \ No newline at end of file diff --git a/test/emu/cpu_test.cc b/test/emu/cpu_test.cc new file mode 100644 index 00000000..b6c40492 --- /dev/null +++ b/test/emu/cpu_test.cc @@ -0,0 +1,4177 @@ +#include "app/emu/cpu/cpu.h" + +#include +#include + +#include "app/emu/cpu/clock.h" +#include "app/emu/cpu/internal/opcodes.h" +#include "app/emu/debug/asm_parser.h" +#include "app/emu/memory/memory.h" +#include "app/emu/memory/mock_memory.h" + +namespace yaze { +namespace app { +namespace emu { + +class CPUTest : public ::testing::Test { + public: + void SetUp() override { + mock_memory.Init(); + EXPECT_CALL(mock_memory, ClearMemory()).Times(::testing::AtLeast(1)); + mock_memory.ClearMemory(); + asm_parser.CreateInternalOpcodeMap(); + } + + MockMemory mock_memory; + MockClock mock_clock; + CPU cpu{mock_memory, mock_clock}; + AsmParser asm_parser; +}; + +using ::testing::_; +using ::testing::Return; + +// ============================================================================ +// Infrastructure +// ============================================================================ + +TEST_F(CPUTest, CheckMemoryContents) { + MockMemory memory; + std::vector data = {0x00, 0x01, 0x02, 0x03, 0x04}; + memory.SetMemoryContents(data); + + EXPECT_CALL(memory, ReadByte(0)).WillOnce(Return(0x00)); + EXPECT_CALL(memory, ReadByte(1)).WillOnce(Return(0x01)); + EXPECT_CALL(memory, ReadByte(2)).WillOnce(Return(0x02)); + EXPECT_CALL(memory, ReadByte(3)).WillOnce(Return(0x03)); + EXPECT_CALL(memory, ReadByte(4)).WillOnce(Return(0x04)); + EXPECT_CALL(memory, ReadByte(63999)).WillOnce(Return(0x00)); + + EXPECT_EQ(memory.ReadByte(0), 0x00); + EXPECT_EQ(memory.ReadByte(1), 0x01); + EXPECT_EQ(memory.ReadByte(2), 0x02); + EXPECT_EQ(memory.ReadByte(3), 0x03); + EXPECT_EQ(memory.ReadByte(4), 0x04); + EXPECT_EQ(memory.ReadByte(63999), 0x00); +} + +// ============================================================================ +// ADC - Add with Carry + +TEST_F(CPUTest, ADC_CheckCarryFlag) { + cpu.A = 0xFF; + cpu.SetAccumulatorSize(true); + std::vector data = {0x15, 0x01}; // Operand at address 0x15 + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadByte(_)).WillOnce(Return(1)); + + cpu.ExecuteInstruction(0x69); // ADC Immediate + + EXPECT_EQ(cpu.A, 0x00); + EXPECT_TRUE(cpu.GetCarryFlag()); +} + +TEST_F(CPUTest, ADC_DirectPageIndexedIndirectX) { + cpu.A = 0x03; + cpu.D = 0x2000; // Setting Direct Page register to 0x2000 + std::vector data = {0x61, 0x10}; // ADC (dp, X) + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2012, {0x00, 0x30}); // [0x2012] = 0x3000 + mock_memory.InsertMemory(0x3000, {0x06}); // [0x3000] = 0x06 + + cpu.X = 0x02; // X register + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + EXPECT_CALL(mock_memory, ReadWord(0x2012)).WillOnce(Return(0x3000)); + EXPECT_CALL(mock_memory, ReadByte(0x3000)).WillOnce(Return(0x06)); + + cpu.ExecuteInstruction(0x61); // ADC (dp, X) + EXPECT_EQ(cpu.A, 0x09); // 0x03 + 0x06 = 0x09 +} + +TEST_F(CPUTest, ADC_StackRelative) { + cpu.A = 0x03; + cpu.SetSP(0x01FF); // Setting Stack Pointer to 0x01FF + std::vector data = {0x63, 0x02}; // ADC sr + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0x06}); // [0x0201] = 0x06 + + EXPECT_CALL(mock_memory, SP()).WillOnce(Return(0x01FF)); + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); // Operand + EXPECT_CALL(mock_memory, ReadByte(0x0201)) + .WillOnce(Return(0x06)); // Memory value + + cpu.ExecuteInstruction(0x63); // ADC Stack Relative + EXPECT_EQ(cpu.A, 0x09); // 0x03 + 0x06 = 0x09 +} + +TEST_F(CPUTest, ADC_DirectPage) { + cpu.A = 0x01; + cpu.D = 0x2000; // Setting Direct Page register to 0x2000 + std::vector data = {0x65, 0x10}; // ADC dp + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0x05}); // [0x2010] = 0x05 + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + EXPECT_CALL(mock_memory, ReadByte(0x2010)).WillOnce(Return(0x05)); + + cpu.ExecuteInstruction(0x65); // ADC Direct Page + EXPECT_EQ(cpu.A, 0x06); +} + +TEST_F(CPUTest, ADC_DirectPageIndirectLong) { + cpu.A = 0x03; + cpu.D = 0x2000; + std::vector data = {0x67, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0x05, 0x00, 0x30}); + mock_memory.InsertMemory(0x030005, {0x06}); + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + EXPECT_CALL(mock_memory, ReadWordLong(0x2010)).WillOnce(Return(0x300005)); + EXPECT_CALL(mock_memory, ReadWord(0x300005)).WillOnce(Return(0x06)); + + cpu.ExecuteInstruction(0x67); // ADC Direct Page Indirect Long + EXPECT_EQ(cpu.A, 0x09); +} + +TEST_F(CPUTest, ADC_Immediate_TwoPositiveNumbers) { + cpu.A = 0x01; + cpu.SetAccumulatorSize(true); + std::vector data = {0x01}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadByte(_)).WillOnce(Return(0x01)); + + cpu.ExecuteInstruction(0x69); // ADC Immediate + EXPECT_EQ(cpu.A, 0x02); +} + +TEST_F(CPUTest, ADC_Immediate_PositiveAndNegativeNumbers) { + cpu.A = 10; + cpu.SetAccumulatorSize(true); + std::vector data = {0x69, static_cast(-20)}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadByte(_)).WillOnce(Return(-20)); + + cpu.ExecuteInstruction(0x69); // ADC Immediate + EXPECT_EQ(cpu.A, static_cast(-10)); +} + +TEST_F(CPUTest, ADC_Absolute) { + cpu.A = 0x01; + cpu.status = 0x00; // 16-bit mode + std::vector data = {0x6D, 0x03, 0x00, 0x05, 0x00}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x0003)); + + EXPECT_CALL(mock_memory, ReadWord(0x0003)).WillOnce(Return(0x0005)); + + cpu.ExecuteInstruction(0x6D); // ADC Absolute + EXPECT_EQ(cpu.A, 0x06); +} + +TEST_F(CPUTest, ADC_AbsoluteLong) { + cpu.A = 0x01; + cpu.SetAccumulatorSize(false); // 16-bit mode + cpu.SetCarryFlag(false); + std::vector data = {0x6F, 0x04, 0x00, 0x00, 0x05, 0x00}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x0004)); + EXPECT_CALL(mock_memory, ReadWord(0x0004)).WillOnce(Return(0x0005)); + + cpu.ExecuteInstruction(0x6F); // ADC Absolute Long + EXPECT_EQ(cpu.A, 0x06); +} + +TEST_F(CPUTest, ADC_DirectPageIndirectIndexedY) { + cpu.A = 0x03; + cpu.Y = 0x02; + cpu.D = 0x2000; + std::vector data = {0x71, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0x00, 0x30}); // [0x2010] = 0x3000 + mock_memory.InsertMemory(0x3002, {0x06}); // [0x3002] = 0x06 + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + EXPECT_CALL(mock_memory, ReadWord(0x2010)).WillOnce(Return(0x3000)); + EXPECT_CALL(mock_memory, ReadByte(0x3002)).WillOnce(Return(0x06)); + + cpu.ExecuteInstruction(0x71); // ADC Direct Page Indirect Indexed, Y + EXPECT_EQ(cpu.A, 0x09); // 0x03 + 0x06 = 0x09 +} + +TEST_F(CPUTest, ADC_DirectPageIndirect) { + cpu.A = 0x02; + cpu.D = 0x2000; // Setting Direct Page register to 0x2000 + std::vector data = {0x72, 0x10}; // ADC (dp) + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0x00, 0x30}); // [0x2010] = 0x3000 + mock_memory.InsertMemory(0x3000, {0x05}); // [0x3000] = 0x05 + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + EXPECT_CALL(mock_memory, ReadWord(0x2010)).WillOnce(Return(0x3000)); + EXPECT_CALL(mock_memory, ReadByte(0x3000)).WillOnce(Return(0x05)); + + cpu.ExecuteInstruction(0x72); // ADC (dp) + EXPECT_EQ(cpu.A, 0x07); // 0x02 + 0x05 = 0x07 +} + +TEST_F(CPUTest, ADC_StackRelativeIndirectIndexedY) { + cpu.A = 0x03; // A register + cpu.Y = 0x02; // Y register + cpu.DB = 0x10; // Setting Data Bank register to 0x20 + cpu.SetSP(0x01FF); // Setting Stack Pointer to 0x01FF + std::vector data = {0x73, 0x02}; // ADC sr, Y + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0x00, 0x30}); // [0x0201] = 0x3000 + mock_memory.InsertMemory(0x103002, {0x06}); // [0x3002] = 0x06 + + EXPECT_CALL(mock_memory, SP()).WillOnce(Return(0x01FF)); + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); + EXPECT_CALL(mock_memory, ReadWord(0x0201)).WillOnce(Return(0x3000)); + EXPECT_CALL(mock_memory, ReadByte(0x103002)).WillOnce(Return(0x06)); + + cpu.ExecuteInstruction(0x73); // ADC Stack Relative Indexed Y + EXPECT_EQ(cpu.A, 0x09); // 0x03 + 0x06 = 0x09 +} + +TEST_F(CPUTest, ADC_DirectPageIndexedX) { + cpu.A = 0x03; + cpu.X = 0x02; + cpu.D = 0x2000; + std::vector data = {0x75, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2012, {0x06}); // [0x2012] = 0x3000 + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + EXPECT_CALL(mock_memory, ReadByte(0x2012)).WillOnce(Return(0x06)); + + cpu.ExecuteInstruction(0x75); // ADC Direct Page Indexed, X + EXPECT_EQ(cpu.A, 0x09); // 0x03 + 0x06 = 0x09 +} + +TEST_F(CPUTest, ADC_DirectPageIndirectLongIndexedY) { + cpu.A = 0x03; + cpu.Y = 0x02; + cpu.D = 0x2000; + cpu.status = 0x00; + std::vector data = {0x77, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0x05, 0x00, 0x01}); + mock_memory.InsertMemory(0x010007, {0x06}); + + EXPECT_CALL(mock_memory, ReadWordLong(0x2010)).WillOnce(Return(0x010005)); + EXPECT_CALL(mock_memory, ReadWord(0x010007)).WillOnce(Return(0x06)); + + cpu.ExecuteInstruction(0x77); // ADC DP Indirect Long Indexed, Y + EXPECT_EQ(cpu.A, 0x09); +} + +TEST_F(CPUTest, ADC_AbsoluteIndexedY) { + cpu.A = 0x03; + cpu.Y = 0x02; // Y register + cpu.DB = 0x20; // Setting Data Bank register to 0x20 + std::vector data = {0x79, 0x03, 0x00, 0x00, 0x05, 0x00}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x0003)); + EXPECT_CALL(mock_memory, ReadWord(0x200005)).WillOnce(Return(0x0005)); + + mock_memory.InsertMemory(0x200005, {0x05}); + + cpu.ExecuteInstruction(0x79); // ADC Absolute Indexed Y + EXPECT_EQ(cpu.A, 0x08); +} + +TEST_F(CPUTest, ADC_AbsoluteIndexedX) { + cpu.A = 0x03; + cpu.X = 0x02; // X register + cpu.DB = 0x20; // Setting Data Bank register to 0x20 + cpu.SetCarryFlag(false); + cpu.SetAccumulatorSize(false); // 16-bit mode + std::vector data = {0x7D, 0x03, 0x00}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x0003)); + EXPECT_CALL(mock_memory, ReadWord(0x200005)).WillOnce(Return(0x0005)); + + mock_memory.InsertMemory(0x200005, {0x05}); // Inserting memory at 0x2005 + + cpu.ExecuteInstruction(0x7D); // ADC Absolute Indexed X + EXPECT_EQ(cpu.A, 0x08); +} + +TEST_F(CPUTest, ADC_AbsoluteLongIndexedX) { + cpu.A = 0x03; + cpu.X = 0x02; // X register + cpu.SetCarryFlag(false); + cpu.SetAccumulatorSize(false); // 16-bit mode + std::vector data = {0x7F, 0x00, 0x00, 0x01}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x010000, {0x03, 0x05}); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x010000)); + EXPECT_CALL(mock_memory, ReadWord(0x010002)).WillOnce(Return(0x0005)); + + cpu.ExecuteInstruction(0x7F); // ADC Absolute Long Indexed X + EXPECT_EQ(cpu.A, 0x08); +} + +// ============================================================================ +// AND - Logical AND + +TEST_F(CPUTest, AND_DirectPageIndexedIndirectX) { + cpu.A = 0b11110000; // A register + cpu.D = 0x2000; // Setting Direct Page register to 0x2000 + cpu.X = 0x02; // X register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x21, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2012, {0x00, 0x30}); // [0x2012] = 0x3000 + mock_memory.InsertMemory(0x3000, {0b10101010}); // [0x3000] = 0b10101010 + + // Get the operand + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + + // Get the value at the operand + EXPECT_CALL(mock_memory, ReadWord(0x2012)).WillOnce(Return(0x3000)); + + // Get the value at the operand + EXPECT_CALL(mock_memory, ReadByte(0x3000)).WillOnce(Return(0b10101010)); + + cpu.ExecuteInstruction(0x21); // AND Direct Page Indexed Indirect X + + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_StackRelative) { + cpu.A = 0b11110000; // A register + cpu.status = 0xFF; // 8-bit mode + cpu.SetSP(0x01FF); // Setting Stack Pointer to 0x01FF + std::vector data = {0x23, 0x02}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0b10101010}); // [0x0201] = 0b10101010 + + // Get the operand + EXPECT_CALL(mock_memory, SP()).WillOnce(Return(0x01FF)); + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); + + // Get the value at the operand + EXPECT_CALL(mock_memory, ReadByte(0x0201)).WillOnce(Return(0b10101010)); + + cpu.ExecuteInstruction(0x23); // AND Stack Relative + + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_DirectPage) { + cpu.A = 0b11110000; // A register + cpu.D = 0x2000; // Setting Direct Page register to 0x2000 + std::vector data = {0x25, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0b10101010}); // [0x2010] = 0b10101010 + + // Get the operand + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + + // Get the value at the operand + EXPECT_CALL(mock_memory, ReadByte(0x2010)).WillOnce(Return(0b10101010)); + + cpu.ExecuteInstruction(0x25); // AND Direct Page + + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_DirectPageIndirectLong) { + cpu.A = 0b11110000; // A register + cpu.D = 0x2000; // Setting Direct Page register to 0x2000 + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x27, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0x05, 0x00, 0x30}); + mock_memory.InsertMemory(0x300005, {0b10101010}); + + // Get the operand + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + + // Get the value at the operand + EXPECT_CALL(mock_memory, ReadWordLong(0x2010)).WillOnce(Return(0x300005)); + + // Get the value at the operand + EXPECT_CALL(mock_memory, ReadByte(0x300005)).WillOnce(Return(0b10101010)); + + cpu.ExecuteInstruction(0x27); // AND Direct Page Indirect Long + + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_Immediate) { + cpu.A = 0b11110000; // A register + std::vector data = {0x29, 0b10101010}; // AND #0b10101010 + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x29); // AND Immediate + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_Absolute_16BitMode) { + cpu.A = 0b11111111; // A register + cpu.E = 0; // 16-bit mode + cpu.status = 0x00; // Clear status flags + std::vector data = {0x2D, 0x03, 0x00, 0b10101010, 0x01, 0x02}; + mock_memory.SetMemoryContents(data); + + // Get the absolute address + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x0003)); + + // Get the value at the absolute address + EXPECT_CALL(mock_memory, ReadWord(0x0003)).WillOnce(Return(0b10101010)); + + cpu.ExecuteInstruction(0x2D); // AND Absolute + + EXPECT_THAT(cpu.PC, testing::Eq(0x03)); + EXPECT_EQ(cpu.A, 0b10101010); // A register should now be 0b10101010 +} + +TEST_F(CPUTest, AND_AbsoluteLong) { + cpu.A = 0x01; + cpu.status = 0x00; // 16-bit mode + std::vector data = {0x2F, 0x04, 0x00, 0x00, 0x05, 0x00}; + + mock_memory.SetMemoryContents(data); + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x000004)); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0004)).WillOnce(Return(0x000005)); + + cpu.ExecuteInstruction(0x2F); // ADC Absolute Long + EXPECT_EQ(cpu.A, 0x01); +} + +TEST_F(CPUTest, AND_DirectPageIndirectIndexedY) { + cpu.A = 0b11110000; // A register + cpu.D = 0x2000; // Setting Direct Page register to 0x2000 + cpu.Y = 0x02; // Y register + std::vector data = {0x31, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0x00, 0x30}); // [0x2010] = 0x3000 + mock_memory.InsertMemory(0x3002, {0b10101010}); // [0x3002] = 0b10101010 + + // Get the value at the operand + EXPECT_CALL(mock_memory, ReadWord(0x2010)).WillOnce(Return(0x3000)); + + cpu.ExecuteInstruction(0x31); // AND Direct Page Indirect Indexed Y + + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_DirectPageIndirect) { + cpu.A = 0b11110000; // A register + cpu.D = 0x2000; // Setting Direct Page register to 0x2000 + std::vector data = {0x32, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0x00, 0x30}); // [0x2010] = 0x3000 + mock_memory.InsertMemory(0x3000, {0b10101010}); // [0x3000] = 0b10101010 + + // Get the value at the operand + EXPECT_CALL(mock_memory, ReadWord(0x2010)).WillOnce(Return(0x3000)); + + cpu.ExecuteInstruction(0x32); // AND Direct Page Indirect + + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_StackRelativeIndirectIndexedY) { + cpu.A = 0b11110000; // A register + cpu.Y = 0x02; // Y register + cpu.DB = 0x10; // Setting Data Bank register to 0x20 + cpu.SetSP(0x01FF); // Setting Stack Pointer to 0x01FF + std::vector data = {0x33, 0x02}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0x00, 0x30}); // [0x0201] = 0x3000 + mock_memory.InsertMemory(0x103002, {0b10101010}); // [0x3002] = 0b10101010 + + EXPECT_CALL(mock_memory, SP()).WillOnce(Return(0x01FF)); + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); + EXPECT_CALL(mock_memory, ReadWord(0x0201)).WillOnce(Return(0x3000)); + EXPECT_CALL(mock_memory, ReadByte(0x103002)).WillOnce(Return(0b10101010)); + cpu.ExecuteInstruction(0x33); // AND Stack Relative Indirect Indexed Y + + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_DirectPageIndexedX) { + cpu.A = 0b11110000; // A register + cpu.D = 0x2000; // Setting Direct Page register to 0x2000 + cpu.X = 0x02; // X register + std::vector data = {0x35, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2012, {0b10101010}); // [0x2012] = 0b10101010 + + // Get the value at the operand + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + EXPECT_CALL(mock_memory, ReadByte(0x2012)).WillOnce(Return(0b10101010)); + + cpu.ExecuteInstruction(0x35); // AND Direct Page Indexed X + + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_DirectPageIndirectLongIndexedY) { + cpu.A = 0b11110000; // A register + cpu.D = 0x2000; // Setting Direct Page register to 0x2000 + cpu.Y = 0x02; // Y register + std::vector data = {0x37, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0x05, 0x00, 0x30}); + mock_memory.InsertMemory(0x300005, {0b10101010}); + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + EXPECT_CALL(mock_memory, ReadWordLong(0x2010)).WillOnce(Return(0x300005)); + EXPECT_CALL(mock_memory, ReadByte(0x300007)).WillOnce(Return(0b10101010)); + + cpu.ExecuteInstruction(0x37); // AND Direct Page Indirect Long Indexed Y + + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_AbsoluteIndexedY) { + cpu.A = 0b11110000; // A register + cpu.Y = 0x02; // Y register + std::vector data = {0x39, 0x03, 0x00, + 0b00000000, 0b10101010, 0b01010101}; + mock_memory.SetMemoryContents(data); + + // Get the absolute address + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x0003)); + + // Add the offset from the Y register to the absolute address + uint16_t address = 0x0003 + cpu.Y; + + // Get the value at the absolute address + Y + EXPECT_CALL(mock_memory, ReadByte(address)).WillOnce(Return(0b10101010)); + + cpu.ExecuteInstruction(0x39); // AND Absolute, Y + + EXPECT_THAT(cpu.PC, testing::Eq(0x03)); + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_AbsoluteIndexedX) { + cpu.A = 0b11110000; // A register + cpu.X = 0x02; // X register + std::vector data = {0x3D, 0x03, 0x00, + 0b00000000, 0b10101010, 0b01010101}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x200005, {0b10101010}); + + // Get the absolute address + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x0003)); + + // Add the offset from the X register to the absolute address + uint16_t address = 0x0003 + static_cast(cpu.X & 0xFF); + + // Get the value at the absolute address + X + EXPECT_CALL(mock_memory, ReadByte(address)).WillOnce(Return(0b10101010)); + + cpu.ExecuteInstruction(0x3D); // AND Absolute, X + + // EXPECT_THAT(cpu.PC, testing::Eq(0x03)); + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +TEST_F(CPUTest, AND_AbsoluteLongIndexedX) { + cpu.A = 0b11110000; // A register + cpu.X = 0x02; // X register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x3F, 0x03, 0x00, 0x00, + 0b00000000, 0b10101010, 0b01010101}; + mock_memory.SetMemoryContents(data); + + // Get the absolute address + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x0003)); + + // Add the offset from the X register to the absolute address + uint16_t address = 0x0003 + static_cast(cpu.X & 0xFF); + + // Get the value at the absolute address + X + EXPECT_CALL(mock_memory, ReadByte(address)).WillOnce(Return(0b10101010)); + + cpu.ExecuteInstruction(0x3F); // AND Absolute Long, X + + EXPECT_THAT(cpu.PC, testing::Eq(0x04)); + EXPECT_EQ(cpu.A, 0b10100000); // A register should now be 0b10100000 +} + +// ============================================================================ +// ASL - Arithmetic Shift Left + +TEST_F(CPUTest, ASL_DirectPage) { + cpu.D = 0x1000; // Setting Direct Page register to 0x1000 + cpu.PC = 0x1000; + std::vector data = {0x06, 0x10}; // ASL dp + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1010, {0x40}); // [0x1010] = 0x40 + + cpu.ExecuteInstruction(0x06); // ASL Direct Page + EXPECT_TRUE(cpu.GetCarryFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); + EXPECT_TRUE(cpu.GetNegativeFlag()); +} + +TEST_F(CPUTest, ASL_Accumulator) { + cpu.status = 0xFF; // 8-bit mode + cpu.A = 0x40; + std::vector data = {0x0A}; // ASL A + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x0A); // ASL Accumulator + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetCarryFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); + EXPECT_TRUE(cpu.GetNegativeFlag()); +} + +TEST_F(CPUTest, ASL_Absolute) { + std::vector data = {0x0E, 0x10, 0x20}; // ASL abs + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0x40}); // [0x2010] = 0x40 + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x2010)); + EXPECT_CALL(mock_memory, ReadByte(0x2010)).WillOnce(Return(0x40)); + + cpu.ExecuteInstruction(0x0E); // ASL Absolute + EXPECT_TRUE(cpu.GetCarryFlag()); + EXPECT_TRUE(cpu.GetZeroFlag()); + EXPECT_FALSE(cpu.GetNegativeFlag()); +} + +TEST_F(CPUTest, ASL_DirectPageIndexedX) { + cpu.D = 0x1000; // Setting Direct Page register to 0x1000 + cpu.X = 0x02; // Setting X register to 0x02 + std::vector data = {0x16, 0x10}; // ASL dp,X + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1012, {0x40}); // [0x1012] = 0x40 + + cpu.ExecuteInstruction(0x16); // ASL DP Indexed, X + EXPECT_TRUE(cpu.GetCarryFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); + EXPECT_TRUE(cpu.GetNegativeFlag()); +} + +TEST_F(CPUTest, ASL_AbsoluteIndexedX) { + cpu.X = 0x02; // Setting X register to 0x02 + std::vector data = {0x1E, 0x10, 0x20}; // ASL abs,X + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2012, {0x40}); // [0x2012] = 0x40 + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x2010)); + EXPECT_CALL(mock_memory, ReadByte(0x2012)).WillOnce(Return(0x40)); + + cpu.ExecuteInstruction(0x1E); // ASL Absolute, X + EXPECT_TRUE(cpu.GetCarryFlag()); + EXPECT_TRUE(cpu.GetZeroFlag()); + EXPECT_FALSE(cpu.GetNegativeFlag()); +} + +// ============================================================================ +// BCC - Branch if Carry Clear + +TEST_F(CPUTest, BCC_WhenCarryFlagClear) { + cpu.SetCarryFlag(false); + std::vector data = {0x90, 0x05, 0x01}; // Operand at address 0x1001 + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadByte(_)).WillOnce(Return(5)); + + cpu.ExecuteInstruction(0x90); // BCC + EXPECT_EQ(cpu.PC, 0x05); +} + +TEST_F(CPUTest, BCC_WhenCarryFlagSet) { + cpu.SetCarryFlag(true); + std::vector data = {0x90, 0x02, 0x01}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadByte(_)).WillOnce(Return(2)); + + cpu.ExecuteInstruction(0x90); // BCC + + EXPECT_EQ(cpu.PC, 2); +} + +// ============================================================================ +// BCS - Branch if Carry Set + +TEST_F(CPUTest, BCS_WhenCarryFlagSet) { + cpu.SetCarryFlag(true); + std::vector data = {0xB0, 0x07, 0x02}; // Operand at address 0x1001 + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadByte(_)).WillOnce(Return(0x07)); + + cpu.ExecuteInstruction(0xB0); // BCS + EXPECT_EQ(cpu.PC, 0x07); +} + +TEST_F(CPUTest, BCS_WhenCarryFlagClear) { + cpu.SetCarryFlag(false); + std::vector data = {0x10, 0x02, 0x01}; + mock_memory.SetMemoryContents(data); + EXPECT_CALL(mock_memory, ReadByte(_)).WillOnce(Return(2)); + cpu.ExecuteInstruction(0xB0); // BCS + EXPECT_EQ(cpu.PC, 2); +} + +// ============================================================================ +// BEQ - Branch if Equal + +TEST_F(CPUTest, BEQ_Immediate_ZeroFlagSet) { + cpu.PB = 0x00; + cpu.SetZeroFlag(true); + std::vector data = {0xF0, 0x09}; // Operand at address 0x1001 + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xF0); // BEQ + + EXPECT_EQ(cpu.PC, 0x09); +} + +TEST_F(CPUTest, BEQ_Immediate_ZeroFlagClear) { + cpu.SetZeroFlag(false); + std::vector data = {0xF0, 0x03}; // Operand at address 0x1001 + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadByte(_)).WillOnce(Return(0x03)); + + cpu.ExecuteInstruction(0xF0); // BEQ + + EXPECT_EQ(cpu.PC, 0x02); +} + +TEST_F(CPUTest, BEQ_Immediate_ZeroFlagSet_OverflowFlagSet) { + cpu.SetZeroFlag(true); + cpu.SetOverflowFlag(true); + std::vector data = {0xF0, 0x03}; // Operand at address 0x1001 + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadByte(_)).WillOnce(Return(0x03)); + + cpu.ExecuteInstruction(0xF0); // BEQ + + EXPECT_EQ(cpu.PC, 0x03); +} + +TEST_F(CPUTest, BEQ_Immediate_ZeroFlagClear_OverflowFlagSet) { + cpu.SetZeroFlag(false); + cpu.SetOverflowFlag(true); + std::vector data = {0xF0, 0x03, 0x02}; // Operand at address 0x1001 + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadByte(_)).WillOnce(Return(0x03)); + + cpu.ExecuteInstruction(0xF0); // BEQ + + EXPECT_EQ(cpu.PC, 0x02); +} + +// ============================================================================ +// BIT - Bit Test + +TEST_F(CPUTest, BIT_DirectPage) { + cpu.A = 0x01; + cpu.D = 0x1000; // Setting Direct Page register to 0x1000 + cpu.status = 0xFF; + std::vector data = {0x24, 0x10}; // BIT + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1010, {0x81}); // [0x1010] = 0x81 + + // Read the operand + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + + // Read the value at the address of the operand + EXPECT_CALL(mock_memory, ReadByte(0x1010)).WillOnce(Return(0x81)); + + cpu.ExecuteInstruction(0x24); // BIT + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetOverflowFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, BIT_Absolute) { + cpu.A = 0x01; + cpu.status = 0xFF; + std::vector data = {0x00, 0x10}; // BIT + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0010, {0x81}); // [0x0010] = 0x81 + + // Read the operand + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + + // Read the value at the address of the operand + EXPECT_CALL(mock_memory, ReadByte(0x0010)).WillOnce(Return(0x81)); + + cpu.ExecuteInstruction(0x24); // BIT + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetOverflowFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, BIT_DirectPageIndexedX) { + cpu.A = 0x01; + cpu.X = 0x02; + cpu.D = 0x1000; // Setting Direct Page register to 0x1000 + cpu.status = 0xFF; + std::vector data = {0x34, 0x10}; // BIT + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1012, {0x81}); // [0x1010] = 0x81 + + // Read the operand + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + + // Read the value at the address of the operand + EXPECT_CALL(mock_memory, ReadByte(0x1012)).WillOnce(Return(0x81)); + + cpu.ExecuteInstruction(0x34); // BIT + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetOverflowFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, BIT_AbsoluteIndexedX) { + cpu.A = 0x01; + cpu.X = 0x02; + cpu.status = 0xFF; + std::vector data = {0x00, 0x10}; // BIT + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0012, {0x81}); // [0x0010] = 0x81 + + // Read the operand + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x10)); + + // Read the value at the address of the operand + EXPECT_CALL(mock_memory, ReadByte(0x0012)).WillOnce(Return(0x81)); + + cpu.ExecuteInstruction(0x3C); // BIT + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetOverflowFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, BIT_Immediate) { + cpu.A = 0x01; + cpu.status = 0xFF; + std::vector data = {0x24, 0x00, 0x10}; // BIT + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0010, {0x81}); // [0x0010] = 0x81 + + // Read the operand + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + + // Read the value at the address of the operand + EXPECT_CALL(mock_memory, ReadByte(0x0010)).WillOnce(Return(0x81)); + + cpu.ExecuteInstruction(0x24); // BIT + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetOverflowFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +// ============================================================================ +// BMI - Branch if Minus + +TEST_F(CPUTest, BMI_BranchTaken) { + cpu.SetNegativeFlag(true); + std::vector data = {0x30, 0x05}; // BMI + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x30); // BMI + EXPECT_EQ(cpu.PC, 0x0005); +} + +TEST_F(CPUTest, BMI_BranchNotTaken) { + cpu.SetNegativeFlag(false); + std::vector data = {0x30, 0x02}; // BMI + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x30); // BMI + EXPECT_EQ(cpu.PC, 0x0002); +} + +// ============================================================================ +// BNE - Branch if Not Equal + +TEST_F(CPUTest, BNE_BranchTaken) { + cpu.SetZeroFlag(false); + std::vector data = {0xD0, 0x05}; // BNE + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xD0); // BNE + EXPECT_EQ(cpu.PC, 0x0007); +} + +TEST_F(CPUTest, BNE_BranchNotTaken) { + cpu.SetZeroFlag(true); + std::vector data = {0xD0, 0x05}; // BNE + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xD0); // BNE + EXPECT_EQ(cpu.PC, 0x0002); +} + +// ============================================================================ +// BPL - Branch if Positive + +TEST_F(CPUTest, BPL_BranchTaken) { + cpu.SetNegativeFlag(false); + std::vector data = {0x10, 0x07}; // BPL + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x10); // BPL + EXPECT_EQ(cpu.PC, 0x0007); +} + +TEST_F(CPUTest, BPL_BranchNotTaken) { + cpu.SetNegativeFlag(true); + std::vector data = {0x10, 0x02}; // BPL + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x10); // BPL + EXPECT_EQ(cpu.PC, 0x0002); +} + +// ============================================================================ +// BRA - Branch Always + +TEST_F(CPUTest, BRA) { + std::vector data = {0x80, 0x02}; // BRA + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x80); // BRA + EXPECT_EQ(cpu.PC, 0x0004); +} + +// ============================================================================ + +TEST_F(CPUTest, BRK) { + std::vector data = {0x00}; // BRK + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0xFFFE, {0x10, 0x20}); // [0xFFFE] = 0x2010 + + EXPECT_CALL(mock_memory, ReadWord(0xFFFE)).WillOnce(Return(0x2010)); + + cpu.ExecuteInstruction(0x00); // BRK + EXPECT_EQ(cpu.PC, 0x2010); + EXPECT_TRUE(cpu.GetInterruptFlag()); +} + +// ============================================================================ +// BRL - Branch Long + +TEST_F(CPUTest, BRL) { + std::vector data = {0x82, 0x10, 0x20}; // BRL + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x2010)); + + cpu.ExecuteInstruction(0x82); // BRL + EXPECT_EQ(cpu.PC, 0x2010); +} + +// ============================================================================ +// BVC - Branch if Overflow Clear + +TEST_F(CPUTest, BVC_BranchTaken) { + cpu.SetOverflowFlag(false); + std::vector data = {0x50, 0x02}; // BVC + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x50); // BVC + EXPECT_EQ(cpu.PC, 0x0002); +} + +// ============================================================================ +// BVS - Branch if Overflow Set + +TEST_F(CPUTest, BVS_BranchTaken) { + cpu.SetOverflowFlag(true); + std::vector data = {0x70, 0x02}; // BVS + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x70); // BVS + EXPECT_EQ(cpu.PC, 0x0002); +} + +// ============================================================================ +// CLC - Clear Carry Flag + +TEST_F(CPUTest, CLC) { + cpu.SetCarryFlag(true); + cpu.PC = 0x0000; + std::vector data = {0x18}; // CLC + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x18); // CLC + EXPECT_FALSE(cpu.GetCarryFlag()); +} + +// ============================================================================ +// CLD - Clear Decimal Mode Flag + +TEST_F(CPUTest, CLD) { + cpu.SetDecimalFlag(true); + cpu.PC = 0x0000; + std::vector data = {0xD8}; // CLD + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xD8); // CLD + EXPECT_FALSE(cpu.GetDecimalFlag()); +} + +// ============================================================================ +// CLI - Clear Interrupt Disable Flag + +TEST_F(CPUTest, CLI) { + cpu.SetInterruptFlag(true); + cpu.PC = 0x0000; + std::vector data = {0x58}; // CLI + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x58); // CLI + EXPECT_FALSE(cpu.GetInterruptFlag()); +} + +// ============================================================================ +// CLV - Clear Overflow Flag + +TEST_F(CPUTest, CLV) { + cpu.SetOverflowFlag(true); + cpu.PC = 0x0000; + std::vector data = {0xB8}; // CLV + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xB8); // CLV + EXPECT_FALSE(cpu.GetOverflowFlag()); +} + +// ============================================================================ +// CMP - Compare Accumulator + +TEST_F(CPUTest, CMP_DirectPageIndexedIndirectX) { + cpu.status = 0x00; + cpu.SetAccumulatorSize(true); + cpu.A = 0x80; + cpu.X = 0x02; + cpu.D = 0x1000; + cpu.DB = 0x01; + std::vector data = {0xC1, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1012, {0x00, 0x30}); // [0x1012] = 0x3000 + mock_memory.InsertMemory(0x013000, {0x40}); // [0x3000] = 0x40 + + cpu.ExecuteInstruction(0xC1); + + EXPECT_TRUE(cpu.GetCarryFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); + EXPECT_TRUE(cpu.GetNegativeFlag()); +} + +TEST_F(CPUTest, CMP_StackRelative) { + cpu.A = 0x80; + cpu.SetSP(0x01FF); + std::vector data = {0xC3, 0x02}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0x40, 0x9F}); + + EXPECT_CALL(mock_memory, SP()).WillRepeatedly(Return(0x01FF)); + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); + EXPECT_CALL(mock_memory, ReadByte(0x0201)).WillOnce(Return(0x30)); + + // Execute the CMP Stack Relative instruction + cpu.ExecuteInstruction(0xC3); + + EXPECT_TRUE(cpu.GetCarryFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); + EXPECT_FALSE(cpu.GetNegativeFlag()); + + mock_memory.InsertMemory(0x0002, {0xC3, 0x03}); + + EXPECT_CALL(mock_memory, SP()).WillRepeatedly(Return(0x01FF)); + EXPECT_CALL(mock_memory, ReadByte(0x0003)).WillOnce(Return(0x03)); + EXPECT_CALL(mock_memory, ReadByte(0x0202)).WillOnce(Return(0x9F)); + + cpu.status = 0b00110000; + cpu.ExecuteInstruction(0xC3); + + EXPECT_TRUE(cpu.GetCarryFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); + EXPECT_TRUE(cpu.GetNegativeFlag()); +} + +TEST_F(CPUTest, CMP_DirectPage) { + // Set the accumulator to 8-bit mode + cpu.status = 0x00; + cpu.SetAccumulatorSize(true); + cpu.A = 0x80; // Set the accumulator to 0x80 + mock_memory.InsertMemory(0x0000, {0xC5}); + + // Execute the CMP Direct Page instruction + cpu.ExecuteInstruction(0xC5); + + // Check the status flags + EXPECT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set + EXPECT_FALSE(cpu.GetZeroFlag()); // Zero flag should not be set + EXPECT_TRUE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +TEST_F(CPUTest, CMP_DirectPageIndirectLong) { + // Set the accumulator to 8-bit mode + cpu.status = 0x00; + cpu.SetAccumulatorSize(true); + cpu.A = 0x80; // Set the accumulator to 0x80 + + // Set up the instruction and operand + mock_memory.InsertMemory(0x0000, {0xC7, 0x02}); + + cpu.D = 0x1000; // Set the Direct Page register to 0x1000 + + mock_memory.InsertMemory(0x1002, {0x00, 0x00, 0x01}); + mock_memory.InsertMemory(0x010000, {0x40}); // [0x010000] = 0x40 + + // Execute the CMP Direct Page Indirect Long instruction + cpu.ExecuteInstruction(0xC7); + + // Check the status flags + EXPECT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set + EXPECT_FALSE(cpu.GetZeroFlag()); // Zero flag should not be set + EXPECT_FALSE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +TEST_F(CPUTest, CMP_Immediate_8Bit) { + // Set the accumulator to 8-bit mode + cpu.status = 0x00; + cpu.SetAccumulatorSize(true); + cpu.A = 0x80; // Set the accumulator to 0x80 + mock_memory.InsertMemory(0x0000, {0x40}); + + // Set up the memory to return 0x40 when the Immediate addressing mode is used + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(::testing::Return(0x40)); + + // Execute the CMP Immediate instruction + cpu.ExecuteInstruction(0xC9); + + // Check the status flags + EXPECT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set + EXPECT_FALSE(cpu.GetZeroFlag()); // Zero flag should not be set + EXPECT_FALSE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +TEST_F(CPUTest, CMP_Absolute_16Bit) { + // Set the accumulator to 16-bit mode + cpu.SetAccumulatorSize(false); + cpu.A = 0x8000; // Set the accumulator to 0x8000 + mock_memory.InsertMemory(0x0000, {0x34, 0x12}); + + // Execute the CMP Absolute instruction + cpu.ExecuteInstruction(0xCD); + + // Check the status flags + EXPECT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set + EXPECT_FALSE(cpu.GetZeroFlag()); // Zero flag should not be set + EXPECT_TRUE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +TEST_F(CPUTest, CMP_AbsoluteLong) { + cpu.A = 0x01; + cpu.status = 0b00000001; // 16-bit mode + std::vector data = {0xCF, 0x04, 0x00, 0x00, 0x05, 0x00}; + + mock_memory.SetMemoryContents(data); + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x000004)); + + EXPECT_CALL(mock_memory, ReadWord(0x0004)).WillOnce(Return(0x000005)); + + cpu.ExecuteInstruction(0xCF); // ADC Absolute Long + + EXPECT_FALSE(cpu.GetCarryFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); + EXPECT_TRUE(cpu.GetNegativeFlag()); +} + +TEST_F(CPUTest, CMP_DirectPageIndirect) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.D = 0x0200; + std::vector data = {0xD1, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00023C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1000)).WillOnce(Return(0x7F)); + + // Execute the CMP Direct Page Indirect instruction + cpu.ExecuteInstruction(0xD1); + + // Check the status flags + EXPECT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set + EXPECT_FALSE(cpu.GetZeroFlag()); // Zero flag should not be set + EXPECT_TRUE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +TEST_F(CPUTest, CMP_StackRelativeIndirectIndexedY) { + cpu.A = 0x03; // A register + cpu.Y = 0x02; // Y register + cpu.DB = 0x10; // Setting Data Bank register to 0x20 + cpu.SetSP(0x01FF); // Setting Stack Pointer to 0x01FF + std::vector data = {0xD3, 0x02}; // ADC sr, Y + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0x00, 0x30}); // [0x0201] = 0x3000 + mock_memory.InsertMemory(0x103002, {0x06}); // [0x3002] = 0x06 + + EXPECT_CALL(mock_memory, SP()).WillOnce(Return(0x01FF)); + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); + EXPECT_CALL(mock_memory, ReadWord(0x0201)).WillOnce(Return(0x3000)); + EXPECT_CALL(mock_memory, ReadByte(0x103002)).WillOnce(Return(0x06)); + + // Execute the CMP Stack Relative Indirect Indexed Y instruction + cpu.ExecuteInstruction(0xD3); + + // Check the status flags + EXPECT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set + EXPECT_FALSE(cpu.GetZeroFlag()); // Zero flag should not be set + EXPECT_TRUE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +TEST_F(CPUTest, CMP_DirectPageIndexedX) { + // Set the accumulator to 8-bit mode + cpu.status = 0x00; + cpu.SetAccumulatorSize(true); + cpu.A = 0x80; // Set the accumulator to 0x80 + cpu.X = 0x02; // Set the X register to 0x02 + mock_memory.InsertMemory(0x0000, {0xD5}); + + // Set up the memory to return 0x40 when the Direct Page Indexed X addressing + // mode is used + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(::testing::Return(0x40)); + EXPECT_CALL(mock_memory, ReadByte(0x0042)).WillOnce(::testing::Return(0x40)); + + // Execute the CMP Direct Page Indexed X instruction + cpu.ExecuteInstruction(0xD5); + + // Check the status flags + EXPECT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set + EXPECT_FALSE(cpu.GetZeroFlag()); // Zero flag should not be set + EXPECT_FALSE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +TEST_F(CPUTest, CMP_DirectPageIndirectLongIndexedY) { + cpu.A = 0b11110000; // A register + cpu.D = 0x2000; // Setting Direct Page register to 0x2000 + cpu.Y = 0x02; // Y register + std::vector data = {0xD7, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2010, {0x05, 0x00, 0x30}); + mock_memory.InsertMemory(0x300005, {0b10101010}); + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x10)); + EXPECT_CALL(mock_memory, ReadWordLong(0x2010)).WillOnce(Return(0x300005)); + EXPECT_CALL(mock_memory, ReadByte(0x300007)).WillOnce(Return(0b10101010)); + + // Execute the CMP Direct Page Indirect Long Indexed Y instruction + cpu.ExecuteInstruction(0xD7); + + // Check the status flags + EXPECT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set + EXPECT_FALSE(cpu.GetZeroFlag()); // Zero flag should not be set + EXPECT_FALSE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +TEST_F(CPUTest, CMP_AbsoluteIndexedY) { + // Set the accumulator to 16-bit mode + cpu.SetAccumulatorSize(false); + cpu.A = 0x8000; // Set the accumulator to 0x8000 + cpu.Y = 0x02; // Set the Y register to 0x02 + mock_memory.InsertMemory(0x0000, {0xD9}); + + // Execute the CMP Absolute Indexed Y instruction + cpu.ExecuteInstruction(0xD9); + + // Check the status flags + EXPECT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set + EXPECT_FALSE(cpu.GetZeroFlag()); // Zero flag should not be set + EXPECT_TRUE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +TEST_F(CPUTest, CMP_AbsoluteIndexedX) { + // Set the accumulator to 16-bit mode + cpu.SetAccumulatorSize(false); + cpu.A = 0x8000; // Set the accumulator to 0x8000 + cpu.X = 0x02; // Set the X register to 0x02 + mock_memory.InsertMemory(0x0000, {0xDD}); + + // Execute the CMP Absolute Indexed X instruction + cpu.ExecuteInstruction(0xDD); + + // Check the status flags + EXPECT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set + EXPECT_FALSE(cpu.GetZeroFlag()); // Zero flag should not be set + EXPECT_TRUE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +TEST_F(CPUTest, CMP_AbsoluteLongIndexedX) { + // Set the accumulator to 16-bit mode + cpu.SetAccumulatorSize(false); + cpu.A = 0x8000; // Set the accumulator to 0x8000 + cpu.X = 0x02; // Set the X register to 0x02 + mock_memory.InsertMemory(0x0000, {0xDF}); + + // Execute the CMP Absolute Long Indexed X instruction + cpu.ExecuteInstruction(0xDF); + + // Check the status flags + EXPECT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set + EXPECT_FALSE(cpu.GetZeroFlag()); // Zero flag should not be set + EXPECT_TRUE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +// ============================================================================ + +TEST_F(CPUTest, COP) { + mock_memory.SetSP(0x01FF); + std::vector data = {0x02}; // COP + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0xFFF4, {0x10, 0x20}); // [0xFFFE] = 0x2010 + + ON_CALL(mock_memory, SetSP(_)).WillByDefault(::testing::Return()); + EXPECT_CALL(mock_memory, PushWord(0x0002)); + EXPECT_CALL(mock_memory, PushByte(0x30)); + EXPECT_CALL(mock_memory, ReadWord(0xFFF4)).WillOnce(Return(0x2010)); + + cpu.ExecuteInstruction(0x02); // COP + EXPECT_TRUE(cpu.GetInterruptFlag()); + EXPECT_FALSE(cpu.GetDecimalFlag()); +} + +// ============================================================================ +// Test for CPX instruction + +TEST_F(CPUTest, CPX_Immediate_ZeroFlagSet) { + cpu.SetIndexSize(false); // Set X register to 16-bit mode + cpu.SetAccumulatorSize(false); + cpu.X = 0x1234; + std::vector data = {0xE0, 0x34, 0x12}; // CPX #0x1234 + mock_memory.SetMemoryContents(data); + cpu.ExecuteInstruction(0xE0); // Immediate CPX + ASSERT_TRUE(cpu.GetZeroFlag()); // Zero flag should be set +} + +TEST_F(CPUTest, CPX_Immediate_NegativeFlagSet) { + cpu.SetIndexSize(false); // Set X register to 16-bit mode + cpu.PC = 0; + cpu.X = 0x9000; + std::vector data = {0xE0, 0xFF, 0xFF}; // CPX #0x8001 + mock_memory.SetMemoryContents(data); + cpu.ExecuteInstruction(0xE0); // Immediate CPX + ASSERT_TRUE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +// Test for CPX instruction +TEST_F(CPUTest, CPX_DirectPage) { + cpu.SetIndexSize(false); // Set Y register to 16-bit mode + cpu.PC = 0; + cpu.X = 0x1234; + std::vector data = {0xE4, 0x34, 0x12}; // CPY #0x1234 + mock_memory.SetMemoryContents(data); + cpu.ExecuteInstruction(0xE4); // Immediate CPY + ASSERT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set +} + +TEST_F(CPUTest, CPX_Absolute) { + cpu.SetIndexSize(false); // Set Y register to 16-bit mode + cpu.PC = 0; + cpu.X = 0x1234; + std::vector data = {0xEC, 0x34, 0x12}; // CPY #0x1234 + mock_memory.SetMemoryContents(data); + cpu.ExecuteInstruction(0xEC); // Immediate CPY + ASSERT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set +} + +TEST_F(CPUTest, CPY_Immediate_ZeroFlagSet) { + cpu.SetIndexSize(false); // Set Y register to 16-bit mode + cpu.SetAccumulatorSize(false); + cpu.Y = 0x5678; + std::vector data = {0xC0, 0x78, 0x56}; // CPY #0x5678 + mock_memory.SetMemoryContents(data); + cpu.ExecuteInstruction(0xC0); // Immediate CPY + ASSERT_TRUE(cpu.GetZeroFlag()); // Zero flag should be set +} + +TEST_F(CPUTest, CPY_Immediate_NegativeFlagSet) { + cpu.SetIndexSize(false); // Set Y register to 16-bit mode + cpu.PC = 0; + cpu.Y = 0x9000; + std::vector data = {0xC0, 0x01, 0x80}; // CPY #0x8001 + mock_memory.SetMemoryContents(data); + cpu.ExecuteInstruction(0xC0); // Immediate CPY + ASSERT_TRUE(cpu.GetNegativeFlag()); // Negative flag should be set +} + +// Test for CPY instruction +TEST_F(CPUTest, CPY_DirectPage) { + cpu.SetIndexSize(false); // Set Y register to 16-bit mode + cpu.PC = 0; + cpu.Y = 0x1234; + std::vector data = {0xC4, 0x34, 0x12}; // CPY #0x1234 + mock_memory.SetMemoryContents(data); + cpu.ExecuteInstruction(0xC4); // Immediate CPY + ASSERT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set +} + +TEST_F(CPUTest, CPY_Absolute) { + cpu.SetIndexSize(false); // Set Y register to 16-bit mode + cpu.PC = 0; + cpu.Y = 0x1234; + std::vector data = {0xCC, 0x34, 0x12}; // CPY #0x1234 + mock_memory.SetMemoryContents(data); + cpu.ExecuteInstruction(0xCC); // Immediate CPY + ASSERT_TRUE(cpu.GetCarryFlag()); // Carry flag should be set +} + +// ============================================================================ +// DEC - Decrement Memory + +TEST_F(CPUTest, DEC_Accumulator) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x02; // Set A register to 2 + cpu.ExecuteInstruction(0x3A); // Execute DEC instruction + EXPECT_EQ(0x01, cpu.A); // Expected value of A register after decrementing + + cpu.A = 0x00; // Set A register to 0 + cpu.ExecuteInstruction(0x3A); // Execute DEC instruction + EXPECT_EQ(0xFF, cpu.A); // Expected value of A register after decrementing + + cpu.A = 0x80; // Set A register to 128 + cpu.ExecuteInstruction(0x3A); // Execute DEC instruction + EXPECT_EQ(0x7F, cpu.A); // Expected value of A register after decrementing +} + +TEST_F(CPUTest, DEC_DirectPage) { + cpu.status = 0xFF; // Set A register to 8-bit mode + cpu.D = 0x1000; // Set Direct Page register to 0x1000 + std::vector data = {0xC6, 0x7F}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x107F, {0x02}); // [0x107F] = 0x02 + + cpu.ExecuteInstruction(0xC6); // Execute DEC instruction + EXPECT_EQ(0x01, mock_memory.ReadByte(0x107F)); // Expected value of memory + // location after decrementing +} + +TEST_F(CPUTest, DEC_Absolute) { + cpu.status = 0xFF; // Set A register to 8-bit mode + std::vector data = {0xCE, 0x00, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1000, {0x02}); // [0x1000] = 0x02 + + cpu.ExecuteInstruction(0xCE); // Execute DEC instruction + EXPECT_EQ(0x01, mock_memory.ReadByte(0x1000)); // Expected value of memory + // location after decrementing +} + +TEST_F(CPUTest, DEC_DirectPageIndexedX) { + cpu.status = 0xFF; // Set A register to 8-bit mode + cpu.D = 0x1000; // Set Direct Page register to 0x1000 + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0xD6, 0x7F}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1081, {0x02}); // [0x1081] = 0x02 + + cpu.ExecuteInstruction(0xD6); // Execute DEC instruction + EXPECT_EQ(0x01, mock_memory.ReadByte(0x1081)); // Expected value of memory + // location after decrementing +} + +TEST_F(CPUTest, DEC_AbsoluteIndexedX) { + cpu.status = 0xFF; // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0xDE, 0x00, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1002, {0x02}); // [0x1002] = 0x02 + + cpu.ExecuteInstruction(0xDE); // Execute DEC instruction + EXPECT_EQ(0x01, mock_memory.ReadByte(0x1002)); // Expected value of memory + // location after decrementing +} + +// ============================================================================ +// Test for DEX instruction + +TEST_F(CPUTest, DEX) { + cpu.SetIndexSize(true); // Set X register to 8-bit mode + cpu.X = 0x02; // Set X register to 2 + cpu.ExecuteInstruction(0xCA); // Execute DEX instruction + EXPECT_EQ(0x01, cpu.X); // Expected value of X register after decrementing + + cpu.X = 0x00; // Set X register to 0 + cpu.ExecuteInstruction(0xCA); // Execute DEX instruction + EXPECT_EQ(0xFF, cpu.X); // Expected value of X register after decrementing + + cpu.X = 0x80; // Set X register to 128 + cpu.ExecuteInstruction(0xCA); // Execute DEX instruction + EXPECT_EQ(0x7F, cpu.X); // Expected value of X register after decrementing +} + +// ============================================================================ +// Test for DEY instruction + +TEST_F(CPUTest, DEY) { + cpu.SetIndexSize(true); // Set Y register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 2 + cpu.ExecuteInstruction(0x88); // Execute DEY instruction + EXPECT_EQ(0x01, cpu.Y); // Expected value of Y register after decrementing + + cpu.Y = 0x00; // Set Y register to 0 + cpu.ExecuteInstruction(0x88); // Execute DEY instruction + EXPECT_EQ(0xFF, cpu.Y); // Expected value of Y register after decrementing + + cpu.Y = 0x80; // Set Y register to 128 + cpu.ExecuteInstruction(0x88); // Execute DEY instruction + EXPECT_EQ(0x7F, cpu.Y); // Expected value of Y register after decrementing +} + +// ============================================================================ +// EOR + +TEST_F(CPUTest, EOR_DirectPageIndexedIndirectX) { + cpu.A = 0b10101010; // A register + cpu.X = 0x02; // X register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x41, 0x7E}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0080, {0x00, 0x10}); // [0x0080] = 0x1000 + mock_memory.InsertMemory(0x1000, {0b01010101}); // [0x1000] = 0b01010101 + + cpu.ExecuteInstruction(0x41); // EOR DP Indexed Indirect, X + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_StackRelative) { + cpu.A = 0b10101010; // A register + cpu.status = 0xFF; // 8-bit mode + cpu.SetSP(0x01FF); // Set Stack Pointer to 0x01FF + std::vector data = {0x43, 0x02}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0b01010101}); // [0x0201] = 0b01010101 + + EXPECT_CALL(mock_memory, SP()).WillRepeatedly(Return(0x01FF)); + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); + EXPECT_CALL(mock_memory, ReadByte(0x0201)).WillOnce(Return(0b01010101)); + + cpu.ExecuteInstruction(0x43); // EOR Stack Relative + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_DirectPage) { + cpu.A = 0b10101010; // A register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x45, 0x7F}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x007F, {0b01010101}); // [0x007F] = 0b01010101 + + cpu.ExecuteInstruction(0x45); // EOR Direct Page + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_DirectPageIndirectLong) { + cpu.A = 0b10101010; // A register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x47, 0x7F}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x007F, {0x00, 0x10, 0x00}); // [0x007F] = 0x1000 + mock_memory.InsertMemory(0x1000, {0b01010101}); // [0x1000] = 0b01010101 + + cpu.ExecuteInstruction(0x47); // EOR Direct Page Indirect Long + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_Immediate_8bit) { + cpu.A = 0b10101010; // A register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x49, 0b01010101}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x49); // EOR Immediate + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_Absolute) { + cpu.A = 0b10101010; // A register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x4D, 0x00, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1000, {0b01010101}); // [0x1000] = 0b01010101 + + cpu.ExecuteInstruction(0x4D); // EOR Absolute + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_AbsoluteLong) { + cpu.A = 0b10101010; // A register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x4F, 0x00, 0x10, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1000, {0b01010101}); // [0x1000] = 0b01010101 + + cpu.ExecuteInstruction(0x4F); // EOR Absolute Long + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_DirectPageIndirectIndexedY) { + cpu.A = 0b10101010; // A register + cpu.Y = 0x02; // Y register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x51, 0x7E}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x007E, {0x00, 0x10}); // [0x007E] = 0x1000 + mock_memory.InsertMemory(0x1002, {0b01010101}); // [0x1002] = 0b01010101 + + cpu.ExecuteInstruction(0x51); // EOR DP Indirect Indexed, Y + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_DirectPageIndirect) { + cpu.A = 0b10101010; // A register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x52, 0x7E}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x007E, {0x00, 0x10}); // [0x007E] = 0x1000 + mock_memory.InsertMemory(0x1000, {0b01010101}); // [0x1000] = 0b01010101 + + cpu.ExecuteInstruction(0x52); // EOR DP Indirect + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_StackRelativeIndirectIndexedY) { + cpu.A = 0b10101010; // A register + cpu.Y = 0x02; // Y register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x53, 0x02}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0x00, 0x10}); // [0x0201] = 0x1000 + mock_memory.InsertMemory(0x1002, {0b01010101}); // [0x1002] = 0b01010101 + + EXPECT_CALL(mock_memory, SP()).WillRepeatedly(Return(0x01FF)); + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); + EXPECT_CALL(mock_memory, ReadWord(0x0201)).WillOnce(Return(0x1000)); + EXPECT_CALL(mock_memory, ReadByte(0x1002)).WillOnce(Return(0b01010101)); + + cpu.ExecuteInstruction(0x53); // EOR Stack Relative Indirect Indexed, Y + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_DirectPageIndexedX) { + cpu.A = 0b10101010; // A register + cpu.X = 0x02; // X register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x55, 0x7E}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0080, {0b01010101}); // [0x0080] = 0b01010101 + + cpu.ExecuteInstruction(0x55); // EOR DP Indexed, X + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_DirectPageIndirectLongIndexedY) { + cpu.A = 0b10101010; // A register + cpu.Y = 0x02; // Y register + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x51, 0x7E}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x007E, {0x00, 0x10, 0x00}); // [0x007E] = 0x1000 + mock_memory.InsertMemory(0x1002, {0b01010101}); // [0x1002] = 0b01010101 + + cpu.ExecuteInstruction(0x51); // EOR DP Indirect Long Indexed, Y + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_AbsoluteIndexedY) { + cpu.A = 0b10101010; // A register + cpu.Y = 0x02; // Y register + cpu.status = 0xFF; // 8-bit mode + // PC register + std::vector data = {0x59, 0x7C, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x007E, {0b01010101}); // [0x007E] = 0b01010101 + + cpu.ExecuteInstruction(0x59); // EOR Absolute Indexed, Y + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_AbsoluteIndexedX) { + cpu.A = 0b10101010; // A register + cpu.X = 0x02; // X register + cpu.status = 0xFF; // 8-bit mode + // PC register + std::vector data = {0x5D, 0x7C, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x007E, {0b01010101}); // [0x007E] = 0b01010101 + + cpu.ExecuteInstruction(0x5D); // EOR Absolute Indexed, X + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +TEST_F(CPUTest, EOR_AbsoluteLongIndexedX) { + cpu.A = 0b10101010; // A register + cpu.X = 0x02; // X register + cpu.status = 0xFF; // 8-bit mode + // PC register + std::vector data = {0x5F, 0x7C, 0x00, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x007E, {0b01010101}); // [0x007E] = 0b01010101 + + cpu.ExecuteInstruction(0x5F); // EOR Absolute Long Indexed, X + EXPECT_EQ(cpu.A, 0b11111111); // A register should now be 0b11111111 +} + +// ============================================================================ +// INC - Increment Memory + +TEST_F(CPUTest, INC_Accumulator) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x02; // Set A register to 2 + cpu.ExecuteInstruction(0x1A); // Execute INC instruction + EXPECT_EQ(0x03, cpu.A); // Expected value of A register after incrementing + + cpu.A = 0xFF; // Set A register to 0xFF + cpu.ExecuteInstruction(0x1A); // Execute INC instruction + EXPECT_EQ(0x00, cpu.A); // Expected value of A register after incrementing + + cpu.A = 0x7F; // Set A register to 127 + cpu.ExecuteInstruction(0x1A); // Execute INC instruction + EXPECT_EQ(0x80, cpu.A); // Expected value of A register after incrementing +} + +TEST_F(CPUTest, INC_DirectPage_8bit) { + cpu.SetAccumulatorSize(true); + cpu.D = 0x0200; // Setting Direct Page register to 0x0200 + std::vector data = {0xE6, 0x20}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0220, {0x40}); // [0x0220] = 0x40 + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x20)); + EXPECT_CALL(mock_memory, ReadByte(0x0220)).WillOnce(Return(0x40)); + + cpu.ExecuteInstruction(0xE6); // INC Direct Page + EXPECT_EQ(mock_memory[0x0220], 0x41); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, INC_Absolute_16bit) { + std::vector data = {0xEE, 0x00, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1000, {0x40}); // [0x1000] = 0x40 + + cpu.SetAccumulatorSize(false); + cpu.ExecuteInstruction(0xEE); // INC Absolute + EXPECT_EQ(mock_memory[0x1000], 0x41); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, INC_DirectPage_ZeroResult_8bit) { + cpu.D = 0x0200; // Setting Direct Page register to 0x0200 + std::vector data = {0xE6, 0x20}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0220, {0xFF}); // [0x0220] = 0xFF + + cpu.SetAccumulatorSize(true); + cpu.ExecuteInstruction(0xE6); // INC Direct Page + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_TRUE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, INC_Absolute_ZeroResult_16bit) { + std::vector data = {0xEE, 0x00, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1000, {0xFF}); // [0x1000] = 0xFF + + cpu.SetAccumulatorSize(false); + cpu.ExecuteInstruction(0xEE); // INC Absolute + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, INC_DirectPage_8bit_Overflow) { + std::vector data = {0xE6, 0x80}; + mock_memory.SetMemoryContents(data); + + cpu.SetAccumulatorSize(true); + cpu.ExecuteInstruction(0xE6); // INC Direct Page + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, INC_DirectPageIndexedX_8bit) { + cpu.X = 0x01; + cpu.D = 0x0200; // Setting Direct Page register to 0x0200 + std::vector data = {0xF6, 0x20}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0221, {0x40}); // [0x0221] = 0x40 + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x20)); + EXPECT_CALL(mock_memory, ReadByte(0x0221)).WillOnce(Return(0x40)); + + cpu.ExecuteInstruction(0xF6); // INC Direct Page Indexed, X + EXPECT_EQ(mock_memory[0x0221], 0x41); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, INC_AbsoluteIndexedX_16bit) { + cpu.X = 0x01; + std::vector data = {0xFE, 0x00, 0x10}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x1001, {0x40}); // [0x1001] = 0x40 + + cpu.SetAccumulatorSize(false); + cpu.ExecuteInstruction(0xFE); // INC Absolute Indexed, X + EXPECT_EQ(mock_memory[0x1001], 0x41); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, INX) { + cpu.SetIndexSize(true); // Set X register to 8-bit mode + cpu.X = 0x7F; + cpu.INX(); + EXPECT_EQ(cpu.X, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); + + cpu.X = 0xFF; + cpu.INX(); + EXPECT_EQ(cpu.X, 0x00); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_TRUE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, INY) { + cpu.SetIndexSize(true); // Set Y register to 8-bit mode + cpu.Y = 0x7F; + cpu.INY(); + EXPECT_EQ(cpu.Y, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); + + cpu.Y = 0xFF; + cpu.INY(); + EXPECT_EQ(cpu.Y, 0x00); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_TRUE(cpu.GetZeroFlag()); +} + +// ============================================================================ +// JMP - Jump to new location + +TEST_F(CPUTest, JMP_Absolute) { + std::vector data = {0x4C, 0x05, 0x20}; // JMP $2005 + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x2005)); + + cpu.ExecuteInstruction(0x4C); // JMP Absolute + cpu.ExecuteInstruction(0xEA); // NOP + + EXPECT_EQ(cpu.PC, 0x2006); +} + +TEST_F(CPUTest, JMP_Indirect) { + std::vector data = {0x6C, 0x03, 0x20, 0x05, 0x30}; // JMP ($2003) + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x2003)); + EXPECT_CALL(mock_memory, ReadWord(0x2003)).WillOnce(Return(0x3005)); + + cpu.ExecuteInstruction(0x6C); // JMP Indirect + EXPECT_EQ(cpu.PC, 0x3005); +} + +// ============================================================================ +// JML - Jump Long + +TEST_F(CPUTest, JML_AbsoluteLong) { + cpu.E = 0; + + std::vector data = {0x5C, 0x05, 0x00, 0x03}; // JML $030005 + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x030005, {0x00, 0x20, 0x00}); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x030005)); + + cpu.ExecuteInstruction(0x5C); // JML Absolute Long + EXPECT_EQ(cpu.PC, 0x0005); + EXPECT_EQ(cpu.PB, 0x03); // The PBR should be updated to 0x03 +} + +TEST_F(CPUTest, JMP_AbsoluteIndexedIndirectX) { + cpu.X = 0x02; + std::vector data = {0x7C, 0x05, 0x20, 0x00}; // JMP ($2005, X) + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2007, {0x30, 0x05}); // [0x2007] = 0x0530 + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x2005)); + EXPECT_CALL(mock_memory, ReadWord(0x2007)).WillOnce(Return(0x3005)); + + cpu.ExecuteInstruction(0x7C); // JMP Absolute Indexed Indirect + EXPECT_EQ(cpu.PC, 0x3005); +} + +TEST_F(CPUTest, JMP_AbsoluteIndirectLong) { + std::vector data = {0xDC, 0x05, 0x20, 0x00}; // JMP [$2005] + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2005, {0x01, 0x30, 0x05}); // [0x2005] = 0x0530 + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x2005)); + EXPECT_CALL(mock_memory, ReadWordLong(0x2005)).WillOnce(Return(0x013005)); + + cpu.ExecuteInstruction(0xDC); // JMP Absolute Indirect Long + EXPECT_EQ(cpu.PC, 0x3005); + EXPECT_EQ(cpu.PB, 0x01); +} + +// ============================================================================ +// JSR - Jump to Subroutine + +TEST_F(CPUTest, JSR_Absolute) { + std::vector data = {0x20, 0x05, 0x20}; // JSR $2005 + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x2005)); + EXPECT_CALL(mock_memory, PushWord(0x0000)).Times(1); + + cpu.ExecuteInstruction(0x20); // JSR Absolute + EXPECT_EQ(cpu.PC, 0x2005); + + // Continue executing some code + cpu.ExecuteInstruction(0x60); // RTS + EXPECT_EQ(cpu.PC, 0x0003); +} + +// ============================================================================ +// JSL - Jump to Subroutine Long + +TEST_F(CPUTest, JSL_AbsoluteLong) { + std::vector data = {0x22, 0x05, 0x20, 0x00}; // JSL $002005 + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x002005)); + EXPECT_CALL(mock_memory, PushLong(0x0000)).Times(1); + + cpu.ExecuteInstruction(0x22); // JSL Absolute Long + EXPECT_EQ(cpu.PC, 0x002005); +} + +TEST_F(CPUTest, JSL_AbsoluteIndexedIndirect) { + cpu.X = 0x02; + std::vector data = {0xFC, 0x05, 0x20, 0x00}; // JSL $002005 + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x2007, {0x00, 0x20, 0x00}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x2005)); + EXPECT_CALL(mock_memory, ReadWord(0x2007)).WillOnce(Return(0x002000)); + + cpu.ExecuteInstruction(0xFC); // JSL Absolute Long + EXPECT_EQ(cpu.PC, 0x2000); +} + +// ============================================================================ +// LDA - Load Accumulator + +TEST_F(CPUTest, LDA_DirectPageIndexedIndirectX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + cpu.D = 0x0200; // Set Direct Page register to 0x0200 + std::vector data = {0xA1, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023E, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00023E)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1000)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xA1); // LDA Direct Page Indexed Indirect, X + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_StackRelative) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0xFF; // 8-bit mode + cpu.SetSP(0x01FF); // Set Stack Pointer to 0x01FF + std::vector data = {0xA3, 0x02}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0x7F}); + + EXPECT_CALL(mock_memory, SP()).WillRepeatedly(Return(0x01FF)); + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); + EXPECT_CALL(mock_memory, ReadByte(0x0201)).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0xA3); // LDA Stack Relative + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_DirectPage) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.D = 0x0200; + std::vector data = {0xA5, 0x3C, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x80}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadByte(0x00023C)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xA5); // LDA Direct Page + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_DirectPageIndirectLong) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.D = 0x0200; + std::vector data = {0xA7, 0x3C, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x00, 0x10, 0x00}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWordLong(0x00023C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1000)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xA7); // LDA Direct Page Indirect Long + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_Immediate_8bit) { + cpu.SetAccumulatorSize(true); + std::vector data = {0xA9, 0xFF}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xA9); // LDA Immediate + EXPECT_EQ(cpu.A, 0xFF); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_Immediate_16bit) { + std::vector data = {0xA9, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + + cpu.SetAccumulatorSize(false); + cpu.ExecuteInstruction(0xA9); // LDA Immediate + EXPECT_EQ(cpu.A, 0xFF7F); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0xAD, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x7F}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + EXPECT_CALL(mock_memory, ReadByte(0x7FFF)).WillOnce(Return(0x7F)); + + cpu.SetAccumulatorSize(true); + cpu.ExecuteInstruction(0xAD); // LDA Absolute + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_AbsoluteLong) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0xAF, 0x7F, 0xFF, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x7F}); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x7FFF)); + + EXPECT_CALL(mock_memory, ReadByte(0x7FFF)).WillOnce(Return(0x7F)); + + cpu.SetAccumulatorSize(true); + cpu.ExecuteInstruction(0xAF); // LDA Absolute Long + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_DirectPageIndirectIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + cpu.D = 0x0200; // Set Direct Page register to 0x0200 + std::vector data = {0xB1, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x00, 0x10}); + mock_memory.InsertMemory(0x1002, {0x80}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00023C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1002)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xB1); // LDA Direct Page Indirect Indexed, Y + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_DirectPageIndirect) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.D = 0x0200; + std::vector data = {0xA1, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00023C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1000)).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0xB2); // LDA Direct Page Indirect + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_StackRelativeIndirectIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0xB3, 0x02}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, SP()).WillRepeatedly(Return(0x01FF)); + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); + EXPECT_CALL(mock_memory, ReadWord(0x0201)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1002)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xB3); // LDA Stack Relative Indirect Indexed, Y + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_DirectPageIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + cpu.D = 0x0200; // Set Direct Page register to 0x0200 + std::vector data = {0xB5, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023E, {0x7F}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadByte(0x00023E)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xB5); // LDA Direct Page Indexed, X + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_DirectPageIndirectLongIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + cpu.D = 0x0200; // Set Direct Page register to 0x0200 + std::vector data = {0xB7, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x00, 0x10, 0x00}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWordLong(0x00023C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1002)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xB7); // LDA Direct Page Indirect Long Indexed, Y + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_AbsoluteIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + std::vector data = {0xB9, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x80}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + EXPECT_CALL(mock_memory, ReadByte(0x8001)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xB9); // LDA Absolute Indexed, Y + EXPECT_EQ(cpu.A, 0x80); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_AbsoluteIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0xBD, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x80}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + EXPECT_CALL(mock_memory, ReadByte(0x8001)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xBD); // LDA Absolute Indexed, X + EXPECT_EQ(cpu.A, 0x80); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDA_AbsoluteLongIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0xBF, 0x7F, 0xFF, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x80}); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x7FFF)); + + EXPECT_CALL(mock_memory, ReadByte(0x8001)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xBF); // LDA Absolute Long Indexed, X + EXPECT_EQ(cpu.A, 0x80); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +// ============================================================================ + +TEST_F(CPUTest, LDX_Immediate) { + cpu.SetIndexSize(true); // Set X register to 8-bit mode + std::vector data = {0xA2, 0x42}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xA2); // LDX Immediate + EXPECT_EQ(cpu.X, 0x42); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDX_DirectPage) { + cpu.SetIndexSize(true); // Set X register to 8-bit mode + std::vector data = {0xA6, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0080, {0x42}); + + cpu.ExecuteInstruction(0xA6); // LDX Direct Page + EXPECT_EQ(cpu.X, 0x42); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDX_Absolute) { + cpu.SetIndexSize(true); // Set X register to 8-bit mode + std::vector data = {0xAE, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x42}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0xAE); // LDX Absolute + EXPECT_EQ(cpu.X, 0x42); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDX_DirectPageIndexedY) { + cpu.SetIndexSize(true); // Set X register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + std::vector data = {0xB6, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0082, {0x42}); + + cpu.ExecuteInstruction(0xB6); // LDX Direct Page Indexed, Y + EXPECT_EQ(cpu.X, 0x42); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDX_AbsoluteIndexedY) { + cpu.SetIndexSize(true); // Set X register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + std::vector data = {0xBE, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x42}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0xBE); // LDX Absolute Indexed, Y + EXPECT_EQ(cpu.X, 0x42); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +// ============================================================================ + +TEST_F(CPUTest, LDY_Immediate) { + cpu.SetIndexSize(true); // Set Y register to 8-bit mode + std::vector data = {0xA0, 0x42}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xA0); // LDY Immediate + EXPECT_EQ(cpu.Y, 0x42); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDY_DirectPage) { + cpu.SetIndexSize(true); // Set Y register to 8-bit mode + std::vector data = {0xA4, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0080, {0x42}); + + cpu.ExecuteInstruction(0xA4); // LDY Direct Page + EXPECT_EQ(cpu.Y, 0x42); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDY_Absolute) { + cpu.SetIndexSize(true); // Set Y register to 8-bit mode + std::vector data = {0xAC, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x42}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0xAC); // LDY Absolute + EXPECT_EQ(cpu.Y, 0x42); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDY_DirectPageIndexedX) { + cpu.SetIndexSize(true); // Set Y register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0xB4, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0082, {0x42}); + + cpu.ExecuteInstruction(0xB4); // LDY Direct Page Indexed, X + EXPECT_EQ(cpu.Y, 0x42); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LDY_AbsoluteIndexedX) { + cpu.SetIndexSize(true); // Set Y register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0xBC, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x42}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0xBC); // LDY Absolute Indexed, X + EXPECT_EQ(cpu.Y, 0x42); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +// ============================================================================ + +TEST_F(CPUTest, LSR_DirectPage) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0x46, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0080, {0x42}); + + cpu.ExecuteInstruction(0x46); // LSR Direct Page + EXPECT_EQ(mock_memory[0x0080], 0x21); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LSR_Accumulator) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.ExecuteInstruction(0x4A); // LSR Accumulator + EXPECT_EQ(cpu.A, 0x21); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LSR_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0x4E, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x42}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0x4E); // LSR Absolute + EXPECT_EQ(mock_memory[0x7FFF], 0x21); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LSR_DirectPageIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x56, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0082, {0x42}); + + cpu.ExecuteInstruction(0x56); // LSR Direct Page Indexed, X + EXPECT_EQ(mock_memory[0x0082], 0x21); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, LSR_AbsoluteIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x5E, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x42}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0x5E); // LSR Absolute Indexed, X + EXPECT_EQ(mock_memory[0x8001], 0x21); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +// ============================================================================ +// Stack Tests + +TEST_F(CPUTest, ORA_DirectPageIndexedIndirectX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + cpu.D = 0x0200; // Set Direct Page register to 0x0200 + std::vector data = {0x01, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023E, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00023E)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1000)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0x01); // ORA Direct Page Indexed Indirect, X + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_StackRelative) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0xFF; // 8-bit mode + cpu.SetSP(0x01FF); // Set Stack Pointer to 0x01FF + std::vector data = {0x03, 0x02}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0x7F}); + + EXPECT_CALL(mock_memory, SP()).WillRepeatedly(Return(0x01FF)); + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); + EXPECT_CALL(mock_memory, ReadByte(0x0201)).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0x03); // ORA Stack Relative + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_DirectPage) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.D = 0x0200; + std::vector data = {0x05, 0x3C, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x80}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadByte(0x00023C)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0x05); // ORA Direct Page + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_DirectPageIndirectLong) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.D = 0x0200; + std::vector data = {0x07, 0x3C, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x00, 0x10, 0x00}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWordLong(0x00023C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1000)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0x07); // ORA Direct Page Indirect Long + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_Immediate) { + cpu.SetAccumulatorSize(true); + std::vector data = {0x09, 0xFF}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x09); // ORA Immediate + EXPECT_EQ(cpu.A, 0xFF); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0x0D, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x7F}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + EXPECT_CALL(mock_memory, ReadByte(0x7FFF)).WillOnce(Return(0x7F)); + + cpu.SetAccumulatorSize(true); + cpu.ExecuteInstruction(0x0D); // ORA Absolute + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_AbsoluteLong) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0x0F, 0x7F, 0xFF, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x7F}); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x7FFF)); + + EXPECT_CALL(mock_memory, ReadByte(0x7FFF)).WillOnce(Return(0x7F)); + + cpu.SetAccumulatorSize(true); + cpu.ExecuteInstruction(0x0F); // ORA Absolute Long + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_DirectPageIndirectIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + cpu.D = 0x0200; // Set Direct Page register to 0x0200 + std::vector data = {0x11, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x00, 0x10}); + mock_memory.InsertMemory(0x1002, {0x80}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00023C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1002)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0x11); // ORA Direct Page Indirect Indexed, Y + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_DirectPageIndirect) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.D = 0x0200; + std::vector data = {0x12, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00023C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1000)).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0x12); // ORA Direct Page Indirect + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_StackRelativeIndirectIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0x13, 0x02}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, SP()).WillRepeatedly(Return(0x01FF)); + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x02)); + EXPECT_CALL(mock_memory, ReadWord(0x0201)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1002)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0x13); // ORA Stack Relative Indirect Indexed, Y + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_DirectPageIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + cpu.D = 0x0200; // Set Direct Page register to 0x0200 + std::vector data = {0x15, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023E, {0x80}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadByte(0x00023E)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0x15); // ORA Direct Page Indexed, X + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_DirectPageIndirectLongIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + cpu.D = 0x0200; // Set Direct Page register to 0x0200 + std::vector data = {0x17, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x00, 0x10, 0x00}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWordLong(0x00023C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1002)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0x17); // ORA Direct Page Indirect Long Indexed, Y + EXPECT_EQ(cpu.A, 0x80); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_AbsoluteIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + std::vector data = {0x19, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x7F}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + EXPECT_CALL(mock_memory, ReadByte(0x8001)).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0x19); // ORA Absolute Indexed, Y + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_AbsoluteIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x1D, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x7F}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + EXPECT_CALL(mock_memory, ReadByte(0x8001)).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0x1D); // ORA Absolute Indexed, X + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ORA_AbsoluteLongIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x1F, 0x7F, 0xFF, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x7F}); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x7FFF)); + + EXPECT_CALL(mock_memory, ReadByte(0x8001)).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0x1F); // ORA Absolute Long Indexed, X + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +// ============================================================================ + +TEST_F(CPUTest, PEA) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0xF4, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + EXPECT_CALL(mock_memory, PushWord(0x7FFF)); + + cpu.ExecuteInstruction(0xF4); // PEA +} + +TEST_F(CPUTest, PEI) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0xD4, 0x3C, 0x00}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00003C, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, ReadWord(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00003C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, PushWord(0x1000)); + + cpu.ExecuteInstruction(0xD4); // PEI +} + +TEST_F(CPUTest, PER) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0x62, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + EXPECT_CALL(mock_memory, PushWord(0x7FFF)); + + cpu.ExecuteInstruction(0x62); // PER +} + +TEST_F(CPUTest, PHD) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.D = 0x7FFF; + std::vector data = {0x0B}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, PushWord(0x7FFF)); + + cpu.ExecuteInstruction(0x0B); // PHD +} + +TEST_F(CPUTest, PHK) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.PB = 0x7F; + std::vector data = {0x4B}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, PushByte(0x7F)); + + cpu.ExecuteInstruction(0x4B); // PHK +} + +TEST_F(CPUTest, PHP) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0x7F; + std::vector data = {0x08}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, PushByte(0x7F)); + + cpu.ExecuteInstruction(0x08); // PHP +} + +TEST_F(CPUTest, PHX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x7F; + std::vector data = {0xDA}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, PushByte(0x7F)); + + cpu.ExecuteInstruction(0xDA); // PHX +} + +TEST_F(CPUTest, PHY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x7F; + std::vector data = {0x5A}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, PushByte(0x7F)); + + cpu.ExecuteInstruction(0x5A); // PHY +} + +TEST_F(CPUTest, PHB) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.DB = 0x7F; + std::vector data = {0x8B}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, PushByte(0x7F)); + + cpu.ExecuteInstruction(0x8B); // PHB +} + +TEST_F(CPUTest, PHA) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x7F; + std::vector data = {0x48}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, PushByte(0x7F)); + + cpu.ExecuteInstruction(0x48); // PHA +} + +TEST_F(CPUTest, PHA_16Bit) { + cpu.SetAccumulatorSize(false); // Set A register to 16-bit mode + cpu.A = 0x7FFF; + std::vector data = {0x48}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, PushWord(0x7FFF)); + + cpu.ExecuteInstruction(0x48); // PHA +} + +TEST_F(CPUTest, PLA) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x00; + std::vector data = {0x68}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0001, {0x7F}); + + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0x68); // PLA + EXPECT_EQ(cpu.A, 0x7F); +} + +TEST_F(CPUTest, PLA_16Bit) { + cpu.SetAccumulatorSize(false); // Set A register to 16-bit mode + cpu.A = 0x0000; + std::vector data = {0x68}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0001, {0x7F, 0xFF}); + + EXPECT_CALL(mock_memory, PopWord()).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0x68); // PLA + EXPECT_EQ(cpu.A, 0x7FFF); +} + +TEST_F(CPUTest, PLB) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.DB = 0x00; + std::vector data = {0xAB}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0001, {0x7F}); + + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0xAB); // PLB + EXPECT_EQ(cpu.DB, 0x7F); +} + +TEST_F(CPUTest, PLD) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.D = 0x0000; + std::vector data = {0x2B}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0001, {0x7F, 0xFF}); + + EXPECT_CALL(mock_memory, PopWord()).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0x2B); // PLD + EXPECT_EQ(cpu.D, 0x7FFF); +} + +TEST_F(CPUTest, PLP) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0x00; + std::vector data = {0x28}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0001, {0x7F}); + + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0x28); // PLP + EXPECT_EQ(cpu.status, 0x7F); +} + +TEST_F(CPUTest, PLX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x00; + std::vector data = {0xFA}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0001, {0x7F}); + + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0xFA); // PLX + EXPECT_EQ(cpu.X, 0x7F); +} + +TEST_F(CPUTest, PLX_16Bit) { + cpu.SetIndexSize(false); // Set A register to 16-bit mode + cpu.X = 0x0000; + + std::vector data = {0xFA}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x01FF, {0x7F, 0xFF}); + + EXPECT_CALL(mock_memory, PopWord()).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0xFA); // PLX + EXPECT_EQ(cpu.X, 0x7FFF); +} + +TEST_F(CPUTest, PLY) { + cpu.SetIndexSize(true); // Set A register to 8-bit mode + cpu.Y = 0x00; + std::vector data = {0x7A}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0001, {0x7F}); + + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0x7A); // PLY + EXPECT_EQ(cpu.Y, 0x7F); +} + +TEST_F(CPUTest, PLY_16Bit) { + cpu.SetIndexSize(false); // Set A register to 16-bit mode + cpu.Y = 0x0000; + std::vector data = {0x7A}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0001, {0x7F, 0xFF}); + + EXPECT_CALL(mock_memory, PopWord()).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0x7A); // PLY + EXPECT_EQ(cpu.Y, 0x7FFF); +} + +// ============================================================================ +// REP - Reset Processor Status Bits + +TEST_F(CPUTest, REP) { + cpu.status = 0xFF; + std::vector data = {0xC2, 0x30}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xC2); // REP + EXPECT_EQ(cpu.status, 0xCF); // 11001111 +} + +TEST_F(CPUTest, REP_16Bit) { + cpu.status = 0xFF; + std::vector data = {0xC2, 0x30}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xC2); // REP + EXPECT_EQ(cpu.status, 0xCF); // 00111111 +} + +TEST_F(CPUTest, PHA_PLA_Ok) { + cpu.A = 0x42; + EXPECT_CALL(mock_memory, PushByte(0x42)).WillOnce(Return()); + cpu.PHA(); + cpu.A = 0x00; + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x42)); + cpu.PLA(); + EXPECT_EQ(cpu.A, 0x42); +} + +TEST_F(CPUTest, PHP_PLP_Ok) { + // Set some status flags + cpu.status = 0; + cpu.SetNegativeFlag(true); + cpu.SetZeroFlag(false); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); + + EXPECT_CALL(mock_memory, PushByte(0x80)).WillOnce(Return()); + cpu.PHP(); + + // Clear status flags + cpu.SetNegativeFlag(false); + cpu.SetZeroFlag(true); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_TRUE(cpu.GetZeroFlag()); + + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x80)); + cpu.PLP(); + + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +// ============================================================================ +// PHA, PHP, PHX, PHY, PHB, PHD, PHK +// ============================================================================ + +TEST_F(CPUTest, PHA_PushAccumulator) { + cpu.A = 0x12; + EXPECT_CALL(mock_memory, PushByte(0x12)); + cpu.ExecuteInstruction(0x48); // PHA +} + +TEST_F(CPUTest, PHP_PushProcessorStatusRegister) { + cpu.status = 0x34; + EXPECT_CALL(mock_memory, PushByte(0x34)); + cpu.ExecuteInstruction(0x08); // PHP +} + +TEST_F(CPUTest, PHX_PushXRegister) { + cpu.X = 0x56; + EXPECT_CALL(mock_memory, PushByte(0x56)); + cpu.ExecuteInstruction(0xDA); // PHX +} + +TEST_F(CPUTest, PHY_PushYRegister) { + cpu.Y = 0x78; + EXPECT_CALL(mock_memory, PushByte(0x78)); + cpu.ExecuteInstruction(0x5A); // PHY +} + +TEST_F(CPUTest, PHB_PushDataBankRegister) { + cpu.DB = 0x9A; + EXPECT_CALL(mock_memory, PushByte(0x9A)); + cpu.ExecuteInstruction(0x8B); // PHB +} + +TEST_F(CPUTest, PHD_PushDirectPageRegister) { + cpu.D = 0xBC; + EXPECT_CALL(mock_memory, PushWord(0xBC)); + cpu.ExecuteInstruction(0x0B); // PHD +} + +TEST_F(CPUTest, PHK_PushProgramBankRegister) { + cpu.PB = 0xDE; + EXPECT_CALL(mock_memory, PushByte(0xDE)); + cpu.ExecuteInstruction(0x4B); // PHK +} + +// ============================================================================ +// PLA, PLP, PLX, PLY, PLB, PLD +// ============================================================================ + +TEST_F(CPUTest, PLA_PullAccumulator) { + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x12)); + cpu.ExecuteInstruction(0x68); // PLA + EXPECT_EQ(cpu.A, 0x12); +} + +TEST_F(CPUTest, PLP_PullProcessorStatusRegister) { + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x34)); + cpu.ExecuteInstruction(0x28); // PLP + EXPECT_EQ(cpu.status, 0x34); +} + +TEST_F(CPUTest, PLX_PullXRegister) { + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x56)); + cpu.ExecuteInstruction(0xFA); // PLX + EXPECT_EQ(cpu.X, 0x56); +} + +TEST_F(CPUTest, PLY_PullYRegister) { + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x78)); + cpu.ExecuteInstruction(0x7A); // PLY + EXPECT_EQ(cpu.Y, 0x78); +} + +TEST_F(CPUTest, PLB_PullDataBankRegister) { + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x9A)); + cpu.ExecuteInstruction(0xAB); // PLB + EXPECT_EQ(cpu.DB, 0x9A); +} + +TEST_F(CPUTest, PLD_PullDirectPageRegister) { + EXPECT_CALL(mock_memory, PopWord()).WillOnce(Return(0xBC)); + cpu.ExecuteInstruction(0x2B); // PLD + EXPECT_EQ(cpu.D, 0xBC); +} + +// ============================================================================ +// SEP - Set Processor Status Bits + +TEST_F(CPUTest, SEP) { + cpu.status = 0x00; // All flags cleared + std::vector data = {0xE2, 0x30, + 0x00}; // SEP #0x30 (set N & Z flags) + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xE2); // SEP + EXPECT_EQ(cpu.status, 0x30); // 00110000 +} + +// ============================================================================ + +TEST_F(CPUTest, ROL_DirectPage) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0x26, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0080, {0x42}); + + cpu.ExecuteInstruction(0x26); // ROL Direct Page + EXPECT_EQ(mock_memory[0x0080], 0x84); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ROL_Accumulator) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.ExecuteInstruction(0x2A); // ROL Accumulator + EXPECT_EQ(cpu.A, 0x84); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ROL_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0x2E, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x42}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0x2E); // ROL Absolute + EXPECT_EQ(mock_memory[0x7FFF], 0x84); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ROL_DirectPageIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x36, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0082, {0x42}); + + cpu.ExecuteInstruction(0x36); // ROL Direct Page Indexed, X + EXPECT_EQ(mock_memory[0x0082], 0x84); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ROL_AbsoluteIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x3E, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x42}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0x3E); // ROL Absolute Indexed, X + EXPECT_EQ(mock_memory[0x8001], 0x84); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +// ============================================================================ + +TEST_F(CPUTest, ROR_DirectPage) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0x66, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0080, {0x42}); + + cpu.ExecuteInstruction(0x66); // ROR Direct Page + EXPECT_EQ(mock_memory[0x0080], 0x21); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ROR_Accumulator) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.ExecuteInstruction(0x6A); // ROR Accumulator + EXPECT_EQ(cpu.A, 0x21); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ROR_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0x6E, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x42}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0x6E); // ROR Absolute + EXPECT_EQ(mock_memory[0x7FFF], 0x21); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ROR_DirectPageIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x76, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0082, {0x42}); + + cpu.ExecuteInstruction(0x76); // ROR Direct Page Indexed, X + EXPECT_EQ(mock_memory[0x0082], 0x21); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, ROR_AbsoluteIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x7E, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x42}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0x7E); // ROR Absolute Indexed, X + EXPECT_EQ(mock_memory[0x8001], 0x21); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +// ============================================================================ + +TEST_F(CPUTest, RTI) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0x00; + std::vector data = {0x40}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0001, {0x7F}); + + EXPECT_CALL(mock_memory, PopByte()).WillOnce(Return(0x7F)); + + cpu.ExecuteInstruction(0x40); // RTI + EXPECT_EQ(cpu.status, 0x7F); +} + +// ============================================================================ + +TEST_F(CPUTest, RTL) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.PC = 0x0000; + std::vector data = {0x6B}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0001, {0x7F, 0xFF}); + + EXPECT_CALL(mock_memory, PopWord()).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0x6B); // RTL + EXPECT_EQ(cpu.PC, 0x7FFF); +} + +// ============================================================================ + +TEST_F(CPUTest, RTS) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.PC = 0x0000; + std::vector data = {0x60}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0001, {0x7F, 0xFF}); + + EXPECT_CALL(mock_memory, PopWord()).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0x60); // RTS + EXPECT_EQ(cpu.PC, 0x7FFF + 3); +} + +// ============================================================================ + +TEST_F(CPUTest, SBC_DirectPageIndexedIndirectX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + cpu.D = 0x0200; // Set Direct Page register to 0x0200 + cpu.A = 0x10; // Set A register to 0x80 + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0xE1, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023E, {0x80}); + mock_memory.InsertMemory(0x0080, {0x80}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00023E)).WillOnce(Return(0x80)); + EXPECT_CALL(mock_memory, ReadByte(0x0080)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xE1); // SBC Direct Page Indexed Indirect, X + EXPECT_EQ(cpu.A, 0x90); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_StackRelative) { + std::vector data = {0xE3, 0x3C}; + mock_memory.SetMemoryContents(data); + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0xFF; // 8-bit mode + cpu.SetSP(0x01FF); // Set Stack Pointer to 0x01FF + mock_memory.InsertMemory(0x00003E, {0x02}); + mock_memory.InsertMemory(0x2002, {0x80}); + + EXPECT_CALL(mock_memory, SP()).Times(1); + // EXPECT_CALL(mock_memory, ReadByte(_)).WillOnce(Return(0x3C)); + + cpu.ExecuteInstruction(0xE3); // SBC Stack Relative + EXPECT_EQ(cpu.A, 0x00); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_TRUE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_DirectPage) { + std::vector data = {0xE5, 0x80}; + mock_memory.SetMemoryContents(data); + cpu.D = 0x0100; // Set Direct Page register to 0x0100 + + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0xFF; // 8-bit mode + cpu.A = 0x42; // Set A register to 0x42 + + mock_memory.InsertMemory(0x0180, {0x01}); + + cpu.ExecuteInstruction(0xE5); // SBC Direct Page + EXPECT_EQ(cpu.A, 0x41); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_DirectPageIndirectLong) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0xFF; // 8-bit mode + cpu.A = 0x80; // Set A register to 0x80 + std::vector data = {0xE7, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00003C, {0x00, 0x10, 0x00}); + mock_memory.InsertMemory(0x1000, {0x8F}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWordLong(0x00003C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1000)).WillOnce(Return(0x8F)); + + cpu.ExecuteInstruction(0xE7); // SBC Direct Page Indirect Long + EXPECT_EQ(cpu.A, 0xF1); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_Immediate) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0xFF; // 8-bit mode + cpu.A = 0x80; // Set A register to 0x80 + std::vector data = {0xE9, 0x80}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xE9); // SBC Immediate + EXPECT_EQ(cpu.A, 0x00); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_TRUE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0xFF; // 8-bit mode + cpu.A = 0xFF; // Set A register to 0x80 + std::vector data = {0xED, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x80}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0xED); // SBC Absolute + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_AbsoluteLong) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0xFF; // 8-bit mode + cpu.A = 0xFF; // Set A register to 0x80 + std::vector data = {0xEF, 0x7F, 0xFF, 0xFF, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFFFF, {0x80}); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x7FFFFF)); + + cpu.ExecuteInstruction(0xEF); // SBC Absolute Long + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_DirectPageIndirectIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + cpu.A = 0xFF; // Set A register to 0x80 + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0xF1, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00003E, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00003C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1002)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xF1); // SBC Direct Page Indirect Indexed, Y + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_DirectPageIndirect) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0xFF; // 8-bit mode + cpu.D = 0x0200; // Set Direct Page register to 0x0200 + cpu.A = 0x10; // Set A register to 0x80 + std::vector data = {0xF2, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023C, {0x00, 0x10}); + mock_memory.InsertMemory(0x1000, {0x80}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + + EXPECT_CALL(mock_memory, ReadWord(0x00023C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1000)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xF2); // SBC Direct Page Indirect + EXPECT_EQ(cpu.A, 0x90); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_StackRelativeIndirectIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + cpu.A = 0xFF; // Set A register to 0x80 + cpu.status = 0xFF; // 8-bit mode + cpu.SetSP(0x01FF); // Set Stack Pointer to 0x01FF + std::vector data = {0xF3, 0x02}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0201, {0x00, 0x30}); + mock_memory.InsertMemory(0x3002, {0x80}); + + EXPECT_CALL(mock_memory, SP()).Times(1); + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x02)); + EXPECT_CALL(mock_memory, ReadWord(0x0201)).WillOnce(Return(0x3000)); + EXPECT_CALL(mock_memory, ReadByte(0x3002)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xF3); // SBC Stack Relative Indirect Indexed, Y + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_DirectPageIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + cpu.A = 0x01; + std::vector data = {0xF5, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0082, {0x01}); + + cpu.ExecuteInstruction(0xF5); // SBC Direct Page Indexed, X + EXPECT_EQ(cpu.A, 0xFF); + EXPECT_TRUE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_DirectPageIndirectLongIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + cpu.status = 0xFF; // 8-bit mode + cpu.A = 0xFF; + std::vector data = {0xF7, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00003C, {0x00, 0x10, 0x00}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWordLong(0x00003C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, ReadByte(0x1002)).WillOnce(Return(0x80)); + + cpu.ExecuteInstruction(0xF7); // SBC Direct Page Indirect Long Indexed, Y + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_AbsoluteIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x02; // Set Y register to 0x02 + cpu.status = 0xFF; // 8-bit mode + cpu.A = 0xFF; + std::vector data = {0xF9, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x80}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0xF9); // SBC Absolute Indexed, Y + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_AbsoluteIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + cpu.status = 0xFF; // 8-bit mode + cpu.A = 0xFF; + std::vector data = {0xFD, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x8001, {0x80}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + + cpu.ExecuteInstruction(0xFD); // SBC Absolute Indexed, X + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +TEST_F(CPUTest, SBC_AbsoluteLongIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + cpu.A = 0xFF; + cpu.status = 0xFF; // 8-bit mode + std::vector data = {0xFF, 0x7F, 0xFF, 0xFF, 0xFF}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x800001, {0x80}); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x7FFFFF)); + + cpu.ExecuteInstruction(0xFF); // SBC Absolute Long Indexed, X + EXPECT_EQ(cpu.A, 0x7F); + EXPECT_FALSE(cpu.GetNegativeFlag()); + EXPECT_FALSE(cpu.GetZeroFlag()); +} + +// ============================================================================ + +TEST_F(CPUTest, SEC) { + cpu.ExecuteInstruction(0x38); // SEC + EXPECT_TRUE(cpu.GetCarryFlag()); +} + +// ============================================================================ + +TEST_F(CPUTest, SED) { + cpu.ExecuteInstruction(0xF8); // SED + EXPECT_TRUE(cpu.GetDecimalFlag()); +} + +// ============================================================================ + +// SEI - Set Interrupt Disable Status Flag + +TEST_F(CPUTest, SEI) { + cpu.ExecuteInstruction(0x78); // SEI + // EXPECT_TRUE(cpu.GetInterruptDisableFlag()); +} + +// ============================================================================ + +TEST_F(CPUTest, SEP_8Bit) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.status = 0x00; + std::vector data = {0xE2, 0x30}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xE2); // SEP + EXPECT_EQ(cpu.status, 0x30); // 00110000 +} + +TEST_F(CPUTest, SEP_16Bit) { + cpu.SetAccumulatorSize(false); // Set A register to 16-bit mode + cpu.status = 0x00; + std::vector data = {0xE2, 0x30}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xE2); // SEP + EXPECT_EQ(cpu.status, 0x30); // 00110000 +} + +// ============================================================================ + +TEST_F(CPUTest, STA_DirectPageIndexedIndirectX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x81, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00003E, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00003E)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, WriteByte(0x1000, 0x42)); + + cpu.ExecuteInstruction(0x81); // STA Direct Page Indexed Indirect, X + EXPECT_EQ(cpu.A, 0x42); +} + +TEST_F(CPUTest, STA_StackRelative) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.SetSP(0x01FF); // Set Stack Pointer to 0x01FF + std::vector data = {0x83, 0x3C}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + + EXPECT_CALL(mock_memory, WriteByte(0x023B, 0x42)); + + cpu.ExecuteInstruction(0x83); // STA Stack Relative +} + +TEST_F(CPUTest, STA_DirectPage) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + std::vector data = {0x85, 0x80}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, WriteByte(0x0080, 0x42)); + + cpu.ExecuteInstruction(0x85); // STA Direct Page +} + +TEST_F(CPUTest, STA_DirectPageIndirectLong) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + std::vector data = {0x87, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00003C, {0x00, 0x10, 0x00}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWordLong(0x00003C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, WriteByte(0x1000, 0x42)); + + cpu.ExecuteInstruction(0x87); // STA Direct Page Indirect Long +} + +TEST_F(CPUTest, STA_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + std::vector data = {0x8D, 0xFF, 0x7F}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, WriteByte(0x7FFF, 0x42)); + + cpu.ExecuteInstruction(0x8D); // STA Absolute +} + +TEST_F(CPUTest, STA_AbsoluteLong) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + std::vector data = {0x8F, 0xFF, 0x7F}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, WriteByte(0x7FFF, 0x42)); + + cpu.ExecuteInstruction(0x8F); // STA Absolute Long +} + +TEST_F(CPUTest, STA_DirectPageIndirectIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.Y = 0x02; // Set Y register to 0x02 + std::vector data = {0x91, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00003E, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00003C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, WriteByte(0x1002, 0x42)); + + cpu.ExecuteInstruction(0x91); // STA Direct Page Indirect Indexed, Y +} + +TEST_F(CPUTest, STA_DirectPageIndirect) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.Y = 0x02; // Set Y register to 0x02 + std::vector data = {0x92, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00003C, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00003C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, WriteByte(0x1000, 0x42)); + + cpu.ExecuteInstruction(0x92); // STA Direct Page Indirect +} + +TEST_F(CPUTest, STA_StackRelativeIndirectIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.Y = 0x02; // Set Y register to 0x02 + cpu.SetSP(0x01FF); // Set Stack Pointer to 0x01FF + std::vector data = {0x93, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00023B, {0x00, 0x10}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWord(0x00023B)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, WriteByte(0x1002, 0x42)); + + cpu.ExecuteInstruction(0x93); // STA Stack Relative Indirect Indexed, Y +} + +TEST_F(CPUTest, STA_DirectPageIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x95, 0x80}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, WriteByte(0x0082, 0x42)); + + cpu.ExecuteInstruction(0x95); // STA Direct Page Indexed, X +} + +TEST_F(CPUTest, STA_DirectPageIndirectLongIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.Y = 0x02; // Set Y register to 0x02 + std::vector data = {0x97, 0x3C}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x00003C, {0x00, 0x10, 0x00}); + + EXPECT_CALL(mock_memory, ReadByte(0x000001)).WillOnce(Return(0x3C)); + EXPECT_CALL(mock_memory, ReadWordLong(0x00003C)).WillOnce(Return(0x1000)); + + EXPECT_CALL(mock_memory, WriteByte(0x1002, 0x42)); + + cpu.ExecuteInstruction(0x97); // STA Direct Page Indirect Long Indexed, Y +} + +TEST_F(CPUTest, STA_AbsoluteIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.Y = 0x02; // Set Y register to 0x02 + std::vector data = {0x99, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + EXPECT_CALL(mock_memory, WriteByte(0x8001, 0x42)); + + cpu.ExecuteInstruction(0x99); // STA Absolute Indexed, Y +} + +TEST_F(CPUTest, STA_AbsoluteIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x9D, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + EXPECT_CALL(mock_memory, WriteByte(0x8001, 0x42)); + + cpu.ExecuteInstruction(0x9D); // STA Absolute Indexed, X +} + +TEST_F(CPUTest, STA_AbsoluteLongIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x9F, 0xFF, 0xFF, 0x7F}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWordLong(0x0001)).WillOnce(Return(0x7FFFFF)); + EXPECT_CALL(mock_memory, WriteByte(0x800001, 0x42)); + + cpu.ExecuteInstruction(0x9F); // STA Absolute Long Indexed, X +} + +// ============================================================================ + +TEST_F(CPUTest, STP) { + cpu.ExecuteInstruction(0xDB); // STP + // EXPECT_TRUE(cpu.GetStoppedFlag()); +} + +// ============================================================================ + +TEST_F(CPUTest, STX_DirectPage) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x42; + std::vector data = {0x86, 0x80}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, WriteByte(0x0080, 0x42)); + + cpu.ExecuteInstruction(0x86); // STX Direct Page +} + +TEST_F(CPUTest, STX_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x42; + std::vector data = {0x8E, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + EXPECT_CALL(mock_memory, WriteByte(0x7FFF, 0x42)); + + cpu.ExecuteInstruction(0x8E); // STX Absolute +} + +TEST_F(CPUTest, STX_DirectPageIndexedY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x42; + cpu.Y = 0x02; // Set Y register to 0x02 + std::vector data = {0x96, 0x80}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, WriteByte(0x0082, 0x42)); + + cpu.ExecuteInstruction(0x96); // STX Direct Page Indexed, Y +} + +// ============================================================================ + +TEST_F(CPUTest, STY_DirectPage) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x42; + std::vector data = {0x84, 0x80}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, WriteByte(0x0080, 0x42)); + + cpu.ExecuteInstruction(0x84); // STY Direct Page +} + +TEST_F(CPUTest, STY_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x42; + std::vector data = {0x8C, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + EXPECT_CALL(mock_memory, WriteByte(0x7FFF, 0x42)); + + cpu.ExecuteInstruction(0x8C); // STY Absolute +} + +TEST_F(CPUTest, STY_DirectPageIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.Y = 0x42; + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x94, 0x80}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, WriteByte(0x0082, 0x42)); + + cpu.ExecuteInstruction(0x94); // STY Direct Page Indexed, X +} + +// ============================================================================ + +TEST_F(CPUTest, STZ_DirectPage) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0x64, 0x80}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, WriteByte(0x0080, 0x00)); + + cpu.ExecuteInstruction(0x64); // STZ Direct Page +} + +TEST_F(CPUTest, STZ_DirectPageIndexedX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x02; // Set X register to 0x02 + std::vector data = {0x74, 0x80}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, WriteByte(0x0082, 0x00)); + + cpu.ExecuteInstruction(0x74); // STZ Direct Page Indexed, X +} + +TEST_F(CPUTest, STZ_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + std::vector data = {0x9C, 0x7F, 0xFF}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + EXPECT_CALL(mock_memory, WriteByte(0x7FFF, 0x00)); + + cpu.ExecuteInstruction(0x9C); // STZ Absolute +} + +// ============================================================================ +// TAX - Transfer Accumulator to Index X + +TEST_F(CPUTest, TAX) { + cpu.A = 0xBC; // A register + std::vector data = {0xAA}; // TAX + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xAA); // TAX + EXPECT_EQ(cpu.X, 0xBC); // X register should now be equal to A +} + +// ============================================================================ +// TAY - Transfer Accumulator to Index Y + +TEST_F(CPUTest, TAY) { + cpu.A = 0xDE; // A register + std::vector data = {0xA8}; // TAY + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xA8); // TAY + EXPECT_EQ(cpu.Y, 0xDE); // Y register should now be equal to A +} + +// ============================================================================ + +TEST_F(CPUTest, TCD) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + std::vector data = {0x5B}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x5B); // TCD + EXPECT_EQ(cpu.D, 0x42); +} + +// ============================================================================ + +TEST_F(CPUTest, TCS) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + std::vector data = {0x1B}; + mock_memory.SetMemoryContents(data); + + EXPECT_CALL(mock_memory, SetSP(0x42)); + + cpu.ExecuteInstruction(0x1B); // TCS + EXPECT_EQ(mock_memory.SP(), 0x42); +} + +// ============================================================================ + +TEST_F(CPUTest, TDC) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.D = 0x42; + std::vector data = {0x7B}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x7B); // TDC + EXPECT_EQ(cpu.A, 0x42); +} + +// ============================================================================ + +TEST_F(CPUTest, TRB_DirectPage) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + std::vector data = {0x14, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0080, {0x00}); + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x0080)); + EXPECT_CALL(mock_memory, ReadByte(0x0080)).WillOnce(Return(0x00)); + EXPECT_CALL(mock_memory, WriteByte(0x0080, 0x00)); + + cpu.ExecuteInstruction(0x14); // TRB Direct Page +} + +TEST_F(CPUTest, TRB_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x42; + std::vector data = {0x1C, 0xFF, 0x7F}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x00}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + EXPECT_CALL(mock_memory, ReadByte(0x7FFF)).WillOnce(Return(0x00)); + EXPECT_CALL(mock_memory, WriteByte(0x7FFF, 0x00)); + + cpu.ExecuteInstruction(0x1C); // TRB Absolute +} + +// ============================================================================ + +TEST_F(CPUTest, TSB_DirectPage) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x00; + std::vector data = {0x04, 0x80}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x0080, {0x42}); + + EXPECT_CALL(mock_memory, ReadByte(0x0001)).WillOnce(Return(0x0080)); + EXPECT_CALL(mock_memory, ReadByte(0x0080)).WillOnce(Return(0x42)); + EXPECT_CALL(mock_memory, WriteByte(0x0080, 0x42)); + + cpu.ExecuteInstruction(0x04); // TSB Direct Page +} + +TEST_F(CPUTest, TSB_Absolute) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x00; + std::vector data = {0x0C, 0xFF, 0x7F}; + mock_memory.SetMemoryContents(data); + mock_memory.InsertMemory(0x7FFF, {0x42}); + + EXPECT_CALL(mock_memory, ReadWord(0x0001)).WillOnce(Return(0x7FFF)); + EXPECT_CALL(mock_memory, ReadByte(0x7FFF)).WillOnce(Return(0x42)); + EXPECT_CALL(mock_memory, WriteByte(0x7FFF, 0x42)); + + cpu.ExecuteInstruction(0x0C); // TSB Absolute +} + +// ============================================================================ + +TEST_F(CPUTest, TSC) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.SetSP(0x42); + std::vector data = {0x3B}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x3B); // TSC + EXPECT_EQ(cpu.A, 0x42); +} + +// ============================================================================ + +TEST_F(CPUTest, TSX) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.SetSP(0x42); + std::vector data = {0xBA}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xBA); // TSX + EXPECT_EQ(cpu.X, 0x42); +} + +// ============================================================================ +// TXA - Transfer Index X to Accumulator + +TEST_F(CPUTest, TXA) { + cpu.X = 0xAB; // X register + std::vector data = {0x8A}; // TXA + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x8A); // TXA + EXPECT_EQ(cpu.A, 0xAB); // A register should now be equal to X +} + +// ============================================================================ + +TEST_F(CPUTest, TXS) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x42; + std::vector data = {0x9A}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x9A); // TXS + EXPECT_EQ(cpu.SP(), 0x42); +} + +// ============================================================================ + +TEST_F(CPUTest, TXY) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.X = 0x42; + std::vector data = {0x9B}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x9B); // TXY + EXPECT_EQ(cpu.Y, 0x42); +} + +// ============================================================================ +// TYA - Transfer Index Y to Accumulator + +TEST_F(CPUTest, TYA) { + cpu.Y = 0xCD; // Y register + std::vector data = {0x98}; // TYA + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x98); // TYA + EXPECT_EQ(cpu.A, 0xCD); // A register should now be equal to Y +} + +// ============================================================================ +// TYX - Transfer Index Y to Index X + +TEST_F(CPUTest, TYX) { + cpu.Y = 0xCD; // Y register + std::vector data = {0xBB}; // TYX + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xBB); // TYX + EXPECT_EQ(cpu.X, 0xCD); // X register should now be equal to Y +} + +// ============================================================================ + +TEST_F(CPUTest, WAI) { + cpu.ExecuteInstruction(0xCB); // WAI + // EXPECT_TRUE(cpu.GetWaitingFlag()); +} + +// ============================================================================ + +TEST_F(CPUTest, WDM) { + std::vector data = {0x42}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0x42); // WDM +} + +// ============================================================================ + +TEST_F(CPUTest, XBA) { + cpu.SetAccumulatorSize(true); // Set A register to 8-bit mode + cpu.A = 0x4002; + std::vector data = {0xEB}; + mock_memory.SetMemoryContents(data); + + cpu.ExecuteInstruction(0xEB); // XBA + EXPECT_EQ(cpu.A, 0x0240); +} + +// ============================================================================ +// XCE - Exchange Carry and Emulation Flags + +TEST_F(CPUTest, XCESwitchToNativeMode) { + cpu.ExecuteInstruction(0x18); // Clear carry flag + cpu.ExecuteInstruction(0xFB); // Switch to native mode + EXPECT_FALSE(cpu.E); // Emulation mode flag should be cleared +} + +TEST_F(CPUTest, XCESwitchToEmulationMode) { + cpu.ExecuteInstruction(0x38); // Set carry flag + cpu.ExecuteInstruction(0xFB); // Switch to emulation mode + EXPECT_TRUE(cpu.E); // Emulation mode flag should be set +} + +TEST_F(CPUTest, XCESwitchBackAndForth) { + cpu.ExecuteInstruction(0x18); // Clear carry flag + cpu.ExecuteInstruction(0xFB); // Switch to native mode + EXPECT_FALSE(cpu.E); // Emulation mode flag should be cleared + + cpu.ExecuteInstruction(0x38); // Set carry flag + cpu.ExecuteInstruction(0xFB); // Switch to emulation mode + EXPECT_TRUE(cpu.E); // Emulation mode flag should be set + + cpu.ExecuteInstruction(0x18); // Clear carry flag + cpu.ExecuteInstruction(0xFB); // Switch to native mode + EXPECT_FALSE(cpu.E); // Emulation mode flag should be cleared +} + +} // namespace emu +} // namespace app +} // namespace yaze diff --git a/test/emu/ppu_test.cc b/test/emu/ppu_test.cc new file mode 100644 index 00000000..ef28eaa7 --- /dev/null +++ b/test/emu/ppu_test.cc @@ -0,0 +1,146 @@ +#include "app/emu/video/ppu.h" + +#include + +#include "app/emu/cpu/clock.h" +#include "app/emu/memory/memory.h" +#include "app/emu/memory/mock_memory.h" + +namespace yaze { +namespace app { +namespace emu { + +class MockPpu : public PpuInterface { + public: + MOCK_METHOD(void, Write, (uint16_t address, uint8_t data), (override)); + MOCK_METHOD(uint8_t, Read, (uint16_t address), (const, override)); + + MOCK_METHOD(void, RenderFrame, (), (override)); + MOCK_METHOD(void, RenderScanline, (), (override)); + MOCK_METHOD(void, RenderBackground, (int layer), (override)); + MOCK_METHOD(void, RenderSprites, (), (override)); + + MOCK_METHOD(void, Init, (), (override)); + MOCK_METHOD(void, Reset, (), (override)); + MOCK_METHOD(void, Update, (double deltaTime), (override)); + MOCK_METHOD(void, UpdateClock, (double deltaTime), (override)); + MOCK_METHOD(void, UpdateInternalState, (int cycles), (override)); + + MOCK_METHOD(const std::vector&, GetFrameBuffer, (), + (const, override)); + MOCK_METHOD(std::shared_ptr, GetScreen, (), (const, override)); + + MOCK_METHOD(void, UpdateModeSettings, (), (override)); + MOCK_METHOD(void, UpdateTileData, (), (override)); + MOCK_METHOD(void, UpdateTileMapData, (), (override)); + MOCK_METHOD(void, UpdatePaletteData, (), (override)); + + MOCK_METHOD(void, ApplyEffects, (), (override)); + MOCK_METHOD(void, ComposeLayers, (), (override)); + + MOCK_METHOD(void, DisplayFrameBuffer, (), (override)); + MOCK_METHOD(void, Notify, (uint32_t address, uint8_t data), (override)); + + std::vector internalFrameBuffer; + std::vector vram; + std::vector sprites; + std::vector tilemaps; + BackgroundMode bgMode; +}; + +class PpuTest : public ::testing::Test { + protected: + MockMemory mock_memory; + MockClock mock_clock; + MockPpu mock_ppu; + + PpuTest() {} + + void SetUp() override { + ON_CALL(mock_ppu, Init()).WillByDefault([this]() { + mock_ppu.internalFrameBuffer.resize(256 * 240); + mock_ppu.vram.resize(0x10000); + }); + + ON_CALL(mock_ppu, Write(::testing::_, ::testing::_)) + .WillByDefault([this](uint16_t address, uint8_t data) { + mock_ppu.vram[address] = data; + }); + + ON_CALL(mock_ppu, Read(::testing::_)) + .WillByDefault( + [this](uint16_t address) { return mock_ppu.vram[address]; }); + + ON_CALL(mock_ppu, RenderScanline()).WillByDefault([this]() { + // Simulate scanline rendering logic... + }); + + ON_CALL(mock_ppu, GetFrameBuffer()).WillByDefault([this]() { + return mock_ppu.internalFrameBuffer; + }); + + // Additional ON_CALL setups as needed... + } + + void TearDown() override { + // Common cleanup (if necessary) + } + + const uint8_t testVRAMValue = 0xAB; + const uint16_t testVRAMAddress = 0x2000; + const std::vector spriteData = {/* ... */}; + const std::vector bgData = {/* ... */}; + const uint8_t testPaletteIndex = 3; + const uint16_t testTileIndex = 42; +}; + +// Test Initialization +TEST_F(PpuTest, InitializationSetsCorrectFrameBufferSize) { + // EXPECT_CALL(mock_ppu, Init()).Times(1); + // mock_ppu.Init(); + // EXPECT_EQ(mock_ppu.GetFrameBuffer().size(), 256 * 240); +} + +// Test State Reset +TEST_F(PpuTest, ResetClearsFrameBuffer) { + // EXPECT_CALL(mock_ppu, Reset()).Times(1); + // mock_ppu.Reset(); + // auto frameBuffer = mock_ppu.GetFrameBuffer(); + // EXPECT_TRUE(std::all_of(frameBuffer.begin(), frameBuffer.end(), + // [](uint8_t val) { return val == 0; })); +} + +// Test Memory Interaction +TEST_F(PpuTest, ReadWriteVRAM) { + // uint16_t address = testVRAMAddress; + // uint8_t value = testVRAMValue; + // EXPECT_CALL(mock_ppu, Write(address, value)).Times(1); + // mock_ppu.Write(address, value); + // EXPECT_EQ(mock_ppu.Read(address), value); +} + +// Test Rendering Mechanics +TEST_F(PpuTest, RenderScanlineUpdatesFrameBuffer) { + // Setup PPU with necessary background and sprite data + // Call RenderScanline and check if the framebuffer is updated correctly +} + +// Test Mode and Register Handling +TEST_F(PpuTest, Mode0Rendering) { + // Set PPU to Mode0 and verify correct rendering behavior +} + +// Test Interrupts and Counters +TEST_F(PpuTest, VBlankInterruptTriggered) { + // Simulate conditions for V-Blank and test if the interrupt is triggered +} + +// Test Composite Rendering and Output +TEST_F(PpuTest, FrameComposition) { + // Setup various layers and sprites, call ComposeLayers, and verify the frame + // buffer +} + +} // namespace emu +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/test/emu/spc700_test.cc b/test/emu/spc700_test.cc new file mode 100644 index 00000000..58aed485 --- /dev/null +++ b/test/emu/spc700_test.cc @@ -0,0 +1,466 @@ +#include "app/emu/audio/spc700.h" + +#include +#include +#include + +namespace yaze { +namespace app { +namespace emu { + +using testing::_; +using testing::Return; + +class MockAudioRam : public AudioRam { + public: + MOCK_METHOD(void, reset, (), (override)); + MOCK_METHOD(uint8_t, read, (uint16_t address), (const, override)); + MOCK_METHOD(uint8_t&, mutable_read, (uint16_t address), (override)); + MOCK_METHOD(void, write, (uint16_t address, uint8_t value), (override)); + + void SetupMemory(uint16_t address, const std::vector& values) { + if (address > internal_audio_ram_.size()) { + internal_audio_ram_.resize(address + values.size()); + } + int i = 0; + for (const auto& each : values) { + internal_audio_ram_[address + i] = each; + i++; + } + } + + void SetUp() { + // internal_audio_ram_.resize(0x10000); // 64 K (0x10000) + // std::fill(internal_audio_ram_.begin(), internal_audio_ram_.end(), 0); + ON_CALL(*this, read(_)).WillByDefault([this](uint16_t address) { + return internal_audio_ram_[address]; + }); + ON_CALL(*this, mutable_read(_)) + .WillByDefault([this](uint16_t address) -> uint8_t& { + return internal_audio_ram_[address]; + }); + ON_CALL(*this, write(_, _)) + .WillByDefault([this](uint16_t address, uint8_t value) { + internal_audio_ram_[address] = value; + }); + ON_CALL(*this, reset()).WillByDefault([this]() { + std::fill(internal_audio_ram_.begin(), internal_audio_ram_.end(), 0); + }); + } + + std::vector internal_audio_ram_ = std::vector(0x10000, 0); +}; + +class Spc700Test : public ::testing::Test { + public: + Spc700Test() = default; + void SetUp() override { + // Set up the mock + audioRAM.SetUp(); + + // Set the Spc700 to bank 01 + spc700.PC = 0x0100; + } + + testing::StrictMock audioRAM; + Spc700 spc700{audioRAM}; +}; + +// ======================================================== +// 8-bit Move Memory to Register + +TEST_F(Spc700Test, MOV_A_Immediate) { + // MOV A, imm + uint8_t opcode = 0xE8; + uint8_t immediate_value = 0x5A; + audioRAM.SetupMemory(0x0100, {opcode, immediate_value}); + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(immediate_value)); + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, immediate_value); + EXPECT_EQ(spc700.PSW.Z, 0); + EXPECT_EQ(spc700.PSW.N, 0); +} + +TEST_F(Spc700Test, MOV_A_X) { + // MOV A, X + uint8_t opcode = 0x7D; + spc700.X = 0x5A; + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, spc700.X); + EXPECT_EQ(spc700.PSW.Z, 0); + EXPECT_EQ(spc700.PSW.N, 0); +} + +TEST_F(Spc700Test, MOV_A_Y) { + // MOV A, Y + uint8_t opcode = 0xDD; + spc700.Y = 0x5A; + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, spc700.Y); + EXPECT_EQ(spc700.PSW.Z, 0); + EXPECT_EQ(spc700.PSW.N, 0); +} + +TEST_F(Spc700Test, MOV_A_dp) { + // MOV A, dp + uint8_t opcode = 0xE4; + uint8_t dp_value = 0x5A; + audioRAM.SetupMemory(0x005A, {0x42}); + audioRAM.SetupMemory(0x0100, {opcode, dp_value}); + + EXPECT_CALL(audioRAM, read(_)) + .WillOnce(Return(dp_value)) + .WillOnce(Return(0x42)); + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, 0x42); + EXPECT_EQ(spc700.PSW.Z, 0); + EXPECT_EQ(spc700.PSW.N, 0); +} + +TEST_F(Spc700Test, MOV_A_dp_plus_x) { + // MOV A, dp+X + uint8_t opcode = 0xF4; + uint8_t dp_value = 0x5A; + spc700.X = 0x01; + audioRAM.SetupMemory(0x005B, {0x42}); + audioRAM.SetupMemory(0x0100, {opcode, dp_value}); + + EXPECT_CALL(audioRAM, read(_)) + .WillOnce(Return(dp_value + spc700.X)) + .WillOnce(Return(0x42)); + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, 0x42); + EXPECT_EQ(spc700.PSW.Z, 0); + EXPECT_EQ(spc700.PSW.N, 0); +} + +TEST_F(Spc700Test, MOV_A_dp_indirect_plus_y) { + // MOV A, [dp]+Y + uint8_t opcode = 0xF7; + uint8_t dp_value = 0x5A; + spc700.Y = 0x01; + audioRAM.SetupMemory(0x005A, {0x00, 0x42}); + audioRAM.SetupMemory(0x0100, {opcode, dp_value}); + audioRAM.SetupMemory(0x4201, {0x69}); + + EXPECT_CALL(audioRAM, read(_)) + .WillOnce(Return(dp_value)) + .WillOnce(Return(0x4200)) + .WillOnce(Return(0x69)); + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, 0x69); + EXPECT_EQ(spc700.PSW.Z, 0); + EXPECT_EQ(spc700.PSW.N, 0); +} + +TEST_F(Spc700Test, MOV_A_dp_plus_x_indirect) { + // MOV A, [dp+X] + uint8_t opcode = 0xE7; + uint8_t dp_value = 0x5A; + spc700.X = 0x01; + audioRAM.SetupMemory(0x005B, {0x00, 0x42}); + audioRAM.SetupMemory(0x0100, {opcode, dp_value}); + audioRAM.SetupMemory(0x4200, {0x69}); + + EXPECT_CALL(audioRAM, read(_)) + .WillOnce(Return(dp_value + 1)) + .WillOnce(Return(0x4200)) + .WillOnce(Return(0x69)); + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, 0x69); + EXPECT_EQ(spc700.PSW.Z, 0); + EXPECT_EQ(spc700.PSW.N, 0); +} + +TEST_F(Spc700Test, MOV_A_abs) { + // MOV A, !abs + uint8_t opcode = 0xE5; + uint16_t abs_addr = 0x1234; + uint8_t abs_value = 0x5A; + + EXPECT_CALL(audioRAM, read(_)) + .WillOnce(Return(abs_addr & 0xFF)) // Low byte + .WillOnce(Return(abs_addr >> 8)); // High byte + + EXPECT_CALL(audioRAM, read(abs_addr)).WillOnce(Return(abs_value)); + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, abs_value); + EXPECT_EQ(spc700.PSW.Z, 0); + EXPECT_EQ(spc700.PSW.N, 0); +} + +// ============================================================================ +// 8-bit Move Register to Memory + +TEST_F(Spc700Test, MOV_Immediate) { + // MOV A, imm + uint8_t opcode = 0xE8; + uint8_t immediate_value = 0x5A; + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(immediate_value)); + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, immediate_value); + EXPECT_EQ(spc700.PSW.Z, 0); + EXPECT_EQ(spc700.PSW.N, 0); +} + +// ============================================================================ + +TEST_F(Spc700Test, NOP_DoesNothing) { + // NOP opcode + uint8_t opcode = 0x00; + + uint16_t initialPC = spc700.PC; + spc700.ExecuteInstructions(opcode); + + // PC should increment by 1, no other changes + EXPECT_EQ(spc700.PC, initialPC + 1); + // Add checks for other registers if needed +} + +TEST_F(Spc700Test, ADC_A_Immediate) { + // ADC A, #imm + uint8_t opcode = 0x88; + uint8_t immediate_value = 0x10; + spc700.A = 0x01; + spc700.PSW.C = 1; // Assume carry is set + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(immediate_value)); + + spc700.ExecuteInstructions(opcode); + + // Verify A, and flags + EXPECT_EQ(spc700.A, 0x12); // 0x01 + 0x10 + 1 (carry) + // Check for other flags (Z, C, etc.) based on the result +} + +TEST_F(Spc700Test, BEQ_BranchesIfZeroFlagSet) { + // BEQ rel + uint8_t opcode = 0xF0; + int8_t offset = 0x05; + spc700.PSW.Z = 1; // Set Zero flag + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(offset)); + + uint16_t initialPC = spc700.PC + 1; + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.PC, initialPC + offset); +} + +TEST_F(Spc700Test, STA_Absolute) { + // STA !abs + uint8_t opcode = 0x85; + uint16_t abs_addr = 0x1234; + spc700.A = 0x80; + + // Set up the mock to return the address for the absolute addressing + EXPECT_CALL(audioRAM, read(_)) + .WillOnce(Return(abs_addr & 0xFF)) // Low byte + .WillOnce(Return(abs_addr >> 8)); // High byte + + spc700.ExecuteInstructions(opcode); +} + +TEST_F(Spc700Test, ExecuteADCWithImmediate) { + // ADC A, imm + uint8_t opcode = 0x88; // Replace with opcode for ADC A, imm + uint8_t immediate_value = 0x10; + spc700.A = 0x15; + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(immediate_value)); + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, 0x25); // 0x15 + 0x10 + EXPECT_EQ(spc700.PSW.Z, 0); + EXPECT_EQ(spc700.PSW.N, 0); + EXPECT_EQ(spc700.PSW.C, 0); +} + +TEST_F(Spc700Test, ExecuteBRA) { + // BRA + uint8_t opcode = 0x2F; + int8_t offset = 0x05; + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(offset)); + + // rel() moves the PC forward one after read + uint16_t initialPC = spc700.PC + 1; + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.PC, initialPC + offset); +} + +TEST_F(Spc700Test, ReadFromAudioRAM) { + uint16_t address = 0x1234; + uint8_t expected_value = 0x5A; + + EXPECT_CALL(audioRAM, read(address)).WillOnce(Return(expected_value)); + + uint8_t value = spc700.read(address); + EXPECT_EQ(value, expected_value); +} + +TEST_F(Spc700Test, WriteToAudioRAM) { + uint16_t address = 0x1234; + uint8_t value = 0x5A; + + EXPECT_CALL(audioRAM, write(address, value)); + + spc700.write(address, value); +} + +TEST_F(Spc700Test, ExecuteANDWithImmediate) { + // AND A, imm + uint8_t opcode = 0x28; + uint8_t immediate_value = 0x0F; + spc700.A = 0x5A; // 0101 1010 + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(immediate_value)); + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, 0x0A); // 0101 1010 & 0000 1111 = 0000 1010 + EXPECT_EQ(spc700.PSW.Z, 0); + EXPECT_EQ(spc700.PSW.N, 0); +} + +TEST_F(Spc700Test, ExecuteORWithImmediate) { + // OR A, imm + uint8_t opcode = 0x08; + uint8_t immediate_value = 0x0F; + spc700.A = 0xA0; // 1010 0000 + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(immediate_value)); + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, 0xAF); // 1010 0000 | 0000 1111 = 1010 1111 + EXPECT_EQ(spc700.PSW.Z, 0); + // EXPECT_EQ(spc700.PSW.N, 1); +} + +TEST_F(Spc700Test, ExecuteEORWithImmediate) { + // EOR A, imm + uint8_t opcode = 0x48; + uint8_t immediate_value = 0x5A; + spc700.A = 0x5A; // 0101 1010 + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(immediate_value)); + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, 0x00); // 0101 1010 ^ 0101 1010 = 0000 0000 + EXPECT_EQ(spc700.PSW.Z, 1); + EXPECT_EQ(spc700.PSW.N, 0); +} + +TEST_F(Spc700Test, ExecuteINC) { + // INC A + uint8_t opcode = 0xBC; + spc700.A = 0xFF; + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, 0x00); + EXPECT_EQ(spc700.PSW.Z, 1); + EXPECT_EQ(spc700.PSW.N, 0); +} + +TEST_F(Spc700Test, ExecuteDEC) { + // DEC A + uint8_t opcode = 0x9C; + spc700.A = 0x01; + + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.A, 0x00); + EXPECT_EQ(spc700.PSW.Z, 1); + EXPECT_EQ(spc700.PSW.N, 0); +} + +TEST_F(Spc700Test, ExecuteBNEWhenNotEqual) { + // BNE + uint8_t opcode = 0xD0; + int8_t offset = 0x05; + spc700.PSW.Z = 0; + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(offset)); + + uint16_t initialPC = spc700.PC + 1; + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.PC, initialPC + offset); +} + +TEST_F(Spc700Test, ExecuteBNEWhenEqual) { + // BNE + uint8_t opcode = 0xD0; + int8_t offset = 0x05; + spc700.PSW.Z = 1; + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(offset)); + + uint16_t initialPC = spc700.PC; + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.PC, initialPC + 1); // +1 because of reading the offset +} + +TEST_F(Spc700Test, ExecuteBEQWhenEqual) { + // BEQ + uint8_t opcode = 0xF0; + int8_t offset = 0x05; + spc700.PSW.Z = 1; + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(offset)); + + uint16_t initialPC = spc700.PC + 1; + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.PC, initialPC + offset); +} + +TEST_F(Spc700Test, ExecuteBEQWhenNotEqual) { + // BEQ + uint8_t opcode = 0xF0; + int8_t offset = 0x05; + spc700.PSW.Z = 0; + + EXPECT_CALL(audioRAM, read(_)).WillOnce(Return(offset)); + + uint16_t initialPC = spc700.PC; + spc700.ExecuteInstructions(opcode); + + EXPECT_EQ(spc700.PC, initialPC + 1); // +1 because of reading the offset +} + +TEST_F(Spc700Test, BootIplRomOk) { + // Boot the IPL ROM + // spc700.BootIplRom(); + // EXPECT_EQ(spc700.PC, 0xFFC1 + 0x3F); +} + +} // namespace emu +} // namespace app +} // namespace yaze diff --git a/test/rom_test.cc b/test/rom_test.cc deleted file mode 100644 index 1f1af99c..00000000 --- a/test/rom_test.cc +++ /dev/null @@ -1,315 +0,0 @@ -#include "app/rom.h" - -#include -#include - -#include - -#include "absl/status/statusor.h" - -#define BUILD_HEADER(command, length) (command << 5) + (length - 1) - -namespace yaze_test { -namespace rom_test { - -using yaze::app::CompressionPiece; -using yaze::app::ROM; - -using ::testing::ElementsAreArray; -using ::testing::TypedEq; - -namespace { - -Bytes ExpectCompressOk(ROM& rom, uchar* in, int in_size) { - auto load_status = rom.LoadFromPointer(in, in_size); - EXPECT_TRUE(load_status.ok()); - auto compression_status = rom.Compress(0, in_size); - EXPECT_TRUE(compression_status.ok()); - auto compressed_bytes = std::move(*compression_status); - return compressed_bytes; -} - -Bytes ExpectDecompressBytesOk(ROM& rom, Bytes& in) { - auto load_status = rom.LoadFromBytes(in); - EXPECT_TRUE(load_status.ok()); - auto decompression_status = rom.Decompress(0, in.size()); - EXPECT_TRUE(decompression_status.ok()); - auto decompressed_bytes = std::move(*decompression_status); - return decompressed_bytes; -} - -Bytes ExpectDecompressOk(ROM& rom, uchar* in, int in_size) { - auto load_status = rom.LoadFromPointer(in, in_size); - EXPECT_TRUE(load_status.ok()); - auto decompression_status = rom.Decompress(0, in_size); - EXPECT_TRUE(decompression_status.ok()); - auto decompressed_bytes = std::move(*decompression_status); - return decompressed_bytes; -} - -std::shared_ptr ExpectNewCompressionPieceOk( - const char command, const int length, const std::string args, - const int argument_length) { - auto new_piece = std::make_shared(command, length, args, - argument_length); - EXPECT_TRUE(new_piece != nullptr); - return new_piece; -} - -} // namespace - -TEST(ROMTest, NewDecompressionPieceOk) { - char command = 1; - int length = 1; - char args[] = "aaa"; - int argument_length = 0x02; - CompressionPiece old_piece; - old_piece.command = command; - old_piece.length = length; - old_piece.argument = args; - old_piece.argument_length = argument_length; - old_piece.next = nullptr; - - auto new_piece = ExpectNewCompressionPieceOk(0x01, 0x01, "aaa", 0x02); - - EXPECT_EQ(old_piece.command, new_piece->command); - EXPECT_EQ(old_piece.length, new_piece->length); - ASSERT_EQ(old_piece.argument_length, new_piece->argument_length); - for (int i = 0; i < old_piece.argument_length; ++i) { - EXPECT_EQ(old_piece.argument[i], new_piece->argument[i]); - } -} - -TEST(ROMTest, DecompressionValidCommand) { - ROM rom; - Bytes 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)); -} - -TEST(ROMTest, DecompressionMixingCommand) { - ROM rom; - uchar random1_i[11] = {BUILD_HEADER(0x01, 0x03), - 0x2A, - BUILD_HEADER(0x00, 0x04), - 0x01, - 0x02, - 0x03, - 0x04, - BUILD_HEADER(0x02, 0x02), - 0x0B, - 0x16, - 0xFF}; - uchar random1_o[9] = {42, 42, 42, 1, 2, 3, 4, 11, 22}; - auto decomp_result = ExpectDecompressOk(rom, random1_i, 11); - EXPECT_THAT(random1_o, ElementsAreArray(decomp_result.data(), 9)); -} - -TEST(ROMTest, CompressionSingleSet) { - ROM rom; - uchar single_set[5] = {0x2A, 0x2A, 0x2A, 0x2A, 0x2A}; - uchar single_set_expected[3] = {BUILD_HEADER(1, 5), 0x2A, 0xFF}; - - auto comp_result = ExpectCompressOk(rom, single_set, 5); - EXPECT_THAT(single_set_expected, ElementsAreArray(comp_result.data(), 3)); -} - -TEST(ROMTest, CompressionSingleWord) { - ROM rom; - uchar single_word[6] = {0x2A, 0x01, 0x2A, 0x01, 0x2A, 0x01}; - uchar single_word_expected[4] = {BUILD_HEADER(0x02, 0x06), 0x2A, 0x01, 0xFF}; - - auto comp_result = ExpectCompressOk(rom, single_word, 6); - EXPECT_THAT(single_word_expected, ElementsAreArray(comp_result.data(), 4)); -} - -TEST(ROMTest, CompressionSingleIncrement) { - ROM rom; - uchar single_inc[3] = {0x01, 0x02, 0x03}; - uchar single_inc_expected[3] = {BUILD_HEADER(0x03, 0x03), 0x01, 0xFF}; - auto comp_result = ExpectCompressOk(rom, single_inc, 3); - EXPECT_THAT(single_inc_expected, ElementsAreArray(comp_result.data(), 3)); -} - -TEST(ROMTest, CompressionSingleCopy) { - ROM rom; - uchar single_copy[4] = {0x03, 0x0A, 0x07, 0x14}; - uchar single_copy_expected[6] = { - BUILD_HEADER(0x00, 0x04), 0x03, 0x0A, 0x07, 0x14, 0xFF}; - auto comp_result = ExpectCompressOk(rom, single_copy, 4); - EXPECT_THAT(single_copy_expected, ElementsAreArray(comp_result.data(), 6)); -} - -/* Hiding tests until I figure out a better PR to address the bug -TEST(ROMTest, CompressionSingleCopyRepeat) { - ROM rom; - uchar single_copy_repeat[8] = {0x03, 0x0A, 0x07, 0x14, 0x03, 10, 0x07, 0x14}; - uchar single_copy_repeat_expected[9] = { - BUILD_HEADER(0x00, 0x04), 0x03, 0x0A, 0x07, 0x14, - BUILD_HEADER(0x04, 0x04), 0x00, 0x00, 0xFF}; - auto comp_result = ExpectCompressOk(rom, single_copy_repeat, 8); - EXPECT_THAT(single_copy_repeat_expected, - ElementsAreArray(comp_result.data(), 9)); -} - -TEST(ROMTest, CompressionSingleOverflowIncrement) { - ROM rom; - uchar overflow_inc[4] = {0xFE, 0xFF, 0x00, 0x01}; - uchar overflow_inc_expected[3] = {BUILD_HEADER(0x03, 0x04), 0xFE, 0xFF}; - - auto comp_result = ExpectCompressOk(rom, overflow_inc, 4); - EXPECT_THAT(overflow_inc_expected, ElementsAreArray(comp_result.data(), 3)); -} - -TEST(ROMTest, CompressionMixedRepeatIncrement) { - ROM rom; - uchar to_compress_string[28] = {0x05, 0x05, 0x05, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0A, 0x0B, 0x05, 0x02, 0x05, 0x02, - 0x05, 0x02, 0x0A, 0x0B, 0x05, 0x02, 0x05, - 0x02, 0x05, 0x02, 0x08, 0x0A, 0x00, 0x05}; - uchar repeat_and_inc_copy_expected[7] = {BUILD_HEADER(0x01, 0x04), - 0x05, - BUILD_HEADER(0x03, 0x06), - 0x06, - BUILD_HEADER(0x00, 0x01), - 0x05, - 0xFF}; - // Mixing, repeat, inc, trailing copy - auto comp_result = ExpectCompressOk(rom, to_compress_string, 28); - EXPECT_THAT(repeat_and_inc_copy_expected, - ElementsAreArray(comp_result.data(), 7)); -} - */ - -TEST(ROMTest, CompressionMixedIncrementIntraCopyOffset) { - ROM rom; - uchar to_compress_string[] = {0x05, 0x05, 0x05, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0A, 0x0B, 0x05, 0x02, 0x05, 0x02, - 0x05, 0x02, 0x0A, 0x0B, 0x05, 0x02, 0x05, - 0x02, 0x05, 0x02, 0x08, 0x0A, 0x00, 0x05}; - uchar inc_word_intra_copy_expected[] = {BUILD_HEADER(0x03, 0x07), - 0x05, - BUILD_HEADER(0x02, 0x06), - 0x05, - 0x02, - BUILD_HEADER(0x04, 0x08), - 0x05, - 0x00, - 0xFF}; - - // "Mixing, inc, alternate, intra copy" - // compress start: 3, length: 21 - // compressed length: 9 - auto comp_result = ExpectCompressOk(rom, to_compress_string + 3, 21); - EXPECT_THAT(inc_word_intra_copy_expected, - ElementsAreArray(comp_result.data(), 9)); -} - -TEST(ROMTest, CompressionMixedIncrementIntraCopySource) { - ROM rom; - uchar to_compress_string[] = {0x05, 0x05, 0x05, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0A, 0x0B, 0x05, 0x02, 0x05, 0x02, - 0x05, 0x02, 0x0A, 0x0B, 0x05, 0x02, 0x05, - 0x02, 0x05, 0x02, 0x08, 0x0A, 0x00, 0x05}; - uchar all_expected[] = {BUILD_HEADER(0x01, 0x04), - 0x05, - BUILD_HEADER(0x03, 0x06), - 0x06, - BUILD_HEADER(0x02, 0x06), - 0x05, - 0x02, - BUILD_HEADER(0x04, 0x08), - 0x08, - 0x00, - BUILD_HEADER(0x00, 0x04), - 0x08, - 0x0A, - 0x00, - 0x05, - 0xFF}; - // "Mixing, inc, alternate, intra copy" - // 0, 28 - // 16 - auto comp_result = ExpectCompressOk(rom, to_compress_string, 28); - EXPECT_THAT(all_expected, ElementsAreArray(comp_result.data(), 16)); -} - -TEST(ROMTest, LengthBorderCompression) { - ROM rom; - uchar buffer[3000]; - - for (unsigned int i = 0; i < 3000; i++) buffer[i] = 0x05; - uchar extended_lenght_expected_42[] = {0b11100100, 0x29, 0x05, 0xFF}; - uchar extended_lenght_expected_400[] = {0b11100101, 0x8F, 0x05, 0xFF}; - uchar extended_lenght_expected_1050[] = { - 0b11100111, 0xFF, 0x05, BUILD_HEADER(0x01, 0x1A), 0x05, 0xFF}; - uchar extended_lenght_expected_2050[] = { - 0b11100111, 0xFF, 0x05, 0b11100111, 0xFF, 0x05, BUILD_HEADER(0x01, 0x02), - 0x05, 0xFF}; - - // "Extended lenght, 42 repeat of 5" - auto comp_result = ExpectCompressOk(rom, buffer, 42); - EXPECT_THAT(extended_lenght_expected_42, - ElementsAreArray(comp_result.data(), 4)); - - // "Extended lenght, 400 repeat of 5" - comp_result = ExpectCompressOk(rom, buffer, 400); - EXPECT_THAT(extended_lenght_expected_400, - ElementsAreArray(comp_result.data(), 4)); - - // "Extended lenght, 1050 repeat of 5" - comp_result = ExpectCompressOk(rom, buffer, 1050); - EXPECT_THAT(extended_lenght_expected_1050, - ElementsAreArray(comp_result.data(), 6)); - - // "Extended lenght, 2050 repeat of 5" - comp_result = ExpectCompressOk(rom, buffer, 2050); - EXPECT_THAT(extended_lenght_expected_2050, - ElementsAreArray(comp_result.data(), 9)); -} - -TEST(ROMTest, CompressionExtendedWordCopy) { - ROM rom; - uchar buffer[3000]; - for (unsigned int i = 0; i < 3000; i += 2) { - buffer[i] = 0x05; - buffer[i + 1] = 0x06; - } - uchar hightlenght_word_1050[] = { - 0b11101011, 0xFF, 0x05, 0x06, BUILD_HEADER(0x02, 0x1A), 0x05, 0x06, 0xFF}; - - // "Extended word copy" - auto comp_result = ExpectCompressOk(rom, buffer, 1050); - EXPECT_THAT(hightlenght_word_1050, ElementsAreArray(comp_result.data(), 8)); -} - -/* Extended Header Command is currently unimplemented -TEST(ROMTest, ExtendedHeaderDecompress) { - ROM rom; - Bytes extendedcmd_i = {0b11100100, 0x8F, 0x2A, 0xFF}; - uchar extendedcmd_o[50]; - for (int i = 0; i < 50; ++i) { - extendedcmd_o[i] = 0x2A; - } - - auto decomp_result = ExpectDecompressBytesOk(rom, extendedcmd_i); - ASSERT_THAT(extendedcmd_o, ElementsAreArray(decomp_result.data(), 50)); -} - -TEST(ROMTest, ExtendedHeaderDecompress2) { - ROM rom; - Bytes extendedcmd_i = {0b11100101, 0x8F, 0x2A, 0xFF}; - uchar extendedcmd_o[50]; - for (int i = 0; i < 50; i++) { - extendedcmd_o[i] = 0x2A; - } - - auto data = ExpectDecompressBytesOk(rom, extendedcmd_i); - for (int i = 0; i < 50; i++) { - ASSERT_EQ(extendedcmd_o[i], data[i]); - } -} -*/ -} // namespace rom_test -} // namespace yaze_test \ No newline at end of file diff --git a/test/room_object_test.cc b/test/room_object_test.cc new file mode 100644 index 00000000..dfd5a58c --- /dev/null +++ b/test/room_object_test.cc @@ -0,0 +1,25 @@ +#include "app/zelda3/dungeon/room_object.h" + +#include +#include + +#include "app/emu/cpu/cpu.h" +#include "app/emu/memory/memory.h" +#include "app/emu/memory/mock_memory.h" +#include "app/emu/video/ppu.h" +#include "app/gfx/bitmap.h" +#include "app/rom.h" + +namespace yaze { +namespace test { + +TEST(DungeonObjectTest, RenderObjectsAsBitmaps) { + app::ROM rom; + // rom.LoadFromFile("/Users/scawful/Code/yaze/build/bin/zelda3.sfc")); + // EXPECT_EQ(rom_status, absl::Status::ok()); + + app::zelda3::dungeon::DungeonObjectRenderer renderer; +} + +} // namespace test +} // namespace yaze \ No newline at end of file diff --git a/test/snes_palette_test.cc b/test/snes_palette_test.cc new file mode 100644 index 00000000..5a38b315 --- /dev/null +++ b/test/snes_palette_test.cc @@ -0,0 +1,106 @@ +#include "app/gfx/snes_palette.h" + +#include +#include + +namespace yaze_test { +namespace gfx_test { + +using ::testing::ElementsAreArray; +using yaze::app::gfx::ConvertRGBtoSNES; +using yaze::app::gfx::ConvertSNEStoRGB; +using yaze::app::gfx::Extract; +using yaze::app::gfx::snes_color; +using yaze::app::gfx::snes_palette; +using yaze::app::gfx::SNESPalette; + +namespace { +unsigned int test_convert(yaze::app::gfx::snes_color col) { + unsigned int toret; + toret = col.red << 16; + toret += col.green << 8; + toret += col.blue; + return toret; +} +} // namespace + +TEST(SNESPaletteTest, AddColor) { + yaze::app::gfx::SNESPalette palette; + yaze::app::gfx::SNESColor color; + palette.AddColor(color); + ASSERT_EQ(palette.size(), 1); +} + +TEST(SNESPaletteTest, GetColorOutOfBounds) { + yaze::app::gfx::SNESPalette palette; + std::vector colors(5); + palette.Create(colors); + + // Now try to get a color at an out-of-bounds index + ASSERT_THROW(palette.GetColor(10), std::exception); + ASSERT_THROW(palette[10], std::exception); +} + +TEST(SNESColorTest, ConvertRGBtoSNES) { + snes_color color = {132, 132, 132}; + uint16_t snes = ConvertRGBtoSNES(color); + ASSERT_EQ(snes, 0x4210); +} + +TEST(SNESColorTest, ConvertSNEStoRGB) { + uint16_t snes = 0x4210; + snes_color color = ConvertSNEStoRGB(snes); + ASSERT_EQ(color.red, 132); + ASSERT_EQ(color.green, 132); + ASSERT_EQ(color.blue, 132); +} + +TEST(SNESColorTest, ConvertSNESToRGB_Binary) { + uint16_t red = 0b0000000000011111; + uint16_t blue = 0b0111110000000000; + uint16_t green = 0b0000001111100000; + uint16_t purple = 0b0111110000011111; + snes_color testcolor; + + testcolor = ConvertSNEStoRGB(red); + ASSERT_EQ(0xFF0000, test_convert(testcolor)); + testcolor = ConvertSNEStoRGB(green); + ASSERT_EQ(0x00FF00, test_convert(testcolor)); + testcolor = ConvertSNEStoRGB(blue); + ASSERT_EQ(0x0000FF, test_convert(testcolor)); + testcolor = ConvertSNEStoRGB(purple); + ASSERT_EQ(0xFF00FF, test_convert(testcolor)); +} + +TEST(SNESColorTest, Extraction) { + // red, blue, green, purple + char data[8] = {0x1F, 0x00, 0x00, 0x7C, static_cast(0xE0), + 0x03, 0x1F, 0x7C}; + auto pal = Extract(data, 0, 4); + ASSERT_EQ(4, pal.size()); + ASSERT_EQ(0xFF0000, test_convert(pal[0])); + ASSERT_EQ(0x0000FF, test_convert(pal[1])); + ASSERT_EQ(0x00FF00, test_convert(pal[2])); + ASSERT_EQ(0xFF00FF, test_convert(pal[3])); +} + +TEST(SNESColorTest, Convert) { + // red, blue, green, purple white + char data[10] = {0x1F, + 0x00, + 0x00, + 0x7C, + static_cast(0xE0), + 0x03, + 0x1F, + 0x7C, + static_cast(0xFF), + 0x1F}; + auto pal = Extract(data, 0, 5); + auto snes_string = Convert(pal); + EXPECT_EQ(10, snes_string.size()); + EXPECT_THAT(data, ElementsAreArray(snes_string.data(), 10)); +} + +} // namespace gfx_test +} // namespace yaze_test \ No newline at end of file