diff --git a/.clang-format b/.clang-format index 4256a03b..125c32c7 100644 --- a/.clang-format +++ b/.clang-format @@ -1,6 +1,6 @@ Language: Cpp BasedOnStyle: LLVM -UseTab: Never +UseTab: Always IndentWidth: 4 TabWidth: 4 BreakBeforeBraces: Custom diff --git a/.gitmodules b/.gitmodules index 180fd589..5c057b9b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "GameResources"] - path = GameResources - url = https://github.com/Estrol/O2GameResources + path = resources + url = https://github.com/AlberttFrgk/O2GameResources diff --git a/.vscode/launch.json b/.vscode/launch.json index c7e17c48..32be3fc7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,11 +4,6 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "name": "(Windows) Attach", - "type": "cppvsdbg", - "request": "attach", - }, { "program": "${workspaceFolder}/build/Game/Game", "name": "Game - Linux Debug", @@ -16,23 +11,36 @@ "request": "launch", "args": [], "cwd": "${workspaceFolder}/build/Game", + "preLaunchTask": "cmake-gcc-build" }, { - "program": "${workspaceFolder}/build/Game/Debug/Game.exe", - "name": "Game - Windows Debug", - "type": "cppvsdbg", + "program": "${workspaceFolder}/build/Game/Game", + "name": "Game - Linux Release", + "type": "cppdbg", "request": "launch", "args": [], "cwd": "${workspaceFolder}/build/Game", + "preLaunchTask": "cmake-gcc-build" }, { - "name": "Game - Compile and Debug", - "type": "cppvsdbg", + "program": "${workspaceFolder}/build/Game/Debug/Game.exe", + "name": "Game - Windows Debug (LLDB)", + "type": "lldb", "request": "launch", - "preLaunchTask": "cmake-msvc-build", - "program": "${workspaceFolder}/build/Game/Debug/Game", + "args": [], "cwd": "${workspaceFolder}/build/Game/Debug", - "args": [] + "console": "integratedTerminal", + "preLaunchTask": "cmake-msvc-build" + }, + { + "program": "${workspaceFolder}/build/Game/Release/Game.exe", + "name": "Game - Windows Release (LLDB)", + "type": "lldb", + "request": "launch", + "args": [], + "cwd": "${workspaceFolder}/build/Game/Release", + "console": "integratedTerminal", + "preLaunchTask": "cmake-msvc-build" } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4f14948d..36fa5041 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,12 +4,22 @@ { "label": "cmake-gcc-build", "type": "shell", - "command": "cmake --build ${workspaceRoot}/build -- -j 4" + "command": "cmake --build ${workspaceRoot}/build -- -j 4", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true + } }, { "label": "cmake-msvc-build", "type": "shell", - "command": "cmake --build ${workspaceRoot}/build" + "command": "cmake --build ${workspaceRoot}/build", + "presentation": { + "reveal": "always", + "panel": "shared", + "focus": true + } } ] } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index f247054b..83827941 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.15) message("Unnamed O2 Game Clone Project - CMake Build System") # check if CXX_COMPILER is not cl.exe then set @@ -61,11 +61,13 @@ find_package(ZLIB REQUIRED) find_package(JPEG REQUIRED) find_package(BZip2 REQUIRED) find_package(Iconv REQUIRED) -find_package(Sol2 REQUIRED) -set(Sol2_LIBRARIES sol2) +find_package(sol2 CONFIG REQUIRED) +set(Sol2_LIBRARIES sol2::sol2) find_package(Lua REQUIRED) -set(Lua_LIBRARIES lua) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET libavcodec libavformat libavutil libswscale) find_package(Vulkan) @@ -87,57 +89,26 @@ include_directories( ${Iconv_INCLUDE_DIRS} ) -# third-party libraries -if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") -set(LIB_INDEX 3) -else() -set(LIB_INDEX 1) -endif() - -message("CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE} ${LIB_INDEX}") - if (WIN32) set(THIRD_PARTY_LIBS - "${PROJECT_SOURCE_DIR}/third-party/lib/${O2GAME_PLATFORM}-windows/${CMAKE_BUILD_TYPE}/bass.lib" - "${PROJECT_SOURCE_DIR}/third-party/lib/${O2GAME_PLATFORM}-windows/${CMAKE_BUILD_TYPE}/bass_fx.lib" + "${PROJECT_SOURCE_DIR}/third-party/lib/${O2GAME_PLATFORM}-windows/$/bass.lib" + "${PROJECT_SOURCE_DIR}/third-party/lib/${O2GAME_PLATFORM}-windows/$/bass_fx.lib" ) elseif (UNIX AND NOT APPLE) set(THIRD_PARTY_LIBS - "${PROJECT_SOURCE_DIR}/third-party/lib/${O2GAME_PLATFORM}-linux/${CMAKE_BUILD_TYPE}/libbass.so" - "${PROJECT_SOURCE_DIR}/third-party/lib/${O2GAME_PLATFORM}-linux/${CMAKE_BUILD_TYPE}/libbass_fx.so" + "${PROJECT_SOURCE_DIR}/third-party/lib/${O2GAME_PLATFORM}-linux/$/libbass.so" + "${PROJECT_SOURCE_DIR}/third-party/lib/${O2GAME_PLATFORM}-linux/$/libbass_fx.so" dl pthread m ) endif() -# check if FREETYPE_LIBRAIES is a list then check first index if optimized -if (FREETYPE_LIBRARIES MATCHES ";") - list(GET FREETYPE_LIBRARIES ${LIB_INDEX} LIB_FREETYPE) -else() - set(LIB_FREETYPE ${FREETYPE_LIBRARIES}) -endif() - -if (JPEG_LIBRARIES MATCHES ";") - list(GET JPEG_LIBRARIES ${LIB_INDEX} LIB_JPEG) -else() - set(LIB_JPEG ${JPEG_LIBRARIES}) -endif() - -list(GET PNG_LIBRARIES ${LIB_INDEX} LIB_PNG) - -if (PNG_LIBRARIES MATCHES ";") - list(GET PNG_LIBRARIES ${LIB_INDEX} LIB_PNG) -else() - set(LIB_PNG ${PNG_LIBRARIES}) -endif() - -if (BZIP2_LIBRARIES MATCHES ";") - list(GET BZIP2_LIBRARIES ${LIB_INDEX} LIB_BZIP2) -else() - set(LIB_BZIP2 ${BZIP2_LIBRARIES}) -endif() +set(LIB_FREETYPE Freetype::Freetype) +set(LIB_JPEG JPEG::JPEG) +set(LIB_PNG PNG::PNG) +set(LIB_BZIP2 BZip2::BZip2) if (WIN32) set(VOLK_STATIC_DEFINES VK_USE_PLATFORM_WIN32_KHR) @@ -150,13 +121,14 @@ set(O2GAME_LIBRARIES ${SDL2_IMAGE_LIBRARIES} ${SQLite3_LIBRARIES} ${Sol2_LIBRARIES} - ${Lua_LIBRARIES} + ${LUA_LIBRARIES} ${LIB_FREETYPE} ${LIB_PNG} ${LIB_JPEG} ${LIB_BZIP2} ${THIRD_PARTY_LIBS} ${Iconv_LIBRARIES} + PkgConfig::FFMPEG ) message("Libraries: ${O2GAME_LIBRARIES}") diff --git a/CMakePresets.json b/CMakePresets.json index 82af92c5..b7317bd9 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -4,7 +4,7 @@ { "name": "windows-base", "hidden": true, - "generator": "Visual Studio 17 2022", + "generator": "Visual Studio 18 2026", "binaryDir": "${sourceDir}/build", "installDir": "${sourceDir}/build", "cacheVariables": { @@ -14,7 +14,7 @@ }, "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", "toolset": { - "value": "host=x64,version=14.29", + "value": "host=x64", "strategy": "external" }, "condition": { diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 411b81bc..b4a7111b 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.0.0) +cmake_minimum_required(VERSION 3.15) project(EstEngine VERSION 0.1.0 LANGUAGES C CXX) @@ -99,12 +99,9 @@ add_library(EstEngine "src/Game.cpp" ) -if (WIN32) - set(glslangValidator "$ENV{VULKAN_SDK}/Bin/glslangValidator.exe") -elseif (UNIX AND NOT APPLE) - set(glslangValidator "$ENV{VULKAN_SDK}/Bin/glslangValidator") -else() - message(FATAL_ERROR "Unsupported platform") +find_program(glslangValidator NAMES glslangValidator PATHS "${CMAKE_SOURCE_DIR}/build/vcpkg_installed/x64-windows/tools/glslang" "$ENV{VULKAN_SDK}/Bin") +if(NOT glslangValidator) + message(FATAL_ERROR "glslangValidator not found! Ensure it is installed via vcpkg or VULKAN_SDK.") endif() execute_process( diff --git a/Engine/include/Game.h b/Engine/include/Game.h index 4c259f58..08dd6c3e 100644 --- a/Engine/include/Game.h +++ b/Engine/include/Game.h @@ -1,4 +1,5 @@ #pragma once +#include #include "Inputs/InputManager.h" #include "Overlay.h" #include "Rendering/Renderer.h" @@ -51,6 +52,7 @@ class Game virtual void Render(double deltaTime); virtual void Input(double deltaTime); virtual void Mouse(double deltaTime); + virtual void OnDropFile(std::string path); GameWindow *m_window; Renderer *m_renderer; @@ -87,4 +89,7 @@ class Game GameThread mRenderThread; GameThread mAudioThread; GameThread mLocalThread; -}; \ No newline at end of file + + std::mutex m_mutex; + std::condition_variable m_cv; +}; diff --git a/Engine/include/Rendering/Renderer.h b/Engine/include/Rendering/Renderer.h index 5ea4aa6e..f0f6eb56 100644 --- a/Engine/include/Rendering/Renderer.h +++ b/Engine/include/Rendering/Renderer.h @@ -11,7 +11,6 @@ enum class RendererMode { OPENGL, // 0 VULKAN, // 1 DIRECTX, // 2 - DIRECTX11, // 3 DIRECTX12, // 4 METAL, // 5 }; diff --git a/Engine/include/Rendering/Threading/GameThread.h b/Engine/include/Rendering/Threading/GameThread.h index 8b61218c..e15a4378 100644 --- a/Engine/include/Rendering/Threading/GameThread.h +++ b/Engine/include/Rendering/Threading/GameThread.h @@ -2,6 +2,7 @@ #include #include #include +#include class GameThread { @@ -19,6 +20,8 @@ class GameThread bool m_run; bool m_background; + std::mutex m_mutex; + std::thread m_thread; std::function m_main_cb; diff --git a/Engine/include/Rendering/Vulkan/Texture2DVulkan.h b/Engine/include/Rendering/Vulkan/Texture2DVulkan.h index a858da16..079a59e3 100644 --- a/Engine/include/Rendering/Vulkan/Texture2DVulkan.h +++ b/Engine/include/Rendering/Vulkan/Texture2DVulkan.h @@ -22,9 +22,12 @@ namespace vkTexture { Texture2D_Vulkan *TexLoadImage(std::filesystem::path imagePath); Texture2D_Vulkan *TexLoadImage(void *buffer, size_t size); + Texture2D_Vulkan *AllocateTexture(int width, int height); Texture2D_Vulkan *GetDummyImage(); VkDescriptorSet GetVkDescriptorSet(Texture2D_Vulkan *image); + void UpdateTexture(Texture2D_Vulkan *handle, void *buffer, int pitch); + void QueryTexture(Texture2D_Vulkan *handle, int &outWidth, int &outHeight); void ReleaseTexture(Texture2D_Vulkan *handle); diff --git a/Engine/include/Rendering/Window.h b/Engine/include/Rendering/Window.h index 6ce2e40d..4f3b575e 100644 --- a/Engine/include/Rendering/Window.h +++ b/Engine/include/Rendering/Window.h @@ -26,7 +26,7 @@ class GameWindow float GetHeightScale(); void SetScaleOutput(bool value); - bool IsScaleOutput(); + bool IsScaleOutput() const; void SetWindowTitle(std::string &title); void SetWindowSubTitle(std::string &subTitle); diff --git a/Engine/include/Texture/NumericTexture.h b/Engine/include/Texture/NumericTexture.h index aef50a45..a34e6f8a 100644 --- a/Engine/include/Texture/NumericTexture.h +++ b/Engine/include/Texture/NumericTexture.h @@ -40,4 +40,5 @@ class NumericTexture protected: std::vector m_numericsTexture; std::map m_numbericsWidth; + int m_maxWidth = 0; }; \ No newline at end of file diff --git a/Engine/include/Texture/Sprite2D.h b/Engine/include/Texture/Sprite2D.h index 82ec986c..c14e9ce5 100644 --- a/Engine/include/Texture/Sprite2D.h +++ b/Engine/include/Texture/Sprite2D.h @@ -18,14 +18,16 @@ class Sprite2D public: Sprite2D() = default; - Sprite2D(std::vector textures, float delay = 1.0f); - Sprite2D(std::vector textures, float delay = 1.0f); - Sprite2D(std::vector textures, float delay = 1.0f); - Sprite2D(std::vector textures, float delay = 1.0f); + Sprite2D(std::vector textures, double delay = 1.0); + Sprite2D(std::vector textures, double delay = 1.0); + Sprite2D(std::vector textures, double delay = 1.0); + Sprite2D(std::vector textures, double delay = 1.0); ~Sprite2D(); bool AlphaBlend; + bool FlipX = false; + bool FlipY = false; Vector2 AnchorPoint; UDim2 Position; UDim2 Position2; @@ -33,15 +35,24 @@ class Sprite2D void Draw(double delta, bool manual = false); void Draw(double delta, Rect *rect, bool manual = false); + void DrawStop(double delta, bool manual = false); + void DrawOnce(double delta, bool manual = false); Texture2D *GetTexture(); - void SetFPS(float fps); + void SetFPS(double fps); void Reset(); + void SetIndex(int index) { m_currentIndex = index; } + int GetFrameCount() const { return static_cast(m_textures.size()); } + + double m_spritespeed = 1.0; private: - double m_delay = 1.0; float m_currentTime = 0; int m_currentIndex = 0; + bool m_drawOnce = false; + std::vector m_textures; + + void DrawInternal(double delta, bool playOnce, Rect* rect, bool manual); }; diff --git a/Engine/include/Texture/Texture2D.h b/Engine/include/Texture/Texture2D.h index 8596abd0..490fb110 100644 --- a/Engine/include/Texture/Texture2D.h +++ b/Engine/include/Texture/Texture2D.h @@ -20,9 +20,10 @@ class Texture2D public: Texture2D(); - Texture2D(std::string fileName); - Texture2D(std::filesystem::path path); - Texture2D(uint8_t *fileData, size_t size); + Texture2D(const std::string& fileName); + Texture2D(int width, int height); + Texture2D(const std::filesystem::path& path); + Texture2D(const uint8_t *fileData, size_t size); Texture2D(SDL_Texture *texture); Texture2D(Texture2D_Vulkan *texture); ~Texture2D(); @@ -33,9 +34,12 @@ class Texture2D void Draw(Rect *clipRect, bool manualDraw); void CalculateSize(); + void UpdateTexture(uint8_t* buffer, int width, int height, int pitch); float Transparency; float Rotation; + bool FlipX = false; + bool FlipY = false; bool AlphaBlend; bool m_ready = false; @@ -51,7 +55,7 @@ class Texture2D Rect GetOriginalRECT(); void SetOriginalRECT(Rect size); - static Texture2D *FromTexture2D(Texture2D *tex); + // static Texture2D *FromTexture2D(Texture2D *tex); static Texture2D *FromBMP(uint8_t *fileData, size_t size); static Texture2D *FromBMP(std::string fileName); diff --git a/Engine/src/Configuration.cpp b/Engine/src/Configuration.cpp index 1628296e..530a53f9 100644 --- a/Engine/src/Configuration.cpp +++ b/Engine/src/Configuration.cpp @@ -86,7 +86,7 @@ void Configuration::Set(std::string key, std::string prop, std::string value) void Configuration::Font_SetPath(std::filesystem::path path) { - FontPath = path; + FontPath = std::filesystem::current_path() / "Resources"; } std::filesystem::path Configuration::Font_GetPath() diff --git a/Engine/src/Fonts/FontResources.cpp b/Engine/src/Fonts/FontResources.cpp index 43b91ba1..f2435516 100644 --- a/Engine/src/Fonts/FontResources.cpp +++ b/Engine/src/Fonts/FontResources.cpp @@ -25,9 +25,9 @@ // BEGIN FONT FALLBACK #include "FallbackFonts/arial.ttf.h" -#include "FallbackFonts/ch.ttf.h" -#include "FallbackFonts/jp.ttf.h" -#include "FallbackFonts/kr.ttf.h" +//#include "FallbackFonts/ch.ttf.h" +//#include "FallbackFonts/jp.ttf.h" +//#include "FallbackFonts/kr.ttf.h" #include // END FONT FALLBACK @@ -92,14 +92,37 @@ void FontResources::PreloadFontCaches() GameWindow *wnd = GameWindow::GetInstance(); - auto skinPath = Configuration::Font_GetPath(); - auto fontPath = skinPath / "Fonts"; + auto path = Configuration::Font_GetPath(); + auto fontPath = path / "Fonts"; auto normalfont = fontPath / "normal.ttf"; auto jpFont = fontPath / "jp.ttf"; auto krFont = fontPath / "kr.ttf"; auto chFont = fontPath / "ch.ttf"; + static const ImWchar glyphRanges[] = { // Optimized + (ImWchar)0x0020, (ImWchar)0x052F, + (ImWchar)0x2000, (ImWchar)0x27BF, + (ImWchar)0x2E80, (ImWchar)0x2FA1, + (ImWchar)0x1F300, (ImWchar)0x1FAFF, + (ImWchar)0x2660, (ImWchar)0x2663, + (ImWchar)0x2665, (ImWchar)0x2666, + (ImWchar)0x2600, (ImWchar)0x2606, + (ImWchar)0x2618, (ImWchar)0x2619, + (ImWchar)0x263A, (ImWchar)0x263B, + (ImWchar)0x2708, (ImWchar)0x2714, + (ImWchar)0x2728, (ImWchar)0x2734, + (ImWchar)0x2740, (ImWchar)0x274B, + (ImWchar)0x2756, (ImWchar)0x2758, + (ImWchar)0x2764, (ImWchar)0x2767, + (ImWchar)0x2794, (ImWchar)0x27BE, + (ImWchar)0x27F0, (ImWchar)0x27FF, + (ImWchar)0x2900, (ImWchar)0x297F, + (ImWchar)0x2A00, (ImWchar)0x2AFF, + (ImWchar)0x0000, (ImWchar)0x0000 + }; + + { float originScale = (wnd->GetBufferWidth() + wnd->GetBufferHeight()) / 15.6f; float targetScale = (wnd->GetWidth() + wnd->GetHeight()) / 15.6f; @@ -118,9 +141,9 @@ void FontResources::PreloadFontCaches() { if (std::filesystem::exists(normalfont)) { - Font.Font = io.Fonts->AddFontFromFileTTF((const char *)normalfont.u8string().c_str(), fontSize, &conf); + Font.Font = io.Fonts->AddFontFromFileTTF((const char *)normalfont.u8string().c_str(), fontSize, &conf, glyphRanges); // glyphRanges, fixing missing fonts } else { - Font.Font = io.Fonts->AddFontFromMemoryTTF((void *)get_arial_font_data(), get_arial_font_size(), fontSize, &conf); + Font.Font = io.Fonts->AddFontFromMemoryTTF((void *)get_arial_font_data(), get_arial_font_size(), fontSize, &conf, glyphRanges); } } @@ -151,7 +174,7 @@ void FontResources::PreloadFontCaches() case TextRegion::Chinese: { if (std::filesystem::exists(chFont)) { - io.Fonts->AddFontFromFileTTF((const char *)chFont.u8string().c_str(), fontSize, &conf, io.Fonts->GetGlyphRangesChineseSimplifiedCommon()); + io.Fonts->AddFontFromFileTTF((const char *)chFont.u8string().c_str(), fontSize, &conf, io.Fonts->GetGlyphRangesChineseFull()); } break; @@ -204,6 +227,7 @@ void FontResources::PreloadFontCaches() } else { ImGui_ImplSDLRenderer2_DestroyDeviceObjects(); ImGui_ImplSDLRenderer2_DestroyFontsTexture(); + ImGui_ImplSDLRenderer2_CreateFontsTexture(); } gImFontRebuild = false; diff --git a/Engine/src/Game.cpp b/Engine/src/Game.cpp index 8194d65a..d7a3318e 100644 --- a/Engine/src/Game.cpp +++ b/Engine/src/Game.cpp @@ -16,9 +16,10 @@ constexpr auto kInputDefaultRate = 1000.0; constexpr auto kMenuDefaultRate = 60.0; -constexpr auto kAudioDefaultRate = 24.0; +constexpr auto kAudioDefaultRate = 60.0; namespace { + bool InitSDL() { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) { @@ -39,21 +40,40 @@ namespace { double FrameLimit(double MaxFrameRate) { - double newTick = SDL_GetTicks(); - double targetTick = lastTick + 1000.0 / MaxFrameRate; - - // If the frame rate is too high, wait to avoid shaky animations - if (newTick < targetTick) { - double delayTicks = targetTick - newTick; - SDL_Delay(static_cast(delayTicks)); - newTick += delayTicks; + double newTick = static_cast(SDL_GetPerformanceCounter()) * 1000.0 / SDL_GetPerformanceFrequency(); + + if (MaxFrameRate > 0.0) { + const double targetFrameTime = 1000.0 / MaxFrameRate; + double frameTime = newTick - curTick; + + if (frameTime < targetFrameTime) + { + double delayTime = targetFrameTime - frameTime; + + // Sleep to save CPU if we have more than 2ms to wait + if (delayTime > 2.0) { + std::this_thread::sleep_for(std::chrono::milliseconds(static_cast(delayTime - 2.0))); + } + + // Spin-lock for precision + while (true) { + newTick = static_cast(SDL_GetPerformanceCounter()) * 1000.0 / SDL_GetPerformanceFrequency(); + if (newTick - curTick >= targetFrameTime) { + break; + } + std::this_thread::yield(); + } + } } double delta = (newTick - curTick) / 1000.0; - curTick = newTick; - // Update lastTick for the next frame lastTick = curTick; + curTick = newTick; + + // Prevent crazy large delta on pause or huge lag spike + if (delta > 0.1) delta = 0.1; + if (delta <= 0.0) delta = 0.0001; // Prevent zero delta return delta; } @@ -79,10 +99,9 @@ Game::~Game() if (m_running) { Stop(); - // Wait for threads to finish - // TODO: Figure a way to do this without using sleep - while (m_notify) { - SDL_Delay(500); + if (m_notify) { + std::unique_lock lock(m_mutex); + m_cv.wait(lock, [this] { return !m_notify; }); } } @@ -123,8 +142,8 @@ bool Game::Init() return false; } - if (m_renderMode == RendererMode::OPENGL) { - m_threadMode = ThreadMode::SINGLE_THREAD; // OpenGL doesnt support multithreading + if (!m_renderer->IsVulkan()) { + m_threadMode = ThreadMode::SINGLE_THREAD; // SDL_Renderer doesn't support multithreading } m_inputManager = InputManager::GetInstance(); @@ -153,8 +172,8 @@ void Game::Run() mAudioThread.Run([&] { double delta = FrameLimit(kAudioDefaultRate); AudioManager::GetInstance()->Update(delta); - }, - true); + }, + true); std::mutex m1, m2; @@ -169,7 +188,7 @@ void Game::Run() switch (m_frameLimitMode) { case FrameLimitMode::GAME: { - delta = FrameLimit(m_frameLimit); + delta = FrameLimit(m_frameLimit); break; } @@ -180,34 +199,34 @@ void Game::Run() } } - CheckFont(); + CheckFont(); - Update(delta); + Update(delta); - UpdateFade(delta); + UpdateFade(delta); - if (!m_minimized && m_renderer->BeginRender()) { - if (frameWithoutSwapchain > 0) { - Logs::Puts("[Game] Game iterate %d times without swap chain", frameWithoutSwapchain); - frameWithoutSwapchain = 0; - } + if (!m_minimized && m_renderer->BeginRender()) { + if (frameWithoutSwapchain > 0) { + Logs::Puts("[Game] Game iterate %d times without swap chain", frameWithoutSwapchain); + frameWithoutSwapchain = 0; + } - std::lock_guard lock(m1); + std::lock_guard lock(m1); - Render(delta); - MsgBox::Draw(); - DrawFade(delta); + Render(delta); + MsgBox::Draw(); + DrawFade(delta); - DrawConsole(); - m_renderer->EndRender(); + DrawConsole(); + m_renderer->EndRender(); - frames++; + frames++; } else { - frameWithoutSwapchain++; - } + frameWithoutSwapchain++; + } } else { - FrameLimit(15.0f); - } + FrameLimit(15.0f); + } }, true); @@ -217,7 +236,7 @@ void Game::Run() } else { m_sceneManager->OnKeyUp(state); } - }); + }); m_inputManager->ListenMouseEvent([&](const MouseState &state) { if (state.isDown) { @@ -225,23 +244,23 @@ void Game::Run() } else { m_sceneManager->OnMouseUp(state); } - }); + }); m_minimized = false; mLocalThread.Run([&] { double delta = 0; switch (m_frameLimitMode) { - case FrameLimitMode::GAME: - { - delta = FrameLimit(m_threadMode == ThreadMode::MULTI_THREAD ? kInputDefaultRate : m_frameLimit); - break; - } + case FrameLimitMode::GAME: + { + delta = FrameLimit(m_threadMode == ThreadMode::MULTI_THREAD ? kInputDefaultRate : m_frameLimit); + break; + } - case FrameLimitMode::MENU: - { - delta = FrameLimit(kMenuDefaultRate); - break; - } + case FrameLimitMode::MENU: + { + delta = FrameLimit(kMenuDefaultRate); + break; + } } m_imguiInterval += delta; @@ -249,9 +268,13 @@ void Game::Run() SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { - case SDL_QUIT: - MsgBox::Show("Quit", "Quit confirmation", "Are you sure you want to quit?", MsgBoxType::YESNO); - break; + case SDL_QUIT: + MsgBox::Show("Quit", "Quit confirmation", "Are you sure you want to quit?", MsgBoxType::YESNO); + break; + case SDL_DROPFILE: + OnDropFile(event.drop.file); + SDL_free(event.drop.file); + break; } m_minimized = SDL_GetWindowFlags(m_window->GetWindow()) & SDL_WINDOW_MINIMIZED; @@ -305,8 +328,8 @@ void Game::Run() frames = 0; time = 0; } - }, - false); + }, + false); while (m_running) { mLocalThread.Update(); @@ -367,9 +390,17 @@ void Game::CheckFont() void Game::Stop() { - m_running = false; + if (m_running) { + { + std::unique_lock lock(m_mutex); + m_running = false; + m_notify = true; + } + m_cv.notify_one(); + } } + void Game::SetThreadMode(ThreadMode mode) { m_threadMode = mode; @@ -447,3 +478,7 @@ void Game::Input(double deltaTime) void Game::Mouse(double deltaTime) { } + +void Game::OnDropFile(std::string path) +{ +} diff --git a/Engine/src/Rendering/GameWindow.cpp b/Engine/src/Rendering/GameWindow.cpp index 9d2cf067..60bdbb19 100644 --- a/Engine/src/Rendering/GameWindow.cpp +++ b/Engine/src/Rendering/GameWindow.cpp @@ -193,7 +193,7 @@ void GameWindow::SetScaleOutput(bool value) m_scaleOutput = value; } -bool GameWindow::IsScaleOutput() +bool GameWindow::IsScaleOutput() const { return m_scaleOutput; } diff --git a/Engine/src/Rendering/Renderer.cpp b/Engine/src/Rendering/Renderer.cpp index 294d2065..8664d089 100644 --- a/Engine/src/Rendering/Renderer.cpp +++ b/Engine/src/Rendering/Renderer.cpp @@ -1,329 +1,319 @@ #include "Rendering/Renderer.h" #include "../Data/Imgui/imgui_impl_sdl2.h" #include "../Data/Imgui/imgui_impl_sdlrenderer2.h" -#include "../Data/SDLRenderStruct.h" #include "Exception/SDLException.h" #include "Imgui/ImguiUtil.h" #include "MsgBox.h" #include "Rendering/Vulkan/VulkanEngine.h" #include -#include -#include +#include "Configuration.h" + +#if defined(_WIN32) +#include +#include +#include +#pragma comment(lib, "dxgi.lib") +#pragma comment(lib, "d3d11.lib") +#endif constexpr auto MAIN_SPRITE_BATCH = 0; -Renderer::Renderer() -{ - m_blendMode = SDL_BLENDMODE_NONE; - m_renderer = NULL; +Renderer::Renderer() { + m_blendMode = SDL_BLENDMODE_NONE; + m_renderer = NULL; } -Renderer::~Renderer() -{ - Destroy(); -} +Renderer::~Renderer() { Destroy(); } Renderer *Renderer::s_instance = nullptr; -bool Renderer::Create(RendererMode mode, GameWindow *window, bool failed) -{ - m_window = window; - - try { - std::string rendererName = ""; - bool bUsedSDLRenderer = true; - - switch (mode) { - case RendererMode::OPENGL: - { - rendererName = "opengl"; - break; - } - - case RendererMode::VULKAN: - { - bUsedSDLRenderer = false; - break; - } - - case RendererMode::DIRECTX: - { - rendererName = "direct3d"; - break; - } - - case RendererMode::DIRECTX11: - { - rendererName = "direct3d11"; - break; - } - - case RendererMode::DIRECTX12: - { - rendererName = "direct3d12"; - break; - } - - case RendererMode::METAL: - { - rendererName = "metal"; - break; - } +bool Renderer::Create(RendererMode mode, GameWindow *window, bool failed) { + m_window = window; + + try { + std::string rendererName = ""; + bool bUsedSDLRenderer = true; + + switch (mode) { + case RendererMode::OPENGL: { + rendererName = "opengl"; + break; + } + + case RendererMode::VULKAN: { + bUsedSDLRenderer = false; + break; + } + + case RendererMode::DIRECTX: { + rendererName = "direct3d"; + break; + } + + case RendererMode::DIRECTX12: { + rendererName = "direct3d12"; + break; + } + + case RendererMode::METAL: { + rendererName = "metal"; + break; + } + } + + if (bUsedSDLRenderer) { + // loop and find the first available renderer + if (!failed) { + bool found = false; + int numRenderers = SDL_GetNumRenderDrivers(); + for (int i = 0; i < numRenderers; i++) { + SDL_RendererInfo info; + SDL_GetRenderDriverInfo(i, &info); + + if (info.flags & SDL_RENDERER_SOFTWARE) { + continue; + } + + if (rendererName == info.name) { + found = true; + break; + } } - if (bUsedSDLRenderer) { - // loop and find the first available renderer - if (!failed) { - bool found = false; - int numRenderers = SDL_GetNumRenderDrivers(); - for (int i = 0; i < numRenderers; i++) { - SDL_RendererInfo info; - SDL_GetRenderDriverInfo(i, &info); - - if (info.flags & SDL_RENDERER_SOFTWARE) { - continue; - } - - if (rendererName == info.name) { - found = true; - break; - } - } - - if (!found) { - // fallback - return Create(mode, window, true); - } - - SDL_SetHint(SDL_HINT_RENDER_DRIVER, rendererName.c_str()); - SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); // STOP CHANGING THIS, LEAVE IT "as is" - } - - m_renderer = SDL_CreateRenderer(window->GetWindow(), -1, SDL_RENDERER_ACCELERATED); - if (!m_renderer) { - throw SDLException(); - } - - if (failed) { - Logs::Puts("[Renderer] Failed to create renderer with backend: %s, and fallback to %s", rendererName.c_str(), SDL_GetCurrentVideoDriver()); - } - - m_blendMode = SDL_ComposeCustomBlendMode( - SDL_BLENDFACTOR_SRC_ALPHA, - SDL_BLENDFACTOR_ONE, - SDL_BLENDOPERATION_ADD, - SDL_BLENDFACTOR_ONE, - SDL_BLENDFACTOR_ZERO, - SDL_BLENDOPERATION_ADD); - - if (SDL_SetRenderDrawBlendMode(m_renderer, m_blendMode) <= -1) { - throw SDLException(); - } - - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - - ImGuiIO &io = ImGui::GetIO(); - (void)io; - - // Manually set the display size - io.DisplaySize.x = (float)window->GetWidth(); - io.DisplaySize.y = (float)window->GetHeight(); - io.DisplayOutputSize.x = (float)window->GetWidth(); - io.DisplayOutputSize.y = (float)window->GetHeight(); - io.IniFilename = NULL; - io.WantSaveIniSettings = false; - - ImGui::StyleColorsDark(); - - if (!ImGui_ImplSDL2_InitForSDLRenderer(window->GetWindow(), m_renderer)) { - throw SDLException(); - } - - if (!ImGui_ImplSDLRenderer2_Init(m_renderer)) { - throw SDLException(); - } - - return true; - } else { - try { - m_vulkan = VulkanEngine::GetInstance(); - m_vulkan->init(window->GetWindow(), window->GetWidth(), window->GetHeight()); - } catch (const std::runtime_error &) { - m_vulkan = nullptr; - - MsgBox::ShowOut("EstEngine Error", "Failed to load vulkan functions, fallback to OpenGL", MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); - return Create(RendererMode::OPENGL, window); - } - - ImGuiIO &io = ImGui::GetIO(); - (void)io; - ImguiUtil::SetVulkan(true); - - // Manually set the display size - io.DisplaySize.x = (float)window->GetWidth(); - io.DisplaySize.y = (float)window->GetHeight(); - io.DisplayOutputSize.x = (float)window->GetWidth(); - io.DisplayOutputSize.y = (float)window->GetHeight(); - io.IniFilename = NULL; - io.WantSaveIniSettings = false; - - return true; + if (!found) { + // fallback + return Create(mode, window, true); } - } catch (SDLException &e) { - MsgBox::ShowOut("EstEngine Error", e.what(), MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); - return false; - } -} -bool Renderer::Resize() -{ - try { - GameWindow *window = GameWindow::GetInstance(); + SDL_SetHint(SDL_HINT_RENDER_DRIVER, rendererName.c_str()); + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best"); + } + + std::string frameLimitStr = Configuration::Load("Game", "FrameLimit"); + Uint32 renderFlags = SDL_RENDERER_ACCELERATED; + if (frameLimitStr == "VSync") { + renderFlags |= SDL_RENDERER_PRESENTVSYNC; + } + + m_renderer = SDL_CreateRenderer( + window->GetWindow(), -1, + renderFlags); + if (!m_renderer) { + throw SDLException(); + } + +#if defined(_WIN32) +#endif + + if (failed) { + Logs::Puts("[Renderer] Failed to create renderer with backend: %s, and " + "fallback to %s", + rendererName.c_str(), SDL_GetCurrentVideoDriver()); + } + + m_blendMode = SDL_ComposeCustomBlendMode( + SDL_BLENDFACTOR_SRC_ALPHA, SDL_BLENDFACTOR_ONE, + SDL_BLENDOPERATION_ADD, SDL_BLENDFACTOR_ONE, SDL_BLENDFACTOR_ZERO, + SDL_BLENDOPERATION_ADD); + + if (SDL_SetRenderDrawBlendMode(m_renderer, m_blendMode) <= -1) { + throw SDLException(); + } + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); - int new_width = window->GetWidth(); - int new_height = window->GetHeight(); + ImGuiIO &io = ImGui::GetIO(); + (void)io; - ImGuiIO &io = ImGui::GetIO(); - (void)io; + // Manually set the display size + io.DisplaySize.x = (float)window->GetWidth(); + io.DisplaySize.y = (float)window->GetHeight(); + io.DisplayOutputSize.x = (float)window->GetWidth(); + io.DisplayOutputSize.y = (float)window->GetHeight(); + io.IniFilename = NULL; + io.WantSaveIniSettings = false; - // Manually set the display size - io.DisplaySize.x = (float)new_width; - io.DisplaySize.y = (float)new_height; - io.DisplayOutputSize.x = (float)new_width; - io.DisplayOutputSize.y = (float)new_height; + ImGui::StyleColorsDark(); - if (!IsVulkan()) { - if (SDL_RenderSetLogicalSize(m_renderer, new_width, new_height) == -1) { - throw SDLException(); - } + if (!ImGui_ImplSDL2_InitForSDLRenderer(window->GetWindow(), m_renderer)) { + throw SDLException(); + } + + if (!ImGui_ImplSDLRenderer2_Init(m_renderer)) { + throw SDLException(); + } + + return true; + } else { + try { + m_vulkan = VulkanEngine::GetInstance(); + + std::string frameLimitStr = Configuration::Load("Game", "FrameLimit"); + if (frameLimitStr == "VSync") { + m_vulkan->set_vsync(true); } - return true; - } catch (SDLException &e) { - MsgBox::ShowOut("EstEngine Error", e.what(), MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); - return false; + m_vulkan->init(window->GetWindow(), window->GetWidth(), + window->GetHeight()); + } catch (const std::exception &) { + m_vulkan = nullptr; + VulkanEngine::Release(); + + MsgBox::ShowOut("EstEngine Error", + "Failed to load vulkan functions, fallback to OpenGL", + MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); + return Create(RendererMode::OPENGL, window, true); + } + + ImGuiIO &io = ImGui::GetIO(); + (void)io; + ImguiUtil::SetVulkan(true); + + // Manually set the display size + io.DisplaySize.x = (float)window->GetWidth(); + io.DisplaySize.y = (float)window->GetHeight(); + io.DisplayOutputSize.x = (float)window->GetWidth(); + io.DisplayOutputSize.y = (float)window->GetHeight(); + io.IniFilename = NULL; + io.WantSaveIniSettings = false; + + return true; } + } catch (SDLException &e) { + MsgBox::ShowOut("EstEngine Error", e.what(), MsgBoxType::OK, + MsgBoxFlags::BTN_ERROR); + return false; + } } -bool Renderer::BeginRender() -{ - if (IsVulkan()) { - if (m_vulkan->_swapChainOutdated) { - return false; - } +bool Renderer::Resize() { + try { + GameWindow *window = GameWindow::GetInstance(); - m_vulkan->begin(); - } else { - SDL_SetRenderDrawColor(m_renderer, 0, 0, 0, 255); - SDL_RenderClear(m_renderer); + int new_width = window->GetWidth(); + int new_height = window->GetHeight(); + + ImGuiIO &io = ImGui::GetIO(); + (void)io; + + // Manually set the display size + io.DisplaySize.x = (float)new_width; + io.DisplaySize.y = (float)new_height; + io.DisplayOutputSize.x = (float)new_width; + io.DisplayOutputSize.y = (float)new_height; + + if (!IsVulkan()) { + if (SDL_RenderSetLogicalSize(m_renderer, new_width, new_height) == -1) { + throw SDLException(); + } } return true; + } catch (SDLException &e) { + MsgBox::ShowOut("EstEngine Error", e.what(), MsgBoxType::OK, + MsgBoxFlags::BTN_ERROR); + return false; + } } -bool Renderer::EndRender() -{ - if (IsVulkan()) { - m_vulkan->flush_queue(); +bool Renderer::BeginRender() { + if (IsVulkan()) { + if (m_vulkan->_swapChainOutdated) { + return false; + } - if (ImguiUtil::HasFrameQueue()) { - ImguiUtil::Reset(); - } + m_vulkan->begin(); + } else { + SDL_SetRenderDrawColor(m_renderer, 0, 0, 0, 255); + SDL_RenderClear(m_renderer); + } - m_vulkan->end(); - } else { - if (ImguiUtil::HasFrameQueue()) { - ImguiUtil::Reset(); + return true; +} - ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData()); - } +bool Renderer::EndRender() { + if (IsVulkan()) { + m_vulkan->flush_queue(); - SDL_RenderPresent(m_renderer); + if (ImguiUtil::HasFrameQueue()) { + ImguiUtil::Reset(); } - return true; -} + m_vulkan->end(); + } else { + if (ImguiUtil::HasFrameQueue()) { + ImguiUtil::Reset(); -void Renderer::ResetImGui() -{ -} + ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData()); + } -SDL_Renderer *Renderer::GetSDLRenderer() -{ - return m_renderer; -} + SDL_RenderPresent(m_renderer); + } -SDL_BlendMode Renderer::GetSDLBlendMode() -{ - return m_blendMode; + return true; } -VulkanEngine *Renderer::GetVulkanEngine() -{ - return m_vulkan; -} +void Renderer::ResetImGui() {} -bool Renderer::ReInitVulkan() -{ - int new_width = m_window->GetWidth(); - int new_height = m_window->GetHeight(); +SDL_Renderer *Renderer::GetSDLRenderer() { return m_renderer; } - m_vulkan->re_init_swapchains(new_width, new_height); +SDL_BlendMode Renderer::GetSDLBlendMode() { return m_blendMode; } - return m_vulkan->_swapChainOutdated == false; -} +VulkanEngine *Renderer::GetVulkanEngine() { return m_vulkan; } + +bool Renderer::ReInitVulkan() { + int new_width = m_window->GetWidth(); + int new_height = m_window->GetHeight(); + + m_vulkan->re_init_swapchains(new_width, new_height); -bool Renderer::IsVulkan() -{ - return GetVulkanEngine() != nullptr; + return m_vulkan->_swapChainOutdated == false; } -Renderer *Renderer::GetInstance() -{ - if (s_instance == nullptr) { - s_instance = new Renderer(); - } +bool Renderer::IsVulkan() { return GetVulkanEngine() != nullptr; } + +Renderer *Renderer::GetInstance() { + if (s_instance == nullptr) { + s_instance = new Renderer(); + } - return s_instance; + return s_instance; } -void Renderer::Release() -{ - if (s_instance != nullptr) { - delete s_instance; - s_instance = nullptr; - } +void Renderer::Release() { + if (s_instance != nullptr) { + delete s_instance; + s_instance = nullptr; + } } -RendererMode Renderer::GetBestRendererMode() -{ +RendererMode Renderer::GetBestRendererMode() { #if _WIN32 - return RendererMode::DIRECTX11; // I hope, all windows user support this + return RendererMode::DIRECTX; #elif __APPLE__ - return RendererMode::METAL; // Not sure if this work + return RendererMode::METAL; // Not sure if this work #else - return RendererMode::OPENGL; // Also I'm not sure if linux users support vulkan + return RendererMode::OPENGL; // Also I'm not sure if linux users support vulkan #endif } -bool Renderer::Destroy() -{ - if (m_renderer) { - ImGui_ImplSDLRenderer2_DestroyDeviceObjects(); - ImGui_ImplSDLRenderer2_DestroyFontsTexture(); +bool Renderer::Destroy() { + if (m_renderer) { + ImGui_ImplSDLRenderer2_DestroyDeviceObjects(); + ImGui_ImplSDLRenderer2_DestroyFontsTexture(); - ImGui_ImplSDLRenderer2_Shutdown(); + ImGui_ImplSDLRenderer2_Shutdown(); - SDL_DestroyRenderer(m_renderer); - } + SDL_DestroyRenderer(m_renderer); + } - if (IsVulkan()) { - VulkanEngine::Release(); - } + if (IsVulkan()) { + VulkanEngine::Release(); + } - ImGui_ImplSDL2_Shutdown(); - ImGui::DestroyContext(); + ImGui_ImplSDL2_Shutdown(); + ImGui::DestroyContext(); - return true; + return true; } diff --git a/Engine/src/Rendering/Vulkan/Texture2DVulkan.cpp b/Engine/src/Rendering/Vulkan/Texture2DVulkan.cpp index 3ff2d798..55997da8 100644 --- a/Engine/src/Rendering/Vulkan/Texture2DVulkan.cpp +++ b/Engine/src/Rendering/Vulkan/Texture2DVulkan.cpp @@ -17,7 +17,7 @@ static int m_textureCount static std::mutex m_textureMutex; static std::unique_ptr m_dummyTexture; -Texture2D_Vulkan *CreateTexture() +Texture2D_Vulkan* CreateTexture() { // find the empty slot for (int i = 0; i < m_textureCount; i++) { @@ -53,7 +53,7 @@ uint32_t vkTexture::FindMemoryType(uint32_t type_filter, uint32_t properties) return findMemoryType(VulkanEngine::GetInstance()->_chosenGPU, type_filter, properties); } -Texture2D_Vulkan *vkTexture::TexLoadImage(std::filesystem::path imagePath) +Texture2D_Vulkan* vkTexture::TexLoadImage(std::filesystem::path imagePath) { std::fstream fs(imagePath, std::ios::binary | std::ios::in); if (!fs.is_open()) { @@ -79,6 +79,14 @@ void InternalLoad( size_t image_size = static_cast(tex_data->Width) * static_cast(tex_data->Height) * tex_data->Channels; + // Premultiply alpha + for (size_t i = 0; i < image_size; i += tex_data->Channels) { // Fix white line issue + float alpha = image_data[i + 3] / 255.0f; + image_data[i] = static_cast(image_data[i] * alpha); + image_data[i + 1] = static_cast(image_data[i + 1] * alpha); + image_data[i + 2] = static_cast(image_data[i + 2] * alpha); + } + VkResult err; { VkImageCreateInfo info = {}; @@ -135,8 +143,8 @@ void InternalLoad( { VkSamplerCreateInfo samplerInfo = {}; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; - samplerInfo.magFilter = VK_FILTER_NEAREST; - samplerInfo.minFilter = VK_FILTER_NEAREST; + samplerInfo.magFilter = VK_FILTER_LINEAR; + samplerInfo.minFilter = VK_FILTER_LINEAR; samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; @@ -184,7 +192,7 @@ void InternalLoad( } { - void *map = NULL; + void* map = NULL; err = vkMapMemory(vulkan_driver->_device, tex_data->UploadBufferMemory, 0, image_size, 0, &map); if (err != VK_SUCCESS) { throw std::runtime_error("Vulkan: Failed to create mapped image memory"); @@ -240,17 +248,17 @@ void InternalLoad( use_barrier[0].subresourceRange.levelCount = 1; use_barrier[0].subresourceRange.layerCount = 1; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, use_barrier); - }); + }); } -Texture2D_Vulkan *vkTexture::TexLoadImage(void *buffer, size_t size) +Texture2D_Vulkan* vkTexture::TexLoadImage(void* buffer, size_t size) { auto vulkan_driver = VulkanEngine::GetInstance(); auto tex_data = CreateTexture(); tex_data->Channels = 4; - unsigned char *image_data = stbi_load_from_memory( - (uint8_t *)buffer, + unsigned char* image_data = stbi_load_from_memory( + (uint8_t*)buffer, (int)size, &tex_data->Width, &tex_data->Height, @@ -265,7 +273,22 @@ Texture2D_Vulkan *vkTexture::TexLoadImage(void *buffer, size_t size) return tex_data; } -Texture2D_Vulkan *vkTexture::GetDummyImage() +Texture2D_Vulkan* vkTexture::AllocateTexture(int width, int height) +{ + auto vulkan_driver = VulkanEngine::GetInstance(); + auto tex_data = CreateTexture(); + tex_data->Channels = 4; + tex_data->Width = width; + tex_data->Height = height; + + size_t image_size = width * height * 4; + unsigned char* empty_data = (unsigned char*)calloc(image_size, 1); + + InternalLoad(vulkan_driver, tex_data, empty_data); + return tex_data; +} + +Texture2D_Vulkan* vkTexture::GetDummyImage() { if (m_dummyTexture) { return m_dummyTexture.get(); @@ -278,8 +301,8 @@ Texture2D_Vulkan *vkTexture::GetDummyImage() // Generate file 1 Pixel Bitmap Image, with header std::vector buffer = ImageGenerator::GenerateImage(40, 40, { 255, 255, 255, 255 }); - unsigned char *image_data = stbi_load_from_memory( - (uint8_t *)buffer.data(), + unsigned char* image_data = stbi_load_from_memory( + (uint8_t*)buffer.data(), (int)buffer.size(), &m_dummyTexture->Width, &m_dummyTexture->Height, @@ -297,18 +320,92 @@ Texture2D_Vulkan *vkTexture::GetDummyImage() return m_dummyTexture.get(); } -VkDescriptorSet vkTexture::GetVkDescriptorSet(Texture2D_Vulkan *handle) +VkDescriptorSet vkTexture::GetVkDescriptorSet(Texture2D_Vulkan* handle) { return handle->DS; } -void vkTexture::QueryTexture(Texture2D_Vulkan *handle, int &outWidth, int &outHeight) +void vkTexture::UpdateTexture(Texture2D_Vulkan* handle, void* buffer, int pitch) +{ + if (!handle) return; + + auto vulkan_driver = VulkanEngine::GetInstance(); + size_t image_size = static_cast(handle->Width) * static_cast(handle->Height) * handle->Channels; + + void* map = NULL; + VkResult err = vkMapMemory(vulkan_driver->_device, handle->UploadBufferMemory, 0, image_size, 0, &map); + if (err != VK_SUCCESS) { + std::cerr << "Vulkan: Failed to map image memory for update!" << std::endl; + return; + } + + // If pitch matches expected width * channels, do a single memcpy + if (pitch == handle->Width * handle->Channels) { + memcpy(map, buffer, image_size); + } else { + // Copy line by line + uint8_t* dst = static_cast(map); + uint8_t* src = static_cast(buffer); + size_t rowSize = static_cast(handle->Width) * handle->Channels; + for (int y = 0; y < handle->Height; y++) { + memcpy(dst, src, rowSize); + dst += rowSize; + src += pitch; + } + } + + VkMappedMemoryRange range[1] = {}; + range[0].sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; + range[0].memory = handle->UploadBufferMemory; + range[0].size = image_size; + vkFlushMappedMemoryRanges(vulkan_driver->_device, 1, range); + vkUnmapMemory(vulkan_driver->_device, handle->UploadBufferMemory); + + vulkan_driver->immediate_submit([&](VkCommandBuffer cmd) { + VkImageMemoryBarrier copy_barrier[1] = {}; + copy_barrier[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + copy_barrier[0].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + copy_barrier[0].oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; // Expected current layout + copy_barrier[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + copy_barrier[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + copy_barrier[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + copy_barrier[0].image = handle->Image; + copy_barrier[0].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + copy_barrier[0].subresourceRange.levelCount = 1; + copy_barrier[0].subresourceRange.layerCount = 1; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 1, copy_barrier); + + VkBufferImageCopy region = {}; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.layerCount = 1; + region.imageExtent.width = handle->Width; + region.imageExtent.height = handle->Height; + region.imageExtent.depth = 1; + vkCmdCopyBufferToImage(cmd, handle->UploadBuffer, handle->Image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + + VkImageMemoryBarrier use_barrier[1] = {}; + use_barrier[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + use_barrier[0].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + use_barrier[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + use_barrier[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + use_barrier[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + use_barrier[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + use_barrier[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + use_barrier[0].image = handle->Image; + use_barrier[0].subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + use_barrier[0].subresourceRange.levelCount = 1; + use_barrier[0].subresourceRange.layerCount = 1; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, use_barrier); + }); +} + +void vkTexture::QueryTexture(Texture2D_Vulkan* handle, int& outWidth, int& outHeight) { outWidth = handle->Width; outHeight = handle->Height; } -void vkTexture::ReleaseTexture(Texture2D_Vulkan *tex_data) +void vkTexture::ReleaseTexture(Texture2D_Vulkan* tex_data) { if (tex_data == nullptr) { return; @@ -326,14 +423,14 @@ void vkTexture::ReleaseTexture(Texture2D_Vulkan *tex_data) ImGui_ImplVulkan_RemoveTexture(tex_data->DS); - auto it = std::find_if(m_textures.begin(), m_textures.end(), [&](auto &pair) { + auto it = std::find_if(m_textures.begin(), m_textures.end(), [&](auto& pair) { return pair.second != nullptr && pair.second->Id == tex_data->Id; - }); + }); if (it != m_textures.end()) { it->second.reset(); } - }); + }); } // This must be called from VulkanEngine! @@ -344,7 +441,7 @@ void vkTexture::Cleanup() std::cout << "[Info] Cleaning up Vulkan textures: " << m_textures.size() << std::endl; - for (auto &[id, tex_data] : m_textures) { + for (auto& [id, tex_data] : m_textures) { if (tex_data == nullptr) { continue; } diff --git a/Engine/src/Rendering/Vulkan/VulkanEngine.cpp b/Engine/src/Rendering/Vulkan/VulkanEngine.cpp index ccc04d31..abc89dda 100644 --- a/Engine/src/Rendering/Vulkan/VulkanEngine.cpp +++ b/Engine/src/Rendering/Vulkan/VulkanEngine.cpp @@ -24,8 +24,6 @@ constexpr bool bUseValidationLayers = true; constexpr bool bUseValidationLayers = false; #endif -// we want to immediately abort when there is an error. -// In normal engines this would give an error message to the user, or perform a dump of state. using namespace std; #if _DEBUG && _WIN32 #define _DEBUGBREAK_WIN32 __debugbreak(); diff --git a/Engine/src/Scenes/SceneManager.cpp b/Engine/src/Scenes/SceneManager.cpp index 283a83ee..86e3b3fb 100644 --- a/Engine/src/Scenes/SceneManager.cpp +++ b/Engine/src/Scenes/SceneManager.cpp @@ -13,358 +13,359 @@ #include static std::condition_variable m_cv; -static bool m_ready_change_state = false; +static bool m_ready_change_state = false; -SceneManager::SceneManager() -{ - m_scenes = {}; - m_overlays = {}; - m_parent = nullptr; +SceneManager::SceneManager() { + m_scenes = {}; + m_overlays = {}; + m_parent = nullptr; } -SceneManager::~SceneManager() -{ - for (auto &it : m_scenes) { - it.second->Detach(); - } +SceneManager::~SceneManager() { + for (auto &it : m_scenes) { + it.second->Detach(); + } - for (auto &it : m_overlays) { - it.second->Detach(); - } + for (auto &it : m_overlays) { + it.second->Detach(); + } - m_scenes.clear(); - m_overlays.clear(); + m_scenes.clear(); + m_overlays.clear(); } SceneManager *SceneManager::s_instance = nullptr; -void SceneManager::Update(double delta) -{ - if (m_nextScene != nullptr) { - // std::lock_guard lock(m_mutex); - if (m_onSceneChange) { - m_onSceneChange(); - } +void SceneManager::Update(double delta) { + if (m_nextScene != nullptr) { + // std::lock_guard lock(m_mutex); + if (m_onSceneChange) { + m_onSceneChange(); + } - if (!m_nextScene->Attach()) { - MsgBox::ShowOut("EstEngine Error", "Failed to init next scene", MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); - m_parent->Stop(); - return; - } + if (!m_nextScene->Attach()) { + MsgBox::ShowOut("EstEngine Error", "Failed to init next scene", + MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); + m_parent->Stop(); + return; + } - if (m_currentScene) { - auto curScene = m_currentScene; - m_currentScene = nullptr; + if (m_currentScene) { + auto curScene = m_currentScene; + m_currentScene = nullptr; - if (!curScene->Detach()) { - MsgBox::ShowOut("EstEngine Error", "Failed to detach current screen", MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); - m_parent->Stop(); - return; - } - } + if (!curScene->Detach()) { + MsgBox::ShowOut("EstEngine Error", "Failed to detach current screen", + MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); + m_parent->Stop(); + return; + } + } + + m_currentScene = m_nextScene; + m_nextScene = nullptr; + } else { + if (m_currentScene) { + m_currentScene->Update(delta); + } - m_currentScene = m_nextScene; - m_nextScene = nullptr; - } else { - m_currentScene->Update(delta); - - if (m_queue_render.size()) { - for (auto it = m_queue_render.begin(); it != m_queue_render.end();) { - if (it->time <= std::chrono::system_clock::now()) { - it->callback(); - it = m_queue_render.erase(it); - } else { - it++; - } - } + if (m_queue_render.size()) { + for (auto it = m_queue_render.begin(); it != m_queue_render.end();) { + if (it->time <= std::chrono::system_clock::now()) { + it->callback(); + it = m_queue_render.erase(it); + } else { + it++; } + } } + } - if (m_nextOverlay != nullptr) { - // std::lock_guard lock(m_mutex); + if (m_nextOverlay != nullptr) { + // std::lock_guard lock(m_mutex); - if (!m_nextOverlay->Attach()) { - MsgBox::ShowOut("EstEngine Error", "Failed to init next overlay", MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); - m_parent->Stop(); - return; - } + if (!m_nextOverlay->Attach()) { + MsgBox::ShowOut("EstEngine Error", "Failed to init next overlay", + MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); + m_parent->Stop(); + return; + } - if (m_currentOverlay) { - auto curOverlay = m_currentOverlay; - m_currentOverlay = nullptr; + if (m_currentOverlay) { + auto curOverlay = m_currentOverlay; + m_currentOverlay = nullptr; - if (!curOverlay->Detach()) { - MsgBox::ShowOut("EstEngine Error", "Failed to detach current overlay", MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); - m_parent->Stop(); - return; - } - } + if (!curOverlay->Detach()) { + MsgBox::ShowOut("EstEngine Error", "Failed to detach current overlay", + MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); + m_parent->Stop(); + return; + } + } - m_currentOverlay = m_nextOverlay; - m_nextOverlay = nullptr; - } else { - if (m_currentOverlay) { - m_currentOverlay->Update(delta); - } + m_currentOverlay = m_nextOverlay; + m_nextOverlay = nullptr; + } else { + if (m_currentOverlay) { + m_currentOverlay->Update(delta); } + } } -void SceneManager::Render(double delta) -{ - m_renderId = std::this_thread::get_id(); - m_ready_change_state = false; +void SceneManager::Render(double delta) { + if (m_currentScene) + m_currentScene->Render(delta); + if (m_currentOverlay && !MsgBox::Any()) { + ImguiUtil::NewFrame(); + auto &io = ImGui::GetIO(); - if (m_currentScene) - m_currentScene->Render(delta); - if (m_currentOverlay && !MsgBox::Any()) { - ImguiUtil::NewFrame(); - auto &io = ImGui::GetIO(); + ImGui::SetNextWindowPos( + ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f), + ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(m_currentOverlay->GetSize(), ImGuiCond_Always); - ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::SetNextWindowSize(m_currentOverlay->GetSize(), ImGuiCond_Always); + std::string title = m_currentOverlay->GetName() + "###SceneManagerOverlay"; + ImGui::OpenPopup(title.c_str()); - std::string title = m_currentOverlay->GetName() + "###SceneManagerOverlay"; - ImGui::OpenPopup(title.c_str()); + if (ImGui::BeginPopupModal(title.c_str(), nullptr, + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_AlwaysAutoResize)) { - if (ImGui::BeginPopupModal( - title.c_str(), - nullptr, - ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize)) { + m_currentOverlay->Render(delta); + ImGui::EndPopup(); + } - m_currentOverlay->Render(delta); - ImGui::EndPopup(); - } + if (m_currentOverlay->IsClosed()) { + if (!m_currentOverlay->Detach()) { + MsgBox::ShowOut("EstEngine Error", "Failed to detach current overlay", + MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); + m_parent->Stop(); + return; + } - if (m_currentOverlay->IsClosed()) { - if (!m_currentOverlay->Detach()) { - MsgBox::ShowOut("EstEngine Error", "Failed to detach current overlay", MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); - m_parent->Stop(); - return; - } + m_currentOverlay = nullptr; + } + } - m_currentOverlay = nullptr; - } + if (m_nextScene != nullptr) { + if (m_currentOverlay) { + // Delay scene change until the overlay is closed + return; + } + + if (m_currentScene) { + if (!m_currentScene->Detach()) { + MsgBox::ShowOut("EstEngine Error", "Failed to detach current scene", + MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); + m_parent->Stop(); + return; + } } - if (m_nextScene != nullptr) { - m_ready_change_state = true; - m_cv.notify_one(); + if (!m_nextScene->Attach()) { + MsgBox::ShowOut("EstEngine Error", "Failed to init next scene", + MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); + m_parent->Stop(); + return; } + + m_currentScene = m_nextScene; + m_nextScene = nullptr; + + if (m_onSceneChange) { + m_onSceneChange(); + } + } } -void SceneManager::Input(double delta) -{ - m_inputId = std::this_thread::get_id(); - - if (m_currentScene) - m_currentScene->Input(delta); - - if (m_queue_input.size()) { - for (auto it = m_queue_input.begin(); it != m_queue_input.end();) { - if (it->time <= std::chrono::system_clock::now()) { - it->callback(); - it = m_queue_input.erase(it); - } else { - it++; - } - } +void SceneManager::Input(double delta) { + m_inputId = std::this_thread::get_id(); + + if (m_currentScene) + m_currentScene->Input(delta); + + if (m_queue_input.size()) { + for (auto it = m_queue_input.begin(); it != m_queue_input.end();) { + if (it->time <= std::chrono::system_clock::now()) { + it->callback(); + it = m_queue_input.erase(it); + } else { + it++; + } } + } } -void SceneManager::OnKeyDown(const KeyState &state) -{ - if (m_currentScene) - m_currentScene->OnKeyDown(state); +void SceneManager::OnKeyDown(const KeyState &state) { + if (m_currentScene) + m_currentScene->OnKeyDown(state); } -void SceneManager::OnKeyUp(const KeyState &state) -{ - if (m_currentScene) - m_currentScene->OnKeyUp(state); +void SceneManager::OnKeyUp(const KeyState &state) { + if (m_currentScene) + m_currentScene->OnKeyUp(state); } -void SceneManager::OnMouseDown(const MouseState &state) -{ - if (m_currentScene) - m_currentScene->OnMouseDown(state); +void SceneManager::OnMouseDown(const MouseState &state) { + if (m_currentScene) + m_currentScene->OnMouseDown(state); } -void SceneManager::OnMouseUp(const MouseState &state) -{ - if (m_currentScene) - m_currentScene->OnMouseUp(state); +void SceneManager::OnMouseUp(const MouseState &state) { + if (m_currentScene) + m_currentScene->OnMouseUp(state); } static const char *notInitialized = "SceneManager is not initalized"; -void SceneManager::AddScene(int idx, Scene *scene) -{ - if (s_instance == nullptr) - throw std::runtime_error(notInitialized); +void SceneManager::AddScene(int idx, Scene *scene) { + if (s_instance == nullptr) + throw std::runtime_error(notInitialized); - Logs::Puts("[SceneManager] Added scene Id: %d", idx); - s_instance->m_scenes[idx] = std::unique_ptr(scene); + Logs::Puts("[SceneManager] Added scene Id: %d", idx); + s_instance->m_scenes[idx] = std::unique_ptr(scene); } -void SceneManager::ChangeScene(int idx) -{ - if (s_instance == nullptr) - throw std::runtime_error(notInitialized); +void SceneManager::ChangeScene(int idx) { + if (s_instance == nullptr) + throw std::runtime_error(notInitialized); - s_instance->IChangeScene(idx); + // Load the next scene asynchronously + std::thread([idx]() { s_instance->IChangeScene(idx); }).detach(); } -void SceneManager::AddOverlay(int Idx, Overlay *overlay) -{ - if (s_instance == nullptr) - throw std::runtime_error(notInitialized); +void SceneManager::AddOverlay(int Idx, Overlay *overlay) { + if (s_instance == nullptr) + throw std::runtime_error(notInitialized); - s_instance->m_overlays[Idx] = std::unique_ptr(overlay); + s_instance->m_overlays[Idx] = std::unique_ptr(overlay); } -void SceneManager::OverlayShow(int idx) -{ - auto &map = s_instance->m_overlays; +void SceneManager::OverlayShow(int idx) { + auto &map = s_instance->m_overlays; - if (map.find(idx) == map.end()) { - std::string msg = "Failed to find OverlayId: " + std::to_string(idx); + if (map.find(idx) == map.end()) { + std::string msg = "Failed to find OverlayId: " + std::to_string(idx); - MsgBox::ShowOut("EstEngine Error", msg.c_str(), MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); - return; - } + MsgBox::ShowOut("EstEngine Error", msg.c_str(), MsgBoxType::OK, + MsgBoxFlags::BTN_ERROR); + return; + } - s_instance->m_nextOverlay = map[idx].get(); + s_instance->m_nextOverlay = map[idx].get(); } -void SceneManager::OverlayClose() -{ - if (s_instance == nullptr) - throw std::runtime_error(notInitialized); +void SceneManager::OverlayClose() { + if (s_instance == nullptr) + throw std::runtime_error(notInitialized); - if (s_instance->m_currentOverlay) { - s_instance->m_currentOverlay->Detach(); - s_instance->m_currentOverlay = nullptr; - } + if (s_instance->m_currentOverlay) { + s_instance->m_currentOverlay->Detach(); + s_instance->m_currentOverlay = nullptr; + } } -void SceneManager::AddOnSceneChange(std::function callback) -{ - s_instance->m_onSceneChange = callback; +void SceneManager::AddOnSceneChange(std::function callback) { + s_instance->m_onSceneChange = callback; } -void SceneManager::IAddScene(int idx, Scene *scene) -{ - m_scenes[idx] = std::unique_ptr(scene); +void SceneManager::IAddScene(int idx, Scene *scene) { + m_scenes[idx] = std::unique_ptr(scene); } -void SceneManager::IChangeScene(int idx) -{ - if (m_scenes.find(idx) == m_scenes.end()) { - std::string msg = "Failed to find SceneId: " + std::to_string(idx); - - MsgBox::ShowOut("EstEngine Error", msg.c_str(), MsgBoxType::OK, MsgBoxFlags::BTN_ERROR); - return; - } +void SceneManager::IChangeScene(int idx) { + if (m_scenes.find(idx) == m_scenes.end()) { + std::string msg = "Failed to find SceneId: " + std::to_string(idx); + MsgBox::ShowOut("EstEngine Error", msg.c_str(), MsgBoxType::OK, + MsgBoxFlags::BTN_ERROR); + return; + } - m_lastSceneId = m_currentSceneId; - m_currentSceneId = idx; + m_lastSceneId = m_currentSceneId; + m_currentSceneId = idx; - m_nextScene = m_scenes[idx].get(); + m_nextScene = m_scenes[idx].get(); + m_ready_change_state = true; + m_cv.notify_one(); } -void SceneManager::SetParent(Game *parent) -{ - m_parent = parent; -} +void SceneManager::SetParent(Game *parent) { m_parent = parent; } -void SceneManager::SetFrameLimit(double frameLimit) -{ - m_parent->SetFramelimit(frameLimit); +void SceneManager::SetFrameLimit(double frameLimit) { + m_parent->SetFramelimit(frameLimit); } -void SceneManager::SetFrameLimitMode(FrameLimitMode mode) -{ - m_parent->SetFrameLimitMode(mode); +void SceneManager::SetFrameLimitMode(FrameLimitMode mode) { + m_parent->SetFrameLimitMode(mode); } -int SceneManager::GetCurrentSceneIndex() const -{ - return m_currentSceneId; -} +int SceneManager::GetCurrentSceneIndex() const { return m_currentSceneId; } -int SceneManager::GetLastSceneIndex() const -{ - return m_lastSceneId; -} +int SceneManager::GetLastSceneIndex() const { return m_lastSceneId; } -void SceneManager::DisplayFade(int transparency, std::function callback) -{ - std::thread([&, transparency, callback] { - std::lock_guard lock(s_instance->m_mutex); +void SceneManager::DisplayFade(int transparency, + std::function callback) { + std::thread([&, transparency, callback] { + std::lock_guard lock(s_instance->m_mutex); - s_instance->m_parent->DisplayFade(transparency); - while (static_cast(s_instance->m_parent->GetDisplayFade()) != transparency) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } + s_instance->m_parent->DisplayFade(transparency); + while (static_cast(s_instance->m_parent->GetDisplayFade()) != + transparency) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } - callback(); - }).detach(); + callback(); + }).detach(); } -void SceneManager::ExecuteAfter(int ms_time, std::function callback) -{ - QueueInfo info = {}; - info.callback = callback; - info.time = std::chrono::system_clock::now() + std::chrono::milliseconds(ms_time); - - auto thread_id = std::this_thread::get_id(); - if (thread_id == s_instance->m_renderId) { - s_instance->m_queue_render.push_back(std::move(info)); - } else { - s_instance->m_queue_input.push_back(std::move(info)); - } +void SceneManager::ExecuteAfter(int ms_time, std::function callback) { + QueueInfo info = {}; + info.callback = callback; + info.time = + std::chrono::system_clock::now() + std::chrono::milliseconds(ms_time); + + auto thread_id = std::this_thread::get_id(); + if (thread_id == s_instance->m_renderId) { + s_instance->m_queue_render.push_back(std::move(info)); + } else { + s_instance->m_queue_input.push_back(std::move(info)); + } } -void SceneManager::GameExecuteAfter(ExecuteThread thread, int ms_time, std::function callback) -{ - Game *game = s_instance->m_parent; - - if (game->GetThreadMode() == ThreadMode::SINGLE_THREAD) { - game->GetMainThread()->QueueAction(callback); - } else { - switch (thread) { - case ExecuteThread::UPDATE: - { - game->GetRenderThread()->QueueAction(callback); - break; - } - - case ExecuteThread::WINDOW: - { - game->GetMainThread()->QueueAction(callback); - break; - } - } +void SceneManager::GameExecuteAfter(ExecuteThread thread, int ms_time, + std::function callback) { + Game *game = s_instance->m_parent; + + if (game->GetThreadMode() == ThreadMode::SINGLE_THREAD) { + game->GetMainThread()->QueueAction(callback); + } else { + switch (thread) { + case ExecuteThread::UPDATE: { + game->GetRenderThread()->QueueAction(callback); + break; } -} -void SceneManager::StopGame() -{ - m_parent->Stop(); + case ExecuteThread::WINDOW: { + game->GetMainThread()->QueueAction(callback); + break; + } + } + } } -SceneManager *SceneManager::GetInstance() -{ - if (s_instance == nullptr) { - s_instance = new SceneManager(); - } +void SceneManager::StopGame() { m_parent->Stop(); } - return s_instance; -} +SceneManager *SceneManager::GetInstance() { + if (s_instance == nullptr) { + s_instance = new SceneManager(); + } -void SceneManager::Release() -{ - if (s_instance != nullptr) { - delete s_instance; - } + return s_instance; } + +void SceneManager::Release() { + if (s_instance != nullptr) { + delete s_instance; + } +} \ No newline at end of file diff --git a/Engine/src/Texture/Color3.cpp b/Engine/src/Texture/Color3.cpp index 02c2917b..92307587 100644 --- a/Engine/src/Texture/Color3.cpp +++ b/Engine/src/Texture/Color3.cpp @@ -1,6 +1,8 @@ #include "Texture/Color3.h" #include #include +#include +#include template T clamp(T value, T min, T max) @@ -8,10 +10,6 @@ T clamp(T value, T min, T max) return std::min(std::max(value, min), max); } -#if defined(__GNUC__) || defined(__GNUG__) -#include -#endif - Color3::Color3(float r, float g, float b) { R = clamp(r, 0.0f, 1.0f); @@ -24,48 +22,50 @@ Color3 Color3::FromRGB(float r, float g, float b) return { r / 255.0f, g / 255.0f, b / 255.0f }; } -Color3 Color3::FromHSV(int hue, int saturnation, int value) +Color3 Color3::FromHSV(int hue, int saturation, int value) { - float R, G, B; + float h = static_cast(hue % 360); + float s = clamp(static_cast(saturation), 0.0f, 100.0f) / 100.0f; + float v = clamp(static_cast(value), 0.0f, 100.0f) / 100.0f; - float h = hue / 60.0f; - float s = saturnation / 100.0f; - float v = value / 100.0f; float c = v * s; - float x = c * (1.0f - std::abs(fmod(h, 2.0f) - 1.0f)); + float x = c * (1.0f - std::abs(std::fmod(h / 60.0f, 2.0f) - 1.0f)); float m = v - c; - if (h >= 0 && h < 1) { - R = c; - G = x; - B = 0; - } else if (h >= 1 && h < 2) { - R = x; - G = c; - B = 0; - } else if (h >= 2 && h < 3) { - R = 0; - G = c; - B = x; - } else if (h >= 3 && h < 4) { - R = 0; - G = x; - B = c; - } else if (h >= 4 && h < 5) { - R = x; - G = 0; - B = c; - } else if (h >= 5 && h < 6) { - R = c; - G = 0; - B = x; - } else { - R = 0; - G = 0; - B = 0; + float r, g, b; + + if (0 <= h && h < 60) { + r = c; + g = x; + b = 0; + } + else if (60 <= h && h < 120) { + r = x; + g = c; + b = 0; + } + else if (120 <= h && h < 180) { + r = 0; + g = c; + b = x; + } + else if (180 <= h && h < 240) { + r = 0; + g = x; + b = c; + } + else if (240 <= h && h < 300) { + r = x; + g = 0; + b = c; + } + else { + r = c; + g = 0; + b = x; } - return { R, G, B }; + return { r + m, g + m, b + m }; } Color3 Color3::FromHex(std::string hexValue) @@ -82,7 +82,7 @@ Color3 Color3::FromHex(std::string hexValue) return { 0, 0, 0 }; } - int r = std::stoi(hexValue.substr(0, 2), nullptr, 16); + int r = std::stoi(hexValue.substr(0, 2), nullptr, 16); int g = std::stoi(hexValue.substr(2, 2), nullptr, 16); int b = std::stoi(hexValue.substr(4, 2), nullptr, 16); @@ -111,27 +111,31 @@ std::string Color3::ToHex() } // operator -Color3 Color3::operator+(Color3 const &color) +Color3 Color3::operator+(Color3 const& color) { return { this->R + color.R, this->G + color.G, this->B + color.B }; } -Color3 Color3::operator-(Color3 const &color) +Color3 Color3::operator-(Color3 const& color) { return { this->R - color.R, this->G - color.G, this->B - color.B }; } -Color3 Color3::operator*(Color3 const &color) +Color3 Color3::operator*(Color3 const& color) { return { this->R * color.R, this->G * color.G, this->B * color.B }; } -Color3 Color3::operator/(Color3 const &color) +Color3 Color3::operator/(Color3 const& color) { + // Handle division by zero + if (color.R == 0 || color.G == 0 || color.B == 0) { + return { 0, 0, 0 }; + } return { this->R / color.R, this->G / color.G, this->B / color.B }; } -Color3 Color3::operator*(float const &value) +Color3 Color3::operator*(float const& value) { return { this->R * value, this->G * value, this->B * value }; -} \ No newline at end of file +} diff --git a/Engine/src/Texture/NumericTexture.cpp b/Engine/src/Texture/NumericTexture.cpp index 5df50be3..cd8721dd 100644 --- a/Engine/src/Texture/NumericTexture.cpp +++ b/Engine/src/Texture/NumericTexture.cpp @@ -18,6 +18,9 @@ NumericTexture::NumericTexture(std::vector numericsFiles) auto path = numericsFiles[i]; m_numericsTexture[i] = new Texture2D(path); m_numbericsWidth[i] = m_numericsTexture[i]->GetOriginalRECT(); + if (m_numbericsWidth[i].right > m_maxWidth) { + m_maxWidth = m_numbericsWidth[i].right; + } } } @@ -35,6 +38,9 @@ NumericTexture::NumericTexture(std::vector numericsPath) auto path = numericsPath[i]; m_numericsTexture[i] = new Texture2D(path); m_numbericsWidth[i] = m_numericsTexture[i]->GetOriginalRECT(); + if (m_numbericsWidth[i].right > m_maxWidth) { + m_maxWidth = m_numbericsWidth[i].right; + } } } @@ -76,9 +82,10 @@ void NumericTexture::DrawNumber(int number) for (int i = (int)numberString.length() - 1; i >= 0; i--) { int digit = numberString[i] - '0'; - tx -= (int)m_numbericsWidth[digit].right + (int)(m_numbericsWidth[digit].right * offsetScl); + tx -= m_maxWidth + (int)(m_maxWidth * offsetScl); auto tex = m_numericsTexture[digit]; - tex->Position = UDim2({ 0, (float)tx }, { 0, (float)yPos }); + float centeredX = tx + (m_maxWidth - m_numbericsWidth[digit].right) / 2.0f; + tex->Position = UDim2({ 0, centeredX }, { 0, (float)yPos }); tex->AlphaBlend = AlphaBlend; tex->AnchorPoint = AnchorPoint; tex->Draw(); @@ -90,20 +97,19 @@ void NumericTexture::DrawNumber(int number) { int totalWidth = 0; for (int i = 0; i < numberString.length(); i++) { - int digit = numberString[i] - '0'; - totalWidth += (int)m_numbericsWidth[digit].right + (int)(m_numbericsWidth[digit].right * offsetScl); + totalWidth += m_maxWidth + (int)(m_maxWidth * offsetScl); } int tx = xPos - totalWidth / 2 + (Offset * totalWidth) / 200; - // int tx = xPos - (totalWidth / 2) * (totalWidth / 100); for (int i = 0; i < numberString.length(); i++) { int digit = numberString[i] - '0'; auto tex = m_numericsTexture[digit]; - tex->Position = UDim2({ 0, (float)tx }, { 0, (float)yPos }); + float centeredX = tx + (m_maxWidth - m_numbericsWidth[digit].right) / 2.0f; + tex->Position = UDim2({ 0, centeredX }, { 0, (float)yPos }); tex->AlphaBlend = AlphaBlend; tex->AnchorPoint = AnchorPoint; tex->Draw(); - tx += (int)m_numbericsWidth[digit].right + (int)(m_numbericsWidth[digit].right * offsetScl); + tx += m_maxWidth + (int)(m_maxWidth * offsetScl); } break; } @@ -114,11 +120,12 @@ void NumericTexture::DrawNumber(int number) for (int i = 0; i < numberString.length(); i++) { int digit = numberString[i] - '0'; auto tex = m_numericsTexture[digit]; - tex->Position = UDim2({ 0, (float)tx }, { 0, (float)yPos }); + float centeredX = tx + (m_maxWidth - m_numbericsWidth[digit].right) / 2.0f; + tex->Position = UDim2({ 0, centeredX }, { 0, (float)yPos }); tex->AnchorPoint = AnchorPoint; tex->AlphaBlend = AlphaBlend; tex->Draw(); - tx += (int)m_numbericsWidth[digit].right + (int)(m_numbericsWidth[digit].right * offsetScl); + tx += m_maxWidth + (int)(m_maxWidth * offsetScl); } break; } diff --git a/Engine/src/Texture/Sprite2D.cpp b/Engine/src/Texture/Sprite2D.cpp index 8b014825..525449e5 100644 --- a/Engine/src/Texture/Sprite2D.cpp +++ b/Engine/src/Texture/Sprite2D.cpp @@ -2,11 +2,11 @@ #include "Rendering/Window.h" #include "Texture/Texture2D.h" -Sprite2D::Sprite2D(std::vector textures, float delay) : Sprite2D::Sprite2D() +Sprite2D::Sprite2D(std::vector textures, double delay) : Sprite2D::Sprite2D() { m_textures = textures; - m_delay = delay; - m_currentTime = 0.0f; + m_spritespeed = delay; + m_currentTime = 0.0; m_currentIndex = 0; Size = UDim2::fromScale(1, 1); @@ -14,9 +14,9 @@ Sprite2D::Sprite2D(std::vector textures, float delay) : Sprite2D::S AnchorPoint = { 0, 0 }; } -Sprite2D::Sprite2D(std::vector textures, float delay) : Sprite2D::Sprite2D() +Sprite2D::Sprite2D(std::vector textures, double delay) : Sprite2D::Sprite2D() { - m_delay = delay; + m_spritespeed = delay; Size = UDim2::fromScale(1, 1); Position = UDim2::fromOffset(0, 0); AnchorPoint = { 0, 0 }; @@ -26,9 +26,9 @@ Sprite2D::Sprite2D(std::vector textures, float delay) : Sprite2D::S } } -Sprite2D::Sprite2D(std::vector textures, float delay) : Sprite2D::Sprite2D() +Sprite2D::Sprite2D(std::vector textures, double delay) : Sprite2D::Sprite2D() { - m_delay = delay; + m_spritespeed = delay; Size = UDim2::fromScale(1, 1); Position = UDim2::fromOffset(0, 0); AnchorPoint = { 0, 0 }; @@ -40,9 +40,9 @@ Sprite2D::Sprite2D(std::vector textures, float delay) : S } } -Sprite2D::Sprite2D(std::vector textures, float delay) : Sprite2D::Sprite2D() +Sprite2D::Sprite2D(std::vector textures, double delay) : Sprite2D::Sprite2D() { - m_delay = delay; + m_spritespeed = delay; Size = UDim2::fromScale(1, 1); Position = UDim2::fromOffset(0, 0); AnchorPoint = { 0, 0 }; @@ -64,10 +64,19 @@ void Sprite2D::Draw(double delta, bool manual) Draw(delta, nullptr, manual); } -void Sprite2D::Draw(double delta, Rect *rect, bool manual) +void Sprite2D::DrawInternal(double delta, bool playOnce, Rect* rect, bool manual) { - auto tex = m_textures[m_currentIndex]; - GameWindow *window = GameWindow::GetInstance(); + if (m_textures.empty()) return; // Safety check to ensure m_textures is not empty + + if (m_currentIndex >= m_textures.size()) { + if (playOnce) { + return; // Stop if playing once and reached end + } + m_currentIndex = 0; // Reset index if looping + } + + auto tex = m_textures[m_currentIndex]; + GameWindow* window = GameWindow::GetInstance(); double xPos = (window->GetBufferWidth() * Position.X.Scale) + (Position.X.Offset); double yPos = (window->GetBufferHeight() * Position.Y.Scale) + (Position.Y.Offset); @@ -82,34 +91,69 @@ void Sprite2D::Draw(double delta, Rect *rect, bool manual) tex->AlphaBlend = AlphaBlend; tex->Size = Size; tex->AnchorPoint = AnchorPoint; + tex->FlipX = FlipX; + tex->FlipY = FlipY; tex->Draw(rect, manual ? false : true); - if (m_delay > 0.0f) { + if (m_spritespeed > 0.0) { m_currentTime += static_cast(delta); - if (m_currentTime >= m_delay) { - m_currentTime = 0.0f; - m_currentIndex = (m_currentIndex + 1) % m_textures.size(); + while (m_currentTime >= m_spritespeed) { + m_currentTime -= static_cast(m_spritespeed); + m_currentIndex++; + + if (m_currentIndex >= m_textures.size()) { + if (playOnce) { + m_currentIndex = m_textures.size(); // Stop after the last frame (hide) + m_currentTime = 0.0; // Prevent infinite loop buildup if stuck at the end + break; + } else { + m_currentIndex = 0; // Loop back to the first frame + } + } } } } +void Sprite2D::Draw(double delta, Rect* rect, bool manual) +{ + DrawInternal(delta, false, rect, manual); // Play loop +} + +void Sprite2D::DrawOnce(double delta, bool manual) +{ + DrawInternal(delta, true, nullptr, manual); // Play once +} + +void Sprite2D::DrawStop(double delta, bool manual) +{ + DrawInternal(delta, true, nullptr, manual); + + if (m_currentIndex >= m_textures.size()) { // Play the stop on last frame + m_currentIndex = m_textures.size() - 1; + } +} + void Sprite2D::Reset() { m_currentIndex = 0; - m_currentTime = 0.0f; + m_currentTime = 0.0; } Texture2D *Sprite2D::GetTexture() { + if (m_textures.empty()) return nullptr; // Safety check to ensure m_textures is not empty + auto tex = m_textures[m_currentIndex]; tex->Position = Position; tex->Size = Size; tex->AnchorPoint = AnchorPoint; + tex->FlipX = FlipX; + tex->FlipY = FlipY; return tex; } -void Sprite2D::SetFPS(float fps) +void Sprite2D::SetFPS(double fps) { - m_delay = 1.0f / fps; -} \ No newline at end of file + m_spritespeed = 1.0f / fps; +} diff --git a/Engine/src/Texture/Texture2D.cpp b/Engine/src/Texture/Texture2D.cpp index 5fd5b3ed..13aa24a6 100644 --- a/Engine/src/Texture/Texture2D.cpp +++ b/Engine/src/Texture/Texture2D.cpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #include "../Data/Imgui/imgui_impl_vulkan.h" #include "../Rendering/Vulkan/Texture2DVulkan_Internal.h" @@ -15,6 +18,29 @@ #include #include +namespace { + // Premultiply alpha + SDL_Surface* PremultiplyAlpha(SDL_Surface* surface) { + if (surface->format->BytesPerPixel != 4) return surface; + + Uint32* pixels = (Uint32*)surface->pixels; + for (int y = 0; y < surface->h; ++y) { + for (int x = 0; x < surface->w; ++x) { + Uint32 pixel = pixels[y * surface->w + x]; + Uint8 r, g, b, a; + SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a); + + r = (r * a) / 255; + g = (g * a) / 255; + b = (b * a) / 255; + + pixels[y * surface->w + x] = SDL_MapRGBA(surface->format, r, g, b, a); + } + } + return surface; + } +} + Texture2D::Texture2D() { TintColor = { 1.0f, 1.0f, 1.0f }; @@ -35,91 +61,79 @@ Texture2D::Texture2D() Size = UDim2::fromScale(1, 1); } -Texture2D::Texture2D(std::string fileName) : Texture2D() +Texture2D::Texture2D(const std::string& fileName) : Texture2D() { - if (!std::filesystem::exists(fileName)) { - fileName = std::filesystem::current_path().string() + fileName; + std::string filePath = fileName; + if (!std::filesystem::exists(filePath)) { + filePath = std::filesystem::current_path().string() + fileName; } - if (!std::filesystem::exists(fileName)) { + if (!std::filesystem::exists(filePath)) { throw std::runtime_error(fileName + " not found!"); } - std::fstream fs(fileName, std::ios::binary | std::ios::in); - if (!fs.is_open()) { - throw std::runtime_error(fileName + " cannot opened!"); + std::ifstream file(filePath, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + throw std::runtime_error(fileName + " cannot be opened!"); } - fs.seekg(0, std::ios::end); - size_t size = fs.tellg(); - fs.seekg(0, std::ios::beg); + size_t size = file.tellg(); + file.seekg(0, std::ios::beg); - uint8_t *buffer = new uint8_t[size]; - fs.read((char *)buffer, size); - fs.close(); - - Rotation = 0; - Transparency = 0.0f; - m_actualSize = { 0, 0, 0, 0 }; - m_bDisposeTexture = true; - TintColor = { 1.0f, 1.0f, 1.0f }; + std::vector buffer(size); + file.read(reinterpret_cast(buffer.data()), size); + file.close(); - LoadImageResources(buffer, size); + LoadImageResources(buffer.data(), size); } -Texture2D::Texture2D(std::filesystem::path path) : Texture2D() -{ +Texture2D::Texture2D(const std::filesystem::path& path) : Texture2D() { if (!std::filesystem::exists(path)) { throw std::runtime_error(path.string() + " not found!"); } - std::fstream fs(path, std::ios::binary | std::ios::in); - if (!fs.is_open()) { - throw std::runtime_error(path.string() + " cannot opened!"); + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + throw std::runtime_error(path.string() + " cannot be opened!"); } - fs.seekg(0, std::ios::end); - size_t size = fs.tellg(); - fs.seekg(0, std::ios::beg); + size_t size = file.tellg(); + file.seekg(0, std::ios::beg); - uint8_t *buffer = new uint8_t[size]; - fs.read((char *)buffer, size); - fs.close(); - - Rotation = 0; - Transparency = 0.0f; - m_actualSize = { 0, 0, 0, 0 }; - m_bDisposeTexture = true; - TintColor = { 1.0f, 1.0f, 1.0f }; + std::vector buffer(size); + file.read(reinterpret_cast(buffer.data()), size); + file.close(); - LoadImageResources(buffer, size); + LoadImageResources(buffer.data(), size); } -// Do base constructor called before derived constructor? -// https://stackoverflow.com/questions/120547/what-are-the-rules-for-calling-the-superclass-constructor - -Texture2D::Texture2D(uint8_t *fileData, size_t size) : Texture2D() +Texture2D::Texture2D(const uint8_t* fileData, size_t size) : Texture2D() { - uint8_t *buffer = new uint8_t[size]; - memcpy(buffer, fileData, size); - - Rotation = 0; - Transparency = 0.0f; - m_actualSize = { 0, 0, 0, 0 }; - m_bDisposeTexture = true; - TintColor = { 1.0f, 1.0f, 1.0f }; - - m_sdl_tex = nullptr; - m_vk_tex = nullptr; + std::vector buffer(fileData, fileData + size); + LoadImageResources(buffer.data(), size); +} - LoadImageResources(buffer, size); +Texture2D::Texture2D(int width, int height) : Texture2D() +{ + if (Renderer::GetInstance()->IsVulkan()) { + m_vk_tex = vkTexture::AllocateTexture(width, height); + m_actualSize = {0, 0, width, height}; + m_bDisposeTexture = true; + m_ready = true; + } else { + m_sdl_surface = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_ABGR8888); + m_sdl_tex = SDL_CreateTexture(Renderer::GetInstance()->GetSDLRenderer(), SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, width, height); + SDL_SetTextureBlendMode(m_sdl_tex, SDL_BLENDMODE_BLEND); + m_actualSize = {0, 0, width, height}; + m_bDisposeTexture = true; + m_ready = true; + } } Texture2D::Texture2D(SDL_Texture *texture) : Texture2D() { m_bDisposeTexture = false; m_sdl_tex = texture; - m_ready = true; } @@ -135,17 +149,13 @@ Texture2D::~Texture2D() { if (m_bDisposeTexture) { if (Renderer::GetInstance()->IsVulkan() && m_vk_tex) { - auto vk_tex = m_vk_tex; - - vkTexture::ReleaseTexture(vk_tex); - + vkTexture::ReleaseTexture(m_vk_tex); m_vk_tex = nullptr; } else { if (m_sdl_tex) { SDL_DestroyTexture(m_sdl_tex); m_sdl_tex = nullptr; } - if (m_sdl_surface) { SDL_FreeSurface(m_sdl_surface); m_sdl_surface = nullptr; @@ -169,11 +179,11 @@ void Texture2D::Draw(Rect *clipRect) Draw(clipRect, true); } -void Texture2D::Draw(Rect *clipRect, bool manualDraw) +void Texture2D::Draw(Rect* clipRect, bool manualDraw) { - Renderer *renderer = Renderer::GetInstance(); - auto window = GameWindow::GetInstance(); - bool scaleOutput = window->IsScaleOutput(); + Renderer* renderer = Renderer::GetInstance(); + auto window = GameWindow::GetInstance(); + bool scaleOutput = window->IsScaleOutput(); CalculateSize(); if (!m_ready) @@ -226,66 +236,50 @@ void Texture2D::Draw(Rect *clipRect, bool manualDraw) ImVec2 uv3(1.0f, 1.0f); // Bottom-right UV coordinate ImVec2 uv4(0.0f, 1.0f); // Bottom-left UV coordinate - ImU32 color = IM_COL32_WHITE; - - std::array vertexData; - - for (int i = 0; i < 6; i++) { - ImDrawVert &vertex = vertexData[i]; - switch (i) { - case 0: - vertex.pos = ImVec2(x1, y1); - vertex.uv = uv1; - break; - case 1: - vertex.pos = ImVec2(x2, y1); - vertex.uv = uv2; - break; - case 2: - vertex.pos = ImVec2(x2, y2); - vertex.uv = uv3; - break; - case 3: - vertex.pos = ImVec2(x1, y1); - vertex.uv = uv1; - break; - case 4: - vertex.pos = ImVec2(x2, y2); - vertex.uv = uv3; - break; - case 5: - vertex.pos = ImVec2(x1, y2); - vertex.uv = uv4; - break; - } - vertex.col = color; + if (FlipX) { + std::swap(uv1.x, uv2.x); + std::swap(uv3.x, uv4.x); } + if (FlipY) { + std::swap(uv1.y, uv4.y); + std::swap(uv2.y, uv3.y); + } + + ImU32 color = IM_COL32((uint8_t)(TintColor.R * 255), (uint8_t)(TintColor.G * 255), (uint8_t)(TintColor.B * 255), (uint8_t)(255 - (Transparency / 100.0) * 255)); + + std::array vertexData = {{ + {ImVec2(x1, y1), uv1, color}, + {ImVec2(x2, y1), uv2, color}, + {ImVec2(x2, y2), uv3, color}, + {ImVec2(x1, y1), uv1, color}, + {ImVec2(x2, y2), uv3, color}, + {ImVec2(x1, y2), uv4, color} + }}; SubmitQueueInfo info = {}; info.AlphaBlend = AlphaBlend; info.descriptor = imageId; - info.vertices = std::vector(vertexData.begin(), vertexData.end()); - info.indices = { 0, 1, 2, 3, 4, 5 }; + info.vertices = {vertexData.begin(), vertexData.end()}; + info.indices = {0, 1, 2, 3, 4, 5}; info.scissor = scissor; - // submit to queue vulkan_driver->queue_submit(info); } else { - SDL_FRect destRect = { m_calculatedSizeF.left, m_calculatedSizeF.top, m_calculatedSizeF.right, m_calculatedSizeF.bottom }; + SDL_FRect destRect = {m_calculatedSizeF.left, m_calculatedSizeF.top, m_calculatedSizeF.right, m_calculatedSizeF.bottom}; if (scaleOutput) { - destRect.x = destRect.x * window->GetWidthScale(); - destRect.y = destRect.y * window->GetHeightScale(); - destRect.w = destRect.w * window->GetWidthScale(); - destRect.h = destRect.h * window->GetHeightScale(); + destRect.x *= window->GetWidthScale(); + destRect.y *= window->GetHeightScale(); + destRect.w *= window->GetWidthScale(); + destRect.h *= window->GetHeightScale(); } - SDL_Rect originClip = {}; + SDL_Rect originClip = {}; SDL_BlendMode oldBlendMode = SDL_BLENDMODE_NONE; if (clipRect) { SDL_RenderGetClipRect(renderer->GetSDLRenderer(), &originClip); - SDL_Rect testClip = { clipRect->left, clipRect->top, clipRect->right - clipRect->left, clipRect->bottom - clipRect->top }; + SDL_Rect testClip = {clipRect->left, clipRect->top, clipRect->right - clipRect->left, clipRect->bottom - clipRect->top}; if (scaleOutput) { testClip.x = static_cast(testClip.x * window->GetWidthScale()); testClip.y = static_cast(testClip.y * window->GetHeightScale()); @@ -301,23 +295,19 @@ void Texture2D::Draw(Rect *clipRect, bool manualDraw) SDL_SetTextureBlendMode(m_sdl_tex, renderer->GetSDLBlendMode()); } - SDL_Color color = { (uint8_t)(TintColor.R * 255.0f), (uint8_t)(TintColor.G * 255.0f), (uint8_t)(TintColor.B * 255.0f), (uint8_t)255 }; + SDL_Color color = {(uint8_t)(TintColor.R * 255.0f), (uint8_t)(TintColor.G * 255.0f), (uint8_t)(TintColor.B * 255.0f), 255}; if (SDL_BYTEORDER == SDL_BIG_ENDIAN) { - color.r = (uint8_t)(TintColor.B * 255.0f); - color.b = (uint8_t)(TintColor.R * 255.0f); + std::swap(color.r, color.b); } SDL_SetTextureColorMod(m_sdl_tex, color.r, color.g, color.b); SDL_SetTextureAlphaMod(m_sdl_tex, static_cast(255 - (Transparency / 100.0) * 255)); - int error = SDL_RenderCopyExF( - renderer->GetSDLRenderer(), - m_sdl_tex, - nullptr, - &destRect, - Rotation, - nullptr, - (SDL_RendererFlip)0); + int flipMode = SDL_FLIP_NONE; + if (FlipX) flipMode |= SDL_FLIP_HORIZONTAL; + if (FlipY) flipMode |= SDL_FLIP_VERTICAL; + + int error = SDL_RenderCopyExF(renderer->GetSDLRenderer(), m_sdl_tex, nullptr, &destRect, Rotation, nullptr, (SDL_RendererFlip)flipMode); if (error != 0) { throw SDLException(); @@ -328,20 +318,16 @@ void Texture2D::Draw(Rect *clipRect, bool manualDraw) } if (clipRect) { - if (originClip.w == 0 || originClip.h == 0) { - SDL_RenderSetClipRect(renderer->GetSDLRenderer(), nullptr); - } else { - SDL_RenderSetClipRect(renderer->GetSDLRenderer(), &originClip); - } + SDL_RenderSetClipRect(renderer->GetSDLRenderer(), originClip.w == 0 || originClip.h == 0 ? nullptr : &originClip); } } } void Texture2D::CalculateSize() { - GameWindow *window = GameWindow::GetInstance(); - int wWidth = window->GetBufferWidth(); - int wHeight = window->GetBufferHeight(); + GameWindow* window = GameWindow::GetInstance(); + int wWidth = window->GetBufferWidth(); + int wHeight = window->GetBufferHeight(); float xPos = static_cast((wWidth * Position.X.Scale) + Position.X.Offset); float yPos = static_cast((wHeight * Position.Y.Scale) + Position.Y.Offset); @@ -349,8 +335,8 @@ void Texture2D::CalculateSize() float width = static_cast((m_actualSize.right * Size.X.Scale) + Size.X.Offset); float height = static_cast((m_actualSize.bottom * Size.Y.Scale) + Size.Y.Offset); - m_preAnchoredSize = { (LONG)xPos, (LONG)yPos, (LONG)width, (LONG)height }; - m_preAnchoredSizeF = { xPos, yPos, width, height }; + m_preAnchoredSize = {(LONG)xPos, (LONG)yPos, (LONG)width, (LONG)height}; + m_preAnchoredSizeF = {xPos, yPos, width, height}; float xAnchor = width * std::clamp((float)AnchorPoint.X, 0.0f, 1.0f); float yAnchor = height * std::clamp((float)AnchorPoint.Y, 0.0f, 1.0f); @@ -358,101 +344,65 @@ void Texture2D::CalculateSize() xPos -= xAnchor; yPos -= yAnchor; - m_calculatedSize = { (LONG)xPos, (LONG)yPos, (LONG)width, (LONG)height }; - m_calculatedSizeF = { xPos, yPos, width, height }; - - AbsolutePosition = { xPos, yPos }; - AbsoluteSize = { width, height }; -} - -Rect Texture2D::GetOriginalRECT() -{ - return m_actualSize; -} - -void Texture2D::SetOriginalRECT(Rect size) -{ - m_actualSize = size; -} - -Texture2D *Texture2D::FromTexture2D(Texture2D *tex) -{ - auto copy = new Texture2D(tex->m_sdl_tex); - copy->m_actualSize = tex->m_actualSize; - copy->Position = tex->Position; - copy->Size = tex->Size; - - return copy; -} - -Texture2D *Texture2D::FromBMP(uint8_t *fileData, size_t size) -{ - return nullptr; -} + m_calculatedSize = {(LONG)xPos, (LONG)yPos, (LONG)width, (LONG)height}; + m_calculatedSizeF = {xPos, yPos, width, height}; -Texture2D *Texture2D::FromBMP(std::string fileName) -{ - return nullptr; + AbsolutePosition = {xPos, yPos}; + AbsoluteSize = {width, height}; } -Texture2D *Texture2D::FromJPEG(uint8_t *fileData, size_t size) +void Texture2D::UpdateTexture(uint8_t* buffer, int width, int height, int pitch) { - return nullptr; -} + if (!m_ready) return; -Texture2D *Texture2D::FromJPEG(std::string fileName) -{ - return nullptr; + if (Renderer::GetInstance()->IsVulkan() && m_vk_tex) { + vkTexture::UpdateTexture(m_vk_tex, buffer, pitch); + } else if (m_sdl_tex) { + SDL_UpdateTexture(m_sdl_tex, nullptr, buffer, pitch); + } } -Texture2D *Texture2D::FromPNG(uint8_t *fileData, size_t size) +Rect Texture2D::GetOriginalRECT() { - return nullptr; + return m_actualSize; } -Texture2D *Texture2D::FromPNG(std::string fileName) +void Texture2D::SetOriginalRECT(Rect size) { - return nullptr; + m_actualSize = size; } -void Texture2D::LoadImageResources(uint8_t *buffer, size_t size) +void Texture2D::LoadImageResources(uint8_t* buffer, size_t size) { if (Renderer::GetInstance()->IsVulkan()) { auto tex_data = vkTexture::TexLoadImage(buffer, size); - - m_actualSize = { 0, 0, tex_data->Width, tex_data->Height }; + m_actualSize = {0, 0, tex_data->Width, tex_data->Height}; m_vk_tex = tex_data; - m_bDisposeTexture = true; m_ready = true; } else { - SDL_RWops *rw = SDL_RWFromMem(buffer, (int)size); + SDL_RWops* rw = SDL_RWFromMem(buffer, static_cast(size)); + std::unique_ptr decompressed_surface(IMG_LoadTyped_RW(rw, 1, "PNG"), SDL_FreeSurface); - // check if buffer magic is BMP - if (buffer[0] == 0x42 && buffer[1] == 0x4D) { - m_sdl_surface = SDL_LoadBMP_RW(rw, 1); - } else { - m_sdl_surface = IMG_Load_RW(rw, 1); + if (!decompressed_surface) { + throw SDLException(); } - if (!m_sdl_surface) { + std::unique_ptr formatted_surface(SDL_ConvertSurfaceFormat(decompressed_surface.get(), SDL_PIXELFORMAT_ABGR8888, 0), SDL_FreeSurface); + + if (!formatted_surface) { throw SDLException(); } + m_sdl_surface = PremultiplyAlpha(formatted_surface.release()); // Fix white line issue m_sdl_tex = SDL_CreateTextureFromSurface(Renderer::GetInstance()->GetSDLRenderer(), m_sdl_surface); + if (!m_sdl_tex) { throw SDLException(); } - // sdl get texture resolution - int w, h; - SDL_QueryTexture(m_sdl_tex, nullptr, nullptr, &w, &h); - + m_actualSize = {0, 0, m_sdl_surface->w, m_sdl_surface->h}; m_bDisposeTexture = true; - m_actualSize = { 0, 0, w, h }; - m_ready = true; } - - delete[] buffer; -} +} \ No newline at end of file diff --git a/Engine/src/Threading/GameThread.cpp b/Engine/src/Threading/GameThread.cpp index e60f32ab..f3f04fd9 100644 --- a/Engine/src/Threading/GameThread.cpp +++ b/Engine/src/Threading/GameThread.cpp @@ -20,39 +20,62 @@ void GameThread::Run(std::function callback, bool background) m_background = background; if (background) { - m_thread = std::thread([&] { + m_thread = std::thread([this] { while (m_run) { - m_main_cb(); - - if (m_queue_cb.size()) { - std::function queue_cb = m_queue_cb.back(); - m_queue_cb.pop_back(); + std::function main_cb; + { + std::lock_guard lock(m_mutex); + main_cb = m_main_cb; + } + if (main_cb) { + main_cb(); + } + std::function queue_cb; + { + std::lock_guard lock(m_mutex); + if (!m_queue_cb.empty()) { + queue_cb = m_queue_cb.front(); + m_queue_cb.pop_back(); + } + } + if (queue_cb) { queue_cb(); } } - }); + }); } } void GameThread::Update() { - if (m_background) { - return; - } - - m_main_cb(); - - if (m_queue_cb.size()) { - std::function queue_cb = m_queue_cb.back(); - m_queue_cb.pop_back(); + if (!m_background) { + std::function main_cb; + { + std::lock_guard lock(m_mutex); + main_cb = m_main_cb; + } + if (main_cb) { + main_cb(); + } - queue_cb(); + std::function queue_cb; + { + std::lock_guard lock(m_mutex); + if (!m_queue_cb.empty()) { + queue_cb = m_queue_cb.back(); + m_queue_cb.pop_back(); + } + } + if (queue_cb) { + queue_cb(); + } } } void GameThread::QueueAction(std::function callback) { + std::lock_guard lock(m_mutex); m_queue_cb.push_back(callback); } @@ -60,6 +83,9 @@ void GameThread::Stop() { if (m_background) { m_run = false; - m_thread.join(); + if (m_thread.joinable()) { + m_thread.join(); + } } } + diff --git a/Game/CMakeLists.txt b/Game/CMakeLists.txt index 8d74a4b9..06a1b9c4 100644 --- a/Game/CMakeLists.txt +++ b/Game/CMakeLists.txt @@ -1,17 +1,17 @@ -cmake_minimum_required(VERSION 3.0.0) +cmake_minimum_required(VERSION 3.15) project(Game VERSION 0.1.0 LANGUAGES C CXX) file(GLOB_RECURSE HEADERS "./src/*.h") -add_executable(Game - ${HEADERS} +add_executable(Game + ${HEADERS} # MAIN - "src/main.cpp" + "src/main.cpp" "src/MyGame.cpp" "src/EnvironmentSetup.cpp" - # Data + # Data "src/Data/bms.cpp" "src/Data/Chart.cpp" "src/Data/OJM.cpp" @@ -20,9 +20,9 @@ add_executable(Game "src/Data/Util/Util.cpp" # Engine - "src/Engine/BGMPreview.cpp" + "src/Engine/BGMPreview.cpp" "src/Engine/Button.cpp" - "src/Engine/Autoplay.cpp" + "src/Engine/Autoplay.cpp" "src/Engine/DrawableNote.cpp" "src/Engine/DrawableTile.cpp" "src/Engine/FrameTimer.cpp" @@ -34,26 +34,27 @@ add_executable(Game "src/Engine/O2Texture.cpp" "src/Engine/RhythmEngine.cpp" "src/Engine/ScoreManager.cpp" - "src/Engine/SkinConfig.cpp" - "src/Engine/LuaScripting.cpp" - "src/Engine/SkinManager.cpp" + "src/Engine/SkinConfig.cpp" + "src/Engine/LuaScripting.cpp" + "src/Engine/SkinManager.cpp" + "src/Engine/VideoPlayer.cpp" "src/Engine/TimingLine.cpp" "src/Engine/TimingLineManager.cpp" - "src/Engine/Timing/StaticTiming.cpp" - "src/Engine/Timing/TimingBase.cpp" + "src/Engine/Timing/StaticTiming.cpp" + "src/Engine/Timing/TimingBase.cpp" "src/Engine/Timing/VelocityTiming.cpp" "src/Engine/Judgements/BeatBasedJudge.cpp" "src/Engine/Judgements/JudgeBase.cpp" "src/Engine/Judgements/MsBasedJudge.cpp" - # Resources - "src/Resources/GameResources.cpp" + # Resources + "src/Resources/GameResources.cpp" "src/Resources/GameDatabase.cpp" "src/Resources/MusicListMaker.cpp" # Scenes "src/Scenes/Converters/ToOsu.cpp" - "src/Scenes/EditorScene.cpp" + "src/Scenes/EditorScene.cpp" "src/Scenes/GameplayScene.cpp" "src/Scenes/IntroScene.cpp" "src/Scenes/LoadingScene.cpp" @@ -66,39 +67,75 @@ add_executable(Game "src/Scenes/Overlays/Settings.cpp" ) -#include "../Engine/include" target_include_directories(Game PRIVATE "../Engine/include") +target_include_directories(Game PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) -if (WIN32) - add_custom_command(TARGET Game POST_BUILD +# Hide console window in Release mode +set_target_properties(Game PROPERTIES WIN32_EXECUTABLE $) + +if(WIN32) + add_custom_command(TARGET Game POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory - ${CMAKE_SOURCE_DIR}/third-party/bin/x64-windows/Debug - $) + ${CMAKE_SOURCE_DIR}/third-party/bin/x64-windows/$ + $ + ) # Create directory $/Skins/Default then - # copy from ${CMAKE_SOURCE_DIR}/GameResources/Resources to $/Skins/Default - + # copy from ${CMAKE_SOURCE_DIR}/resources/Resources to $/Skins/Default add_custom_command(TARGET Game POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory $/Skins/Default COMMAND ${CMAKE_COMMAND} -E copy_directory - ${CMAKE_SOURCE_DIR}/GameResources/Resources - $/Skins/Default) -else () + ${CMAKE_SOURCE_DIR}/resources/Resources + $/Skins/Default + COMMAND ${CMAKE_COMMAND} -E make_directory + $/Resources/Notes + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/resources/Resources/Notes + $/Resources/Notes + COMMAND ${CMAKE_COMMAND} -E make_directory + $/Resources/Fonts + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/resources/Resources/Fonts + $/Resources/Fonts + COMMAND ${CMAKE_COMMAND} -E make_directory + $/Resources/Notes/Scripts + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/resources/Resources/Scripts + $/Resources/Notes/Scripts + ) +else() add_custom_command(TARGET Game POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory - $/Skins/Default - COMMAND ${CMAKE_COMMAND} -E copy_directory - ${CMAKE_SOURCE_DIR}/GameResources/Resources - $/Skins/Default) -endif () + COMMAND ${CMAKE_COMMAND} -E make_directory + $/Skins/Default + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/resources/Resources + $/Skins/Default + COMMAND ${CMAKE_COMMAND} -E make_directory + $/Resources/Notes + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/resources/Resources/Notes + $/Resources/Notes + COMMAND ${CMAKE_COMMAND} -E make_directory + $/Resources/Fonts + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/resources/Resources/Fonts + $/Resources/Fonts + COMMAND ${CMAKE_COMMAND} -E make_directory + $/Resources/Notes/Scripts + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/resources/Resources/Scripts + $/Resources/Notes/Scripts + ) +endif() -if (MSVC) +if(MSVC) target_link_options(Game PRIVATE $<$:/INCREMENTAL>) + target_link_options(Game PRIVATE $<$:/SUBSYSTEM:WINDOWS> $<$:/ENTRY:mainCRTStartup>) target_compile_options(Game PRIVATE $<$:/ZI> "/MP") endif() # set preprocessor MEM_LEAK_DEBUG target_compile_definitions(Game PRIVATE MEM_LEAK_DEBUG=1 _CRT_SECURE_NO_WARNINGS) -target_link_libraries(Game PRIVATE EstEngine ${O2GAME_LIBRARIES}) \ No newline at end of file +target_link_libraries(Game PRIVATE EstEngine ${O2GAME_LIBRARIES}) diff --git a/Game/src/Data/Chart.cpp b/Game/src/Data/Chart.cpp index 2f98fd57..84b52fce 100644 --- a/Game/src/Data/Chart.cpp +++ b/Game/src/Data/Chart.cpp @@ -1,687 +1,850 @@ #include "Chart.hpp" +#include "../EnvironmentSetup.hpp" +#include "../GameScenes.h" #include "Util/Util.hpp" #include #include +#include +#include #include #include #include #include +#include #include +#include -float float_floor(float value) -{ +float float_floor(float value) { #if __GNUC__ - return (float)::floorf(value); + return (float)::floorf(value); #else - return (float)std::floorf(value); + return (float)std::floorf(value); #endif } -double TimingInfo::CalculateBeat(double offset) -{ - if (Type == TimingType::SV) { - return 0; - } - - return Beat + (offset - StartTime) * Value / 60000.0; +namespace { +std::string getFileExtension(const std::string &filename) { + size_t dotPos = filename.find_last_of('.'); + if (dotPos != std::string::npos) { + std::string ext = filename.substr(dotPos + 1); + // Convert extension to lowercase + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + return ext; + } + return ""; // No extension found } - -Chart::Chart() -{ - InitialSvMultiplier = 1.0f; - m_keyCount = 7; +} // namespace + +static std::vector GetMappedLanes(int keyCount) { + static const std::map> mappedKeyIndex = { + {4, {0, 1, 5, 6}}, + {5, {1, 2, 3, 4, 5}}, + {6, {0, 1, 2, 4, 5, 6}}, + {7, {0, 1, 2, 3, 4, 5, 6}}, + }; + if (mappedKeyIndex.find(keyCount) != mappedKeyIndex.end()) { + return mappedKeyIndex.at(keyCount); + } else { + std::vector lanes; + for (int i = 0; i < keyCount; i++) + lanes.push_back(i); + return lanes; + } } -Chart::Chart(Osu::Beatmap &beatmap) -{ - if (!beatmap.IsValid()) { - throw std::invalid_argument("Invalid osu beatmap!"); - } +double TimingInfo::CalculateBeat(double offset) const { + if (Type == TimingType::SV) { + return 0; + } - if (beatmap.Mode != 3) { - throw std::invalid_argument("osu beatmap's Mode must be 3 (or mania mode)"); - } - - if (beatmap.CircleSize < 1 || beatmap.CircleSize > 7) { - throw std::invalid_argument("osu beatmap's Mania key must be 7"); - } - - // m_audio = beatmap.AudioFilename; - m_title = std::u8string(beatmap.Title.begin(), beatmap.Title.end()); - m_keyCount = (int)beatmap.CircleSize; - m_artist = std::u8string(beatmap.Artist.begin(), beatmap.Artist.end()); - m_beatmapDirectory = beatmap.CurrentDir; - - for (auto &event : beatmap.Events) { - switch (event.Type) { - case Osu::OsuEventType::Background: - { - std::string fileName = event.params[0]; - fileName.erase(std::remove(fileName.begin(), fileName.end(), '\"'), fileName.end()); - - m_backgroundFile = fileName; - break; - } - - case Osu::OsuEventType::Sample: - { - std::string fileName = event.params[1]; - fileName.erase(std::remove(fileName.begin(), fileName.end(), '\"'), fileName.end()); - - auto path = beatmap.CurrentDir / fileName; - if (std::filesystem::exists(path)) { - AutoSample sample = {}; - sample.StartTime = event.StartTime; - sample.Index = beatmap.GetCustomSampleIndex(fileName); - sample.Volume = 1; - sample.Pan = 0; + return Beat + (offset - StartTime) * Value / 60000.0; +} - m_autoSamples.push_back(sample); - } +Chart::Chart() { + InitialSvMultiplier = 1.0f; + m_keyCount = 7; +} - break; +Chart::Chart(Osu::Beatmap &beatmap) // Refactor +{ + if (!beatmap.IsValid()) { + throw std::invalid_argument("Invalid osu beatmap!"); + } + + if (beatmap.Mode != 3) { + throw std::invalid_argument("osu beatmap's Mode must be 3 (or mania mode)"); + } + + if (beatmap.CircleSize < 1 || beatmap.CircleSize > 7) { + throw std::invalid_argument( + "osu beatmap's Mania key must be between 1 and 7"); + } + + m_title = std::u8string(beatmap.Title.begin(), beatmap.Title.end()); + m_keyCount = (int)beatmap.CircleSize; + m_artist = std::u8string(beatmap.Artist.begin(), beatmap.Artist.end()); + m_difname = std::u8string(beatmap.Version.begin(), beatmap.Version.end()); + m_beatmapDirectory = beatmap.CurrentDir; + + for (auto &event : beatmap.Events) { + if (event.Type == Osu::OsuEventType::Background) { + std::string fileName = event.params[0]; + fileName.erase(std::remove(fileName.begin(), fileName.end(), '\"'), + fileName.end()); + m_backgroundFile = fileName; + } else if (event.Type == Osu::OsuEventType::Videos) { + if (!event.params.empty()) { + std::string fileName = event.params[0]; + fileName.erase(std::remove(fileName.begin(), fileName.end(), '\"'), + fileName.end()); + m_videoFile = fileName; + m_videoOffset = event.StartTime; + } + } + } + + for (auto &event : beatmap.Events) { + if (event.Type == Osu::OsuEventType::Sample) { + std::string fileName = event.params[1]; + if (fileName.empty()) { + continue; + } + if (fileName.front() == '\"' && fileName.back() == '\"') { + fileName = fileName.substr(1, fileName.size() - 2); + } + + auto path = beatmap.CurrentDir / fileName; + if (!std::filesystem::exists(path)) { + std::string extension = getFileExtension(fileName); + if (!extension.empty()) { + std::vector extensionsToTry = {".ogg", ".wav", ".mp3"}; + for (const auto &ext : extensionsToTry) { + std::string newFileName = + fileName.substr(0, fileName.find_last_of('.')) + ext; + path = beatmap.CurrentDir / newFileName; + if (std::filesystem::exists(path)) { + fileName = newFileName; + break; } + } } - } + } - { + if (std::filesystem::exists(path)) { AutoSample sample = {}; - sample.StartTime = beatmap.AudioLeadIn; - sample.Index = beatmap.GetCustomSampleIndex(beatmap.AudioFilename); + sample.StartTime = event.StartTime; + sample.Index = beatmap.GetCustomSampleIndex(fileName); sample.Volume = 1; sample.Pan = 0; - m_autoSamples.push_back(sample); - } - - for (auto ¬e : beatmap.HitObjects) { - NoteInfo info = {}; - info.StartTime = note.StartTime; - info.Type = NoteType::NORMAL; - info.Keysound = note.KeysoundIndex; - info.LaneIndex = static_cast(float_floor(note.X * static_cast(beatmap.CircleSize) / 512.0f)); - info.Volume = static_cast(note.Volume) / 100.0f; - info.Pan = 0; - - if (note.Type == 128) { - info.Type = NoteType::HOLD; - info.EndTime = note.EndTime; - } - - m_notes.push_back(info); - } - - for (auto &timing : beatmap.TimingPoints) { - bool IsSV = timing.Inherited == 0 || timing.BeatLength < 0; + } else { + Logs::Puts("[Chart] Custom sample file not found: %s", + fileName.c_str()); + } + } + } + + AutoSample autoSample = {}; + autoSample.StartTime = -1; // The main BGM always starts at time 0. AudioLeadIn is just extra silence before time 0, it shouldn't shift the actual BGM trigger time. + autoSample.Index = beatmap.GetCustomSampleIndex(beatmap.AudioFilename); + autoSample.Volume = 1.0f; + autoSample.Pan = 0; + m_autoSamples.push_back(autoSample); + + for (auto ¬e : beatmap.HitObjects) { + NoteInfo info = {}; + info.StartTime = note.StartTime; + info.Type = NoteType::NORMAL; + info.Keysound = note.KeysoundIndex; + int lane = static_cast( + float_floor(note.X * static_cast(beatmap.CircleSize) / 512.0f)); + if (lane < 0 || lane >= beatmap.CircleSize) + continue; + + info.LaneIndex = GetMappedLanes(beatmap.CircleSize)[lane]; + info.Volume = + note.Volume > 0 ? static_cast(note.Volume) / 100.0f : 1.0f; + info.Pan = 0; + + if (note.Type == 128) { + info.Type = NoteType::HOLD; + info.EndTime = note.EndTime; + } + + m_notes.push_back(info); + } + + for (auto &timing : beatmap.TimingPoints) { + if (timing.Inherited == 0 || timing.BeatLength < 0) { + TimingInfo info = {}; + info.StartTime = timing.Offset; + info.Value = std::clamp(-100.0f / timing.BeatLength, 0.1f, 10.0f); + info.Type = TimingType::SV; + m_svs.push_back(info); + } else { + TimingInfo info = {}; + info.StartTime = timing.Offset; + info.Value = 60000.0f / timing.BeatLength; + info.TimeSignature = timing.TimeSignature; + info.Type = TimingType::BPM; + m_bpms.push_back(info); + } + } + + for (int i = 0; i < beatmap.HitSamples.size(); i++) { + auto &keysound = beatmap.HitSamples[i]; + auto path = beatmap.CurrentDir / keysound; + + Sample sm = {}; + sm.FileName = path; + sm.Index = i; + m_samples.push_back(sm); + } + + CalculateBeat(); + SortTimings(); + NormalizeTimings(); + ComputeHash(); +} - if (IsSV) { - TimingInfo info = {}; - info.StartTime = timing.Offset; - info.Value = std::clamp(-100.0f / timing.BeatLength, 0.1f, 10.0f); - info.Type = TimingType::SV; +Chart::Chart(BMS::BMSFile &file) { + if (!file.IsValid()) { + throw std::invalid_argument("Invalid BMS file!"); + } + + m_beatmapDirectory = file.CurrentDir; + m_title = std::u8string(file.Title.begin(), file.Title.end()); + m_audio = ""; // file.FileDirectory + "/" + "test.mp3"; + m_keyCount = 7; + m_artist = std::u8string(file.Artist.begin(), file.Artist.end()); + m_backgroundFile = file.StageFile; + BaseBPM = file.BPM; + m_customMeasures = file.Measures; + + double lastTime[7] = {}; + std::sort(file.Notes.begin(), file.Notes.end(), + [](const BMS::BMSNote ¬e1, const BMS::BMSNote note2) { + return note1.StartTime < note2.StartTime; + }); + + for (auto ¬e : file.Notes) { + NoteInfo info = {}; + info.StartTime = note.StartTime; + info.Type = NoteType::NORMAL; + info.LaneIndex = note.Lane; + if (info.LaneIndex < 0 || info.LaneIndex >= 7) + continue; + info.Keysound = note.SampleIndex; + info.Volume = 1.0f; + info.Pan = 0; + + if (note.EndTime != -1) { + info.Type = NoteType::HOLD; + info.EndTime = note.EndTime; + } + + // check if overlap lastTime + if (info.StartTime < lastTime[info.LaneIndex]) { + Logs::Puts("[Chart] Overlapped note found on file %s at %.2f ms and " + "conflict with %.2f ms", + m_beatmapDirectory.string().c_str(), info.StartTime, + lastTime[info.LaneIndex]); + + if (note.SampleIndex != -1) { + AutoSample sm = {}; + sm.StartTime = note.StartTime; + sm.Index = note.SampleIndex; + sm.Volume = 1.0f; + sm.Pan = 0; - m_svs.push_back(info); - } else { - TimingInfo info = {}; - info.StartTime = timing.Offset; - info.Value = 60000.0f / timing.BeatLength; - info.TimeSignature = timing.TimeSignature; - info.Type = TimingType::BPM; + m_autoSamples.push_back(sm); + } + } else { + lastTime[info.LaneIndex] = + info.Type == NoteType::HOLD ? info.EndTime : info.StartTime; + m_notes.push_back(info); + } + } + + for (auto &timing : file.Timings) { + bool IsSV = timing.Value < 0; + + if (IsSV) { + TimingInfo info = {}; + info.StartTime = timing.StartTime; + info.Value = (float)std::clamp(timing.Value, 0.1, 10.0); + info.Type = TimingType::SV; + + m_svs.push_back(info); + } else { + TimingInfo info = {}; + info.StartTime = timing.StartTime; + info.Value = (float)timing.Value; + info.Type = TimingType::BPM; + info.TimeSignature = 4.0f * timing.TimeSignature; + + m_bpms.push_back(info); + } + } + + for (auto &sample : file.Samples) { + Sample sm = {}; + sm.FileName = file.CurrentDir / sample.second; + sm.Index = sample.first; + + m_samples.push_back(sm); + } + + for (auto &autoSample : file.AutoSamples) { + AutoSample sm = {}; + sm.StartTime = autoSample.StartTime; + sm.Index = autoSample.SampleIndex; + sm.Volume = 1.0f; + sm.Pan = 0; + + m_autoSamples.push_back(sm); + } + + if (m_bpms.size() == 0) { + TimingInfo info = {}; + info.StartTime = 0; + info.Value = file.BPM; + info.Type = TimingType::BPM; + + m_bpms.push_back(info); + } + + CalculateBeat(); + + SortTimings(); + + PredefinedAudioLength = file.AudioLength; + NormalizeTimings(); + ComputeKeyCount(); + ComputeHash(); +} - m_bpms.push_back(info); - } - } +Chart::Chart(O2::OJN &file, int diffIndex) { + auto &diff = file.Difficulties[diffIndex]; + + m_title = + CodepageToUtf8(file.Header.title, sizeof(file.Header.title), "euc-kr"); + m_artist = + CodepageToUtf8(file.Header.artist, sizeof(file.Header.artist), "euc-kr"); + + m_backgroundBuffer = file.BackgroundImage; + m_keyCount = 7; + m_customMeasures = diff.Measures; + m_level = file.Header.level[diffIndex]; + O2JamId = file.Header.songid; + + double lastTime[7] = {}; + for (auto ¬e : diff.Notes) { + NoteInfo info = {}; + info.StartTime = note.StartTime; + info.Keysound = note.SampleRefId; + info.LaneIndex = note.LaneIndex; + if (info.LaneIndex < 0 || info.LaneIndex >= 7) + continue; + info.Type = NoteType::NORMAL; + info.Volume = note.Volume; + info.Pan = note.Pan; + + if (note.IsLN) { + info.Type = NoteType::HOLD; + info.EndTime = note.EndTime; + } + + // check if overlap lastTime + if (info.StartTime < lastTime[info.LaneIndex]) { + Logs::Puts("[Chart] Overlapped note found on file o2ma%d at %.2f ms and " + "conflict with %.2f ms", + O2JamId, info.StartTime, lastTime[info.LaneIndex]); + + if (note.SampleRefId != -1) { + AutoSample sm = {}; + sm.StartTime = note.StartTime; + sm.Index = note.SampleRefId; + sm.Volume = note.Volume; + sm.Pan = note.Pan; - for (int i = 0; i < beatmap.HitSamples.size(); i++) { - auto &keysound = beatmap.HitSamples[i]; + m_autoSamples.push_back(sm); + } + } else { + lastTime[info.LaneIndex] = + info.Type == NoteType::HOLD ? info.EndTime : info.StartTime; + m_notes.push_back(info); + } + } + + for (auto &timing : diff.Timings) { + TimingInfo info = {}; + info.StartTime = timing.Time; + info.Value = (float)timing.BPM; + info.TimeSignature = 4; + info.Type = TimingType::BPM; + + m_bpms.push_back(info); + } + + CalculateBeat(); + + for (auto &autoSample : diff.AutoSamples) { + AutoSample sm = {}; + sm.StartTime = autoSample.StartTime; + sm.Index = autoSample.SampleRefId; + sm.Volume = autoSample.Volume; + sm.Pan = autoSample.Pan; + + m_autoSamples.push_back(sm); + } + + for (auto &sample : diff.Samples) { + Sample sm = {}; + sm.FileBuffer = sample.AudioData; + sm.Index = sample.RefValue; + sm.Type = 2; + + m_samples.push_back(sm); + } + + SortTimings(); + + PredefinedAudioLength = diff.AudioLength; + NormalizeTimings(); + ComputeKeyCount(); + ComputeHash(); +} - auto path = beatmap.CurrentDir / keysound; +void Chart::CalculateBeat() { + m_bpms[0].Beat = 0; + for (size_t i = 1; i < m_bpms.size(); i++) { + m_bpms[i].Beat = m_bpms[i - 1].CalculateBeat(m_bpms[i].StartTime); + } +} - Sample sm = {}; - sm.FileName = path; - sm.Index = i; +void Chart::SortTimings() { + std::sort(m_autoSamples.begin(), m_autoSamples.end(), + [](const AutoSample &a, const AutoSample &b) { + return a.StartTime < b.StartTime; + }); + + std::sort(m_bpms.begin(), m_bpms.end(), + [](const TimingInfo &a, const TimingInfo &b) { + return a.StartTime < b.StartTime; + }); + + std::sort(m_svs.begin(), m_svs.end(), + [](const TimingInfo &a, const TimingInfo &b) { + return a.StartTime < b.StartTime; + }); +} - m_samples.push_back(sm); - } +Chart::~Chart() {} - for (auto ¬e : m_notes) { - switch (m_keyCount) { - case 4: - { - if (note.LaneIndex >= 2) { - note.LaneIndex += 3; - } - break; - } +int Chart::GetO2JamId() { return O2JamId; } - case 5: - { - if (note.LaneIndex == 3) { - note.LaneIndex += 1; - } else if (note.LaneIndex >= 4) { - note.LaneIndex += 2; - } - break; - } - } +double Chart::GetLength() { + double max_length = 0; + for (const auto ¬e : m_notes) { + double note_end = + (note.Type == NoteType::HOLD) ? note.EndTime : note.StartTime; + if (note_end > max_length) { + max_length = note_end; } + } - m_bpms[0].Beat = 0; - for (int i = 1; i < m_bpms.size(); i++) { - m_bpms[i].Beat = m_bpms[i - 1].CalculateBeat(m_bpms[i].StartTime); + for (const auto &sample : m_autoSamples) { + if (sample.StartTime > max_length) { + max_length = sample.StartTime; } + } - std::sort(m_autoSamples.begin(), m_autoSamples.end(), [](const AutoSample &a, const AutoSample &b) { - return a.StartTime < b.StartTime; - }); - - std::sort(m_bpms.begin(), m_bpms.end(), [](const TimingInfo &a, const TimingInfo &b) { - return a.StartTime < b.StartTime; - }); - - std::sort(m_svs.begin(), m_svs.end(), [](const TimingInfo &a, const TimingInfo &b) { - return a.StartTime < b.StartTime; - }); - - NormalizeTimings(); - ComputeHash(); + return max_length; } -Chart::Chart(BMS::BMSFile &file) -{ - if (!file.IsValid()) { - throw std::invalid_argument("Invalid BMS file!"); - } - - m_beatmapDirectory = file.CurrentDir; - m_title = std::u8string(file.Title.begin(), file.Title.end()); - m_audio = ""; // file.FileDirectory + "/" + "test.mp3"; - m_keyCount = 7; - m_artist = std::u8string(file.Artist.begin(), file.Artist.end()); - m_backgroundFile = file.StageFile; - BaseBPM = file.BPM; - m_customMeasures = file.Measures; - - double lastTime[7] = {}; - std::sort(file.Notes.begin(), file.Notes.end(), [](const BMS::BMSNote ¬e1, const BMS::BMSNote note2) { - return note1.StartTime < note2.StartTime; - }); - - for (auto ¬e : file.Notes) { - NoteInfo info = {}; - info.StartTime = note.StartTime; - info.Type = NoteType::NORMAL; - info.LaneIndex = note.Lane; - info.Keysound = note.SampleIndex; - info.Volume = 1; - info.Pan = 0; - - if (note.EndTime != -1) { - info.Type = NoteType::HOLD; - info.EndTime = note.EndTime; - } - - // check if overlap lastTime - if (info.StartTime < lastTime[info.LaneIndex]) { - Logs::Puts("[Chart] overlapped note found at %.2f ms and conflict with %.2f ms", info.StartTime, lastTime[info.LaneIndex]); - - if (note.SampleIndex != -1) { - AutoSample sm = {}; - sm.StartTime = note.StartTime; - sm.Index = note.SampleIndex; - sm.Volume = 1; - sm.Pan = 0; - - m_autoSamples.push_back(sm); - } - } else { - lastTime[info.LaneIndex] = info.Type == NoteType::HOLD ? info.EndTime : info.StartTime; - m_notes.push_back(info); - } - } - - for (auto &timing : file.Timings) { - bool IsSV = timing.Value < 0; - - if (IsSV) { - TimingInfo info = {}; - info.StartTime = timing.StartTime; - info.Value = (float)std::clamp(timing.Value, 0.1, 10.0); - info.Type = TimingType::SV; +void Chart::ApplyMod(Mod mod, void *data) { + std::vector mappedLanes = GetMappedLanes(m_keyCount); - m_svs.push_back(info); - } else { - TimingInfo info = {}; - info.StartTime = timing.StartTime; - info.Value = (float)timing.Value; - info.Type = TimingType::BPM; - info.TimeSignature = 4.0f * timing.TimeSignature; - - m_bpms.push_back(info); - } + // 1. Un-map notes to logical lanes + for (auto ¬e : m_notes) { + auto it = std::find(mappedLanes.begin(), mappedLanes.end(), note.LaneIndex); + if (it != mappedLanes.end()) { + note.LaneIndex = std::distance(mappedLanes.begin(), it); + } else { + note.LaneIndex = -1; // Unmapped/invalid } + } - for (auto &sample : file.Samples) { - Sample sm = {}; - sm.FileName = file.CurrentDir / sample.second; - sm.Index = sample.first; - - m_samples.push_back(sm); + // 2. Apply Mod on logical lanes (0 to m_keyCount - 1) + switch (mod) { + case Mod::MIRROR: { + for (auto ¬e : m_notes) { + if (note.LaneIndex >= 0 && note.LaneIndex < m_keyCount) { + note.LaneIndex = m_keyCount - 1 - note.LaneIndex; + } } + break; + } - for (auto &autoSample : file.AutoSamples) { - AutoSample sm = {}; - sm.StartTime = autoSample.StartTime; - sm.Index = autoSample.SampleIndex; - sm.Volume = 1; - sm.Pan = 0; + case Mod::RANDOM: { + std::vector shuffled(m_keyCount); + std::iota(shuffled.begin(), shuffled.end(), 0); + auto rng = std::default_random_engine{}; + rng.seed((uint32_t)time(NULL)); + std::shuffle(std::begin(shuffled), std::end(shuffled), rng); - m_autoSamples.push_back(sm); + for (auto ¬e : m_notes) { + if (note.LaneIndex >= 0 && note.LaneIndex < m_keyCount) { + note.LaneIndex = shuffled[note.LaneIndex]; + } + } + break; + } + + case Mod::PANIC: { + auto rng = std::default_random_engine{}; + rng.seed((uint32_t)time(NULL)); + + std::sort(m_notes.begin(), m_notes.end(), + [](const NoteInfo &a, const NoteInfo &b) { + return a.StartTime < b.StartTime; + }); + + double maxTime = 0; + for (const auto &n : m_notes) { + maxTime = std::max(maxTime, n.StartTime); + if (n.Type == NoteType::HOLD) + maxTime = std::max(maxTime, n.EndTime); + } + + std::vector measureBoundaries = m_customMeasures; + if (measureBoundaries.empty()) { + double currentTime = 0; + for (size_t i = 0; i < m_bpms.size(); i++) { + double bpm = m_bpms[i].Value; + double timeSig = m_bpms[i].TimeSignature; + if (timeSig <= 0) + timeSig = 4; + double measureDuration = (60000.0 / bpm) * timeSig; + + double nextTimingTime = + (i + 1 < m_bpms.size()) ? m_bpms[i + 1].StartTime : maxTime + 5000; + + while (currentTime < nextTimingTime) { + measureBoundaries.push_back(currentTime); + currentTime += measureDuration; + } + } } - if (m_bpms.size() == 0) { - TimingInfo info = {}; - info.StartTime = 0; - info.Value = file.BPM; - info.Type = TimingType::BPM; - - m_bpms.push_back(info); + if (measureBoundaries.empty()) { + measureBoundaries.push_back(0); } - m_bpms[0].Beat = 0; - for (int i = 1; i < m_bpms.size(); i++) { - m_bpms[i].Beat = m_bpms[i - 1].CalculateBeat(m_bpms[i].StartTime); + while (measureBoundaries.back() <= maxTime + 5000) { + double last = measureBoundaries.back(); + double step = + measureBoundaries.size() >= 2 + ? (last - measureBoundaries[measureBoundaries.size() - 2]) + : 2000.0; + if (step <= 0) + step = 2000.0; + measureBoundaries.push_back(last + step); } - std::sort(m_autoSamples.begin(), m_autoSamples.end(), [](const AutoSample &a, const AutoSample &b) { - return a.StartTime < b.StartTime; - }); + auto getMeasureIndex = [&](double time) -> int { + auto it = std::upper_bound(measureBoundaries.begin(), + measureBoundaries.end(), time); + if (it == measureBoundaries.begin()) + return 0; + return std::distance(measureBoundaries.begin(), it) - 1; + }; - std::sort(m_bpms.begin(), m_bpms.end(), [](const TimingInfo &a, const TimingInfo &b) { - return a.StartTime < b.StartTime; - }); + std::unordered_map> measureMappings; + std::vector holdEndTime(m_keyCount, -1.0); + std::vector prevMapping(m_keyCount); + for (int i = 0; i < m_keyCount; i++) + prevMapping[i] = i; - std::sort(m_svs.begin(), m_svs.end(), [](const TimingInfo &a, const TimingInfo &b) { - return a.StartTime < b.StartTime; - }); + int currentMeasure = -1; - PredefinedAudioLength = file.AudioLength; - NormalizeTimings(); - ComputeKeyCount(); - ComputeHash(); -} + for (auto ¬e : m_notes) { + if (note.LaneIndex < 0 || note.LaneIndex >= m_keyCount) + continue; -Chart::Chart(O2::OJN &file, int diffIndex) -{ - auto &diff = file.Difficulties[diffIndex]; + int measure = getMeasureIndex(note.StartTime); - m_title = CodepageToUtf8(file.Header.title, sizeof(file.Header.title), "euc-kr"); - m_artist = CodepageToUtf8(file.Header.artist, sizeof(file.Header.artist), "euc-kr"); + while (currentMeasure < measure) { + currentMeasure++; - m_backgroundBuffer = file.BackgroundImage; - m_keyCount = 7; - m_customMeasures = diff.Measures; - m_level = file.Header.level[diffIndex]; - O2JamId = file.Header.songid; - - double lastTime[7] = {}; - for (auto ¬e : diff.Notes) { - NoteInfo info = {}; - info.StartTime = note.StartTime; - info.Keysound = note.SampleRefId; - info.LaneIndex = note.LaneIndex; - info.Type = NoteType::NORMAL; - info.Volume = note.Volume; - info.Pan = note.Pan; - - if (note.IsLN) { - info.Type = NoteType::HOLD; - info.EndTime = note.EndTime; + // O2Jam behavior: if ANY hold note extends into this measure, skip the + // entire shuffle and reuse the previous mapping + bool hasActiveHold = false; + for (int i = 0; i < m_keyCount; i++) { + if (holdEndTime[i] > measureBoundaries[currentMeasure]) { + hasActiveHold = true; + break; + } } - // check if overlap lastTime - if (info.StartTime < lastTime[info.LaneIndex]) { - Logs::Puts("[Chart] overlapped note found at %.2f ms and conflict with %.2f ms", info.StartTime, lastTime[info.LaneIndex]); - - if (note.SampleRefId != -1) { - AutoSample sm = {}; - sm.StartTime = note.StartTime; - sm.Index = note.SampleRefId; - sm.Volume = note.Volume; - sm.Pan = note.Pan; - - m_autoSamples.push_back(sm); - } + if (hasActiveHold) { + measureMappings[currentMeasure] = prevMapping; } else { - lastTime[info.LaneIndex] = info.Type == NoteType::HOLD ? info.EndTime : info.StartTime; - m_notes.push_back(info); - } - } + std::vector newMapping(m_keyCount); + std::iota(newMapping.begin(), newMapping.end(), 0); + std::shuffle(newMapping.begin(), newMapping.end(), rng); - for (auto &timing : diff.Timings) { - TimingInfo info = {}; - info.StartTime = timing.Time; - info.Value = (float)timing.BPM; - info.TimeSignature = 4; - info.Type = TimingType::BPM; + measureMappings[currentMeasure] = newMapping; + prevMapping = newMapping; + } + } - m_bpms.push_back(info); - } + if (note.Type == NoteType::HOLD) { + holdEndTime[note.LaneIndex] = + std::max(holdEndTime[note.LaneIndex], note.EndTime); + } - m_bpms[0].Beat = 0; - for (int i = 1; i < m_bpms.size(); i++) { - m_bpms[i].Beat = m_bpms[i - 1].CalculateBeat(m_bpms[i].StartTime); + note.LaneIndex = measureMappings[measure][note.LaneIndex]; } + break; + } - for (auto &autoSample : diff.AutoSamples) { - AutoSample sm = {}; - sm.StartTime = autoSample.StartTime; - sm.Index = autoSample.SampleRefId; - sm.Volume = autoSample.Volume; - sm.Pan = autoSample.Pan; + case Mod::REARRANGE: { + int *lanes = reinterpret_cast(data); - m_autoSamples.push_back(sm); + for (auto ¬e : m_notes) { + if (note.LaneIndex >= 0 && note.LaneIndex < m_keyCount) { + note.LaneIndex = lanes[note.LaneIndex]; + } } + break; + } + } - for (auto &sample : diff.Samples) { - Sample sm = {}; - sm.FileBuffer = sample.AudioData; - sm.Index = sample.RefValue; - sm.Type = 2; - - m_samples.push_back(sm); + // 3. Re-map notes to physical lanes + for (auto ¬e : m_notes) { + if (note.LaneIndex >= 0 && note.LaneIndex < m_keyCount) { + note.LaneIndex = mappedLanes[note.LaneIndex]; } - - std::sort(m_autoSamples.begin(), m_autoSamples.end(), [](const AutoSample &a, const AutoSample &b) { - return a.StartTime < b.StartTime; - }); - - std::sort(m_bpms.begin(), m_bpms.end(), [](const TimingInfo &a, const TimingInfo &b) { - return a.StartTime < b.StartTime; - }); - - std::sort(m_svs.begin(), m_svs.end(), [](const TimingInfo &a, const TimingInfo &b) { - return a.StartTime < b.StartTime; - }); - - PredefinedAudioLength = diff.AudioLength; - NormalizeTimings(); - ComputeKeyCount(); - ComputeHash(); + } } -Chart::~Chart() -{ -} +void Chart::ComputeHash() { + std::string result; + for (int i = 0; i < m_notes.size(); i++) { + result += std::to_string(m_notes[i].StartTime + m_notes[i].EndTime); + } -int Chart::GetO2JamId() -{ - return O2JamId; -} + uint8_t data[16]; + md5String((char *)result.c_str(), data); -double Chart::GetLength() -{ - if (PredefinedAudioLength != -1) { - return PredefinedAudioLength; - } + std::stringstream ss; + ss << std::hex << std::setfill('0'); + for (int i = 0; i < 16; i++) { + ss << std::setw(2) << static_cast(data[i]); + } - return m_notes[m_notes.size() - 1].EndTime != 0 - ? m_notes[m_notes.size() - 1].EndTime - : m_notes[m_notes.size() - 1].StartTime; + MD5Hash = ss.str(); } -void Chart::ApplyMod(Mod mod, void *data) -{ - switch (mod) { - case Mod::MIRROR: - { - for (auto ¬e : m_notes) { - note.LaneIndex = m_keyCount - 1 - note.LaneIndex; - } - break; - } - - case Mod::RANDOM: - { - std::vector lanes(m_keyCount); - for (int i = 0; i < 7; i++) { - lanes[i] = i; - } +float Chart::GetCommonBPM() { + if (m_bpms.size() == 0) { + return 0.0f; + } - auto rng = std::default_random_engine{}; - rng.seed((uint32_t)time(NULL)); + std::vector orderedByDescending(m_notes); + std::sort(orderedByDescending.begin(), orderedByDescending.end(), + [](const NoteInfo &a, const NoteInfo &b) { + return a.StartTime > b.StartTime; + }); - std::shuffle(std::begin(lanes), std::end(lanes), rng); + auto &lastObject = orderedByDescending[0]; + double lastTime = lastObject.Type == NoteType::HOLD ? lastObject.EndTime + : lastObject.StartTime; - for (auto ¬e : m_notes) { - note.LaneIndex = lanes[note.LaneIndex]; - } - break; - } + std::unordered_map durations; + for (int i = (int)m_bpms.size() - 1; i >= 0; i--) { + auto &tp = m_bpms[i]; - case Mod::REARRANGE: - { - int *lanes = reinterpret_cast(data); + if (tp.StartTime > lastTime) { + continue; + } - for (auto ¬e : m_notes) { - note.LaneIndex = lanes[note.LaneIndex]; - } + int duration = (int)(lastTime - (i == 0 ? 0 : tp.StartTime)); + lastTime = tp.StartTime; - break; - } + if (durations.find(tp.Value) != durations.end()) { + durations[tp.Value] += duration; + } else { + durations[tp.Value] = duration; } -} + } -void Chart::ComputeHash() -{ - std::string result; - for (int i = 0; i < m_notes.size(); i++) { - result += std::to_string(m_notes[i].StartTime + m_notes[i].EndTime); - } + if (durations.size() == 0) { + return m_bpms[0].Value; + } - uint8_t data[16]; - md5String((char *)result.c_str(), data); + int currentDuration = 0; + float currentBPM = 0.0f; - std::stringstream ss; - ss << std::hex << std::setfill('0'); - for (int i = 0; i < 16; i++) { - ss << std::setw(2) << static_cast(data[i]); + for (auto &[bpm, duration] : durations) { + if (duration > currentDuration) { + currentDuration = duration; + currentBPM = bpm; } + } - MD5Hash = ss.str(); + return currentBPM; } -float Chart::GetCommonBPM() -{ - if (m_bpms.size() == 0) { - return 0.0f; - } - - std::vector orderedByDescending(m_notes); - std::sort(orderedByDescending.begin(), orderedByDescending.end(), [](const NoteInfo &a, const NoteInfo &b) { - return a.StartTime > b.StartTime; - }); - - auto &lastObject = orderedByDescending[0]; - double lastTime = lastObject.Type == NoteType::HOLD ? lastObject.EndTime : lastObject.StartTime; +void Chart::NormalizeTimings() { + EnvironmentSetup::SetInt("KeyCount", m_keyCount); + std::vector result; - std::unordered_map durations; - for (int i = (int)m_bpms.size() - 1; i >= 0; i--) { - auto &tp = m_bpms[i]; + float baseBPM = GetCommonBPM(); + float currentBPM = m_bpms[0].Value; + int currentSvIdx = 0; - if (tp.StartTime > lastTime) { - continue; - } - - int duration = (int)(lastTime - (i == 0 ? 0 : tp.StartTime)); - lastTime = tp.StartTime; + // Ambigous + BaseBPM = baseBPM; - if (durations.find(tp.Value) != durations.end()) { - durations[tp.Value] += duration; - } else { - durations[tp.Value] = duration; - } - } + double currentSvMultiplier = 1.0f; - if (durations.size() == 0) { - return m_bpms[0].Value; - } + double currentSvStartTime = -1.0f; + double currentAdjustedSvMultiplier = -1.0f; + double initialSvMultiplier = -1.0f; - int currentDuration = 0; - float currentBPM = 0.0f; + for (int i = 0; i < m_bpms.size(); i++) { + auto &tp = m_bpms[i]; - for (auto &[bpm, duration] : durations) { - if (duration > currentDuration) { - currentDuration = duration; - currentBPM = bpm; - } + bool exist = false; + if ((i + 1) < m_bpms.size() && m_bpms[i + 1].StartTime == tp.StartTime) { + exist = true; } - return currentBPM; -} - -void Chart::NormalizeTimings() -{ - std::vector result; - - float baseBPM = GetCommonBPM(); - float currentBPM = m_bpms[0].Value; - int currentSvIdx = 0; + while (true) { + if (currentSvIdx >= m_svs.size()) { + break; + } - // Ambigous - BaseBPM = baseBPM; + auto &sv = m_svs[currentSvIdx]; + if (sv.StartTime > tp.StartTime) { + break; + } - double currentSvMultiplier = 1.0f; + if (exist && sv.StartTime == tp.StartTime) { + break; + } - double currentSvStartTime = -1.0f; - double currentAdjustedSvMultiplier = -1.0f; - double initialSvMultiplier = -1.0f; - - for (int i = 0; i < m_bpms.size(); i++) { - auto &tp = m_bpms[i]; + if (sv.StartTime < tp.StartTime) { + float multiplier = sv.Value * (currentBPM / baseBPM); - bool exist = false; - if ((i + 1) < m_bpms.size() && m_bpms[i + 1].StartTime == tp.StartTime) { - exist = true; + if (currentAdjustedSvMultiplier == -1.0f) { + currentAdjustedSvMultiplier = multiplier; + initialSvMultiplier = multiplier; } - while (true) { - if (currentSvIdx >= m_svs.size()) { - break; - } - - auto &sv = m_svs[currentSvIdx]; - if (sv.StartTime > tp.StartTime) { - break; - } - - if (exist && sv.StartTime == tp.StartTime) { - break; - } - - if (sv.StartTime < tp.StartTime) { - float multiplier = sv.Value * (currentBPM / baseBPM); - - if (currentAdjustedSvMultiplier == -1.0f) { - currentAdjustedSvMultiplier = multiplier; - initialSvMultiplier = multiplier; - } - - if (multiplier != currentAdjustedSvMultiplier) { - TimingInfo info = {}; - info.StartTime = sv.StartTime; - info.Value = multiplier; - info.Type = TimingType::SV; - - result.push_back(info); - currentAdjustedSvMultiplier = multiplier; - } - } + if (multiplier != currentAdjustedSvMultiplier) { + TimingInfo info = {}; + info.StartTime = sv.StartTime; + info.Value = multiplier; + info.Type = TimingType::SV; - currentSvStartTime = sv.StartTime; - currentSvMultiplier = sv.Value; - currentSvIdx += 1; + result.push_back(info); + currentAdjustedSvMultiplier = multiplier; } + } - if (currentSvStartTime == -1.0f || currentSvStartTime < tp.StartTime) { - currentSvMultiplier = 1.0f; - } + currentSvStartTime = sv.StartTime; + currentSvMultiplier = sv.Value; + currentSvIdx += 1; + } - currentBPM = tp.Value; + if (currentSvStartTime == -1.0f || currentSvStartTime < tp.StartTime) { + currentSvMultiplier = 1.0f; + } - float multiplier = (float)(currentSvMultiplier * (currentBPM / baseBPM)); + currentBPM = tp.Value; - if (currentAdjustedSvMultiplier == -1.0f) { - currentAdjustedSvMultiplier = multiplier; - initialSvMultiplier = multiplier; - } + float multiplier = (float)(currentSvMultiplier * (currentBPM / baseBPM)); - if (multiplier != currentAdjustedSvMultiplier) { - TimingInfo info = {}; - info.StartTime = tp.StartTime; - info.Value = multiplier; - info.Type = TimingType::SV; + if (currentAdjustedSvMultiplier == -1.0f) { + currentAdjustedSvMultiplier = multiplier; + initialSvMultiplier = multiplier; + } - result.push_back(info); - currentAdjustedSvMultiplier = multiplier; - } + if (multiplier != currentAdjustedSvMultiplier) { + TimingInfo info = {}; + info.StartTime = tp.StartTime; + info.Value = multiplier; + info.Type = TimingType::SV; + + result.push_back(info); + currentAdjustedSvMultiplier = multiplier; } + } - for (; currentSvIdx < m_svs.size(); currentSvIdx++) { - auto &sv = m_svs[currentSvIdx]; - float multiplier = sv.Value * (currentBPM / baseBPM); + for (; currentSvIdx < m_svs.size(); currentSvIdx++) { + auto &sv = m_svs[currentSvIdx]; + float multiplier = sv.Value * (currentBPM / baseBPM); - if (multiplier != currentAdjustedSvMultiplier) { - TimingInfo info = {}; - info.StartTime = sv.StartTime; - info.Value = multiplier; - info.Type = TimingType::SV; + if (multiplier != currentAdjustedSvMultiplier) { + TimingInfo info = {}; + info.StartTime = sv.StartTime; + info.Value = multiplier; + info.Type = TimingType::SV; - result.push_back(info); - currentAdjustedSvMultiplier = multiplier; - } + result.push_back(info); + currentAdjustedSvMultiplier = multiplier; } + } - InitialSvMultiplier = (float)initialSvMultiplier == -1.0f ? 1.0f : (float)initialSvMultiplier; + InitialSvMultiplier = + (float)initialSvMultiplier == -1.0f ? 1.0f : (float)initialSvMultiplier; - m_svs.clear(); - m_svs = result; + m_svs.clear(); + m_svs = result; } -void Chart::ComputeKeyCount() -{ - bool Lanes[7] = { false, false, false, false, false, false }; +void Chart::ComputeKeyCount() { + bool Lanes[7] = {false, false, false, false, false, false}; - for (auto ¬e : m_notes) { - if (!Lanes[note.LaneIndex]) { - Lanes[note.LaneIndex] = true; - } + for (auto ¬e : m_notes) { + if (note.LaneIndex >= 0 && note.LaneIndex < 7) { + if (!Lanes[note.LaneIndex]) { + Lanes[note.LaneIndex] = true; + } } + } - // BMS-O2 4K is: X X - - - X X - // BMS-O2 5K is: X X - X - X X - // BMS-O2 6K is: X X X X X X - - // BMS-O2 7K is: X X X X X X X + // BMS-O2 4K is: X X - - - X X + // BMS-O2 5K is: X X - X - X X + // BMS-O2 6K is: X X X X X X - + // BMS-O2 7K is: X X X X X X X - // Check for 7K first since it has the highest priority - if (Lanes[0] && Lanes[1] && Lanes[2] && Lanes[3] && Lanes[4] && Lanes[5] && Lanes[6]) { - m_keyCount = 7; - } - // Check for 6K - else if (Lanes[0] && Lanes[1] && Lanes[2] && Lanes[3] && Lanes[4] && Lanes[5] && !Lanes[6]) { - m_keyCount = 6; - } - // Check for 5K - else if (Lanes[0] && Lanes[1] && !Lanes[2] && Lanes[3] && !Lanes[4] && Lanes[5] && Lanes[6]) { - m_keyCount = 5; - } - // Check for 4K - else if (Lanes[0] && Lanes[1] && !Lanes[2] && !Lanes[3] && !Lanes[4] && Lanes[5] && Lanes[6]) { - m_keyCount = 4; - } - // Otherwise, the pattern does not match any of the known K values - else { - Logs::Puts("[Chart] Unknown lane pattern, fallback to 7K"); - m_keyCount = 7; - } + // Check for 7K first since it has the highest priority + if (Lanes[0] && Lanes[1] && Lanes[2] && Lanes[3] && Lanes[4] && Lanes[5] && + Lanes[6]) { + m_keyCount = 7; + } + // Check for 6K + else if (Lanes[0] && Lanes[1] && Lanes[2] && Lanes[3] && Lanes[4] && + Lanes[5] && !Lanes[6]) { + m_keyCount = 6; + } + // Check for 5K + else if (Lanes[0] && Lanes[1] && !Lanes[2] && Lanes[3] && !Lanes[4] && + Lanes[5] && Lanes[6]) { + m_keyCount = 5; + } + // Check for 4K + else if (Lanes[0] && Lanes[1] && !Lanes[2] && !Lanes[3] && !Lanes[4] && + Lanes[5] && Lanes[6]) { + m_keyCount = 4; + } else { + Logs::Puts("[Chart] Unknown lane pattern, fallback to 7K"); + m_keyCount = 7; + } } diff --git a/Game/src/Data/Chart.hpp b/Game/src/Data/Chart.hpp index fd5b25bc..eaf41e1a 100644 --- a/Game/src/Data/Chart.hpp +++ b/Game/src/Data/Chart.hpp @@ -52,7 +52,8 @@ struct TimingInfo float TimeSignature; TimingType Type; - double CalculateBeat(double offset); + //double CalculateBeat(double offset); + double CalculateBeat(double offset) const; }; struct Sample @@ -75,6 +76,7 @@ struct AutoSample enum class Mod { MIRROR, RANDOM, + PANIC, REARRANGE }; @@ -85,6 +87,8 @@ class Chart Chart(Osu::Beatmap &beatmap); Chart(BMS::BMSFile &bmsfile); Chart(O2::OJN &ojnfile, int diffIndex = 2); + void CalculateBeat(); + void SortTimings(); ~Chart(); void ApplyMod(Mod mod, void *data = NULL); @@ -102,9 +106,13 @@ class Chart std::vector m_backgroundBuffer; std::u8string m_title; std::u8string m_artist; + std::u8string m_difname; std::string m_audio; std::filesystem::path m_beatmapDirectory; + std::string m_videoFile = ""; + double m_videoOffset = 0; + std::vector m_notes; std::vector m_bpms; std::vector m_svs; diff --git a/Game/src/Data/OJM.cpp b/Game/src/Data/OJM.cpp index c6a38ced..1d8b654a 100644 --- a/Game/src/Data/OJM.cpp +++ b/Game/src/Data/OJM.cpp @@ -9,16 +9,20 @@ constexpr int kM30Signature = 0x0030334D; constexpr int kOMCSignature = 0x00434D4F; constexpr int kOJMSignature = 0x004D4A4F; +const char MASK_SCRAMBLE1[] = { 0x73, 0x63, 0x72, 0x61, 0x6D, 0x62, 0x6C, 0x65, 0x31 }; +const char MASK_SCRAMBLE2[] = { 0x73, 0x63, 0x72, 0x61, 0x6D, 0x62, 0x6C, 0x65, 0x32 }; +const char MASK_DECODE[] = { 0x64, 0x65, 0x63, 0x6F, 0x64, 0x65 }; +const char MASK_DECRYPT[] = { 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74 }; const char MASK_NAMI[] = { 0x6E, 0x61, 0x6D, 0x69 }; const char MASK_0412[] = { 0x30, 0x34, 0x31, 0x32 }; void M30Xor(char *data, size_t sz, const char *xorKey) { for (int i = 0; i + 3 < sz; i += 4) { - data[i] ^= xorKey[0]; - data[i + 1] ^= xorKey[1]; - data[i + 2] ^= xorKey[2]; - data[i + 3] ^= xorKey[3]; + data[i] ^= xorKey[i % 4]; + data[i + 1] ^= xorKey[(i + 1) % 4]; + data[i + 2] ^= xorKey[(i + 2) % 4]; + data[i + 3] ^= xorKey[(i + 3) % 4]; } } @@ -182,7 +186,7 @@ void OJM::LoadM30Data(std::fstream &fs) fs.read((char *)&Header, sizeof(M30Header)); - for (int i = 0; i < Header.sampleSize; i++) { + for (int i = 0; i < Header.sampleCount; i++) { // This fix no sound in few OJM struct M30SampleHeader { char sampleName[32]; @@ -204,18 +208,26 @@ void OJM::LoadM30Data(std::fstream &fs) fs.read((char *)buffer, SampleHeader.sampleSize); switch (Header.encryptionFlag) { - case 0: - break; - case 16: - { - M30Xor((char *)buffer, SampleHeader.sampleSize, MASK_NAMI); - break; - } - case 32: - { - M30Xor((char *)buffer, SampleHeader.sampleSize, MASK_0412); - break; - } + case 0: + break; + case 1: + M30Xor((char*)buffer, SampleHeader.sampleSize, MASK_SCRAMBLE1); + break; + case 2: + M30Xor((char*)buffer, SampleHeader.sampleSize, MASK_SCRAMBLE2); + break; + case 4: + M30Xor((char*)buffer, SampleHeader.sampleSize, MASK_DECODE); + break; + case 8: + M30Xor((char*)buffer, SampleHeader.sampleSize, MASK_DECRYPT); + break; + case 16: + M30Xor((char*)buffer, SampleHeader.sampleSize, MASK_NAMI); + break; + case 32: + M30Xor((char*)buffer, SampleHeader.sampleSize, MASK_0412); + break; } // OGG Sample diff --git a/Game/src/Data/Util/Util.cpp b/Game/src/Data/Util/Util.cpp index f4db3980..14dcc3a8 100644 --- a/Game/src/Data/Util/Util.cpp +++ b/Game/src/Data/Util/Util.cpp @@ -12,6 +12,9 @@ #include #include #include +#include +#include +#include std::vector splitString(std::string &input, char delimeter) { @@ -130,51 +133,46 @@ void flipArray(uint8_t *arr, size_t size) } } -std::u8string CodepageToUtf8(const char *string, size_t str_len, const char *encoding) -{ - std::u8string result; - iconv_t conv = iconv_open("UTF-8", encoding); +std::u8string CodepageToUtf8(const char* string, size_t str_len, const char* encoding) { // Refactor + // Open iconv conversion descriptor + iconv_t conv = iconv_open("UTF-8", encoding); if (conv == (iconv_t)-1) { return u8""; } - // size_t inbytesleft = str_len; - // size_t outbytesleft = str_len * 4; - // char* inbuf = (char*)string; - // char* outbuf = (char*)malloc(outbytesleft); - // char* outbufptr = outbuf; - - // if (iconv(conv, &inbuf, &inbytesleft, &outbufptr, &outbytesleft) == (size_t)-1) { - // free(outbuf); - // iconv_close(conv); - // return std::u8string((const char8_t*)strerror(errno)); - // } - - // result = std::u8string((char8_t*)outbuf, str_len * 4 - outbytesleft); - // free(outbuf); - // iconv_close(conv); - // return result; - - size_t inbytesleft = str_len; - size_t outbytesleft = str_len * 4; - char *inbuf = (char *)string; - std::vector outbuf; - outbuf.resize(outbytesleft, 0); - - char *outbufptr = outbuf.data(); - - if (iconv(conv, &inbuf, &inbytesleft, &outbufptr, &outbytesleft) == (size_t)-1) { - iconv_close(conv); - - // return whatever we have left, even if it's not valid - size_t len = strlen(outbuf.data()); - std::u8string remaining = std::u8string((const char8_t *)outbuf.data(), len); - - return remaining; + std::u8string result; + size_t inbytesleft = str_len; + const char* inbuf = string; + size_t outbufsize = str_len * 4; // Initial output buffer size + std::vector outbuf(outbufsize); + + while (true) { + char* outbufptr = outbuf.data(); + size_t outbytesleft = outbuf.size(); + + // Reset errno to check for errors after iconv call + errno = 0; + + size_t ret = iconv(conv, (char**)&inbuf, &inbytesleft, &outbufptr, &outbytesleft); + if (ret != (size_t)-1) { + // Successfully converted + result.append((const char8_t*)outbuf.data(), outbuf.size() - outbytesleft); + break; + } + else if (errno == E2BIG) { + // Resize buffer and retry if buffer was too small + size_t processed = outbuf.size() - outbytesleft; + result.append((const char8_t*)outbuf.data(), processed); + outbufsize *= 2; + outbuf.resize(outbufsize); + } + else { + // Return whatever we have left, even if it's not valid + result.append((const char8_t*)outbuf.data(), outbuf.size() - outbytesleft); + break; + } } - result = std::u8string((char8_t *)outbuf.data(), str_len * 4 - outbytesleft); iconv_close(conv); - return result; -} +} \ No newline at end of file diff --git a/Game/src/Data/osu.cpp b/Game/src/Data/osu.cpp index 14569b9a..42f078fb 100644 --- a/Game/src/Data/osu.cpp +++ b/Game/src/Data/osu.cpp @@ -135,6 +135,7 @@ void Osu::Beatmap::ParseString(std::stringstream &ss) } OsuEvent ev = {}; + ev.Type = OsuEventType::Unknown; try { ev.StartTime = std::stof(event[1]); } catch (const std::invalid_argument &) { @@ -170,6 +171,10 @@ void Osu::Beatmap::ParseString(std::stringstream &ss) ev.params.push_back(copy); } + if (ev.Type == OsuEventType::Background && ev.params.size() > 0 && BackgroundFile.empty()) { + BackgroundFile = ev.params[0]; + } + Events.push_back(ev); } diff --git a/Game/src/Data/osu.hpp b/Game/src/Data/osu.hpp index a36fdb31..22035ed7 100644 --- a/Game/src/Data/osu.hpp +++ b/Game/src/Data/osu.hpp @@ -36,7 +36,8 @@ namespace Osu { Background, Videos, Break, - Sample + Sample, + Unknown }; struct OsuEvent diff --git a/Game/src/Engine/Autoplay.cpp b/Game/src/Engine/Autoplay.cpp index 747c0fee..648ed1cf 100644 --- a/Game/src/Engine/Autoplay.cpp +++ b/Game/src/Engine/Autoplay.cpp @@ -1,13 +1,10 @@ #include "Autoplay.h" #include "./Timing/TimingBase.h" -constexpr int kReleaseDelay = 25; - -const double kBaseBPM = 240.0; -const double kMaxTicks = 192.0; +constexpr double kReleaseDelay = 33.34; const double kNoteCoolHitRatio = 6.0; -NoteInfo *GetNextHitObject(std::vector &hitObject, int index) +NoteInfo* GetNextHitObject(std::vector& hitObject, int index) { int lane = hitObject[index].LaneIndex; @@ -20,30 +17,30 @@ NoteInfo *GetNextHitObject(std::vector &hitObject, int index) return nullptr; } -double CalculateReleaseTime(NoteInfo *currentHitObject, NoteInfo *nextHitObject) +double CalculateReleaseTime(NoteInfo* currentHitObject, NoteInfo* nextHitObject) { if (currentHitObject->Type == NoteType::HOLD) { return currentHitObject->EndTime; } double Time = currentHitObject->Type == NoteType::HOLD ? currentHitObject->EndTime - : currentHitObject->StartTime; + : currentHitObject->StartTime; bool canDelayFully = nextHitObject == nullptr || - nextHitObject->StartTime > Time + kReleaseDelay; + nextHitObject->StartTime > Time + kReleaseDelay; return Time + (canDelayFully ? kReleaseDelay : (nextHitObject->StartTime - Time) * 0.9); } -std::vector Autoplay::CreateReplay(Chart *chart) +std::vector Autoplay::CreateReplay(Chart* chart) { std::vector result; TimingBase timingBase(chart->m_bpms, chart->m_svs, chart->InitialSvMultiplier); for (int i = 0; i < chart->m_notes.size(); i++) { - auto ¤tHitObject = chart->m_notes[i]; - auto nextHitObject = GetNextHitObject(chart->m_notes, i); + auto& currentHitObject = chart->m_notes[i]; + auto nextHitObject = GetNextHitObject(chart->m_notes, i); double HitTime = currentHitObject.StartTime; double ReleaseTime = CalculateReleaseTime(¤tHitObject, nextHitObject); @@ -51,56 +48,26 @@ std::vector Autoplay::CreateReplay(Chart *chart) double bpmAtHit = timingBase.GetBPMAt(HitTime); double bpmAtRelease = timingBase.GetBPMAt(ReleaseTime); - int tries = 0; - - while (true) { - double beat = kBaseBPM / kMaxTicks / bpmAtHit * 1000.0; - double cool = beat * kNoteCoolHitRatio; - double late_cool = -cool; - - double _HIT = currentHitObject.StartTime - HitTime; - - if (_HIT >= late_cool && _HIT <= cool) { - break; - } - - if (_HIT > cool) { - HitTime++; - } else { - HitTime--; - } + double beat = 60000.0 / bpmAtHit; + double coolWindow = beat * kNoteCoolHitRatio; + double hitError = currentHitObject.StartTime - HitTime; - if (++tries >= 500) { - break; - } + while (std::abs(hitError) > coolWindow) { + HitTime += hitError > 0 ? 1 : -1; + hitError = currentHitObject.StartTime - HitTime; } - tries = 0; - - while (true) { - double beat = kBaseBPM / kMaxTicks / bpmAtRelease * 1000.0; - double cool = beat * kNoteCoolHitRatio; - double late_cool = -cool; - - double _HIT = (currentHitObject.Type == NoteType::HOLD ? currentHitObject.EndTime : currentHitObject.StartTime) - ReleaseTime; - - if (_HIT >= late_cool && _HIT <= cool) { - break; - } - - if (_HIT > cool) { - ReleaseTime++; - } else { - ReleaseTime--; - } + beat = 60000.0 / bpmAtRelease; + coolWindow = beat * kNoteCoolHitRatio; + double releaseError = (currentHitObject.Type == NoteType::HOLD ? currentHitObject.EndTime : currentHitObject.StartTime) - ReleaseTime; - if (++tries >= 500) { - break; - } + while (std::abs(releaseError) > coolWindow) { + ReleaseTime += releaseError > 0 ? 1 : -1; + releaseError = (currentHitObject.Type == NoteType::HOLD ? currentHitObject.EndTime : currentHitObject.StartTime) - ReleaseTime; } - result.push_back({ HitTime, (int)currentHitObject.LaneIndex, ReplayHitType::KEY_DOWN }); - result.push_back({ ReleaseTime, (int)currentHitObject.LaneIndex, ReplayHitType::KEY_UP }); + result.push_back({ HitTime, static_cast(currentHitObject.LaneIndex), ReplayHitType::KEY_DOWN }); + result.push_back({ ReleaseTime, static_cast(currentHitObject.LaneIndex), ReplayHitType::KEY_UP }); } return result; diff --git a/Game/src/Engine/BGMPreview.cpp b/Game/src/Engine/BGMPreview.cpp index 5d9adc0b..c47eda1c 100644 --- a/Game/src/Engine/BGMPreview.cpp +++ b/Game/src/Engine/BGMPreview.cpp @@ -42,27 +42,49 @@ void BGMPreview::Load() std::lock_guard lock(*m_mutex); Ready = false; - int index = EnvironmentSetup::GetInt("Key"); - DB_MusicItem item = GameDatabase::GetInstance()->Find(index); + std::filesystem::path file; + if (EnvironmentSetup::GetInt("FileOpen") == 1) { + file = EnvironmentSetup::GetPath("FILE"); + } else { + int index = EnvironmentSetup::GetInt("Key"); + DB_MusicItem item = GameDatabase::GetInstance()->Find(index); - std::filesystem::path file = GameDatabase::GetInstance()->GetPath(); - file /= "o2ma" + std::to_string(index) + ".ojn"; + file = GameDatabase::GetInstance()->GetPath(); + file /= "o2ma" + std::to_string(index) + ".ojn"; + } if (file.string() != m_currentFilePath || GameAudioSampleCache::SetRate() != 1.0 || GameAudioSampleCache::IsEmpty()) { try { - O2::OJN o2jamFile; - o2jamFile.Load(file); - - if (!o2jamFile.IsValid()) { - return; - } + std::string ext = file.extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); if (m_currentChart != nullptr && file.string() != m_currentFilePath) { delete m_currentChart; + m_currentChart = nullptr; + } + + if (ext == ".ojn") { + O2::OJN o2jamFile; + o2jamFile.Load(file); + if (!o2jamFile.IsValid()) { + if (m_callback && m_currentState == state) m_callback(false); + return; + } + m_currentChart = new Chart(o2jamFile, 2); + } else if (ext == ".bms" || ext == ".bme" || ext == ".bml") { + BMS::BMSFile bmsFile; + bmsFile.Load(file); + if (!bmsFile.IsValid()) { + if (m_callback && m_currentState == state) m_callback(false); + return; + } + m_currentChart = new Chart(bmsFile); + } else { + if (m_callback && m_currentState == state) m_callback(false); + return; } m_currentFilePath = file.string(); - m_currentChart = new Chart(o2jamFile, 2); } catch (std::exception &e) { Logs::Puts("[BGMPreview] Failed to load the audio chart: %s", e.what()); return; @@ -71,7 +93,7 @@ void BGMPreview::Load() m_autoSamples.clear(); for (auto &sample : m_currentChart->m_autoSamples) { - m_autoSamples.push_back(sample); + m_autoSamples.emplace_back(sample); } for (auto ¬e : m_currentChart->m_notes) { @@ -82,7 +104,7 @@ void BGMPreview::Load() sm.Volume = note.Volume; sm.Pan = note.Pan; - m_autoSamples.push_back(sm); + m_autoSamples.emplace_back(sm); } } @@ -118,7 +140,7 @@ void BGMPreview::Update(double delta) auto &sample = m_autoSamples[i]; if (m_currentAudioPosition >= sample.StartTime) { if (sample.StartTime - m_currentAudioPosition < 5) { - GameAudioSampleCache::Play(sample.Index, (int)::round(sample.Volume * 50.0f), (int)::round(sample.Pan * 100.0f)); + GameAudioSampleCache::Play(sample.Index, (int)::round(sample.Volume * 100.0f), (int)::round(sample.Pan * 100.0f)); } m_currentSampleIndex++; diff --git a/Game/src/Engine/DrawableNote.cpp b/Game/src/Engine/DrawableNote.cpp index 4b36fffb..84562e1b 100644 --- a/Game/src/Engine/DrawableNote.cpp +++ b/Game/src/Engine/DrawableNote.cpp @@ -3,25 +3,23 @@ #include "Rendering/Renderer.h" #include "Texture/Texture2D.h" -DrawableNote::DrawableNote(NoteImage *frame) : FrameTimer::FrameTimer() +DrawableNote::DrawableNote(NoteImage* frame) : FrameTimer::FrameTimer() { - m_frames = std::vector(); + SetFPS(0.0); // HACK: Stop note glitching by force it to 0 FPS (idk why it still initialized by itself if i remove SETFPS) + m_frames.reserve(frame->Texture.size()); // Reserve memory for frames vector if (Renderer::GetInstance()->IsVulkan()) { - for (auto &frame : frame->VulkanTexture) { - m_frames.push_back(new Texture2D(frame)); + for (auto& texture : frame->VulkanTexture) { + m_frames.emplace_back(new Texture2D(texture)); + m_frames.back()->SetOriginalRECT(frame->TextureRect); // Set original RECT for each texture } - } else { - for (auto &frame : frame->Texture) { - m_frames.push_back(new Texture2D(frame)); + } + else { + for (auto& texture : frame->Texture) { + m_frames.emplace_back(new Texture2D(texture)); + m_frames.back()->SetOriginalRECT(frame->TextureRect); // Set original RECT for each texture } } AnchorPoint = { 0.0, 1.0 }; - - for (auto &_frame : m_frames) { - _frame->SetOriginalRECT(frame->TextureRect); - } - - SetFPS(30); } diff --git a/Game/src/Engine/FrameTimer.cpp b/Game/src/Engine/FrameTimer.cpp index 246d455c..b55a647f 100644 --- a/Game/src/Engine/FrameTimer.cpp +++ b/Game/src/Engine/FrameTimer.cpp @@ -2,57 +2,53 @@ #include "Rendering/Renderer.h" #include "Texture/Texture2D.h" -FrameTimer::FrameTimer() +FrameTimer::FrameTimer() : + Repeat(false), + m_currentFrame(0), + m_frameTime(1.0 / 60.0), + m_currentTime(0), + AlphaBlend(false), + Size(UDim2::fromScale(1, 1)), + TintColor({ 1.0f, 1.0f, 1.0f }) { - Repeat = false; - m_currentFrame = 0; - m_frameTime = 1.0f / 60; - m_currentTime = 0; - AlphaBlend = false; - Size = UDim2::fromScale(1, 1); - TintColor = { 1.0f, 1.0f, 1.0f }; } -FrameTimer::FrameTimer(std::vector frames) : FrameTimer::FrameTimer() +FrameTimer::FrameTimer(std::vector frames) : FrameTimer() { - m_frames = frames; + m_frames = std::move(frames); } -FrameTimer::FrameTimer(std::vector frames) : FrameTimer::FrameTimer() +FrameTimer::FrameTimer(std::vector frames) : FrameTimer() { - m_frames = std::vector(); - for (auto frame : frames) { - m_frames.push_back(new Texture2D(frame)); + for (const auto& frame : frames) { + m_frames.emplace_back(new Texture2D(frame)); } } -FrameTimer::FrameTimer(std::vector frames) : FrameTimer::FrameTimer() +FrameTimer::FrameTimer(std::vector frames) : FrameTimer() { - m_frames = std::vector(); - for (auto frame : frames) { - m_frames.push_back(new Texture2D(frame)); + for (const auto& frame : frames) { + m_frames.emplace_back(new Texture2D(frame)); } } -FrameTimer::FrameTimer(std::vector frames) : FrameTimer::FrameTimer() +FrameTimer::FrameTimer(std::vector frames) : FrameTimer() { - m_frames = std::vector(); - for (auto frame : frames) { - m_frames.push_back(new Texture2D(frame)); + for (const auto& frame : frames) { + m_frames.emplace_back(new Texture2D(frame)); } } -FrameTimer::FrameTimer(std::vector frames) : FrameTimer::FrameTimer() +FrameTimer::FrameTimer(std::vector frames) : FrameTimer() { - m_frames = std::vector(); - for (auto frame : frames) { - m_frames.push_back(new Texture2D(frame)); + for (const auto& frame : frames) { + m_frames.emplace_back(new Texture2D(frame)); } } FrameTimer::~FrameTimer() { - for (auto &f : m_frames) { + for (auto& f : m_frames) { delete f; } } @@ -62,13 +58,12 @@ void FrameTimer::Draw(double delta) Draw(delta, nullptr); } -void FrameTimer::Draw(double delta, Rect *clip) +void FrameTimer::Draw(double delta, Rect* clip) { m_currentTime += delta; - if (m_currentTime >= m_frameTime) { + while (m_currentTime >= m_frameTime) { m_currentTime -= m_frameTime; - m_currentFrame++; } @@ -79,21 +74,22 @@ void FrameTimer::Draw(double delta, Rect *clip) if (m_currentFrame < m_frames.size()) { CalculateSize(); - m_frames[m_currentFrame]->AlphaBlend = AlphaBlend; - m_frames[m_currentFrame]->TintColor = TintColor; + auto currentFrame = m_frames[m_currentFrame]; + currentFrame->AlphaBlend = AlphaBlend; + currentFrame->TintColor = TintColor; if (m_currentFrame != 0) { - m_frames[m_currentFrame]->Position = UDim2::fromOffset(AbsolutePosition.X, AbsolutePosition.Y); - m_frames[m_currentFrame]->Size = UDim2::fromOffset(AbsoluteSize.X, AbsoluteSize.Y); - m_frames[m_currentFrame]->AnchorPoint = { 0, 0 }; + currentFrame->Position = UDim2::fromOffset(AbsolutePosition.X, AbsolutePosition.Y); + currentFrame->Size = UDim2::fromOffset(AbsoluteSize.X, AbsoluteSize.Y); + currentFrame->AnchorPoint = { 0, 0 }; } - m_frames[m_currentFrame]->Draw(clip); + currentFrame->Draw(clip); } } -void FrameTimer::SetFPS(float fps) +void FrameTimer::SetFPS(double fps) { - m_frameTime = 1.0f / fps; + m_frameTime = 1.0 / fps; } void FrameTimer::ResetIndex() @@ -103,7 +99,7 @@ void FrameTimer::ResetIndex() void FrameTimer::LastIndex() { - m_currentFrame = (int)m_frames.size() - 1; + m_currentFrame = static_cast(m_frames.size()) - 1; } void FrameTimer::SetIndexAt(int idx) @@ -113,11 +109,12 @@ void FrameTimer::SetIndexAt(int idx) void FrameTimer::CalculateSize() { - m_frames[0]->AnchorPoint = AnchorPoint; - m_frames[0]->Size = Size; - m_frames[0]->Position = Position; - m_frames[0]->CalculateSize(); - - AbsoluteSize = m_frames[0]->AbsoluteSize; - AbsolutePosition = m_frames[0]->AbsolutePosition; + auto& firstFrame = *m_frames[0]; + firstFrame.AnchorPoint = AnchorPoint; + firstFrame.Size = Size; + firstFrame.Position = Position; + firstFrame.CalculateSize(); + + AbsoluteSize = firstFrame.AbsoluteSize; + AbsolutePosition = firstFrame.AbsolutePosition; } diff --git a/Game/src/Engine/FrameTimer.hpp b/Game/src/Engine/FrameTimer.hpp index 2d73dc62..b8846be7 100644 --- a/Game/src/Engine/FrameTimer.hpp +++ b/Game/src/Engine/FrameTimer.hpp @@ -16,11 +16,11 @@ class FrameTimer { public: FrameTimer(); - FrameTimer(std::vector frames); + FrameTimer(std::vector frames); FrameTimer(std::vector frames); FrameTimer(std::vector frames); - FrameTimer(std::vector frames); - FrameTimer(std::vector frames); + FrameTimer(std::vector frames); + FrameTimer(std::vector frames); ~FrameTimer(); bool Repeat; @@ -35,16 +35,17 @@ class FrameTimer Vector2 AbsoluteSize; void Draw(double delta); - void Draw(double delta, Rect *clip); - void SetFPS(float fps); + void Draw(double delta, Rect* clip); + void SetFPS(double fps); void ResetIndex(); void LastIndex(); void SetIndexAt(int idx); void CalculateSize(); + std::vector m_frames; + protected: - std::vector m_frames; int m_currentFrame; double m_frameTime; double m_currentTime; diff --git a/Game/src/Engine/GameAudioSampleCache.cpp b/Game/src/Engine/GameAudioSampleCache.cpp index 629fc6fd..fe130635 100644 --- a/Game/src/Engine/GameAudioSampleCache.cpp +++ b/Game/src/Engine/GameAudioSampleCache.cpp @@ -281,6 +281,19 @@ void GameAudioSampleCache::StopAll() sampleIndex.clear(); } +bool GameAudioSampleCache::IsAnyPlaying() +{ + std::lock_guard lock(m_lock); + + for (auto &kv : sampleIndex) { + if (kv.second != nullptr && kv.second->IsPlaying()) { + return true; + } + } + + return false; +} + std::vector GameAudioSampleCache::QueryMixerData() { throw std::runtime_error("Not implemented"); diff --git a/Game/src/Engine/GameAudioSampleCache.hpp b/Game/src/Engine/GameAudioSampleCache.hpp index 1f417771..87916b5f 100644 --- a/Game/src/Engine/GameAudioSampleCache.hpp +++ b/Game/src/Engine/GameAudioSampleCache.hpp @@ -17,6 +17,7 @@ namespace GameAudioSampleCache { void ResumeAll(); void PauseAll(); void StopAll(); + bool IsAnyPlaying(); std::vector QueryMixerData(); diff --git a/Game/src/Engine/GameTrack.cpp b/Game/src/Engine/GameTrack.cpp index 7b83c4c3..12a3e84b 100644 --- a/Game/src/Engine/GameTrack.cpp +++ b/Game/src/Engine/GameTrack.cpp @@ -42,7 +42,7 @@ void GameTrack::Update(double delta) auto ¬e = *_note; if (note->IsPassed()) { - m_inactive_notes.push_back(note); + m_inactive_notes.emplace_back(note); _note = m_notes.erase(_note); } else { if (!note->IsDrawable()) { @@ -71,7 +71,7 @@ void GameTrack::Update(double delta) if (note->IsRemoveable()) { note->Release(); - m_noteCaches.push_back(note); + m_noteCaches.emplace_back(note); _note = m_inactive_notes.erase(_note); } else { note->Update(delta); @@ -212,7 +212,7 @@ void GameTrack::AddNote(NoteInfoDesc *desc) m_keySound = note->GetKeysoundId(); } - m_notes.push_back(std::move(note)); + m_notes.emplace_back(std::move(note)); } void GameTrack::ListenEvent(std::function callback) diff --git a/Game/src/Engine/LuaScripting.cpp b/Game/src/Engine/LuaScripting.cpp index 949a2506..901baf75 100644 --- a/Game/src/Engine/LuaScripting.cpp +++ b/Game/src/Engine/LuaScripting.cpp @@ -147,15 +147,30 @@ void GetLuaState(ScriptState &state, sol::table &table) state.init = table["init"]; } -void LuaScripting::TryLoadGroup(SkinGroup group) + +void LuaScripting::TryLoadGroup(SkinGroup group) // Not fully testes unless someone want make skin with lua script { - auto fullPath = m_lua_dir_path / m_expected_files[group]; + std::filesystem::path fullPath; + + if (group == SkinGroup::Notes && EnvironmentSetup::GetInt("NoteSkin") != 2) { + auto path = std::filesystem::current_path() / "Resources"; + if (EnvironmentSetup::GetInt("NoteSkin") == 1) { + fullPath = path / "Scripts" / "Notes.lua"; + } + else { + fullPath = path / "Scripts" / "Notes.lua"; + } + } + else { + fullPath = m_lua_dir_path / m_expected_files[group]; + } + if (!std::filesystem::exists(fullPath)) { throw std::runtime_error("Missing file: " + fullPath.string()); } m_states[group] = {}; - auto &state = m_states[group]; + auto& state = m_states[group]; state.state = sol::state(); if (state.state.lua_state() == nullptr) { @@ -341,12 +356,11 @@ SpriteValue LuaScripting::GetSprite(SkinGroup group, std::string key) sol::table key_array = result_table[SkinDataType::Sprite][key]; SpriteValue sprite_value = {}; - sprite_value.numOfFrames = key_array[1]; - sprite_value.X = key_array[2]; - sprite_value.Y = key_array[3]; - sprite_value.AnchorPointX = key_array[4]; - sprite_value.AnchorPointY = key_array[5]; - sprite_value.FrameTime = key_array[6]; + sprite_value.X = key_array[1]; + sprite_value.Y = key_array[2]; + sprite_value.AnchorPointX = key_array[3]; + sprite_value.AnchorPointY = key_array[4]; + sprite_value.FrameTime = key_array[5]; return sprite_value; } catch (const sol::error &err) { @@ -354,343 +368,3 @@ SpriteValue LuaScripting::GetSprite(SkinGroup group, std::string key) } } -void LuaScripting::Arena_SetIndex(int index) -{ - if (m_arena != index) { - m_arena_states.reset(); - } - - m_arena = index; -} - -std::vector LuaScripting::Arena_GetNumeric(std::string key) -{ - if (!m_arena_states) { - TryLoadArena(); - } - - try { - sol::table result_table = m_arena_states->init(); - if (sol::type::table != result_table.get_type()) { - throw std::runtime_error("expected returned function is table!"); - } - - if (sol::type::table != result_table[SkinDataType::Numeric].get_type()) { - throw std::runtime_error("The table missing DataType.Numeric"); - } - - sol::table key_array = result_table[SkinDataType::Numeric][key]; - if (sol::type::table != key_array.get_type()) { - throw std::runtime_error("The table has no key: " + key); - } - - std::vector result; - for (auto &value : key_array) { - if (value.second.get_type() != sol::type::table) { - throw std::runtime_error("Array key is not table"); - } - - NumericValue numeric_value = {}; - - sol::table value_table = value.second; - if (value_table.size() < 5) { - throw std::runtime_error("Array key table size is less than 7"); - } - - auto x = value_table[1]; - if (x.get_type() != sol::type::number) { - throw std::runtime_error("Numeric::X is not a number, Key: " + key); - } - - auto y = value_table[2]; - if (y.get_type() != sol::type::number) { - throw std::runtime_error("Numeric::Y is not a number, Key: " + key); - } - - auto maxDigit = value_table[3]; - if (maxDigit.get_type() != sol::type::number) { - throw std::runtime_error("Numeric::MaxDigit is not a number, Key: " + key); - } - - numeric_value.X = x; - numeric_value.Y = y; - numeric_value.MaxDigit = maxDigit; - - auto direct = value_table[4]; - if (direct.get_type() == sol::type::string) { - std::string direction = value_table[4]; - std::transform(direction.begin(), direction.end(), direction.begin(), ::tolower); - - if (direction == "mid") - numeric_value.Direction = 0; - else if (direction == "left") - numeric_value.Direction = -1; - else if (direction == "right") - numeric_value.Direction = 1; - } else if (direct.get_type() == sol::type::number) { - numeric_value.Direction = std::clamp((int)direct, -1, 1); - } else { - throw std::runtime_error("Numeric::Direction is not either LEFT, MID, RIGHT or -1, 0, 1, Key: " + key); - } - - auto fillWithZero = value_table[5]; - if (fillWithZero.get_type() != sol::type::number && fillWithZero.get_type() != sol::type::boolean) { - throw std::runtime_error("Numeric::FillWithZero is not number or boolean, Key: " + key); - } - - if (fillWithZero.get_type() == sol::type::boolean) { - numeric_value.FillWithZero = fillWithZero; - } else { - int value = std::clamp((int)fillWithZero, 0, 1); - numeric_value.FillWithZero = value > 0; - } - - result.push_back(numeric_value); - } - - return result; - } catch (const sol::error &err) { - throw std::runtime_error(err.what()); - } -} - -std::vector LuaScripting::Arena_GetPosition(std::string key, int KeyCount) -{ - try { - if (!m_arena_states) { - TryLoadArena(); - } - - sol::table result_table = m_arena_states->init(); - if (sol::type::table != result_table.get_type()) { - throw std::runtime_error("expected returned function is table, but got other than table"); - } - - if (sol::type::table != result_table[SkinDataType::Position].get_type()) { - throw std::runtime_error("The table missing DataType.Position"); - } - - sol::table key_array = result_table[SkinDataType::Position][KeyCount]; - if (sol::type::table != key_array.get_type()) { - throw std::runtime_error("The position table has no item for KeyCount: " + std::to_string(KeyCount)); - } - - key_array = key_array[key]; - if (sol::type::table != key_array.get_type()) { - throw std::runtime_error("The table has no key: " + key); - } - - std::string prefix = "Positions::" + std::to_string(KeyCount) + "::"; - - std::vector result; - for (auto &value : key_array) { - if (value.second.get_type() != sol::type::table) { - throw std::runtime_error("Array key is not table"); - } - - PositionValue position_value = {}; - - sol::table value_table = value.second; - if (value_table.size() < 7) { - throw std::runtime_error("Array key table size is less than 7"); - } - - auto item1 = value_table[1]; - auto item2 = value_table[2]; - auto item3 = value_table[3]; - auto item4 = value_table[4]; - - if (item1.get_type() != sol::type::number) { - throw std::runtime_error(prefix + "X is not a number"); - } - - if (item2.get_type() != sol::type::number) { - throw std::runtime_error(prefix + "Y is not a number"); - } - - if (item3.get_type() != sol::type::number) { - throw std::runtime_error(prefix + "AnchorPointX is not a number"); - } - - if (item4.get_type() != sol::type::number) { - throw std::runtime_error(prefix + "AnchorPointY is not a number"); - } - - position_value.X = item1; - position_value.Y = item2; - position_value.AnchorPointX = item3; - position_value.AnchorPointY = item4; - - auto item5 = value_table[5]; - auto item6 = value_table[6]; - auto item7 = value_table[7]; - - if (item5.get_type() != sol::type::number) { - throw std::runtime_error(prefix + "RGB index Red is not a number"); - } - - if (item6.get_type() != sol::type::number) { - throw std::runtime_error(prefix + "RGB index Green is not a number"); - } - - if (item7.get_type() != sol::type::number) { - throw std::runtime_error(prefix + "RGB index Blue is not a number"); - } - - int r = std::clamp((int)item5, 0, 255); - int g = std::clamp((int)item6, 0, 255); - int b = std::clamp((int)item7, 0, 255); - - position_value.RGB[0] = r; - position_value.RGB[1] = g; - position_value.RGB[2] = b; - - result.push_back(position_value); - } - - return result; - } catch (const sol::error &err) { - throw std::runtime_error(err.what()); - } -} - -std::vector LuaScripting::Arena_GetRect(std::string key) -{ - try { - if (!m_arena_states) { - TryLoadArena(); - } - - sol::table result_table = m_arena_states->init(); - if (sol::type::table != result_table.get_type()) { - throw std::runtime_error("expected returned function is table!"); - } - - if (sol::type::table != result_table[SkinDataType::Rect].get_type()) { - throw std::runtime_error("The table missing DataType.Rect"); - } - - sol::table key_array = result_table[SkinDataType::Rect][key]; - if (sol::type::table != key_array.get_type()) { - throw std::runtime_error("The table has no key: " + key); - } - - std::vector result; - - for (auto &value : key_array) { - if (value.second.get_type() != sol::type::table) { - throw std::runtime_error("Array key is not table"); - } - - RectInfo rect_info = {}; - - sol::table value_table = value.second; - if (value_table.size() < 4) { - throw std::runtime_error("Array key table size is less than 4"); - } - - auto item1 = value_table[1]; - auto item2 = value_table[2]; - auto item3 = value_table[3]; - auto item4 = value_table[4]; - - if (item1.get_type() != sol::type::number) { - throw std::runtime_error("Array key table item 1 is not number"); - } - - if (item2.get_type() != sol::type::number) { - throw std::runtime_error("Array key table item 2 is not number"); - } - - if (item3.get_type() != sol::type::number) { - throw std::runtime_error("Array key table item 3 is not number"); - } - - if (item4.get_type() != sol::type::number) { - throw std::runtime_error("Array key table item 4 is not number"); - } - - rect_info.X = item1; - rect_info.Y = item2; - rect_info.Width = item3; - rect_info.Height = item4; - - result.push_back(rect_info); - } - - return result; - } catch (const sol::error &err) { - throw std::runtime_error(err.what()); - } -} - -SpriteValue LuaScripting::Arena_GetSprite(std::string key) -{ - try { - if (!m_arena_states) { - TryLoadArena(); - } - - sol::table result_table = m_arena_states->init(); - if (sol::type::table != result_table.get_type()) { - throw std::runtime_error("expected returned function is table!"); - } - - if (sol::type::table != result_table[SkinDataType::Sprite].get_type()) { - throw std::runtime_error("The table missing DataType.Sprite"); - } - - sol::table key_array = result_table[SkinDataType::Sprite][key]; - if (sol::type::table != key_array.get_type()) { - throw std::runtime_error("The table has no key: " + key); - } - - if (key_array.size() < 6) { - throw std::runtime_error("Expect table size is 6 at key: " + key); - } - - SpriteValue sprite_value = {}; - - auto item1 = key_array[1]; - if (sol::type::number != item1.get_type()) { - throw std::runtime_error("Sprite value 1 is not a number"); - } - - sprite_value.numOfFrames = item1; - - auto item2 = key_array[2]; - if (sol::type::number != item2.get_type()) { - throw std::runtime_error(key + ":Sprite value 2 is not a number"); - } - - sprite_value.X = item2; - - auto item3 = key_array[3]; - if (sol::type::number != item3.get_type()) { - throw std::runtime_error(key + ":Sprite value 3 is not a number"); - } - sprite_value.Y = item3; - - auto item4 = key_array[4]; - if (sol::type::number != item4.get_type()) { - throw std::runtime_error(key + ":Sprite value 4 is not a number"); - } - sprite_value.AnchorPointX = item4; - - auto item5 = key_array[5]; - if (sol::type::number != item5.get_type()) { - throw std::runtime_error(key + ":Sprite value 5 is not a number"); - } - sprite_value.AnchorPointY = item5; - - auto item6 = key_array[6]; - if (sol::type::number != item6.get_type()) { - throw std::runtime_error(key + ":Sprite value 6 is not a number"); - } - sprite_value.FrameTime = item6; - - return sprite_value; - } catch (const sol::error &err) { - throw std::runtime_error(err.what()); - } -} diff --git a/Game/src/Engine/LuaScripting.h b/Game/src/Engine/LuaScripting.h index 07474240..f49a9489 100644 --- a/Game/src/Engine/LuaScripting.h +++ b/Game/src/Engine/LuaScripting.h @@ -33,11 +33,7 @@ class LuaScripting NoteValue GetNote(SkinGroup group, std::string key, int KeyCount); SpriteValue GetSprite(SkinGroup group, std::string key); - void Arena_SetIndex(int index); - std::vector Arena_GetNumeric(std::string key); - std::vector Arena_GetPosition(std::string key, int KeyCount); - std::vector Arena_GetRect(std::string key); - SpriteValue Arena_GetSprite(std::string key); + void Update(double delta); diff --git a/Game/src/Engine/Note.cpp b/Game/src/Engine/Note.cpp index 547e299f..944f5cd5 100644 --- a/Game/src/Engine/Note.cpp +++ b/Game/src/Engine/Note.cpp @@ -6,578 +6,582 @@ #include "GameAudioSampleCache.hpp" #include "NoteImageCacheManager.hpp" #include "RhythmEngine.hpp" +#include #define REMOVE_TIME 800 #define HOLD_COMBO_TICK 100 namespace { - double CalculateNotePosition(double offset, double initialTrackPos, double hitPosition, double noteSpeed, bool upscroll) - { - return hitPosition + ((initialTrackPos - offset) * (upscroll ? noteSpeed : -noteSpeed) / 100); - } +double CalculateNotePosition(double offset, double initialTrackPos, + double hitPosition, double noteSpeed, + bool upscroll) { + return hitPosition + ((initialTrackPos - offset) * + (upscroll ? noteSpeed : -noteSpeed) / 100); +} - double lerp(double min, double max, float alpha) - { - return min * (1.0 - alpha) + (max * alpha); - } +double lerp(double min, double max, float alpha) { + return min * (1.0 - alpha) + (max * alpha); +} - bool isWithinRange(double point, double minRange, double maxRange) - { - return (point >= minRange && point <= maxRange); - } +bool isWithinRange(double point, double minRange, double maxRange) { + return (point >= minRange && point <= maxRange); +} - bool isCollision(double top, double bottom, double min, double max) - { - // Check if the top value of the rectangle is within the range - if (top >= min && top <= max) { - return true; // Collision detected - } +bool isCollision(double top, double bottom, double min, double max) { + // Check if the top value of the rectangle is within the range + if (top >= min && top <= max) { + return true; // Collision detected + } - // Check if the bottom value of the rectangle is within the range - if (bottom >= min && bottom <= max) { - return true; // Collision detected - } + // Check if the bottom value of the rectangle is within the range + if (bottom >= min && bottom <= max) { + return true; // Collision detected + } - // Check if the range is completely inside the rectangle - if (top <= min && bottom >= max) { - return true; // Collision detected - } + // Check if the range is completely inside the rectangle + if (top <= min && bottom >= max) { + return true; // Collision detected + } - // No collision detected - return false; - } + // No collision detected + return false; +} - const int length_multiplier[4] = { - 0, - 2, - 4, - 6 - }; +const int length_multiplier[4] = {0, 2, 4, 6}; } // namespace -Note::Note(RhythmEngine *engine, GameTrack *track) -{ - m_engine = engine; - m_track = track; +Note::Note(RhythmEngine *engine, GameTrack *track) { + m_engine = engine; + m_track = track; - m_imageType = NoteImageType::LANE_1; - m_imageBodyType = NoteImageType::HOLD_LANE_1; + m_imageType = NoteImageType::LANE_1; + m_imageBodyType = NoteImageType::HOLD_LANE_1; - m_head = nullptr; - m_tail = nullptr; - m_body = nullptr; - - m_startTime = 0; - m_endTime = 0; - m_startBPM = 0; - m_endBPM = 0; + m_head = nullptr; + m_tail = nullptr; + m_body = nullptr; - m_type = NoteType::NORMAL; - m_lane = 0; - m_initialTrackPosition = 0; - m_endTrackPosition = 0; + m_startTime = 0; + m_endTime = 0; + m_startBPM = 0; + m_endBPM = 0; - m_keysoundIndex = -1; - m_state = NoteState::NORMAL_NOTE; + m_type = NoteType::NORMAL; + m_lane = 0; + m_initialTrackPosition = 0; + m_endTrackPosition = 0; - m_drawAble = false; - m_laneOffset = 0; - m_removeAble = false; + m_keysoundIndex = -1; + m_state = NoteState::NORMAL_NOTE; - m_didHitHead = false; - m_didHitTail = false; - m_shouldDrawHoldEffect = true; + m_drawAble = false; + m_laneOffset = 0; + m_removeAble = false; - m_hitPos = 0; - m_relPos = 0; + m_didHitHead = false; + m_didHitTail = false; + m_shouldDrawHoldEffect = true; - m_keyVolume = 50; - m_keyPan = 0; + m_hitPos = 0; + m_relPos = 0; - m_lastScoreTime = -1; -} + m_keyVolume = 100; + m_keyPan = 0; -Note::~Note() -{ - Release(); + m_lastScoreTime = -1; } -void Note::Load(NoteInfoDesc *desc) -{ - m_imageType = desc->ImageType; - m_imageBodyType = desc->ImageBodyType; - - m_head = NoteImageCacheManager::GetInstance()->Depool(m_imageType); - if (desc->Type == NoteType::HOLD) { - m_tail = NoteImageCacheManager::GetInstance()->Depool(m_imageType); - m_body = NoteImageCacheManager::GetInstance()->DepoolHold(m_imageBodyType); - m_body->AnchorPoint = { 0, 0.5 }; - - m_startBPM = desc->StartBPM; - m_endBPM = desc->EndBPM; - m_state = NoteState::HOLD_PRE; - } else { - m_tail = nullptr; - m_body = nullptr; - - m_startBPM = desc->StartBPM; - m_endBPM = 0; - m_state = NoteState::NORMAL_NOTE; - } - - m_trail_up = NoteImageCacheManager::GetInstance()->DepoolTrail(NoteImageType::TRAIL_UP); - m_trail_down = NoteImageCacheManager::GetInstance()->DepoolTrail(NoteImageType::TRAIL_DOWN); - - m_startTime = desc->StartTime; - m_endTime = desc->EndTime; - m_type = desc->Type; - m_lane = desc->Lane; - m_initialTrackPosition = desc->InitialTrackPosition; - m_endTrackPosition = desc->EndTrackPosition; - m_keysoundIndex = desc->KeysoundIndex; +Note::~Note() { Release(); } - m_keyVolume = desc->Volume; - m_keyPan = desc->Pan; +void Note::Load(NoteInfoDesc *desc) { + m_imageType = desc->ImageType; + m_imageBodyType = desc->ImageBodyType; - m_laneOffset = 0; - m_drawAble = false; - m_removeAble = false; - m_ignore = true; + m_head = NoteImageCacheManager::GetInstance()->Depool(m_imageType); + if (desc->Type == NoteType::HOLD) { + m_tail = NoteImageCacheManager::GetInstance()->Depool(m_imageType); + m_body = NoteImageCacheManager::GetInstance()->DepoolHold(m_imageBodyType); + m_body->AnchorPoint = {0, 0.5}; - m_didHitHead = false; - m_didHitTail = false; - m_shouldDrawHoldEffect = true; - - m_hitPos = 0; - m_relPos = 0; + m_startBPM = desc->StartBPM; + m_endBPM = desc->EndBPM; + m_state = NoteState::HOLD_PRE; + } else { + m_tail = nullptr; + m_body = nullptr; - m_lastScoreTime = -1; + m_startBPM = desc->StartBPM; + m_endBPM = 0; + m_state = NoteState::NORMAL_NOTE; + } + + m_trail_up = NoteImageCacheManager::GetInstance()->DepoolTrail( + NoteImageType::TRAIL_UP); + m_trail_down = NoteImageCacheManager::GetInstance()->DepoolTrail( + NoteImageType::TRAIL_DOWN); + + m_startTime = desc->StartTime; + m_endTime = desc->EndTime; + m_type = desc->Type; + m_lane = desc->Lane; + m_initialTrackPosition = desc->InitialTrackPosition; + m_endTrackPosition = desc->EndTrackPosition; + m_keysoundIndex = desc->KeysoundIndex; + + m_keyVolume = desc->Volume; + m_keyPan = desc->Pan; + + m_laneOffset = 0; + m_drawAble = false; + m_removeAble = false; + m_ignore = true; + + m_didHitHead = false; + m_didHitTail = false; + m_shouldDrawHoldEffect = true; + + m_hitPos = 0; + m_relPos = 0; + + m_lastScoreTime = -1; + GetNoteSize(); } -void Note::Update(double delta) -{ - if (IsRemoveable()) - return; +void Note::Update(double delta) { + if (IsRemoveable()) + return; - JudgeBase *judge = m_engine->GetJudge(); - double audioPos = m_engine->GetGameAudioPosition(); - m_hitTime = m_startTime - audioPos; + JudgeBase *judge = m_engine->GetJudge(); + double audioPos = m_engine->GetGameAudioPosition(); + m_hitTime = m_startTime - audioPos; - if (m_type == NoteType::NORMAL) { - if (judge->IsMissed(this)) { - if (!IsPassed()) { - m_hitPos = m_startTime + m_hitTime; - OnHit(NoteResult::MISS); - } + if (m_type == NoteType::NORMAL) { + if (judge->IsMissed(this)) { + if (!IsPassed()) { + m_hitPos = m_startTime + m_hitTime; + OnHit(NoteResult::MISS); + } - m_state = NoteState::DO_REMOVE; + m_state = NoteState::DO_REMOVE; + } + } else { + if (m_state == NoteState::HOLD_PRE) { + if (judge->IsMissed(this)) { + m_state = NoteState::HOLD_MISSED_ACTIVE; + + m_hitPos = m_startTime + m_hitTime; + OnHit(NoteResult::MISS); + } + } else if (m_state == NoteState::HOLD_ON_HOLDING || + m_state == NoteState::HOLD_MISSED_ACTIVE || + m_state == NoteState::HOLD_PASSED) { + + if (m_state == NoteState::HOLD_ON_HOLDING) { + if (m_lastScoreTime != -1 && audioPos <= m_endTime && + audioPos > m_startTime) { + if (audioPos - m_lastScoreTime > HOLD_COMBO_TICK) { + m_lastScoreTime += HOLD_COMBO_TICK; + m_track->HandleHoldScore(HoldResult::HoldAdd); + } } - } else { - if (m_state == NoteState::HOLD_PRE) { - if (judge->IsMissed(this)) { - m_state = NoteState::HOLD_MISSED_ACTIVE; - - m_hitPos = m_startTime + m_hitTime; - OnHit(NoteResult::MISS); - } - } else if ( - m_state == NoteState::HOLD_ON_HOLDING || - m_state == NoteState::HOLD_MISSED_ACTIVE || - m_state == NoteState::HOLD_PASSED) { - - if (m_state == NoteState::HOLD_ON_HOLDING) { - if (m_lastScoreTime != -1 && audioPos <= m_endTime && audioPos > m_startTime) { - if (audioPos - m_lastScoreTime > HOLD_COMBO_TICK) { - m_lastScoreTime += HOLD_COMBO_TICK; - m_track->HandleHoldScore(HoldResult::HoldAdd); - } - } - } - - if (judge->IsMissed(this)) { - if (m_state == NoteState::HOLD_ON_HOLDING || m_state == NoteState::HOLD_MISSED_ACTIVE) { - m_hitPos = m_endTime + (m_endTime - audioPos); - if (m_state == NoteState::HOLD_ON_HOLDING) { - OnRelease(NoteResult::MISS); - } - } - - m_state = NoteState::DO_REMOVE; - } + } + + if (judge->IsMissed(this)) { + if (m_state == NoteState::HOLD_ON_HOLDING || + m_state == NoteState::HOLD_MISSED_ACTIVE) { + m_hitPos = m_endTime + (m_endTime - audioPos); + if (m_state == NoteState::HOLD_ON_HOLDING) { + OnRelease(NoteResult::MISS); + } } + + m_state = NoteState::DO_REMOVE; + } } + } } -void Note::Render(double delta) -{ - if (IsRemoveable()) - return; - if (!m_drawAble) - return; - - auto resolution = m_engine->GetResolution(); - auto hitPos = m_engine->GetHitPosition(); - double trackPosition = m_engine->GetTrackPosition(); - - int min = -100, max = hitPos + 25; - auto playRect = m_engine->GetPlayRectangle(); - - int guideLineIndex = m_engine->GetGuideLineIndex(); - - int guideLineLength = 24 * length_multiplier[guideLineIndex]; - - if (m_type == NoteType::HOLD) { - double y1 = CalculateNotePosition(trackPosition, m_initialTrackPosition, 1000.0, m_engine->GetNotespeed(), false) / 1000.0; - double y2 = CalculateNotePosition(trackPosition, m_endTrackPosition, 1000.0, m_engine->GetNotespeed(), false) / 1000.0; - - m_head->Position = UDim2::fromOffset(m_laneOffset, lerp(0.0, (double)hitPos, (float)y1)); - m_tail->Position = UDim2::fromOffset(m_laneOffset, lerp(0.0, (double)hitPos, (float)y2)); - - float Transparency = 0.9f; - - if (m_hitResult >= NoteResult::GOOD && m_state == NoteState::HOLD_ON_HOLDING) { - // m_head->Position.Y.Offset = hitPos; - Transparency = 1.0f; - } - - m_head->CalculateSize(); - m_tail->CalculateSize(); - - double headPos = m_head->AbsolutePosition.Y + (m_head->AbsoluteSize.Y / 2.0); - double tailPos = m_tail->AbsolutePosition.Y + (m_tail->AbsoluteSize.Y / 2.0); +// I ended refactor whole note code +void Note::DrawHead(double delta, double headPosY, int guideLineLength, + Rect &playRect) { + m_head->Position = UDim2::fromOffset(m_laneOffset, headPosY); + m_head->SetIndexAt(GetDynamicBPMFrameIndex(m_head)); + m_head->Draw(delta, &playRect); + + if (guideLineLength > 0) { + m_trail_down->Position = m_head->Position; + m_trail_down->Size = UDim2::fromOffset(1, guideLineLength); + m_trail_down->AnchorPoint = {0, 0}; + m_trail_down->AlphaBlend = true; + m_trail_down->Draw(delta, &playRect); + + m_trail_down->Position = + m_head->Position + UDim2::fromOffset(m_head->AbsoluteSize.X, 0); + m_trail_down->AnchorPoint = {1, 0}; + m_trail_down->AlphaBlend = true; + m_trail_down->Draw(delta, &playRect); + + m_trail_up->Position = + m_head->Position + UDim2::fromOffset(0, -m_head->AbsoluteSize.Y); + m_trail_up->Size = UDim2::fromOffset(1, guideLineLength); + m_trail_up->AnchorPoint = {0, 1}; + m_trail_up->AlphaBlend = true; + m_trail_up->Draw(delta, &playRect); + + m_trail_up->Position = + m_head->Position + + UDim2::fromOffset(m_head->AbsoluteSize.X, -m_head->AbsoluteSize.Y); + m_trail_up->AnchorPoint = {1, 1}; + m_trail_up->AlphaBlend = true; + m_trail_up->Draw(delta, &playRect); + } +} - double height = headPos - tailPos; - double position = (height / 2.0) + tailPos; +void Note::DrawBody(double delta, double bodyPosY, double bodyHeight, + Rect &playRect) { + m_body->Position = UDim2::fromOffset( + m_laneOffset, bodyPosY - (m_head->AbsoluteSize.Y / 2.0)); + m_body->Size = {1, 0, 0, bodyHeight}; + m_body->SetIndexAt(GetDynamicBPMFrameIndex(m_body)); + m_body->Draw(delta, &playRect); +} - m_body->Position = UDim2::fromOffset(m_laneOffset, position); - m_body->Size = { 1, 0, 0, height }; +void Note::DrawTail(double delta, double tailPosY, int guideLineLength, + Rect &playRect) { + m_tail->Position = UDim2::fromOffset(m_laneOffset, tailPosY); + m_tail->SetIndexAt(GetDynamicBPMFrameIndex(m_tail)); + m_tail->Draw(delta, &playRect); + + if (guideLineLength > 0) { + m_trail_down->Position = m_tail->Position; + m_trail_down->Size = UDim2::fromOffset(1, guideLineLength); + m_trail_down->AnchorPoint = {0, 0}; + m_trail_down->AlphaBlend = true; + m_trail_down->Draw(delta, &playRect); + + m_trail_down->Position = + m_tail->Position + UDim2::fromOffset(m_tail->AbsoluteSize.X, 0); + m_trail_down->AnchorPoint = {1, 0}; + m_trail_down->AlphaBlend = true; + m_trail_down->Draw(delta, &playRect); + + m_trail_up->Position = + m_tail->Position + UDim2::fromOffset(0, -m_tail->AbsoluteSize.Y); + m_trail_up->Size = UDim2::fromOffset(1, guideLineLength); + m_trail_up->AnchorPoint = {0, 1}; + m_trail_up->AlphaBlend = true; + m_trail_up->Draw(delta, &playRect); + + m_trail_up->Position = + m_tail->Position + + UDim2::fromOffset(m_tail->AbsoluteSize.X, -m_tail->AbsoluteSize.Y); + m_trail_up->AnchorPoint = {1, 1}; + m_trail_up->AlphaBlend = true; + m_trail_up->Draw(delta, &playRect); + } +} - m_body->TintColor = { Transparency, Transparency, Transparency }; +void Note::SetTransparency() { + float transparency = 0.9f; + if (m_hitResult >= NoteResult::GOOD && + m_state == NoteState::HOLD_ON_HOLDING) { + transparency = 1.0f; + } else if (m_hitResult >= NoteResult::MISS && + m_state == NoteState::HOLD_MISSED_ACTIVE) { + transparency = 0.7f; + } + m_head->TintColor = {transparency, transparency, transparency}; + m_body->TintColor = {transparency, transparency, transparency}; + m_tail->TintColor = {transparency, transparency, transparency}; +} - bool b1 = isWithinRange(m_head->Position.Y.Offset, min, max); - bool b2 = isWithinRange(m_tail->Position.Y.Offset, min, max); +void Note::Render(double delta) { + if (IsRemoveable()) { + return; + } + if (!m_drawAble) { + return; + } + + auto resolution = m_engine->GetResolution(); + auto hitPos = m_engine->GetHitPosition(); + double trackPosition = m_engine->GetTrackPosition(); + + int min = -100, max = GameWindow::GetInstance()->GetBufferHeight(); + auto playRect = m_engine->GetPlayRectangle(); + + int guideLineIndex = m_engine->GetGuideLineIndex(); + int guideLineLength = 24 * length_multiplier[guideLineIndex]; + + double y1 = CalculateNotePosition(trackPosition, m_initialTrackPosition, + 1000.0, m_engine->GetNotespeed(), false) / + 1000.0; + double headPosY = + lerp(0.0, static_cast(hitPos), static_cast(y1)); + bool isHeadVisible = isWithinRange(headPosY, min, max); + + if (m_type == NoteType::HOLD) { + double y2 = CalculateNotePosition(trackPosition, m_endTrackPosition, 1000.0, + m_engine->GetNotespeed(), false) / + 1000.0; + double tailPosY = + lerp(0.0, static_cast(hitPos), static_cast(y2)); + bool isTailVisible = isWithinRange(tailPosY, min, max); + + if (EnvironmentSetup::GetInt("NewLN") == 1) { + tailPosY += m_tail->AbsoluteSize.Y; + } - if (isCollision(m_tail->Position.Y.Offset, m_head->Position.Y.Offset, min, max)) { - m_body->SetIndexAt(m_engine->GetNoteImageIndex()); - m_body->Draw(delta, &playRect); - } + double bodyPosY = (headPosY + tailPosY) / 2.0; + double bodyHeight = std::abs(headPosY - (m_head->AbsoluteSize.Y / 2.0)) - + (tailPosY - (m_head->AbsoluteSize.Y / 2.0)); - if (b1) { - if (guideLineLength > 0) { - m_trail_down->Position = m_head->Position; - m_trail_down->Size = UDim2::fromOffset(1, guideLineLength); - m_trail_down->AnchorPoint = { 0, 0 }; - m_trail_down->Draw(delta, &playRect); + if (EnvironmentSetup::GetInt("LNBodyOnTop") == 1) { + if (isHeadVisible) { + DrawHead(delta, headPosY, guideLineLength, playRect); + } - m_trail_down->Position = m_head->Position + UDim2::fromOffset(m_head->AbsoluteSize.X, 0); - m_trail_down->AnchorPoint = { 1, 0 }; - m_trail_down->Draw(delta, &playRect); - } + if (isTailVisible) { + DrawTail(delta, tailPosY, guideLineLength, playRect); + } - m_head->SetIndexAt(m_engine->GetNoteImageIndex()); - m_head->Draw(delta, &playRect); - } + SetTransparency(); + DrawBody(delta, bodyPosY, bodyHeight, playRect); + } else { + SetTransparency(); + DrawBody(delta, bodyPosY, bodyHeight, playRect); - if (b2) { - if (guideLineLength > 0) { - m_trail_up->Position = m_tail->Position + UDim2::fromOffset(0, -m_tail->AbsoluteSize.Y); - m_trail_up->Size = UDim2::fromOffset(1, guideLineLength); - m_trail_up->AnchorPoint = { 0, 1 }; - m_trail_up->Draw(delta, &playRect); + if (isTailVisible) { + DrawTail(delta, tailPosY, guideLineLength, playRect); + } - m_trail_up->Position = m_tail->Position + UDim2::fromOffset(m_tail->AbsoluteSize.X, -m_tail->AbsoluteSize.Y); - m_trail_up->AnchorPoint = { 1, 1 }; - m_trail_up->Draw(delta, &playRect); - } + if (isHeadVisible) { + DrawHead(delta, headPosY, guideLineLength, playRect); + } + } + } else { - m_tail->SetIndexAt(m_engine->GetNoteImageIndex()); - m_tail->Draw(delta, &playRect); - } - } else { - double y1 = CalculateNotePosition(trackPosition, m_initialTrackPosition, 1000.0, m_engine->GetNotespeed(), false) / 1000.0; - m_head->Position = UDim2::fromOffset(m_laneOffset, lerp(0.0, (double)hitPos, (float)y1)); - m_head->CalculateSize(); - - bool b1 = isWithinRange(m_head->Position.Y.Offset, min, max); - - if (b1) { - if (guideLineLength > 0) { - m_trail_down->Position = m_head->Position; - m_trail_down->Size = UDim2::fromOffset(1, guideLineLength); - m_trail_down->AnchorPoint = { 0, 0 }; - m_trail_down->Draw(delta, &playRect); - - m_trail_down->Position = m_head->Position + UDim2::fromOffset(m_head->AbsoluteSize.X, 0); - m_trail_down->AnchorPoint = { 1, 0 }; - m_trail_down->Draw(delta, &playRect); - - m_trail_up->Position = m_head->Position + UDim2::fromOffset(0, -m_head->AbsoluteSize.Y); - m_trail_up->Size = UDim2::fromOffset(1, guideLineLength); - m_trail_up->AnchorPoint = { 0, 1 }; - m_trail_up->Draw(delta, &playRect); - - m_trail_up->Position = m_head->Position + UDim2::fromOffset(m_head->AbsoluteSize.X, -m_head->AbsoluteSize.Y); - m_trail_up->AnchorPoint = { 1, 1 }; - m_trail_up->Draw(delta, &playRect); - } - - m_head->SetIndexAt(m_engine->GetNoteImageIndex()); - m_head->Draw(delta, &playRect); - } + if (isHeadVisible) { + DrawHead(delta, headPosY, guideLineLength, playRect); } + } } -double Note::GetInitialTrackPosition() const -{ - return m_initialTrackPosition; -} +double Note::GetInitialTrackPosition() const { return m_initialTrackPosition; } -double Note::GetStartTime() const -{ - return m_startTime; -} +double Note::GetStartTime() const { return m_startTime; } -double Note::GetBPMTime() const -{ - if (GetType() == NoteType::HOLD) { - if (m_state == NoteState::HOLD_PRE) { - return m_startBPM; - } else { - return m_endBPM; - } +double Note::GetBPMTime() const { + if (GetType() == NoteType::HOLD) { + if (m_state == NoteState::HOLD_PRE) { + return m_startBPM; } else { - return m_startBPM; + return m_endBPM; } + } else { + return m_startBPM; + } } -double Note::GetHitTime() const -{ - if (GetType() == NoteType::HOLD) { - if (m_state == NoteState::HOLD_PRE) { - return m_startTime; - } else { - return m_endTime; - } +double Note::GetHitTime() const { + if (GetType() == NoteType::HOLD) { + if (m_state == NoteState::HOLD_PRE) { + return m_startTime; } else { - return m_startTime; + return m_endTime; } + } else { + return m_startTime; + } } -int Note::GetKeysoundId() const -{ - return m_keysoundIndex; -} - -int Note::GetKeyVolume() const -{ - return m_keyVolume; -} +int Note::GetKeysoundId() const { return m_keysoundIndex; } -int Note::GetKeyPan() const -{ - return m_keyPan; -} +int Note::GetKeyVolume() const { return m_keyVolume; } -NoteType Note::GetType() const -{ - return m_type; -} +int Note::GetKeyPan() const { return m_keyPan; } -std::tuple Note::CheckHit() -{ - JudgeBase *judge = m_engine->GetJudge(); +NoteType Note::GetType() const { return m_type; } - if (m_type == NoteType::NORMAL) { - double time_to_end = m_engine->GetGameAudioPosition() - m_startTime; - auto result = judge->CalculateResult(this); - if (std::get(result)) { - m_ignore = false; - } +std::tuple Note::CheckHit() { + JudgeBase *judge = m_engine->GetJudge(); - return result; - } else { - if (m_state == NoteState::HOLD_PRE) { - double time_to_end = m_engine->GetGameAudioPosition() - m_startTime; - auto result = judge->CalculateResult(this); - if (std::get(result)) { - m_ignore = false; - } - - return result; - } else if (m_state == NoteState::HOLD_MISSED_ACTIVE) { - double time_to_end = m_engine->GetGameAudioPosition() - m_endTime; - auto result = judge->CalculateResult(this); - if (std::get(result)) { - m_ignore = false; - } - - return result; - } + if (m_type == NoteType::NORMAL) { + double time_to_end = m_engine->GetGameAudioPosition() - m_startTime; + auto result = judge->CalculateResult(this); + if (std::get(result)) { + m_ignore = false; + } else if (time_to_end) { + m_ignore = true; + } - return { false, NoteResult::MISS }; + return result; + } else { + if (m_state == NoteState::HOLD_PRE) { + double time_to_end = m_engine->GetGameAudioPosition() - m_startTime; + auto result = judge->CalculateResult(this); + if (std::get(result)) { + m_ignore = false; + } else if (time_to_end) { + m_ignore = true; + } + + return result; + } else if (m_state == NoteState::HOLD_MISSED_ACTIVE) { + double time_to_end = m_engine->GetGameAudioPosition() - m_endTime; + auto result = judge->CalculateResult(this); + if (std::get(result)) { + m_ignore = false; + } else if (time_to_end) { + m_ignore = true; + } + + return result; } + + return {false, NoteResult::MISS}; + } } -std::tuple Note::CheckRelease() -{ - if (m_type == NoteType::HOLD) { - double time_to_end = m_engine->GetGameAudioPosition() - m_endTime; - JudgeBase *judge = m_engine->GetJudge(); +std::tuple Note::CheckRelease() { + if (m_type == NoteType::HOLD) { + double time_to_end = m_engine->GetGameAudioPosition() - m_endTime; + JudgeBase *judge = m_engine->GetJudge(); - if (m_state == NoteState::HOLD_ON_HOLDING || m_state == NoteState::HOLD_MISSED_ACTIVE) { - auto result = judge->CalculateResult(this); + if (m_state == NoteState::HOLD_ON_HOLDING || + m_state == NoteState::HOLD_MISSED_ACTIVE) { + auto result = judge->CalculateResult(this); - if (std::get(result)) { - if (m_state == NoteState::HOLD_MISSED_ACTIVE) { - return { true, NoteResult::BAD }; - } + if (std::get(result)) { + if (m_state == NoteState::HOLD_MISSED_ACTIVE) { + m_ignore = false; + return {true, NoteResult::BAD}; + } - return result; - } + return result; + } - if (m_state == NoteState::HOLD_ON_HOLDING) { - return { true, NoteResult::MISS }; - } else { - return { false, NoteResult::MISS }; - } + if (m_state == NoteState::HOLD_ON_HOLDING) { + if (time_to_end) { + m_ignore = true; + } + return {true, NoteResult::MISS}; + } else { + if (time_to_end) { + m_ignore = true; } + return {false, NoteResult::MISS}; + } } + } - return { false, NoteResult::MISS }; + return {false, NoteResult::MISS}; } -void Note::OnHit(NoteResult result) -{ - if (m_type == NoteType::HOLD) { - if (m_state == NoteState::HOLD_PRE) { - m_didHitHead = true; - m_state = NoteState::HOLD_ON_HOLDING; - m_lastScoreTime = m_engine->GetGameAudioPosition(); - - m_hitResult = result; - m_track->HandleHoldScore(HoldResult::HoldAdd); - m_track->HandleScore({ result, - m_hitPos, - false, - m_ignore, - 2 }); - } else if (m_state == NoteState::HOLD_MISSED_ACTIVE) { - m_didHitHead = true; - m_state = NoteState::HOLD_PASSED; - - m_track->HandleHoldScore(HoldResult::HoldBreak); - m_track->HandleScore({ result, - m_hitPos, - true, - m_ignore, - 2 }); - } - } else { - m_state = NoteState::NORMAL_NOTE_PASSED; - m_track->HandleScore({ result, - m_hitPos, - false, - m_ignore, - 1 }); +void Note::OnHit(NoteResult result) { + if (m_type == NoteType::HOLD) { + if (m_state == NoteState::HOLD_PRE) { + m_didHitHead = true; + m_state = NoteState::HOLD_ON_HOLDING; + m_lastScoreTime = m_engine->GetGameAudioPosition(); + + m_hitResult = result; + m_track->HandleHoldScore(HoldResult::HoldAdd); + m_track->HandleScore({result, m_hitPos, false, m_ignore, 2}); + } else if (m_state == NoteState::HOLD_MISSED_ACTIVE) { + m_didHitHead = true; + m_state = NoteState::HOLD_PASSED; + + m_track->HandleHoldScore(HoldResult::HoldBreak); + m_track->HandleScore({result, m_hitPos, true, m_ignore, 2}); } + } else { + m_state = NoteState::NORMAL_NOTE_PASSED; + m_track->HandleScore({result, m_hitPos, false, m_ignore, 1}); + } } -void Note::OnRelease(NoteResult result) -{ - if (m_type == NoteType::HOLD) { - if (m_state == NoteState::HOLD_ON_HOLDING || m_state == NoteState::HOLD_MISSED_ACTIVE) { - m_lastScoreTime = -1; - - if (result == NoteResult::MISS) { - GameAudioSampleCache::Stop(m_keysoundIndex); - m_state = NoteState::HOLD_MISSED_ACTIVE; - - m_track->HandleHoldScore(HoldResult::HoldBreak); - m_track->HandleScore({ result, - m_hitPos, - true, - m_ignore, - 2 }); - } else { - m_state = NoteState::HOLD_PASSED; - m_didHitTail = true; - m_track->HandleScore({ result, - m_hitPos, - true, - m_ignore, - 2 }); - } - } +void Note::OnRelease(NoteResult result) { + if (m_type == NoteType::HOLD) { + if (m_state == NoteState::HOLD_ON_HOLDING || + m_state == NoteState::HOLD_MISSED_ACTIVE) { + m_lastScoreTime = -1; + + if (result == NoteResult::MISS) { + // GameAudioSampleCache::Stop(m_keysoundIndex); + m_state = NoteState::HOLD_MISSED_ACTIVE; + + m_track->HandleHoldScore(HoldResult::HoldBreak); + m_track->HandleScore({result, m_hitPos, true, m_ignore, 2}); + } else { + m_state = NoteState::HOLD_PASSED; + m_didHitTail = true; + m_track->HandleScore({result, m_hitPos, true, m_ignore, 2}); + } } + } } -void Note::SetXPosition(int x) -{ - m_laneOffset = x; -} +void Note::SetXPosition(int x) { m_laneOffset = x; } -void Note::SetDrawable(bool drawable) -{ - m_drawAble = drawable; -} +void Note::SetDrawable(bool drawable) { m_drawAble = drawable; } -bool Note::IsHoldEffectDrawable() -{ - return m_shouldDrawHoldEffect; +void Note::GetNoteSize() { + EnvironmentSetup::SetInt("NoteSize", + static_cast(m_head->AbsoluteSize.Y)); } -bool Note::IsDrawable() -{ - if (m_removeAble) - return false; +int Note::GetDynamicBPMFrameIndex(FrameTimer *frameTimer) { + int maxFrames = static_cast(frameTimer->m_frames.size()); + if (maxFrames <= 0) + return 0; - return m_drawAble; + return m_engine->GetBPMAnimationIndex(maxFrames); } -bool Note::IsRemoveable() -{ - return m_state == NoteState::DO_REMOVE; -} +bool Note::IsHoldEffectDrawable() const { return m_shouldDrawHoldEffect; } -bool Note::IsPassed() -{ - return m_state == NoteState::NORMAL_NOTE_PASSED || m_state == NoteState::HOLD_PASSED; -} +bool Note::IsDrawable() const { + if (m_removeAble) + return false; -bool Note::IsHeadHit() -{ - return m_didHitHead; + return m_drawAble; } -bool Note::IsTailHit() -{ - return m_didHitTail; +bool Note::IsRemoveable() const { return m_state == NoteState::DO_REMOVE; } + +bool Note::IsPassed() const { + return m_state == NoteState::NORMAL_NOTE_PASSED || + m_state == NoteState::HOLD_PASSED || IsRemoveable(); } -void Note::Release() -{ - m_state = NoteState::DO_REMOVE; - m_removeAble = true; +bool Note::IsHeadHit() const { return m_didHitHead; } - auto cacheManager = NoteImageCacheManager::GetInstance(); +bool Note::IsTailHit() const { return m_didHitTail; } - cacheManager->RepoolTrail(m_trail_down, NoteImageType::TRAIL_DOWN); - cacheManager->RepoolTrail(m_trail_up, NoteImageType::TRAIL_UP); +void Note::Release() { + m_state = NoteState::DO_REMOVE; + m_removeAble = true; - m_trail_up = nullptr; - m_trail_down = nullptr; + auto cacheManager = NoteImageCacheManager::GetInstance(); - if (m_type == NoteType::HOLD) { - cacheManager->Repool(m_head, m_imageType); - m_head = nullptr; + cacheManager->RepoolTrail(m_trail_down, NoteImageType::TRAIL_DOWN); + cacheManager->RepoolTrail(m_trail_up, NoteImageType::TRAIL_UP); - cacheManager->Repool(m_tail, m_imageType); - m_tail = nullptr; + m_trail_up = nullptr; + m_trail_down = nullptr; - cacheManager->RepoolHold(m_body, m_imageBodyType); - m_body = nullptr; - } else { - cacheManager->Repool(m_head, m_imageType); - m_head = nullptr; - } + if (m_type == NoteType::HOLD) { + cacheManager->Repool(m_head, m_imageType); + m_head = nullptr; + + cacheManager->Repool(m_tail, m_imageType); + m_tail = nullptr; + + cacheManager->RepoolHold(m_body, m_imageBodyType); + m_body = nullptr; + } else { + cacheManager->Repool(m_head, m_imageType); + m_head = nullptr; + } } \ No newline at end of file diff --git a/Game/src/Engine/Note.hpp b/Game/src/Engine/Note.hpp index 8061113a..c14d1d8a 100644 --- a/Game/src/Engine/Note.hpp +++ b/Game/src/Engine/Note.hpp @@ -10,6 +10,7 @@ class DrawableNote; class DrawableTile; class AudioSampleChannel; class ResizableImage; +class FrameTimer; enum class NoteState { NORMAL_NOTE, @@ -52,6 +53,10 @@ class Note { void Load(NoteInfoDesc* desc); void Update(double delta); + void DrawHead(double delta, double headPosY, int guideLineLength, Rect& playRect); + void DrawBody(double delta, double bodyPosY, double bodyHeight, Rect& playRect); + void DrawTail(double delta, double tailPosY, int guideLineLength, Rect& playRect); + void SetTransparency(); void Render(double delta); double GetInitialTrackPosition() const; @@ -71,14 +76,17 @@ class Note { void SetXPosition(int x); void SetDrawable(bool drawable); - - bool IsHoldEffectDrawable(); - bool IsDrawable(); - bool IsRemoveable(); - bool IsPassed(); - bool IsHeadHit(); - bool IsTailHit(); + void GetNoteSize(); + int GetDynamicBPMFrameIndex(FrameTimer* frameTimer); + + bool IsHoldEffectDrawable() const; + bool IsDrawable() const; + bool IsRemoveable() const; + bool IsPassed() const; + + bool IsHeadHit() const; + bool IsTailHit() const; void Release(); diff --git a/Game/src/Engine/NoteImageCacheManager.cpp b/Game/src/Engine/NoteImageCacheManager.cpp index d2a7d9bf..a5debe85 100644 --- a/Game/src/Engine/NoteImageCacheManager.cpp +++ b/Game/src/Engine/NoteImageCacheManager.cpp @@ -1,154 +1,114 @@ #include "NoteImageCacheManager.hpp" -#include #include -constexpr int MAX_OBJECTS = 50; +constexpr int MAX_OBJECTS = 64; -NoteImageCacheManager::NoteImageCacheManager() -{ - m_noteTextures = std::unordered_map>(); +NoteImageCacheManager::NoteImageCacheManager() { } -NoteImageCacheManager::~NoteImageCacheManager() -{ - for (auto &it : m_noteTextures) { - for (auto &it2 : it.second) { - delete it2; - } - } +NoteImageCacheManager::~NoteImageCacheManager() { - for (auto &it : m_holdTextures) { - for (auto &it2 : it.second) { - delete it2; - } - } +} - for (auto &it : m_trailTextures) { - for (auto &it2 : it.second) { - delete it2; - } +NoteImageCacheManager* NoteImageCacheManager::s_instance = nullptr; + +NoteImageCacheManager* NoteImageCacheManager::GetInstance() { + if (s_instance == nullptr) { + s_instance = new NoteImageCacheManager(); } + return s_instance; } -NoteImageCacheManager *NoteImageCacheManager::s_instance = nullptr; +void NoteImageCacheManager::Release() { + if (s_instance) { + // Clean up all note textures + for (auto& pair : s_instance->m_noteTextures) { + for (auto& note : pair.second) { + delete note; + } + } + s_instance->m_noteTextures.clear(); -void NoteImageCacheManager::Repool(DrawableNote *image, NoteImageType noteType) -{ - if (image == nullptr) - return; + // Clean up all hold textures + for (auto& pair : s_instance->m_holdTextures) { + for (auto& note : pair.second) { + delete note; + } + } + s_instance->m_holdTextures.clear(); - auto &it = m_noteTextures[noteType]; + // Clean up all trail textures + for (auto& pair : s_instance->m_trailTextures) { + for (auto& note : pair.second) { + delete note; + } + } + s_instance->m_trailTextures.clear(); - if (it.size() >= MAX_OBJECTS) { - delete image; - return; + delete s_instance; + s_instance = nullptr; } - - it.push_back(image); } -void NoteImageCacheManager::RepoolHold(DrawableNote *image, NoteImageType noteType) -{ - if (image == nullptr) +void NoteImageCacheManager::Repool(DrawableNote* image, NoteImageType noteType) { + if (image == nullptr || m_noteTextures[noteType].size() >= MAX_OBJECTS) return; - auto &it = m_holdTextures[noteType]; - - if (it.size() >= MAX_OBJECTS) { - delete image; - return; - } - - it.push_back(image); + m_noteTextures[noteType].push_back(image); } -void NoteImageCacheManager::RepoolTrail(DrawableNote *image, NoteImageType noteType) -{ - if (image == nullptr) +void NoteImageCacheManager::RepoolHold(DrawableNote* image, NoteImageType noteType) { + if (image == nullptr || m_holdTextures[noteType].size() >= MAX_OBJECTS) return; - auto &it = m_trailTextures[noteType]; + m_holdTextures[noteType].push_back(image); +} - if (it.size() >= MAX_OBJECTS) { - delete image; +void NoteImageCacheManager::RepoolTrail(DrawableNote* image, NoteImageType noteType) { + if (image == nullptr || m_trailTextures[noteType].size() >= MAX_OBJECTS) return; - } - it.push_back(image); + m_trailTextures[noteType].push_back(image); } -DrawableNote *NoteImageCacheManager::Depool(NoteImageType noteType) -{ - if (noteType >= NoteImageType::LANE_1 && noteType <= NoteImageType::LANE_7) { - auto &it = m_noteTextures[noteType]; - DrawableNote *image = nullptr; - if (it.size() > 0) { - image = it.back(); - it.pop_back(); - } else { - image = new DrawableNote(GameNoteResource::GetNoteTexture(noteType)); - image->Repeat = true; - } - - return image; +DrawableNote* NoteImageCacheManager::Depool(NoteImageType noteType) { + auto& textureMap = m_noteTextures[noteType]; + if (!textureMap.empty()) { + DrawableNote* note = textureMap.back(); + textureMap.pop_back(); + return note; } else { - return nullptr; + // Create a new note if the pool is empty + DrawableNote* note = new DrawableNote(GameNoteResource::GetNoteTexture(noteType)); + note->Repeat = true; + return note; } } -DrawableNote *NoteImageCacheManager::DepoolHold(NoteImageType noteType) -{ - if (noteType >= NoteImageType::HOLD_LANE_1 && noteType <= NoteImageType::HOLD_LANE_7) { - auto &it = m_holdTextures[noteType]; - DrawableNote *image = nullptr; - if (it.size() > 0) { - image = it.back(); - it.pop_back(); - } else { - image = new DrawableNote(GameNoteResource::GetNoteTexture(noteType)); - image->Repeat = true; - } - - return image; +DrawableNote* NoteImageCacheManager::DepoolHold(NoteImageType noteType) { + auto& textureMap = m_holdTextures[noteType]; + if (!textureMap.empty()) { + DrawableNote* note = textureMap.back(); + textureMap.pop_back(); + return note; } else { - return nullptr; + // Create a new hold note if the pool is empty + DrawableNote* note = new DrawableNote(GameNoteResource::GetNoteTexture(noteType)); + note->Repeat = true; + return note; } } -DrawableNote *NoteImageCacheManager::DepoolTrail(NoteImageType noteType) -{ - if (noteType >= NoteImageType::TRAIL_UP && noteType <= NoteImageType::TRAIL_DOWN) { - auto &it = m_trailTextures[noteType]; - DrawableNote *image = nullptr; - if (it.size() > 0) { - image = it.back(); - it.pop_back(); - } else { - image = new DrawableNote(GameNoteResource::GetNoteTexture(noteType)); - image->Repeat = true; - } - - return image; +DrawableNote* NoteImageCacheManager::DepoolTrail(NoteImageType noteType) { + auto& textureMap = m_trailTextures[noteType]; + if (!textureMap.empty()) { + DrawableNote* note = textureMap.back(); + textureMap.pop_back(); + return note; } else { - return nullptr; - } -} - -NoteImageCacheManager *NoteImageCacheManager::GetInstance() -{ - if (s_instance == nullptr) { - s_instance = new NoteImageCacheManager(); - } - - return s_instance; -} - -void NoteImageCacheManager::Release() -{ - if (s_instance) { - Logs::Puts("[NoteImageCacheManager] Release about: hold=%d, note=%d, trail=%d", s_instance->m_holdTextures.size(), s_instance->m_noteTextures.size(), s_instance->m_trailTextures.size()); - - delete s_instance; - s_instance = nullptr; + // Create a new trail note if the pool is empty + DrawableNote* note = new DrawableNote(GameNoteResource::GetNoteTexture(noteType)); + note->Repeat = true; + return note; } } diff --git a/Game/src/Engine/NoteImageCacheManager.hpp b/Game/src/Engine/NoteImageCacheManager.hpp index b72333e8..3850a41b 100644 --- a/Game/src/Engine/NoteImageCacheManager.hpp +++ b/Game/src/Engine/NoteImageCacheManager.hpp @@ -12,22 +12,22 @@ class NoteImageCacheManager ~NoteImageCacheManager(); // Store a note texture in the cache - void Repool(DrawableNote *image, NoteImageType noteType); - void RepoolHold(DrawableNote *image, NoteImageType noteType); - void RepoolTrail(DrawableNote *image, NoteImageType noteType); + void Repool(DrawableNote* image, NoteImageType noteType); + void RepoolHold(DrawableNote* image, NoteImageType noteType); + void RepoolTrail(DrawableNote* image, NoteImageType noteType); // Get a note texture from the cache if exists, otherwise create a new one - DrawableNote *Depool(NoteImageType noteType); - DrawableNote *DepoolHold(NoteImageType noteType); - DrawableNote *DepoolTrail(NoteImageType noteType); + DrawableNote* Depool(NoteImageType noteType); + DrawableNote* DepoolHold(NoteImageType noteType); + DrawableNote* DepoolTrail(NoteImageType noteType); - static NoteImageCacheManager *GetInstance(); + static NoteImageCacheManager* GetInstance(); static void Release(); private: - static NoteImageCacheManager *s_instance; + static NoteImageCacheManager* s_instance; - std::unordered_map> m_noteTextures; - std::unordered_map> m_holdTextures; - std::unordered_map> m_trailTextures; + std::unordered_map> m_noteTextures; + std::unordered_map> m_holdTextures; + std::unordered_map> m_trailTextures; }; \ No newline at end of file diff --git a/Game/src/Engine/O2NumericTexture.cpp b/Game/src/Engine/O2NumericTexture.cpp index a2251acd..3233076b 100644 --- a/Game/src/Engine/O2NumericTexture.cpp +++ b/Game/src/Engine/O2NumericTexture.cpp @@ -15,7 +15,7 @@ O2NumericTexture::O2NumericTexture(OJS *ojs) auto frame = ojs->Frames[i].get(); auto tex = new O2Texture(frame); - m_numericsTexture.push_back(tex); + m_numericsTexture.emplace_back(tex); m_numbericsWidth[i] = tex->GetOriginalRECT(); } } \ No newline at end of file diff --git a/Game/src/Engine/RhythmEngine.cpp b/Game/src/Engine/RhythmEngine.cpp index fef99697..c880eb01 100644 --- a/Game/src/Engine/RhythmEngine.cpp +++ b/Game/src/Engine/RhythmEngine.cpp @@ -1,5 +1,6 @@ #include "RhythmEngine.hpp" #include +#include #include #include #include @@ -17,736 +18,767 @@ #include "Judgements/BeatBasedJudge.h" #include "Judgements/MsBasedJudge.h" -#include -#include - #define MAX_BUFFER_TXT_SIZE 256 -struct ManiaKeyState -{ - Keys key; - bool isPressed; +struct ManiaKeyState { + Keys key; + bool isPressed; }; namespace { - std::vector Key2Type = { - NoteImageType::LANE_1, - NoteImageType::LANE_2, - NoteImageType::LANE_3, - NoteImageType::LANE_4, - NoteImageType::LANE_5, - NoteImageType::LANE_6, - NoteImageType::LANE_7, - }; - - std::vector Key2HoldType = { - NoteImageType::HOLD_LANE_1, - NoteImageType::HOLD_LANE_2, - NoteImageType::HOLD_LANE_3, - NoteImageType::HOLD_LANE_4, - NoteImageType::HOLD_LANE_5, - NoteImageType::HOLD_LANE_6, - NoteImageType::HOLD_LANE_7, - }; - - std::unordered_map KeyMapping = { - { 0, { Keys::A, false } }, - { 1, { Keys::S, false } }, - { 2, { Keys::D, false } }, - { 3, { Keys::Space, false } }, - { 4, { Keys::J, false } }, - { 5, { Keys::K, false } }, - { 6, { Keys::L, false } }, - }; - - int trackOffset[] = { 5, 33, 55, 82, 114, 142, 164 }; -} // namespace - -RhythmEngine::RhythmEngine() -{ - m_currentAudioGamePosition = 0; - m_currentVisualPosition = 0; - m_currentTrackPosition = 0; - m_rate = 1; - m_offset = 0; - m_scrollSpeed = 180; - - m_timingPositionMarkers = std::vector(); -} - -RhythmEngine::~RhythmEngine() -{ - Release(); -} - -bool RhythmEngine::Load(Chart *chart) -{ - GameNoteResource::Load(); - - m_state = GameState::PreParing; - m_currentChart = chart; - - m_autoHitIndex.clear(); - m_autoHitInfos.clear(); - - // default is 99 - m_noteMaxImageIndex = 99; +std::vector Key2Type = { + NoteImageType::LANE_1, NoteImageType::LANE_2, NoteImageType::LANE_3, + NoteImageType::LANE_4, NoteImageType::LANE_5, NoteImageType::LANE_6, + NoteImageType::LANE_7, +}; - int currentX = m_laneOffset; - for (int i = 0; i < 7; i++) { - m_tracks.push_back(new GameTrack(this, i, currentX)); - m_autoHitIndex[i] = 0; +std::vector Key2HoldType = { + NoteImageType::HOLD_LANE_1, NoteImageType::HOLD_LANE_2, + NoteImageType::HOLD_LANE_3, NoteImageType::HOLD_LANE_4, + NoteImageType::HOLD_LANE_5, NoteImageType::HOLD_LANE_6, + NoteImageType::HOLD_LANE_7, +}; - if (m_eventCallback) { - m_tracks[i]->ListenEvent([&](GameTrackEvent e) { - m_eventCallback(e); - }); - } +std::unordered_map KeyMapping = { + {0, {Keys::A, false}}, {1, {Keys::S, false}}, {2, {Keys::D, false}}, + {3, {Keys::Space, false}}, {4, {Keys::J, false}}, {5, {Keys::K, false}}, + {6, {Keys::L, false}}, +}; - auto noteTex = GameNoteResource::GetNoteTexture(Key2Type[i]); +int trackOffset[] = {5, 33, 55, 82, 114, 142, 164}; +} // namespace - int size = noteTex->TextureRect.right; - m_noteMaxImageIndex = (std::min)(noteTex->MaxFrames, m_noteMaxImageIndex); +RhythmEngine::RhythmEngine() + : m_lanePos{0}, m_laneSize{0}, m_playRectangle{0, 0, 0, 0} { + m_currentAudioGamePosition = 0; + m_currentVisualPosition = 0; + m_currentTrackPosition = 0; + m_rate = 1; + m_offset = 0; + m_scrollSpeed = 180; + m_PlayTime = 0.0; + m_timingPositionMarkers = std::vector(); + m_baseBPM = 0; + m_currentChart = nullptr; +} + +RhythmEngine::~RhythmEngine() { Release(); } + +bool RhythmEngine::Load(Chart *chart) { + GameNoteResource::Load(); + + m_state = GameState::PreParing; + m_currentChart = chart; + + m_autoHitIndex.clear(); + m_autoHitInfos.clear(); + + // default is 99 + m_noteMaxImageIndex = 99; + + int currentX = m_laneOffset; + for (int i = 0; i < 7; i++) { + m_tracks.push_back(new GameTrack(this, i, currentX)); + m_autoHitIndex[i] = 0; + + if (m_eventCallback) { + m_tracks[i]->ListenEvent([&](GameTrackEvent e) { m_eventCallback(e); }); + } + + auto noteImage = GameNoteResource::GetNoteTexture(Key2Type[i]); + + int size = noteImage->TextureRect.right; + m_noteMaxImageIndex = (std::min)(noteImage->MaxFrames, m_noteMaxImageIndex); + + m_lanePos[i] = static_cast(currentX); + m_laneSize[i] = static_cast(size); + currentX += size; + } + + currentX = + static_cast(std::accumulate(m_laneSize, m_laneSize + 7, 0.0f)); + m_playRectangle = {m_laneOffset, 0, m_laneOffset + currentX, m_hitPosition}; + + std::filesystem::path audioPath = chart->m_beatmapDirectory; + audioPath /= chart->m_audio; + + bool isSV = true; + if (EnvironmentSetup::GetInt("Mirror")) { + chart->ApplyMod(Mod::MIRROR); + } else if (EnvironmentSetup::GetInt("Random")) { + chart->ApplyMod(Mod::RANDOM); + } else if (EnvironmentSetup::GetInt("Panic")) { + chart->ApplyMod(Mod::PANIC); + } else if (EnvironmentSetup::GetInt("Rearrange")) { + void *lane_data = EnvironmentSetup::GetObj("LaneData"); + chart->ApplyMod(Mod::REARRANGE, lane_data); + } else if (EnvironmentSetup::GetInt("NoSV")) { + isSV = true; + } + + if (isSV) { + m_timings = new VelocityTiming(chart->m_bpms, chart->m_svs, + chart->InitialSvMultiplier); + } else { + m_timings = new StaticTiming(chart->m_bpms, chart->m_svs, + chart->InitialSvMultiplier); + } + + m_judge = new BeatBasedJudge(this); + + if (std::filesystem::exists(audioPath) && audioPath.has_extension()) { + m_audioPath = audioPath; + } + + for (auto &sample : chart->m_autoSamples) { + m_autoSamples.push_back(sample); + } + + if (EnvironmentSetup::GetInt("Autoplay") == 1) { + Logs::Puts("[Gameplay] Autoplay enabled"); + + auto replay = Autoplay::CreateReplay(chart); + std::sort( + replay.begin(), replay.end(), + [](const Autoplay::ReplayHitInfo &a, const Autoplay::ReplayHitInfo &b) { + if (a.Time == b.Time) { + return a.Type < b.Type; + } + + return a.Time < b.Time; + }); - m_lanePos[i] = static_cast(currentX); - m_laneSize[i] = static_cast(size); - currentX += size; + for (auto &hit : replay) { + m_autoHitInfos[hit.Lane].push_back(hit); } - currentX = static_cast(std::accumulate(m_laneSize, m_laneSize + 7, 0.0f)); - m_playRectangle = { m_laneOffset, 0, m_laneOffset + currentX, m_hitPosition }; - - std::filesystem::path audioPath = chart->m_beatmapDirectory; - audioPath /= chart->m_audio; + m_autoFrames = replay; + m_is_autoplay = true; + } - bool isSV = true; - if (EnvironmentSetup::GetInt("Mirror")) { - chart->ApplyMod(Mod::MIRROR); - } else if (EnvironmentSetup::GetInt("Random")) { - chart->ApplyMod(Mod::RANDOM); - } else if (EnvironmentSetup::GetInt("Rearrange")) { - void *lane_data = EnvironmentSetup::GetObj("LaneData"); - - chart->ApplyMod(Mod::REARRANGE, lane_data); - } else if (EnvironmentSetup::GetInt("NoSV")) { - isSV = true; + auto audioVolume = Configuration::Load("Game", "AudioVolume"); + if (audioVolume.size() > 0) { + try { + m_audioVolume = std::stoi(audioVolume); + } catch (const std::invalid_argument &) { + Logs::Puts("[Gameplay] Invalid volume: %s reverting to 100 value", + audioVolume.c_str()); + m_audioVolume = 100; } + } - if (isSV) { - m_timings = new VelocityTiming(chart->m_bpms, chart->m_svs, chart->InitialSvMultiplier); - } else { - m_timings = new StaticTiming(chart->m_bpms, chart->m_svs, chart->InitialSvMultiplier); + auto audioOffset = Configuration::Load("Game", "AudioOffset"); + if (audioOffset.size() > 0) { + try { + m_audioOffset = std::stoi(audioOffset); } - m_judge = new BeatBasedJudge(this); - - if (std::filesystem::exists(audioPath) && audioPath.has_extension()) { - m_audioPath = audioPath; + catch (const std::invalid_argument &) { + Logs::Puts("[Gameplay] Invalid offset: %s reverting to 0 value", + audioOffset.c_str()); + m_audioOffset = 0; } + } - for (auto &sample : chart->m_autoSamples) { - m_autoSamples.push_back(sample); + auto autoSound = Configuration::Load("Game", "AutoSound"); + bool IsAutoSound = false; + if (autoSound.size() > 0) { + try { + IsAutoSound = std::stoi(autoSound) == 1; } - if (EnvironmentSetup::GetInt("Autoplay") == 1) { - Logs::Puts("[Gameplay] Autoplay enabled"); - - auto replay = Autoplay::CreateReplay(chart); - std::sort(replay.begin(), replay.end(), [](const Autoplay::ReplayHitInfo &a, const Autoplay::ReplayHitInfo &b) { - if (a.Time == b.Time) { - return a.Type < b.Type; - } - - return a.Time < b.Time; - }); - - for (auto &hit : replay) { - m_autoHitInfos[hit.Lane].push_back(hit); - } - - m_autoFrames = replay; - m_is_autoplay = true; + catch (const std::invalid_argument &) { + Logs::Puts("[Gameplay] Invalid auto sound: %s reverting to 0 value", + autoSound.c_str()); + IsAutoSound = false; } + } - auto audioVolume = Configuration::Load("Game", "AudioVolume"); - if (audioVolume.size() > 0) { - try { - m_audioVolume = std::stoi(audioVolume); - } catch (const std::invalid_argument &) { - Logs::Puts("[Gameplay] Invalid volume: %s reverting to 100 value", audioVolume.c_str()); - m_audioVolume = 100; - } + auto noteSpeed = Configuration::Load("Gameplay", "Notespeed"); + if (noteSpeed.size() > 0) { + try { + m_scrollSpeed = std::stoi(noteSpeed); + } catch (const std::invalid_argument &) { + Logs::Puts("[Gameplay] Invalid notespeed: %s reverting to 210 value", + noteSpeed.c_str()); + m_scrollSpeed = 210; } + } - auto audioOffset = Configuration::Load("Game", "AudioOffset"); - if (audioOffset.size() > 0) { - try { - m_audioOffset = std::stoi(audioOffset); - } + if (EnvironmentSetup::Get("SongRate").size() > 0) { + m_rate = std::stod(EnvironmentSetup::Get("SongRate").c_str()); + m_rate = std::clamp(m_rate, 0.5, 2.0); + } - catch (const std::invalid_argument &) { - Logs::Puts("[Gameplay] Invalid offset: %s reverting to 0 value", audioOffset.c_str()); - m_audioOffset = 0; - } - } - - auto autoSound = Configuration::Load("Game", "AutoSound"); - bool IsAutoSound = false; - if (autoSound.size() > 0) { - try { - IsAutoSound = std::stoi(autoSound) == 1; - } - - catch (const std::invalid_argument &) { - Logs::Puts("[Gameplay] Invalid auto sound: %s reverting to 0 value", autoSound.c_str()); - IsAutoSound = false; - } - } - - auto noteSpeed = Configuration::Load("Gameplay", "Notespeed"); - if (noteSpeed.size() > 0) { - try { - m_scrollSpeed = std::stoi(noteSpeed); - } catch (const std::invalid_argument &) { - Logs::Puts("[Gameplay] Invalid notespeed: %s reverting to 210 value", noteSpeed.c_str()); - m_scrollSpeed = 210; - } - } - - if (EnvironmentSetup::Get("SongRate").size() > 0) { - m_rate = std::stod(EnvironmentSetup::Get("SongRate").c_str()); - m_rate = std::clamp(m_rate, 0.5, 2.0); - } + char buffer[MAX_BUFFER_TXT_SIZE]; + if (EnvironmentSetup::GetInt("SongType") == 1) { + m_title = chart->m_title; + std::string titleStr = + std::string(chart->m_title.begin(), chart->m_title.end()); + std::snprintf(buffer, MAX_BUFFER_TXT_SIZE, "Lv.%d %s", chart->m_level, + titleStr.c_str()); + } else if (EnvironmentSetup::GetInt("SongType") == 2) { m_title = chart->m_title; - char buffer[MAX_BUFFER_TXT_SIZE]; - sprintf(buffer, "Lv.%d %s", chart->m_level, (const char *)chart->m_title.c_str()); + std::string difnameStr = + std::string(chart->m_difname.begin(), chart->m_difname.end()); + std::string titleStr = + std::string(chart->m_title.begin(), chart->m_title.end()); + std::snprintf(buffer, MAX_BUFFER_TXT_SIZE, "[%s] %s", difnameStr.c_str(), + titleStr.c_str()); + } else { + m_title = chart->m_title; + std::string titleStr = + std::string(chart->m_title.begin(), chart->m_title.end()); + std::snprintf(buffer, MAX_BUFFER_TXT_SIZE, "%d %s", chart->m_level, + titleStr.c_str()); + } + + // Reset the u8string with the result of snprintf + m_title = std::u8string(buffer, buffer + std::strlen(buffer)); + if (m_rate != 1.0) { + memset(buffer, 0, MAX_BUFFER_TXT_SIZE); + sprintf(buffer, "[%.2fx] %s", m_rate, (const char *)m_title.c_str()); m_title = std::u8string(buffer, buffer + strlen(buffer)); - if (m_rate != 1.0) { - memset(buffer, 0, MAX_BUFFER_TXT_SIZE); - sprintf(buffer, "[%.2fx] %s", m_rate, (const char *)m_title.c_str()); - - m_title = std::u8string(buffer, buffer + strlen(buffer)); - } - - m_beatmapOffset = chart->m_bpms[0].StartTime; - m_audioLength = chart->GetLength(); - m_baseBPM = chart->BaseBPM; - m_currentBPM = m_baseBPM; - m_currentSVMultiplier = chart->InitialSvMultiplier; - - bool isPitch = Configuration::Load("Game", "AudioPitch") == "1"; - GameAudioSampleCache::SetRate(m_rate); - GameAudioSampleCache::Load(chart, isPitch); - - CreateTimingMarkers(); - UpdateVirtualResolution(); - - for (auto ¬e : chart->m_notes) { - NoteInfoDesc desc = {}; - desc.ImageType = Key2Type[note.LaneIndex]; - desc.ImageBodyType = Key2HoldType[note.LaneIndex]; - desc.StartTime = note.StartTime; - desc.Lane = note.LaneIndex; - desc.Type = note.Type; - desc.EndTime = -1; - desc.InitialTrackPosition = m_timings->GetOffsetAt(note.StartTime); - desc.EndTrackPosition = -1; - desc.KeysoundIndex = note.Keysound; - desc.StartBPM = m_timings->GetBPMAt(note.StartTime); - desc.Volume = (int)round(note.Volume * (float)m_audioVolume); - desc.Pan = (int)round(note.Pan * (float)m_audioVolume); - - if (note.Type == NoteType::HOLD) { - desc.EndTime = note.EndTime; - desc.EndTrackPosition = m_timings->GetOffsetAt(note.EndTime); - desc.EndBPM = m_timings->GetBPMAt(note.EndTime); - } - - if ((m_audioOffset != 0 && desc.KeysoundIndex != -1) || IsAutoSound) { - AutoSample newSample = {}; - newSample.StartTime = desc.StartTime; - newSample.Pan = note.Pan; - newSample.Volume = note.Volume; - newSample.Index = desc.KeysoundIndex; - - m_autoSamples.push_back(newSample); - desc.KeysoundIndex = -1; - } - - m_noteDescs.push_back(desc); + } + + m_beatmapOffset = chart->m_bpms[0].StartTime; + m_audioLength = chart->GetLength(); + m_baseBPM = chart->BaseBPM; + m_currentBPM = m_baseBPM; + m_currentSVMultiplier = chart->InitialSvMultiplier; + + bool isPitch = Configuration::Load("Game", "AudioPitch") == "1"; + GameAudioSampleCache::SetRate(m_rate); + GameAudioSampleCache::Load(chart, isPitch); + + CreateTimingMarkers(); + UpdateVirtualResolution(); + + for (auto ¬e : chart->m_notes) { + NoteInfoDesc desc = {}; + desc.ImageType = Key2Type[note.LaneIndex]; + desc.ImageBodyType = Key2HoldType[note.LaneIndex]; + desc.StartTime = note.StartTime; + desc.Lane = note.LaneIndex; + desc.Type = note.Type; + desc.EndTime = -1; + desc.InitialTrackPosition = m_timings->GetOffsetAt(note.StartTime); + desc.EndTrackPosition = -1; + desc.KeysoundIndex = note.Keysound; + desc.StartBPM = m_timings->GetBPMAt(note.StartTime); + desc.Volume = (int)round(note.Volume * (float)m_audioVolume); + desc.Pan = (int)round(note.Pan * (float)m_audioVolume); + + if (note.Type == NoteType::HOLD) { + desc.EndTime = note.EndTime; + desc.EndTrackPosition = m_timings->GetOffsetAt(note.EndTime); + desc.EndBPM = m_timings->GetBPMAt(note.EndTime); + } + + if ((m_audioOffset != 0 && desc.KeysoundIndex != -1) || IsAutoSound) { + AutoSample newSample = {}; + newSample.StartTime = desc.StartTime; + newSample.Pan = note.Pan; + newSample.Volume = note.Volume; + newSample.Index = desc.KeysoundIndex; + + m_autoSamples.push_back(newSample); + desc.KeysoundIndex = -1; + } + + m_noteDescs.push_back(desc); + } + + std::sort(m_noteDescs.begin(), m_noteDescs.end(), [](auto &a, auto &b) { + if (a.StartTime != b.StartTime) { + return a.StartTime < b.StartTime; + } else { + return a.EndTime < b.EndTime; } + }); - std::sort(m_noteDescs.begin(), m_noteDescs.end(), [](auto &a, auto &b) { - if (a.StartTime != b.StartTime) { - return a.StartTime < b.StartTime; - } else { - return a.EndTime < b.EndTime; - } - }); - - std::sort(m_autoSamples.begin(), m_autoSamples.end(), [](const AutoSample &a, const AutoSample &b) { - return a.StartTime < b.StartTime; - }); + std::sort(m_autoSamples.begin(), m_autoSamples.end(), + [](const AutoSample &a, const AutoSample &b) { + return a.StartTime < b.StartTime; + }); - UpdateGamePosition(); - UpdateNotes(); + UpdateGamePosition(); + UpdateNotes(); - m_timingLineManager = chart->m_customMeasures.size() > 0 ? new TimingLineManager(this, chart->m_customMeasures) : new TimingLineManager(this); - m_scoreManager = new ScoreManager(); + m_timingLineManager = + chart->m_customMeasures.size() > 0 + ? new TimingLineManager(this, chart->m_customMeasures) + : new TimingLineManager(this); + m_scoreManager = new ScoreManager(); - m_startClock = std::chrono::system_clock::now(); + // m_startClock = std::chrono::system_clock::now(); - m_timingLineManager->Init(); - m_state = GameState::NotGame; - return true; + m_timingLineManager->Init(); + m_state = GameState::NotGame; + return true; } -void RhythmEngine::SetKeys(Keys *keys) -{ - for (int i = 0; i < 7; i++) { - KeyMapping[i].key = keys[i]; - } +void RhythmEngine::SetKeys(Keys *keys) { + for (int i = 0; i < 7; i++) { + KeyMapping[i].key = keys[i]; + } } -bool RhythmEngine::Start() -{ // no, use update event instead - m_currentAudioPosition -= 3000; - m_state = GameState::Playing; - - m_startClock = std::chrono::system_clock::now(); - return true; +bool RhythmEngine::Start() { // no, use update event instead + EnvironmentSetup::SetInt("NowPlaying", 1); + m_currentAudioPosition -= 3000; + m_state = GameState::Playing; + // m_startClock = std::chrono::system_clock::now(); + return true; } -bool RhythmEngine::Stop() -{ - m_state = GameState::PosGame; - return true; +bool RhythmEngine::Stop() { + m_state = GameState::PosGame; + GameAudioSampleCache::StopAll(); + return true; } -bool RhythmEngine::Ready() -{ - return m_state == GameState::NotGame; +bool RhythmEngine::Fail() { + m_state = GameState::Fail; + GameAudioSampleCache::StopAll(); + return true; } -void RhythmEngine::Update(double delta) -{ - if (m_state == GameState::NotGame || m_state == GameState::PosGame) - return; +bool RhythmEngine::Ready() { return m_state == GameState::NotGame; } + +void RhythmEngine::Update(double delta) { + if (m_state == GameState::NotGame || m_state == GameState::PosGame) + return; - // Since I'm coming from Roblox, and I had no idea how to Real-Time sync the audio - // I decided to use this method again from Roblox project I did in past. - double last = m_currentAudioPosition; - m_currentAudioPosition += (delta * m_rate) * 1000; + // Since I'm coming from Roblox, and I had no idea how to Real-Time sync the + // audio I decided to use this method again from Roblox project I did in past. + double last = m_currentAudioPosition; + m_PlayTime += delta; + m_currentAudioPosition += (delta * m_rate) * 1000; - // check difference between last and current audio position - // if it's too big, then it means the game is lagging + // check difference between last and current audio position + // if it's too big, then it means the game is lagging - if (m_currentAudioPosition - last > 1000 * 5) { - // assert(false); // TODO: Handle this + if (m_currentAudioPosition - last > 1000 * 5) { + // assert(false); // TODO: Handle this + } + + // Check if game should end: all notes and autosamples dispatched, + // past the last event time, and no audio is still playing + bool allNotesDispatched = m_currentNoteIndex >= m_noteDescs.size(); + bool allSamplesDispatched = m_currentSampleIndex >= m_autoSamples.size(); + + if (allNotesDispatched && allSamplesDispatched) { + // Get the time of the last event (note or autosample) + double lastEventTime = 0; + if (!m_noteDescs.empty()) { + auto &lastNote = m_noteDescs.back(); + lastEventTime = + (std::max)(lastEventTime, lastNote.EndTime > 0 ? lastNote.EndTime + : lastNote.StartTime); + } + if (!m_autoSamples.empty()) { + lastEventTime = (std::max)(lastEventTime, m_autoSamples.back().StartTime); } - if (m_currentAudioPosition > m_audioLength + 2500) { // Avoid game ended too early + // Wait at least 1 second past the last event before checking audio state + if (m_currentAudioPosition > lastEventTime + 1000) { + if (!GameAudioSampleCache::IsAnyPlaying()) { + GameAudioSampleCache::StopAll(); m_state = GameState::PosGame; - ::printf("Audio stopped!\n"); + } } + } - if (static_cast(m_currentAudioPosition) % 1000 == 0) { - m_noteImageIndex = (m_noteImageIndex + 1) % m_noteMaxImageIndex; - } + static std::chrono::steady_clock::time_point lastUpdateTime = + std::chrono::steady_clock::now(); + auto currentTime = std::chrono::steady_clock::now(); + auto lastUpdate = std::chrono::duration_cast( + currentTime - lastUpdateTime); - UpdateVirtualResolution(); - UpdateGamePosition(); - UpdateNotes(); + if (lastUpdate.count() >= 100) { + m_noteImageIndex = (m_noteImageIndex + 1) % m_noteMaxImageIndex; + lastUpdateTime = currentTime; + } - m_timingLineManager->Update(delta); + UpdateVirtualResolution(); + UpdateGamePosition(); - for (auto &it : m_tracks) { - it->Update(delta); - } + UpdateNotes(); - // Sample event updates - for (int i = m_currentSampleIndex; i < m_autoSamples.size(); i++) { - auto &sample = m_autoSamples[i]; - if (m_currentAudioPosition >= sample.StartTime) { - if (sample.StartTime - m_currentAudioPosition < 5) { - GameAudioSampleCache::Play(sample.Index, (int)round(sample.Volume * m_audioVolume), (int)round(sample.Pan * 100)); - } - - m_currentSampleIndex++; - } else { - break; - } + m_timingLineManager->Update(delta); + + for (auto &it : m_tracks) { + it->Update(delta); + } + + // Sample event updates + for (int i = m_currentSampleIndex; i < m_autoSamples.size(); i++) { + auto &sample = m_autoSamples[i]; + if (m_currentAudioPosition >= sample.StartTime) { + if (sample.StartTime - m_currentAudioPosition < 5) { + GameAudioSampleCache::Play(sample.Index, + (int)round(sample.Volume * m_audioVolume), + (int)round(sample.Pan * 100)); + } + + m_currentSampleIndex++; + } else { + break; } + } - if (m_is_autoplay) { - auto frame = GetAutoplayAtThisFrame(m_currentAudioPosition); + if (m_is_autoplay) { + auto frame = GetAutoplayAtThisFrame(m_currentAudioPosition); - for (auto &frame : frame.KeyDowns) { - m_tracks[frame.Lane]->OnKeyDown(); - } + for (auto &frame : frame.KeyDowns) { + m_tracks[frame.Lane]->OnKeyDown(); + } - for (auto &frame : frame.KeyUps) { - m_tracks[frame.Lane]->OnKeyUp(); - } + for (auto &frame : frame.KeyUps) { + m_tracks[frame.Lane]->OnKeyUp(); } + } - auto currentTime = std::chrono::system_clock::now(); - auto elapsedTime = std::chrono::duration_cast(currentTime - m_startClock); - m_PlayTime = static_cast(elapsedTime.count()); + // auto currentTime = std::chrono::system_clock::now(); + // auto elapsedTime = + // std::chrono::duration_cast(currentTime - + // m_startClock); m_PlayTime = static_cast(elapsedTime.count() - 4); } -void RhythmEngine::Render(double delta) -{ - if (m_state == GameState::NotGame || m_state == GameState::PosGame) - return; +void RhythmEngine::Render(double delta) { + if (m_state == GameState::NotGame || m_state == GameState::PosGame) + return; + bool MeasureLine = EnvironmentSetup::GetInt("MeasureLine") == 1; + + if (MeasureLine) { m_timingLineManager->Render(delta); + } - for (auto &it : m_tracks) { - it->Render(delta); - } + for (auto &it : m_tracks) { + it->Render(delta); + } } -void RhythmEngine::Input(double delta) -{ - if (m_state == GameState::NotGame || m_state == GameState::PosGame) - return; +void RhythmEngine::Input(double delta) { + if (m_state == GameState::NotGame) + return; } -void RhythmEngine::OnKeyDown(const KeyState &state) -{ - if (m_state == GameState::NotGame || m_state == GameState::PosGame) - return; +void RhythmEngine::OnKeyDown(const KeyState &state) { + if (m_state == GameState::NotGame) + return; - if (state.key == Keys::F3) { - m_scrollSpeed -= 10; - } else if (state.key == Keys::F4) { - m_scrollSpeed += 10; - } + if (state.key == Keys::F3) { + m_scrollSpeed -= 10; + } else if (state.key == Keys::F4) { + m_scrollSpeed += 10; + } - if (!m_is_autoplay) { - for (auto &key : KeyMapping) { - if (key.second.key == state.key) { - key.second.isPressed = true; + if (!m_is_autoplay) { + for (auto &key : KeyMapping) { + if (key.second.key == state.key) { + key.second.isPressed = true; - if (key.first < m_tracks.size()) { - m_tracks[key.first]->OnKeyDown(); - } - } + if (key.first < m_tracks.size()) { + m_tracks[key.first]->OnKeyDown(); } + } } + } } -void RhythmEngine::OnKeyUp(const KeyState &state) -{ - if (m_state == GameState::NotGame || m_state == GameState::PosGame) - return; +void RhythmEngine::OnKeyUp(const KeyState &state) { + if (m_state == GameState::NotGame) + return; - if (!m_is_autoplay) { - for (auto &key : KeyMapping) { - if (key.second.key == state.key) { - key.second.isPressed = false; + if (!m_is_autoplay) { + for (auto &key : KeyMapping) { + if (key.second.key == state.key) { + key.second.isPressed = false; - if (key.first < m_tracks.size()) { - m_tracks[key.first]->OnKeyUp(); - } - } + if (key.first < m_tracks.size()) { + m_tracks[key.first]->OnKeyUp(); } + } } + } } -void RhythmEngine::ListenKeyEvent(std::function callback) -{ - m_eventCallback = callback; +void RhythmEngine::ListenKeyEvent( + std::function callback) { + m_eventCallback = callback; } -double RhythmEngine::GetAudioPosition() const -{ - return m_currentAudioPosition; -} +double RhythmEngine::GetAudioPosition() const { return m_currentAudioPosition; } -double RhythmEngine::GetVisualPosition() const -{ - return m_currentVisualPosition; +double RhythmEngine::GetVisualPosition() const { + return m_currentVisualPosition; } -double RhythmEngine::GetGameAudioPosition() const -{ - return m_currentAudioGamePosition; +double RhythmEngine::GetGameAudioPosition() const { + return m_currentAudioGamePosition; } -double RhythmEngine::GetTrackPosition() const -{ - return m_currentTrackPosition; -} +double RhythmEngine::GetTrackPosition() const { return m_currentTrackPosition; } -double RhythmEngine::GetPrebufferTiming() const -{ - return -300000.0 / GetNotespeed(); +double RhythmEngine::GetPrebufferTiming() const { + return -300000.0 / GetNotespeed(); } -double RhythmEngine::GetNotespeed() const -{ - double speed = static_cast(m_scrollSpeed); - double scrollingFactor = 1920.0 / 1366.0; - float virtualRatio = (float)(m_virtualResolution.Y / m_gameResolution.Y); - float value = (float)((speed / 10.0) / (20.0 * m_rate) * scrollingFactor * virtualRatio); +double RhythmEngine::GetNotespeed() const { + double speed = static_cast(m_scrollSpeed); + double scrollingFactor = 1920.0 / 1366.0; + float virtualRatio = (float)(m_virtualResolution.Y / m_gameResolution.Y); + float value = (float)((speed / 10.0) / (20.0 * m_rate) * scrollingFactor * + virtualRatio); - return value; + return value; } -double RhythmEngine::GetBPMAt(double offset) const -{ - auto &bpms = m_currentChart->m_bpms; - int min = 0, max = (int)(bpms.size() - 1); +double RhythmEngine::GetBPMAt(double offset) const { + auto &bpms = m_currentChart->m_bpms; + int min = 0, max = (int)(bpms.size() - 1); - if (max == 0) { - return bpms[0].Value; - } + if (max == 0) { + return bpms[0].Value; + } - while (min <= max) { - int mid = (min + max) / 2; + while (min <= max) { + int mid = (min + max) / 2; - bool afterMid = mid < 0 || bpms[mid].StartTime <= offset; - bool beforeMid = mid + 1 >= bpms.size() || bpms[mid + 1].StartTime > offset; + bool afterMid = mid < 0 || bpms[mid].StartTime <= offset; + bool beforeMid = mid + 1 >= bpms.size() || bpms[mid + 1].StartTime > offset; - if (afterMid && beforeMid) { - return bpms[mid].Value; - } else if (afterMid) { - max = mid - 1; - } else { - min = mid + 1; - } + if (afterMid && beforeMid) { + return bpms[mid].Value; + } else if (afterMid) { + max = mid - 1; + } else { + min = mid + 1; } + } - return bpms[0].Value; + return bpms[0].Value; } -double RhythmEngine::GetCurrentBPM() const -{ - return m_currentBPM; -} +double RhythmEngine::GetCurrentBPM() const { return m_currentBPM; } -double RhythmEngine::GetSongRate() const -{ - return m_rate; +float RhythmEngine::GetCurrentSVMultiplier() const { + return m_currentSVMultiplier; } -double RhythmEngine::GetAudioLength() const -{ - return m_audioLength; -} +int RhythmEngine::GetBPMAnimationIndex(int maxFrames) const { + if (maxFrames <= 0 || m_baseBPM <= 0.0) + return 0; -int RhythmEngine::GetGameVolume() const -{ - return m_audioVolume; -} + double cycleLength = 6000000.0 / m_baseBPM; -GameState RhythmEngine::GetState() const -{ - return m_state; -} + bool hasBPMChanges = m_currentChart && m_currentChart->m_bpms.size() > 1; + bool hasSVs = m_currentChart && m_currentChart->m_svs.size() > 0; + bool isNormalizedSV = hasBPMChanges && hasSVs; -ScoreManager *RhythmEngine::GetScoreManager() const -{ - return m_scoreManager; -} + double currentPos = 0; + if (m_timings) { + if (isNormalizedSV) { + currentPos = GetTrackPosition(); -std::vector RhythmEngine::GetBPMs() const -{ - return m_currentChart->m_bpms; -} + double endPos = m_timings->GetOffsetAt(m_audioLength); + double endCyclePos = ceil(endPos / cycleLength) * cycleLength; -std::vector RhythmEngine::GetSVs() const -{ - return m_currentChart->m_svs; -} + if (currentPos >= endCyclePos) { + return maxFrames - 1; + } + } else { + currentPos = m_timings->GetBeatAt(m_currentVisualPosition) * cycleLength; + + double endPos = m_timings->GetBeatAt(m_audioLength) * cycleLength; + double endCyclePos = ceil(endPos / cycleLength) * cycleLength; + + if (currentPos >= endCyclePos) { + return maxFrames - 1; + } + } + } else { + currentPos = GetTrackPosition(); + } -double RhythmEngine::GetElapsedTime() const -{ // Get game frame - return static_cast(SDL_GetTicks()) / 1000.0; + double progress = fmod(currentPos, cycleLength) / cycleLength; + if (progress < 0) + progress += 1.0; + + return static_cast(progress * maxFrames) % maxFrames; } -int RhythmEngine::GetPlayTime() const -{ // Get game time - return m_PlayTime; +double RhythmEngine::GetSongRate() const { return m_rate; } + +double RhythmEngine::GetAudioLength() const { return m_audioLength; } + +int RhythmEngine::GetGameVolume() const { return m_audioVolume; } + +GameState RhythmEngine::GetState() const { return m_state; } + +ScoreManager *RhythmEngine::GetScoreManager() const { return m_scoreManager; } + +std::vector RhythmEngine::GetBPMs() const { + return m_currentChart->m_bpms; } -int RhythmEngine::GetNoteImageIndex() -{ - return m_noteImageIndex; +std::vector RhythmEngine::GetSVs() const { + return m_currentChart->m_svs; } -int RhythmEngine::GetGuideLineIndex() const -{ - return m_guideLineIndex; +double RhythmEngine::GetGameFrame() const { + return static_cast(SDL_GetTicks()) / 1000.0; } -void RhythmEngine::SetGuideLineIndex(int idx) -{ - m_guideLineIndex = idx; +int RhythmEngine::GetPlayTime() const { + return static_cast(m_PlayTime - 5); } -void RhythmEngine::UpdateNotes() -{ - for (int i = m_currentNoteIndex; i < m_noteDescs.size(); i++) { - auto &desc = m_noteDescs[i]; +int RhythmEngine::GetNoteImageIndex() { return m_noteImageIndex; } - if (m_currentAudioGamePosition + (3000.0 / GetNotespeed()) > desc.StartTime || (m_currentTrackPosition - desc.InitialTrackPosition > GetPrebufferTiming())) { +int RhythmEngine::GetGuideLineIndex() const { return m_guideLineIndex; } - m_tracks[desc.Lane]->AddNote(&desc); +void RhythmEngine::SetGuideLineIndex(int idx) { m_guideLineIndex = idx; } - m_currentNoteIndex += 1; - } else { - break; - } +void RhythmEngine::UpdateNotes() { + for (int i = m_currentNoteIndex; i < m_noteDescs.size(); i++) { + auto &desc = m_noteDescs[i]; + + if (m_currentAudioGamePosition + (3000.0 / GetNotespeed()) > + desc.StartTime || + (m_currentTrackPosition - desc.InitialTrackPosition > + GetPrebufferTiming())) { + + m_tracks[desc.Lane]->AddNote(&desc); + + m_currentNoteIndex += 1; + } else { + break; } + } } -void RhythmEngine::UpdateGamePosition() -{ - m_currentAudioGamePosition = m_currentAudioPosition + m_offset; - m_currentVisualPosition = m_currentAudioGamePosition; // * m_rate; +void RhythmEngine::UpdateGamePosition() { + m_currentAudioGamePosition = m_currentAudioPosition + m_offset; + m_currentVisualPosition = m_currentAudioGamePosition; // * m_rate; - while (m_currentBPMIndex + 1 < m_currentChart->m_bpms.size() && m_currentVisualPosition >= m_currentChart->m_bpms[m_currentBPMIndex + 1].StartTime) { - m_currentBPMIndex += 1; - } + while (m_currentBPMIndex + 1 < m_currentChart->m_bpms.size() && + m_currentVisualPosition >= + m_currentChart->m_bpms[m_currentBPMIndex + 1].StartTime) { + m_currentBPMIndex += 1; + } - while (m_currentSVIndex < m_currentChart->m_svs.size() && m_currentVisualPosition >= m_currentChart->m_svs[m_currentSVIndex].StartTime) { - m_currentSVIndex += 1; - } + while (m_currentSVIndex < m_currentChart->m_svs.size() && + m_currentVisualPosition >= + m_currentChart->m_svs[m_currentSVIndex].StartTime) { + m_currentSVIndex += 1; + } - m_currentTrackPosition = m_timings->GetOffsetAt(m_currentVisualPosition, m_currentSVIndex); // GetPositionFromOffset(m_currentVisualPosition, m_currentSVIndex); + m_currentTrackPosition = m_timings->GetOffsetAt( + m_currentVisualPosition, + m_currentSVIndex); // GetPositionFromOffset(m_currentVisualPosition, + // m_currentSVIndex); - if (m_currentSVIndex > 0) { - float svMultiplier = m_currentChart->m_svs[m_currentSVIndex - 1].Value; - if (svMultiplier != m_currentSVMultiplier) { - m_currentSVMultiplier = svMultiplier; - } + if (m_currentSVIndex > 0) { + float svMultiplier = m_currentChart->m_svs[m_currentSVIndex - 1].Value; + if (svMultiplier != m_currentSVMultiplier) { + m_currentSVMultiplier = svMultiplier; } + } - if (m_currentBPMIndex > 0) { - m_currentBPM = m_currentChart->m_bpms[m_currentBPMIndex - 1].Value; - } + if (m_currentBPMIndex > 0) { + m_currentBPM = m_currentChart->m_bpms[m_currentBPMIndex - 1].Value; + } } -void RhythmEngine::UpdateVirtualResolution() -{ - double width = GameWindow::GetInstance()->GetBufferHeight(); - double height = GameWindow::GetInstance()->GetBufferHeight(); +void RhythmEngine::UpdateVirtualResolution() { + double width = GameWindow::GetInstance()->GetBufferHeight(); + double height = GameWindow::GetInstance()->GetBufferHeight(); - m_gameResolution = { width, height }; + m_gameResolution = {width, height}; - float ratio = (float)width / (float)height; - if (ratio >= 16.0f / 9.0f) { - m_virtualResolution = { width * ratio, height }; - } else { - m_virtualResolution = { width, height / ratio }; - } + float ratio = (float)width / (float)height; + if (ratio >= 16.0f / 9.0f) { + m_virtualResolution = {width * ratio, height}; + } else { + m_virtualResolution = {width, height / ratio}; + } } -void RhythmEngine::CreateTimingMarkers() -{ - if (m_currentChart->m_svs.size() > 0) { - auto &svs = m_currentChart->m_svs; - double pos = ::round(svs[0].StartTime * m_currentChart->InitialSvMultiplier * 100); - m_timingPositionMarkers.push_back(pos); +void RhythmEngine::CreateTimingMarkers() { + if (m_currentChart->m_svs.size() > 0) { + auto &svs = m_currentChart->m_svs; + double pos = + ::round(svs[0].StartTime * m_currentChart->InitialSvMultiplier * 100); + m_timingPositionMarkers.push_back(pos); - for (int i = 1; i < svs.size(); i++) { - pos += ::round((svs[i].StartTime - svs[i - 1].StartTime) * (svs[i - 1].Value * 100)); + for (int i = 1; i < svs.size(); i++) { + pos += ::round((svs[i].StartTime - svs[i - 1].StartTime) * + (svs[i - 1].Value * 100)); - m_timingPositionMarkers.push_back(pos); - } + m_timingPositionMarkers.push_back(pos); } + } } -ReplayFrameData RhythmEngine::GetAutoplayAtThisFrame(double offset) -{ - ReplayFrameData data; +ReplayFrameData RhythmEngine::GetAutoplayAtThisFrame(double offset) { + ReplayFrameData data; - for (int i = m_autoMinIndex; i < m_autoFrames.size(); i++) { - auto &hit = m_autoFrames[i]; + for (int i = m_autoMinIndex; i < m_autoFrames.size(); i++) { + auto &hit = m_autoFrames[i]; - if (offset >= hit.Time) { - if (hit.Type == Autoplay::ReplayHitType::KEY_UP) { - data.KeyUps.push_back(hit); - } else { - data.KeyDowns.push_back(hit); - } + if (offset >= hit.Time) { + if (hit.Type == Autoplay::ReplayHitType::KEY_UP) { + data.KeyUps.push_back(hit); + } else { + data.KeyDowns.push_back(hit); + } - m_autoMinIndex++; - } else { - break; - } + m_autoMinIndex++; + } else { + break; } + } - return std::move(data); + return data; } -const float *RhythmEngine::GetLaneSizes() const -{ - return &m_laneSize[0]; -} +const float *RhythmEngine::GetLaneSizes() const { return &m_laneSize[0]; } -const float *RhythmEngine::GetLanePos() const -{ - return &m_lanePos[0]; -} +const float *RhythmEngine::GetLanePos() const { return &m_lanePos[0]; } -void RhythmEngine::SetHitPosition(int offset) -{ - m_hitPosition = offset; -} +void RhythmEngine::SetHitPosition(int offset) { m_hitPosition = offset; } -void RhythmEngine::SetLaneOffset(int offset) -{ - m_laneOffset = offset; -} +void RhythmEngine::SetLaneOffset(int offset) { m_laneOffset = offset; } -int RhythmEngine::GetHitPosition() const -{ - return m_hitPosition; -} +int RhythmEngine::GetHitPosition() const { return m_hitPosition; } -Vector2 RhythmEngine::GetResolution() const -{ - return m_gameResolution; -} +Vector2 RhythmEngine::GetResolution() const { return m_gameResolution; } -Rect RhythmEngine::GetPlayRectangle() const -{ - return m_playRectangle; -} +Rect RhythmEngine::GetPlayRectangle() const { return m_playRectangle; } -std::u8string RhythmEngine::GetTitle() const -{ - return m_title; -} +std::u8string RhythmEngine::GetTitle() const { return m_title; } -TimingBase *RhythmEngine::GetTiming() const -{ - return m_timings; -} +TimingBase *RhythmEngine::GetTiming() const { return m_timings; } -JudgeBase *RhythmEngine::GetJudge() const -{ - return m_judge; -} +JudgeBase *RhythmEngine::GetJudge() const { return m_judge; } -void RhythmEngine::Release() -{ - for (int i = 0; i < m_tracks.size(); i++) { - delete m_tracks[i]; - } +void RhythmEngine::Release() { + for (int i = 0; i < m_tracks.size(); i++) { + delete m_tracks[i]; + } - delete m_timingLineManager; - delete m_timings; - delete m_scoreManager; - delete m_judge; + delete m_timingLineManager; + delete m_timings; + delete m_scoreManager; + delete m_judge; - NoteImageCacheManager::Release(); - GameNoteResource::Dispose(); - GameAudioSampleCache::StopAll(); + NoteImageCacheManager::Release(); + GameNoteResource::Dispose(); + GameAudioSampleCache::StopAll(); } diff --git a/Game/src/Engine/RhythmEngine.hpp b/Game/src/Engine/RhythmEngine.hpp index 7e00cf97..854402af 100644 --- a/Game/src/Engine/RhythmEngine.hpp +++ b/Game/src/Engine/RhythmEngine.hpp @@ -11,141 +11,136 @@ #include "Timing/TimingBase.h" #include "TimingLineManager.hpp" -enum class GameState { - PreParing, - NotGame, - PreGame, - Playing, - PosGame -}; +enum class GameState { PreParing, NotGame, PreGame, Playing, Fail, PosGame }; -struct ReplayFrameData -{ - std::vector KeyDowns; - std::vector KeyUps; +struct ReplayFrameData { + std::vector KeyDowns; + std::vector KeyUps; }; -class RhythmEngine -{ +class RhythmEngine { public: - RhythmEngine(); - ~RhythmEngine(); - - bool Load(Chart *chart); - void SetKeys(Keys *keys); - - bool Start(); - bool Stop(); - bool Ready(); - - void Update(double delta); - void Render(double delta); - void Input(double delta); - - void OnKeyDown(const KeyState &key); - void OnKeyUp(const KeyState &key); - - void ListenKeyEvent(std::function callback); - - double GetAudioPosition() const; - double GetVisualPosition() const; - double GetGameAudioPosition() const; - double GetTrackPosition() const; - double GetPrebufferTiming() const; - double GetNotespeed() const; - double GetBPMAt(double offset) const; - double GetCurrentBPM() const; - double GetSongRate() const; - double GetAudioLength() const; - int GetGameVolume() const; - - const float *GetLaneSizes() const; - const float *GetLanePos() const; - - void SetHitPosition(int offset); - void SetLaneOffset(int offset); - int GetHitPosition() const; - Vector2 GetResolution() const; - Rect GetPlayRectangle() const; - std::u8string GetTitle() const; - - GameState GetState() const; - TimingBase *GetTiming() const; - JudgeBase *GetJudge() const; - ScoreManager *GetScoreManager() const; - std::vector GetBPMs() const; - std::vector GetSVs() const; - - double GetElapsedTime() const; - int GetPlayTime() const; - int GetNoteImageIndex(); - - int GetGuideLineIndex() const; - void SetGuideLineIndex(int idx); + RhythmEngine(); + ~RhythmEngine(); + + bool Load(Chart *chart); + void SetKeys(Keys *keys); + + bool Start(); + bool Stop(); + bool Fail(); + bool Ready(); + + void Update(double delta); + void Render(double delta); + void Input(double delta); + + void OnKeyDown(const KeyState &key); + void OnKeyUp(const KeyState &key); + + void ListenKeyEvent(std::function callback); + + double GetAudioPosition() const; + double GetVisualPosition() const; + double GetGameAudioPosition() const; + double GetTrackPosition() const; + double GetPrebufferTiming() const; + double GetNotespeed() const; + double GetBPMAt(double offset) const; + double GetCurrentBPM() const; + float GetCurrentSVMultiplier() const; + int GetBPMAnimationIndex(int maxFrames) const; + double GetSongRate() const; + double GetAudioLength() const; + int GetGameVolume() const; + + const float *GetLaneSizes() const; + const float *GetLanePos() const; + + void SetHitPosition(int offset); + void SetLaneOffset(int offset); + int GetHitPosition() const; + Vector2 GetResolution() const; + Rect GetPlayRectangle() const; + std::u8string GetTitle() const; + + GameState GetState() const; + TimingBase *GetTiming() const; + JudgeBase *GetJudge() const; + ScoreManager *GetScoreManager() const; + std::vector GetBPMs() const; + std::vector GetSVs() const; + + double GetGameFrame() const; + int GetPlayTime() const; + int GetNoteImageIndex(); + + int GetGuideLineIndex() const; + void SetGuideLineIndex(int idx); private: - void UpdateNotes(); - void UpdateGamePosition(); - void UpdateVirtualResolution(); - void CreateTimingMarkers(); - ReplayFrameData GetAutoplayAtThisFrame(double offset); - - void Release(); - - double m_rate = 0.0, m_offset = 0.0, m_beatmapOffset = 0.0; - double m_currentAudioPosition = 0.0; - double m_currentVisualPosition = 0.0; - double m_currentAudioGamePosition = 0.0; - double m_currentTrackPosition = 0.0; - float m_baseBPM, m_currentBPM = 0.0; - float m_currentSVMultiplier = 0.0; - - int m_currentSampleIndex = 0; - int m_currentNoteIndex = 0; - int m_currentBPMIndex = 0; - int m_currentSVIndex = 0; - int m_scrollSpeed = 0; - double m_audioLength = 0; - int m_hitPosition = 0; - int m_laneOffset = 0; - int m_audioVolume = 100; - int m_audioOffset = 0; - - int m_noteImageIndex = 0; - int m_noteMaxImageIndex = 0; - - int m_guideLineIndex = 0; - int m_autoMinIndex = 0; - - bool m_started = false; - bool m_is_autoplay = false; - - GameState m_state = GameState::NotGame; - std::u8string m_title; - - Rect m_playRectangle; - float m_laneSize[7]; - float m_lanePos[7]; - - std::filesystem::path m_audioPath = ""; - Chart *m_currentChart; - Vector2 m_virtualResolution = { 0, 0 }; - Vector2 m_gameResolution = { 0, 0 }; - std::vector m_timingPositionMarkers; - std::vector m_tracks; - std::vector m_noteDescs; - std::vector m_autoSamples; - std::unordered_map m_autoHitIndex; - std::unordered_map> m_autoHitInfos; - std::vector m_autoFrames; - - /* clock system */ - int m_PlayTime = 0; - std::chrono::system_clock::time_point m_startClock; - - TimingBase *m_timings = nullptr; - JudgeBase *m_judge = nullptr; - ScoreManager *m_scoreManager = nullptr; - TimingLineManager *m_timingLineManager = nullptr; - std::function m_eventCallback = nullptr; + void UpdateNotes(); + void UpdateGamePosition(); + void UpdateVirtualResolution(); + void CreateTimingMarkers(); + ReplayFrameData GetAutoplayAtThisFrame(double offset); + + void Release(); + + double m_rate = 0.0, m_offset = 0.0, m_beatmapOffset = 0.0; + double m_currentAudioPosition = 0.0; + double m_currentVisualPosition = 0.0; + double m_currentAudioGamePosition = 0.0; + double m_currentTrackPosition = 0.0; + float m_baseBPM, m_currentBPM = 0.0; + float m_currentSVMultiplier = 0.0; + + int m_currentSampleIndex = 0; + int m_currentNoteIndex = 0; + int m_currentBPMIndex = 0; + int m_currentSVIndex = 0; + int m_scrollSpeed = 0; + double m_audioLength = 0; + int m_hitPosition = 0; + int m_laneOffset = 0; + int m_audioVolume = 100; + int m_audioOffset = 0; + + int m_noteImageIndex = 0; + int m_noteMaxImageIndex = 0; + + int m_guideLineIndex = 0; + int m_autoMinIndex = 0; + + bool m_started = false; + bool m_is_autoplay = false; + + GameState m_state = GameState::NotGame; + std::u8string m_title; + + Rect m_playRectangle; + float m_laneSize[7]; + float m_lanePos[7]; + + std::filesystem::path m_audioPath = ""; + Chart *m_currentChart; + Vector2 m_virtualResolution = {0, 0}; + Vector2 m_gameResolution = {0, 0}; + std::vector m_timingPositionMarkers; + std::vector m_tracks; + std::vector m_noteDescs; + std::vector m_autoSamples; + std::unordered_map m_autoHitIndex; + std::unordered_map> m_autoHitInfos; + std::vector m_autoFrames; + + /* clock system */ + double m_PlayTime; + // std::chrono::system_clock::time_point m_startClock; + + TimingBase *m_timings = nullptr; + JudgeBase *m_judge = nullptr; + ScoreManager *m_scoreManager = nullptr; + TimingLineManager *m_timingLineManager = nullptr; + std::function m_eventCallback = nullptr; }; \ No newline at end of file diff --git a/Game/src/Engine/ScoreManager.cpp b/Game/src/Engine/ScoreManager.cpp index 4c450188..2f7a181b 100644 --- a/Game/src/Engine/ScoreManager.cpp +++ b/Game/src/Engine/ScoreManager.cpp @@ -1,8 +1,13 @@ #include "ScoreManager.hpp" #include #include -// TODO: Make Proper O2Jam Health Rules with MAXHP 1000 instead 100 -// This health system look like on Hard only, not good +#include "../EnvironmentSetup.hpp" + +// Cool, Good, Bad, Miss (Divide by 10 from O2Jam) +const HealthDifficulty easy = {0.3f, 0.2f, -1.0f, -5.0f}; +const HealthDifficulty normal = {0.2f, 0.1f, -0.7f, -4.0f}; +const HealthDifficulty hard = {0.1f, 0.0f, -0.5f, -3.0f}; + ScoreManager::ScoreManager() { m_cool = 0; @@ -34,72 +39,43 @@ ScoreManager::~ScoreManager() void ScoreManager::OnHit(NoteHitInfo info) { - switch (info.Result) { + bool isPlaying = EnvironmentSetup::GetInt("NowPlaying") == 1; + bool isFail = EnvironmentSetup::GetInt("Failed") == 1; - case NoteResult::COOL: - { - AddLife(0.1f); - m_jamGauge += 4; - m_score += 200 + (10 * m_jamCombo); - m_cool++; - break; - } + if (!isPlaying) { + return; + } - case NoteResult::GOOD: - { - AddLife(0); - m_jamGauge += 2; - m_score += 100 + (5 * m_jamCombo); - m_good++; + HealthDifficulty health; + int difficulty = EnvironmentSetup::GetInt("Difficulty"); + switch (difficulty) { + case 0: + health = easy; break; - } - - case NoteResult::BAD: - { - if (m_numOfPills > 0) { - m_numOfPills = std::clamp(m_numOfPills - 1, 0, 5); - m_score += 200 + (10 * m_jamCombo); - m_cool++; - - info.Result = NoteResult::COOL; - } else { - AddLife(-0.5); - m_jamGauge = 0; - m_coolCombo = 0; - m_score += 4; - m_combo = 0; - m_bad++; - } + case 1: + health = normal; break; - } - - default: - { - AddLife(-3); - m_combo = 0; - m_jamCombo = 0; - m_jamGauge = 0; - - if (m_life > 0) { - m_score -= 10; - } - - m_miss++; + case 2: + health = hard; break; - } } + HandleHit(info, health); + m_combo = std::clamp(m_combo, 0, INT_MAX); - m_jamCombo = std::clamp(m_jamCombo, 0, INT_MAX); - m_jamGauge = std::clamp(m_jamGauge, 0.0f, 100.0f); + if (!isFail) { + m_jamCombo = std::clamp(m_jamCombo, 0, INT_MAX); + m_jamGauge = std::clamp(m_jamGauge, 0.0f, 100.0f); + } m_score = std::clamp(m_score, 0, INT_MAX); if (info.Result == NoteResult::COOL) { m_coolCombo += 1; - - if (m_coolCombo > 15) { - m_coolCombo = 0; - m_numOfPills = std::clamp(m_numOfPills + 1, 0, 5); + if (!isFail) { + if (m_coolCombo > 15) { + m_coolCombo = 0; + m_numOfPills = std::clamp(m_numOfPills + 1, 0, 5); + } } } else { m_coolCombo = 0; @@ -110,13 +86,15 @@ void ScoreManager::OnHit(NoteHitInfo info) m_maxCombo = std::max(m_maxCombo, m_combo); } - if (m_jamGauge >= kMaxJamGauge) { - m_jamGauge = 0; - m_jamCombo += 1; - m_maxJamCombo = std::max(m_maxJamCombo, m_jamCombo); + if (!isFail) { + if (m_jamGauge >= kMaxJamGauge) { + m_jamGauge = 0; + m_jamCombo += 1; + m_maxJamCombo = std::max(m_maxJamCombo, m_jamCombo); - if (m_jamCallback) { - m_jamCallback(m_jamCombo); + if (m_jamCallback) { + m_jamCallback(m_jamCombo); + } } } @@ -129,8 +107,63 @@ void ScoreManager::OnHit(NoteHitInfo info) } } +void ScoreManager::HandleHit(NoteHitInfo& info, const HealthDifficulty& health) +{ + switch (info.Result) { + case NoteResult::COOL: + AddLife(health.cool); + m_jamGauge += 4.0f; + m_score += 200 + (10 * m_jamCombo); + m_cool++; + break; + case NoteResult::GOOD: + AddLife(health.good); + m_jamGauge += 2.0f; + m_score += 100 + (5 * m_jamCombo); + m_good++; + break; + case NoteResult::BAD: + if (m_numOfPills > 0) { + m_numOfPills = std::clamp(m_numOfPills - 1, 0, 5); + m_score += 200 + (10 * m_jamCombo); + m_cool++; + info.Result = NoteResult::COOL; + } + else { + AddLife(health.bad); + m_jamGauge = 0.0f; + m_coolCombo = 0; + m_score += 4; + m_combo = 0; + m_bad++; + } + break; + default: + AddLife(health.miss); + m_combo = 0; + m_jamCombo = 0; + m_jamGauge = 0.0f; + if (m_life > 0) { + m_score -= 10; + } + m_miss++; + break; + } +} + void ScoreManager::OnLongNoteHold(HoldResult result) { + bool isPlaying = EnvironmentSetup::GetInt("NowPlaying") == 1; + bool isFail = EnvironmentSetup::GetInt("Failed") == 1; + + if (!isPlaying) { + return; + } + + if (isFail) { + return; + } + switch (result) { case HoldResult::HoldBreak: { @@ -189,8 +222,11 @@ std::tuple ScoreManager:: void ScoreManager::AddLife(float sz) { - if (m_life > 0) { - m_life += sz; - m_life = std::clamp(m_life, 0.0f, 100.0f); + bool isFail = EnvironmentSetup::GetInt("Failed") == 1; + if (!isFail) { + if (m_life > 0) { + m_life += sz; + m_life = std::clamp(m_life, 0.0f, 100.0f); + } } } diff --git a/Game/src/Engine/ScoreManager.hpp b/Game/src/Engine/ScoreManager.hpp index d15c37d4..9c29e053 100644 --- a/Game/src/Engine/ScoreManager.hpp +++ b/Game/src/Engine/ScoreManager.hpp @@ -5,6 +5,13 @@ constexpr float kMaxJamGauge = 100; constexpr float kMaxLife = 100; +struct HealthDifficulty { + float cool; + float good; + float bad; + float miss; +}; + struct NoteHitInfo { NoteResult Result; @@ -21,6 +28,7 @@ class ScoreManager ~ScoreManager(); void OnHit(NoteHitInfo info); + void HandleHit(NoteHitInfo& info, const HealthDifficulty& health); void OnLongNoteHold(HoldResult result); void ListenHit(std::function); void ListenJam(std::function); diff --git a/Game/src/Engine/SkinConfig.cpp b/Game/src/Engine/SkinConfig.cpp index 75d817fc..ff3bd1fb 100644 --- a/Game/src/Engine/SkinConfig.cpp +++ b/Game/src/Engine/SkinConfig.cpp @@ -85,12 +85,70 @@ void SkinConfig::Load(std::filesystem::path path, int keyCount) } } - std::string keyName = "positions"; + std::string posName = "positions"; + std::string keyName = "Key"; if (keyCount != -1) { + posName += "#" + std::to_string(keyCount); keyName += "#" + std::to_string(keyCount); } + // Parse legacy Positions + for (auto const &[key, value] : ini[posName]) { + if (value.find(',') == std::string::npos) { + m_imageMappings[key] = value; + continue; + } + + auto rows = splitString(value, '|'); + for (auto &value2 : rows) { + auto split = splitString(value2, ','); + + PositionValue e = {}; + e.X = std::stoi(split[0]); + e.Y = std::stoi(split[1]); + + if (split.size() > 2) { + e.AnchorPointX = std::stof(split[2]); + } + + if (split.size() > 3) { + e.AnchorPointY = std::stof(split[3]); + } + + if (split.size() > 4) { + if (split[4].find(':') != std::string::npos) { + auto splitRGB = splitString(split[4], ':'); + if (splitRGB.size() >= 3) { + e.RGB[0] = std::stoi(splitRGB[0]); + e.RGB[1] = std::stoi(splitRGB[1]); + e.RGB[2] = std::stoi(splitRGB[2]); + } + } else { + e.FPS = std::stof(split[4]); + } + } + + if (split.size() > 5) { + if (split[5].find(':') != std::string::npos) { + auto splitRGB = splitString(split[5], ':'); + if (splitRGB.size() >= 3) { + e.RGB[0] = std::stoi(splitRGB[0]); + e.RGB[1] = std::stoi(splitRGB[1]); + e.RGB[2] = std::stoi(splitRGB[2]); + } + } + } + + m_positionValues[key].push_back(std::move(e)); + } + } + for (auto const &[key, value] : ini[keyName]) { + if (value.find(',') == std::string::npos) { + m_imageMappings[key] = value; + continue; + } + auto rows = splitString(value, '|'); for (auto &value2 : rows) { auto split = splitString(value2, ','); @@ -108,10 +166,27 @@ void SkinConfig::Load(std::filesystem::path path, int keyCount) } if (split.size() > 4) { - auto splitRGB = splitString(split[4], ':'); - e.RGB[0] = std::stoi(splitRGB[0]); - e.RGB[1] = std::stoi(splitRGB[1]); - e.RGB[2] = std::stoi(splitRGB[2]); + if (split[4].find(':') != std::string::npos) { + auto splitRGB = splitString(split[4], ':'); + if (splitRGB.size() >= 3) { + e.RGB[0] = std::stoi(splitRGB[0]); + e.RGB[1] = std::stoi(splitRGB[1]); + e.RGB[2] = std::stoi(splitRGB[2]); + } + } else { + e.FPS = std::stof(split[4]); + } + } + + if (split.size() > 5) { + if (split[5].find(':') != std::string::npos) { + auto splitRGB = splitString(split[5], ':'); + if (splitRGB.size() >= 3) { + e.RGB[0] = std::stoi(splitRGB[0]); + e.RGB[1] = std::stoi(splitRGB[1]); + e.RGB[2] = std::stoi(splitRGB[2]); + } + } } m_positionValues[key].push_back(std::move(e)); @@ -137,25 +212,106 @@ void SkinConfig::Load(std::filesystem::path path, int keyCount) auto split = splitString(value, ','); SpriteValue e = {}; - e.numOfFrames = std::stoi(split[0]); - e.X = std::stoi(split[1]); - e.Y = std::stoi(split[2]); + e.X = std::stoi(split[0]); + e.Y = std::stoi(split[1]); + + if (split.size() > 2) { + e.AnchorPointX = std::stof(split[2]); + } if (split.size() > 3) { - e.AnchorPointX = std::stof(split[3]); + e.AnchorPointY = std::stof(split[3]); } if (split.size() > 4) { - e.AnchorPointY = std::stof(split[4]); + e.FrameTime = std::stof(split[4]); } - if (split.size() > 5) { - e.FrameTime = std::stof(split[5]); + m_spriteValues[key] = std::move(e); + } + + for (auto const &[key, value] : ini["Effects"]) { + auto split = splitString(value, ','); + + SpriteValue e = {}; + e.X = std::stoi(split[0]); + e.Y = std::stoi(split[1]); + + // Effects default to 0.5, 0.5 AnchorPoint as they are centered on the lane + e.AnchorPointX = 0.5f; + e.AnchorPointY = 0.5f; + + // Effects format: X, Y, FPS?, TintColor? + if (split.size() > 2) { + e.FrameTime = std::stof(split[2]); + } else { + e.FrameTime = 30.0f; // Default FPS for effects } m_spriteValues[key] = std::move(e); } + for (auto const &[key, value] : ini["UI"]) { + auto rows = splitString(value, '|'); + for (auto &value2 : rows) { + auto split = splitString(value2, ','); + + PositionValue p = {}; + p.X = std::stoi(split[0]); + p.Y = std::stoi(split[1]); + + RectInfo r = {}; + r.X = std::stoi(split[0]); + r.Y = std::stoi(split[1]); + + if (split.size() > 2) { + float v = std::stof(split[2]); + if (v <= 1.0f) { + p.AnchorPointX = v; + } else { + r.Width = std::stoi(split[2]); + } + } + + if (split.size() > 3) { + float v = std::stof(split[3]); + if (v <= 1.0f) { + p.AnchorPointY = v; + } else { + r.Height = std::stoi(split[3]); + } + } + + if (split.size() > 4) { + if (split[4].find(':') != std::string::npos) { + auto splitRGB = splitString(split[4], ':'); + if (splitRGB.size() >= 3) { + p.RGB[0] = std::stoi(splitRGB[0]); + p.RGB[1] = std::stoi(splitRGB[1]); + p.RGB[2] = std::stoi(splitRGB[2]); + } + } else { + p.FPS = std::stof(split[4]); + } + } + + if (split.size() > 5) { + if (split[5].find(':') != std::string::npos) { + auto splitRGB = splitString(split[5], ':'); + if (splitRGB.size() >= 3) { + p.RGB[0] = std::stoi(splitRGB[0]); + p.RGB[1] = std::stoi(splitRGB[1]); + p.RGB[2] = std::stoi(splitRGB[2]); + } + } + } + + m_positionValues[key].push_back(std::move(p)); + m_rectValues[key].push_back(std::move(r)); + } + } + + // Parse legacy rects for (auto const &[key, value] : ini["rects"]) { auto rows = splitString(value, '|'); for (auto &value2 : rows) { @@ -170,6 +326,14 @@ void SkinConfig::Load(std::filesystem::path path, int keyCount) m_rectValues[key].push_back(std::move(e)); } } + + std::string imgName = "Images"; + if (keyCount != -1) { + imgName += "#" + std::to_string(keyCount); + } + for (auto const &[key, value] : ini[imgName]) { + m_imageMappings[key] = value; + } } SkinConfig::~SkinConfig() @@ -211,6 +375,17 @@ NoteValue &SkinConfig::GetNote(std::string key) return m_noteValues[key]; } +std::string SkinConfig::GetImageMapping(std::string key) +{ + std::transform(key.begin(), key.end(), key.begin(), ::tolower); + + if (m_imageMappings.find(key) == m_imageMappings.end()) { + return ""; + } + + return m_imageMappings[key]; +} + std::vector &SkinConfig::GetNumeric(std::string key) { std::transform(key.begin(), key.end(), key.begin(), ::tolower); diff --git a/Game/src/Engine/SkinConfig.hpp b/Game/src/Engine/SkinConfig.hpp index 2ff54b01..f1687acb 100644 --- a/Game/src/Engine/SkinConfig.hpp +++ b/Game/src/Engine/SkinConfig.hpp @@ -19,6 +19,7 @@ class SkinConfig std::vector &GetRect(std::string key); NoteValue &GetNote(std::string key); SpriteValue &GetSprite(std::string key); + std::string GetImageMapping(std::string key); private: void Load(std::filesystem::path path, int keyCount); @@ -28,4 +29,5 @@ class SkinConfig std::unordered_map m_spriteValues; std::unordered_map> m_rectValues; std::unordered_map m_noteValues; + std::unordered_map m_imageMappings; }; \ No newline at end of file diff --git a/Game/src/Engine/SkinManager.cpp b/Game/src/Engine/SkinManager.cpp index c1d49b60..fc843945 100644 --- a/Game/src/Engine/SkinManager.cpp +++ b/Game/src/Engine/SkinManager.cpp @@ -1,4 +1,5 @@ #include "SkinManager.hpp" +#include "../EnvironmentSetup.hpp" SkinManager *SkinManager::m_instance = nullptr; @@ -45,6 +46,35 @@ void SkinManager::LoadSkin(std::string skinName) if (m_useLua) { m_luaScripting = std::make_unique(selectedSkin / "Scripts"); } + + m_arenas.clear(); + m_arenas.push_back("Random"); + + auto arenaPath = GetPath() / "Playing" / "Arena"; + int max_arena = 0; + if (std::filesystem::exists(arenaPath)) { + for (const auto& entry : std::filesystem::directory_iterator(arenaPath)) { + if (entry.is_directory()) { + try { + int num = std::stoi(entry.path().filename().string()); + if (num > max_arena) max_arena = num; + } catch (...) {} + } + } + } + + for (int i = 1; i <= max_arena; i++) { + m_arenas.push_back("Arena " + std::to_string(i)); + } + + if (m_arenas.size() == 1) { + m_arenas.push_back("Arena 1"); + } +} + +const std::vector& SkinManager::GetArenas() +{ + return m_arenas; } void SkinManager::ReloadSkin() @@ -150,54 +180,21 @@ SpriteValue SkinManager::GetSprite(SkinGroup group, std::string key) } } -void SkinManager::Arena_SetIndex(int index) -{ - m_arena = index; - - if (m_luaScripting) { - m_luaScripting->Arena_SetIndex(index); - } else { - m_arenaConfig = std::make_unique( - GetPath() / m_expected_directory[SkinGroup::Playing] / "Arena" / std::to_string(index) / "Arena.ini", - m_keyCount); - } -} - -std::vector SkinManager::Arena_GetNumeric(std::string key) +std::string SkinManager::GetImageMapping(SkinGroup group, std::string key) { if (m_luaScripting) { - return m_luaScripting->Arena_GetNumeric(key); + // Lua doesn't support ImageMappings currently, return empty + return ""; } else { - return m_arenaConfig->GetNumeric(key); - } -} + if (m_skinConfigs.find(group) == m_skinConfigs.end()) { + TryLoadGroup(group); + } -std::vector SkinManager::Arena_GetPosition(std::string key) -{ - if (m_luaScripting) { - return m_luaScripting->Arena_GetPosition(key, m_keyCount); - } else { - return m_arenaConfig->GetPosition(key); + return m_skinConfigs[group]->GetImageMapping(key); } } -std::vector SkinManager::Arena_GetRect(std::string key) -{ - if (m_luaScripting) { - return m_luaScripting->Arena_GetRect(key); - } else { - return m_arenaConfig->GetRect(key); - } -} -SpriteValue SkinManager::Arena_GetSprite(std::string key) -{ - if (m_luaScripting) { - return m_luaScripting->Arena_GetSprite(key); - } else { - return m_arenaConfig->GetSprite(key); - } -} SkinManager *SkinManager::GetInstance() { @@ -225,8 +222,20 @@ SkinManager::~SkinManager() void SkinManager::TryLoadGroup(SkinGroup group) { - m_skinConfigs[group] = std::make_unique( - GetPath() / m_expected_directory[group] / m_expected_skin_config[group], m_keyCount); + std::filesystem::path configPath; + + if (group == SkinGroup::Notes && EnvironmentSetup::GetInt("NoteSkin") != 2) { + auto path = std::filesystem::current_path() / "Resources"; + if (EnvironmentSetup::GetInt("NoteSkin") == 1) { + configPath = path / "Notes" / "Circle" / "Notes.ini"; + } else { + configPath = path / "Notes" / "Square" / "Notes.ini"; + } + } else { + configPath = GetPath() / m_expected_directory[group] / m_expected_skin_config[group]; + } + + m_skinConfigs[group] = std::make_unique(configPath, m_keyCount); } void SkinManager::Update(double delta) diff --git a/Game/src/Engine/SkinManager.hpp b/Game/src/Engine/SkinManager.hpp index b9694080..867f3dcf 100644 --- a/Game/src/Engine/SkinManager.hpp +++ b/Game/src/Engine/SkinManager.hpp @@ -14,6 +14,8 @@ class SkinManager void LoadSkin(std::string skinName); void ReloadSkin(); + const std::vector& GetArenas(); + LaneInfo GetLaneInfo(); std::string GetSkinProp(std::string group, std::string key, std::string defaultValue = ""); @@ -25,12 +27,9 @@ class SkinManager std::vector GetRect(SkinGroup group, std::string key); NoteValue GetNote(SkinGroup group, std::string key); SpriteValue GetSprite(SkinGroup group, std::string key); + std::string GetImageMapping(SkinGroup group, std::string key); + - void Arena_SetIndex(int index); - std::vector Arena_GetNumeric(std::string key); - std::vector Arena_GetPosition(std::string key); - std::vector Arena_GetRect(std::string key); - SpriteValue Arena_GetSprite(std::string key); void Update(double delta); @@ -54,6 +53,7 @@ class SkinManager std::string m_currentSkin; mINI::INIStructure ini; std::unique_ptr m_arenaConfig; + std::vector m_arenas; int m_keyCount, m_previousKeyCount; int m_arena; diff --git a/Game/src/Engine/Timing/TimingBase.cpp b/Game/src/Engine/Timing/TimingBase.cpp index 98a58f72..287b3230 100644 --- a/Game/src/Engine/Timing/TimingBase.cpp +++ b/Game/src/Engine/Timing/TimingBase.cpp @@ -7,7 +7,7 @@ TimingBase::TimingBase(std::vector &_timings, std::vector &timings, double offset) +static TimingInfo& FindTimingAt(std::vector& timings, double offset) { int min = 0, max = (int)timings.size() - 1; int left = min, right = max; @@ -16,13 +16,15 @@ TimingInfo &FindTimingAt(std::vector &timings, double offset) int mid = (left + right) / 2; bool afterMid = mid < 0 || timings[mid].StartTime < offset; - bool beforeMid = mid + 1 >= timings.size() || offset < timings[mid + 1].StartTime; + bool beforeMid = static_cast(mid) + 1 >= timings.size() || offset < timings[static_cast>::size_type>(mid) + 1].StartTime; if (afterMid && beforeMid) { return timings[mid]; - } else if (afterMid) { + } + else if (afterMid) { left = mid + 1; - } else { + } + else { right = mid - 1; } } @@ -37,7 +39,14 @@ double TimingBase::GetBeatAt(double offset) double TimingBase::GetBPMAt(double offset) { - return FindTimingAt(timings, offset).Value; + double BPM = FindTimingAt(timings, offset).Value; + + // Handle BPM overflow at int32_t range + if (BPM >= INT_MAX || BPM <= -INT_MAX) { + BPM = fmod(BPM, INT_MAX); + } + + return BPM; } double TimingBase::GetOffsetAt(double offset) diff --git a/Game/src/Engine/TimingLine.cpp b/Game/src/Engine/TimingLine.cpp index 1a8894ba..866975c2 100644 --- a/Game/src/Engine/TimingLine.cpp +++ b/Game/src/Engine/TimingLine.cpp @@ -1,6 +1,7 @@ #include "TimingLine.hpp" #include "RhythmEngine.hpp" #include "Texture/ResizableImage.h" +#include "../EnvironmentSetup.hpp" namespace { double CalculateLinePosition(double trackOffset, double offset, double noteSpeed, bool upscroll = false) @@ -62,15 +63,26 @@ void TimingLine::Render(double delta) double min = 0, max = hitPos; double pos_y = min + (max - min) * alpha; + int halfNoteSize; + + if (EnvironmentSetup::GetInt("MeasureLineType") == 1) { + halfNoteSize = static_cast(EnvironmentSetup::GetInt("NoteSize") / 2); + } + else { + halfNoteSize = 0; + } + m_line->Size = UDim2::fromOffset(m_imageSize, 1); - m_line->Position = UDim2::fromOffset(m_imagePos, pos_y); //+ start.Lerp(end, alpha); + m_line->Position = UDim2::fromOffset(m_imagePos, pos_y - halfNoteSize); //+ start.Lerp(end, alpha); if (m_line->Position.Y.Offset >= 0 && m_line->Position.Y.Offset < hitPos + 10) { + m_line->TintColor = { 0.7f, 0.7f, 0.7f }; m_line->Draw(&playRect); } } void TimingLine::Release() { + EnvironmentSetup::SetInt("HalfNoteSize", 0); delete m_line; } diff --git a/Game/src/Engine/VideoPlayer.cpp b/Game/src/Engine/VideoPlayer.cpp new file mode 100644 index 00000000..417bc53d --- /dev/null +++ b/Game/src/Engine/VideoPlayer.cpp @@ -0,0 +1,184 @@ +#include "VideoPlayer.hpp" +#include +#include +#include "Rendering/Window.h" + +VideoPlayer::VideoPlayer() { +} + +VideoPlayer::~VideoPlayer() { + Stop(); +} + +bool VideoPlayer::Load(const std::string& path) { + Stop(); + m_path = path; + + m_formatCtx = avformat_alloc_context(); + if (avformat_open_input(&m_formatCtx, m_path.c_str(), nullptr, nullptr) != 0) { + return false; + } + + if (avformat_find_stream_info(m_formatCtx, nullptr) < 0) { + avformat_close_input(&m_formatCtx); + return false; + } + + m_videoStreamIndex = -1; + const AVCodec* codec = nullptr; + + for (unsigned int i = 0; i < m_formatCtx->nb_streams; i++) { + if (m_formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + m_videoStreamIndex = i; + codec = avcodec_find_decoder(m_formatCtx->streams[i]->codecpar->codec_id); + break; + } + } + + if (m_videoStreamIndex == -1 || !codec) { + avformat_close_input(&m_formatCtx); + return false; + } + + m_codecCtx = avcodec_alloc_context3(codec); + avcodec_parameters_to_context(m_codecCtx, m_formatCtx->streams[m_videoStreamIndex]->codecpar); + + m_codecCtx->thread_count = 0; + m_codecCtx->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE; + + if (avcodec_open2(m_codecCtx, codec, nullptr) < 0) { + avcodec_free_context(&m_codecCtx); + avformat_close_input(&m_formatCtx); + return false; + } + + m_width = m_codecCtx->width; + m_height = m_codecCtx->height; + + m_frame = av_frame_alloc(); + m_frameRGB = av_frame_alloc(); + m_packet = av_packet_alloc(); + + int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGBA, m_width, m_height, 32); + m_buffer = (uint8_t*)av_malloc(numBytes * sizeof(uint8_t)); + + av_image_fill_arrays(m_frameRGB->data, m_frameRGB->linesize, m_buffer, AV_PIX_FMT_RGBA, m_width, m_height, 32); + + m_swsCtx = sws_getContext(m_width, m_height, m_codecCtx->pix_fmt, + m_width, m_height, AV_PIX_FMT_RGBA, + SWS_FAST_BILINEAR, nullptr, nullptr, nullptr); + + m_texture = std::make_unique(m_width, m_height); + + m_valid = true; + return true; +} + +void VideoPlayer::Play() { + if (!m_valid) return; + + m_quit = false; + m_playing = true; + m_decodeThread = std::thread(&VideoPlayer::DecodeThread, this); +} + +void VideoPlayer::Stop() { + m_quit = true; + if (m_decodeThread.joinable()) { + m_decodeThread.join(); + } + + m_playing = false; + m_valid = false; + + if (m_buffer) { av_free(m_buffer); m_buffer = nullptr; } + if (m_frameRGB) { av_frame_free(&m_frameRGB); m_frameRGB = nullptr; } + if (m_frame) { av_frame_free(&m_frame); m_frame = nullptr; } + if (m_packet) { av_packet_free(&m_packet); m_packet = nullptr; } + if (m_swsCtx) { sws_freeContext(m_swsCtx); m_swsCtx = nullptr; } + if (m_codecCtx) { avcodec_free_context(&m_codecCtx); m_codecCtx = nullptr; } + if (m_formatCtx) { avformat_close_input(&m_formatCtx); m_formatCtx = nullptr; } + + m_texture.reset(); +} + +void VideoPlayer::DecodeThread() { + while (!m_quit) { + if (!m_playing) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; + } + + // Wait if video is too far ahead of audio + if (m_videoTime > m_currentAudioTime + 50.0) { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + continue; + } + + if (av_read_frame(m_formatCtx, m_packet) >= 0) { + if (m_packet->stream_index == m_videoStreamIndex) { + if (avcodec_send_packet(m_codecCtx, m_packet) == 0) { + while (avcodec_receive_frame(m_codecCtx, m_frame) == 0) { + + double pts = 0; + if (m_frame->pts != AV_NOPTS_VALUE) { + pts = m_frame->pts; + } + + double timeBase = av_q2d(m_formatCtx->streams[m_videoStreamIndex]->time_base); + double frameTimeMs = pts * timeBase * 1000.0; + + // Drop frame if video are way behind + if (frameTimeMs < m_currentAudioTime - 50.0) { + continue; + } + + // Convert to RGBA + sws_scale(m_swsCtx, (uint8_t const* const*)m_frame->data, + m_frame->linesize, 0, m_height, + m_frameRGB->data, m_frameRGB->linesize); + + m_videoTime = frameTimeMs; + + // Lock and copy to shared buffer + { + std::lock_guard lock(m_frameMutex); + int pitch = m_frameRGB->linesize[0]; + size_t bufSize = (size_t)pitch * m_height; + if (m_pixelBuffer.size() != bufSize) { + m_pixelBuffer.resize(bufSize); + } + memcpy(m_pixelBuffer.data(), m_frameRGB->data[0], bufSize); + m_pitch = pitch; + m_frameReady = true; + } + } + } + } + av_packet_unref(m_packet); + } else { + // EOF or error + m_playing = false; + } + } +} + +void VideoPlayer::Update(double currentAudioTime) { + if (!m_playing) return; + m_currentAudioTime = currentAudioTime; + + std::lock_guard lock(m_frameMutex); + if (m_frameReady && m_texture && m_pixelBuffer.size() > 0) { + m_texture->UpdateTexture(m_pixelBuffer.data(), m_width, m_height, m_pitch); + m_frameReady = false; + } +} + +void VideoPlayer::Render() { + if (!m_valid || !m_texture) return; + + // Draw the texture covering the screen + m_texture->Size = UDim2::fromOffset(GameWindow::GetInstance()->GetBufferWidth(), GameWindow::GetInstance()->GetBufferHeight()); + m_texture->Position = UDim2::fromOffset(0, 0); + m_texture->Draw(); +} diff --git a/Game/src/Engine/VideoPlayer.hpp b/Game/src/Engine/VideoPlayer.hpp new file mode 100644 index 00000000..035d482c --- /dev/null +++ b/Game/src/Engine/VideoPlayer.hpp @@ -0,0 +1,65 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "Texture/Texture2D.h" + +extern "C" { +#include +#include +#include +#include +} + +class VideoPlayer { +public: + VideoPlayer(); + ~VideoPlayer(); + + bool Load(const std::string& path); + void Play(); + void Stop(); + + // Call this every frame with the current audio time to sync the video + void Update(double currentAudioTime); + void Render(); + + bool IsValid() const { return m_valid; } + +private: + void DecodeThread(); + + bool m_valid = false; + std::string m_path; + + AVFormatContext* m_formatCtx = nullptr; + AVCodecContext* m_codecCtx = nullptr; + int m_videoStreamIndex = -1; + SwsContext* m_swsCtx = nullptr; + + AVFrame* m_frame = nullptr; + AVFrame* m_frameRGB = nullptr; + AVPacket* m_packet = nullptr; + + uint8_t* m_buffer = nullptr; + + std::atomic m_playing{false}; + std::atomic m_quit{false}; + std::thread m_decodeThread; + std::mutex m_frameMutex; + + std::unique_ptr m_texture; + + double m_currentAudioTime = 0.0; + std::atomic m_videoTime{0.0}; + + int m_width = 0; + int m_height = 0; + + bool m_frameReady = false; + std::vector m_pixelBuffer; + int m_pitch = 0; +}; diff --git a/Game/src/EnvironmentSetup.cpp b/Game/src/EnvironmentSetup.cpp index f4a3733b..7aa320d5 100644 --- a/Game/src/EnvironmentSetup.cpp +++ b/Game/src/EnvironmentSetup.cpp @@ -1,64 +1,83 @@ #include "EnvironmentSetup.hpp" #include +#include namespace { std::unordered_map m_stores; - std::unordered_map m_storesPtr; + std::unordered_map m_storesPtr; std::unordered_map m_paths; std::unordered_map m_ints; + std::mutex m_mutex; } // namespace void EnvironmentSetup::OnExitCheck() { - for (auto &[key, value] : m_stores) { - value = ""; - } - - for (auto &[key, value] : m_paths) { - value = ""; - } - - for (auto &[key, value] : m_ints) { - value = 0; - } + std::lock_guard lock(m_mutex); + m_stores.clear(); + m_paths.clear(); + m_ints.clear(); } void EnvironmentSetup::Set(std::string key, std::string value) { - m_stores[key] = value; + std::lock_guard lock(m_mutex); + m_stores[key] = std::move(value); } std::string EnvironmentSetup::Get(std::string key) { - return m_stores[key]; + std::lock_guard lock(m_mutex); + auto it = m_stores.find(key); + if (it != m_stores.end()) { + return it->second; + } + return ""; } -void EnvironmentSetup::SetObj(std::string key, void *ptr) +void EnvironmentSetup::SetObj(std::string key, void* ptr) { + std::lock_guard lock(m_mutex); m_storesPtr[key] = ptr; } -void *EnvironmentSetup::GetObj(std::string key) +void* EnvironmentSetup::GetObj(std::string key) { - return m_storesPtr[key]; + std::lock_guard lock(m_mutex); + auto it = m_storesPtr.find(key); + if (it != m_storesPtr.end()) { + return it->second; + } + return nullptr; } void EnvironmentSetup::SetPath(std::string key, std::filesystem::path path) { - m_paths[key] = path; + std::lock_guard lock(m_mutex); + m_paths[key] = std::move(path); } std::filesystem::path EnvironmentSetup::GetPath(std::string key) { - return m_paths[key]; + std::lock_guard lock(m_mutex); + auto it = m_paths.find(key); + if (it != m_paths.end()) { + return it->second; + } + return {}; } void EnvironmentSetup::SetInt(std::string key, int value) { + std::lock_guard lock(m_mutex); m_ints[key] = value; } int EnvironmentSetup::GetInt(std::string key) { - return m_ints[key]; + std::lock_guard lock(m_mutex); + auto it = m_ints.find(key); + if (it != m_ints.end()) { + return it->second; + } + return 0; } diff --git a/Game/src/MyGame.cpp b/Game/src/MyGame.cpp index 316a4966..670c2052 100644 --- a/Game/src/MyGame.cpp +++ b/Game/src/MyGame.cpp @@ -68,7 +68,7 @@ bool MyGame::Init() /* Overlays */ SceneManager::AddOverlay(GameOverlay::SETTINGS, new SettingsOverlay()); - std::string title = std::string(O2GAME_TITLE) + " " + std::string(O2GAME_VERSION); + std::string title = std::string(O2GAME_TITLE) + "" + ""/*std::string(O2GAME_VERSION)*/; m_window->SetWindowTitle(title); if (EnvironmentSetup::GetPath("FILE").empty()) { @@ -117,10 +117,6 @@ bool MyGame::LoadConfiguration() break; case 3: - SetRenderMode(RendererMode::DIRECTX11); - break; - - case 4: SetRenderMode(RendererMode::DIRECTX12); break; @@ -209,3 +205,10 @@ void MyGame::Input(double delta) { m_sceneManager->Input(delta); } + +void MyGame::OnDropFile(std::string path) +{ + EnvironmentSetup::SetInt("FileOpen", 1); + EnvironmentSetup::SetPath("FILE", path); + SceneManager::ChangeScene(GameScene::SONGSELECT); +} diff --git a/Game/src/MyGame.h b/Game/src/MyGame.h index 800bf320..3196498d 100644 --- a/Game/src/MyGame.h +++ b/Game/src/MyGame.h @@ -17,4 +17,5 @@ class MyGame : public Game void Update(double deltaTime) override; void Render(double deltaTime) override; void Input(double deltaTime) override; + void OnDropFile(std::string path) override; }; \ No newline at end of file diff --git a/Game/src/Resources/DefaultConfiguration.h b/Game/src/Resources/DefaultConfiguration.h index 0fbd3b2c..951ea0f8 100644 --- a/Game/src/Resources/DefaultConfiguration.h +++ b/Game/src/Resources/DefaultConfiguration.h @@ -7,10 +7,15 @@ std::string defaultConfiguration = "[game]\n" "framelimit = 240\n" // Default 144 but i set this for more reasonable "audiooffset = 0\n" "audiovolume = 100\n" - "autosound = 1\n" + "autosound = 0\n" "resolution = 1280x720\n" // Fix for most monitor "renderer = 0\n" - "guideline = 1\n\n" + "guideline = 1\n" + "background = 0\n" + "measureline = 1\n" + "measurelinetype = 0\n" + "newlongnote = 0\n" + "lnbodyontop = 0\n\n" "[keymapping]\n" "lane1 = A\n" @@ -42,4 +47,8 @@ std::string defaultConfiguration = "[game]\n" "folder =\n" "[gameplay]\n" - "notespeed = 225\n"; \ No newline at end of file + "notespeed = 250\n" + "difficulty = \n" + "modifiers =\n" + "arena =\n"; + diff --git a/Game/src/Resources/GameResources.cpp b/Game/src/Resources/GameResources.cpp index 01c043b9..b82fe1cd 100644 --- a/Game/src/Resources/GameResources.cpp +++ b/Game/src/Resources/GameResources.cpp @@ -23,6 +23,7 @@ #endif #include "../Engine/SkinConfig.hpp" +#include "../EnvironmentSetup.hpp" #pragma warning(disable : 26451) @@ -393,36 +394,73 @@ namespace GameNoteResource { } bool IsVulkan = Renderer::GetInstance()->IsVulkan(); + std::filesystem::path skinNotePath; - auto skinPath = SkinManager::GetInstance()->GetPath(); - auto skinNotePath = skinPath / "Notes"; + if (EnvironmentSetup::GetInt("NoteSkin") == 1) { + auto path = std::filesystem::current_path() / "Resources"; + skinNotePath = path / "Notes" / "Circle"; + } + else if (EnvironmentSetup::GetInt("NoteSkin") == 2) { + auto skinPath = SkinManager::GetInstance()->GetPath(); + skinNotePath = skinPath / "Notes"; + } + else { + auto path = std::filesystem::current_path() / "Resources"; + skinNotePath = path / "Notes" / "Square"; + } auto manager = SkinManager::GetInstance(); + std::string laneMap[7] = { "Note1", "Note2", "Note1", "NoteS", "Note1", "Note2", "Note1" }; + for (int i = 0; i < 7; i++) { - NoteValue note = manager->GetNote(SkinGroup::Notes, "LaneHit" + std::to_string(i)); - NoteValue hold = manager->GetNote(SkinGroup::Notes, "LaneHold" + std::to_string(i)); + std::string noteBase = laneMap[i] + "N"; + std::string holdBase = laneMap[i] + "L"; + + std::vector notePaths; + std::vector holdPaths; + + if (std::filesystem::exists(skinNotePath / (noteBase + "-0.png"))) { + int frame = 0; + while (std::filesystem::exists(skinNotePath / (noteBase + "-" + std::to_string(frame) + ".png"))) { + notePaths.push_back(skinNotePath / (noteBase + "-" + std::to_string(frame) + ".png")); + frame++; + } + } else if (std::filesystem::exists(skinNotePath / (noteBase + ".png"))) { + notePaths.push_back(skinNotePath / (noteBase + ".png")); + } else { + throw std::runtime_error("Missing note files for lane " + std::to_string(i) + " base: " + noteBase); + } + + if (std::filesystem::exists(skinNotePath / (holdBase + "-0.png"))) { + int frame = 0; + while (std::filesystem::exists(skinNotePath / (holdBase + "-" + std::to_string(frame) + ".png"))) { + holdPaths.push_back(skinNotePath / (holdBase + "-" + std::to_string(frame) + ".png")); + frame++; + } + } else if (std::filesystem::exists(skinNotePath / (holdBase + ".png"))) { + holdPaths.push_back(skinNotePath / (holdBase + ".png")); + } else { + throw std::runtime_error("Missing hold files for lane " + std::to_string(i) + " base: " + holdBase); + } NoteImage *noteImage = new NoteImage(); NoteImage *holdImage = new NoteImage(); - noteImage->MaxFrames = note.numOfFiles; - holdImage->MaxFrames = hold.numOfFiles; + noteImage->MaxFrames = notePaths.size(); + holdImage->MaxFrames = holdPaths.size(); - noteImage->Surface.resize(note.numOfFiles); - holdImage->Surface.resize(hold.numOfFiles); + noteImage->Surface.resize(notePaths.size()); + holdImage->Surface.resize(holdPaths.size()); - noteImage->Texture.resize(note.numOfFiles); - holdImage->Texture.resize(hold.numOfFiles); + noteImage->Texture.resize(notePaths.size()); + holdImage->Texture.resize(holdPaths.size()); - noteImage->VulkanTexture.resize(note.numOfFiles); - holdImage->VulkanTexture.resize(hold.numOfFiles); + noteImage->VulkanTexture.resize(notePaths.size()); + holdImage->VulkanTexture.resize(holdPaths.size()); - for (int j = 0; j < note.numOfFiles; j++) { - auto path = skinNotePath / (note.fileName + std::to_string(j) + ".png"); - if (!std::filesystem::exists(path)) { - throw std::runtime_error("File: " + path.string() + " is not found!"); - } + for (int j = 0; j < notePaths.size(); j++) { + auto path = notePaths[j]; if (IsVulkan) { auto tex_data = vkTexture::TexLoadImage(path); @@ -461,11 +499,8 @@ namespace GameNoteResource { } } - for (int j = 0; j < hold.numOfFiles; j++) { - auto path = skinNotePath / (hold.fileName + std::to_string(j) + ".png"); - if (!std::filesystem::exists(path)) { - throw std::runtime_error("File: " + path.string() + " is not found!"); - } + for (int j = 0; j < holdPaths.size(); j++) { + auto path = holdPaths[j]; if (IsVulkan) { auto tex_data = vkTexture::TexLoadImage(path); @@ -508,24 +543,42 @@ namespace GameNoteResource { noteTextures[(NoteImageType)(i + 7)] = holdImage; } - NoteValue trailUp = manager->GetNote(SkinGroup::Notes, "NoteTrailUp"); - NoteValue trailDown = manager->GetNote(SkinGroup::Notes, "NoteTrailDown"); + std::vector trailUpPaths; + if (std::filesystem::exists(skinNotePath / "TrailUp0.png")) { + int frame = 0; + while (std::filesystem::exists(skinNotePath / ("TrailUp" + std::to_string(frame) + ".png"))) { + trailUpPaths.push_back(skinNotePath / ("TrailUp" + std::to_string(frame) + ".png")); + frame++; + } + } else if (std::filesystem::exists(skinNotePath / "TrailUp.png")) { + trailUpPaths.push_back(skinNotePath / "TrailUp.png"); + } + + std::vector trailDownPaths; + if (std::filesystem::exists(skinNotePath / "TrailDown0.png")) { + int frame = 0; + while (std::filesystem::exists(skinNotePath / ("TrailDown" + std::to_string(frame) + ".png"))) { + trailDownPaths.push_back(skinNotePath / ("TrailDown" + std::to_string(frame) + ".png")); + frame++; + } + } else if (std::filesystem::exists(skinNotePath / "TrailDown.png")) { + trailDownPaths.push_back(skinNotePath / "TrailDown.png"); + } NoteImage *trailUpImg = new NoteImage(); - trailUpImg->Texture.resize(trailUp.numOfFiles); - trailUpImg->Surface.resize(trailUp.numOfFiles); - trailUpImg->VulkanTexture.resize(trailUp.numOfFiles); + trailUpImg->MaxFrames = trailUpPaths.size(); + trailUpImg->Texture.resize(trailUpPaths.size()); + trailUpImg->Surface.resize(trailUpPaths.size()); + trailUpImg->VulkanTexture.resize(trailUpPaths.size()); NoteImage *trailDownImg = new NoteImage(); - trailDownImg->Texture.resize(trailDown.numOfFiles); - trailDownImg->Surface.resize(trailDown.numOfFiles); - trailDownImg->VulkanTexture.resize(trailUp.numOfFiles); - - for (int i = 0; i < trailUp.numOfFiles; i++) { - auto path = skinNotePath / (trailUp.fileName + std::to_string(i) + ".png"); - if (!std::filesystem::exists(path)) { - throw std::runtime_error("File: " + path.string() + " is not found!"); - } + trailDownImg->MaxFrames = trailDownPaths.size(); + trailDownImg->Texture.resize(trailDownPaths.size()); + trailDownImg->Surface.resize(trailDownPaths.size()); + trailDownImg->VulkanTexture.resize(trailDownPaths.size()); + + for (int i = 0; i < trailUpPaths.size(); i++) { + auto path = trailUpPaths[i]; if (IsVulkan) { auto tex_data = vkTexture::TexLoadImage(path); @@ -564,11 +617,8 @@ namespace GameNoteResource { } } - for (int i = 0; i < trailDown.numOfFiles; i++) { - auto path = skinNotePath / (trailDown.fileName + std::to_string(i) + ".png"); - if (!std::filesystem::exists(path)) { - throw std::runtime_error("File: " + path.string() + " is not found!"); - } + for (int i = 0; i < trailDownPaths.size(); i++) { + auto path = trailDownPaths[i]; if (IsVulkan) { auto tex_data = vkTexture::TexLoadImage(path); diff --git a/Game/src/Resources/SkinStructs.hpp b/Game/src/Resources/SkinStructs.hpp index 52ae7240..9ba33add 100644 --- a/Game/src/Resources/SkinStructs.hpp +++ b/Game/src/Resources/SkinStructs.hpp @@ -23,12 +23,13 @@ struct LaneInfo int LaneOffset; }; -// Value Format: X, Y, AnchorPointX?, AnchorPointY?, TintColor? +// Value Format: X, Y, AnchorPointX?, AnchorPointY?, FPS?, TintColor? struct PositionValue { double X, Y; - float AnchorPointX, AnchorPointY; - unsigned char RGB[3]; + float AnchorPointX = 0, AnchorPointY = 0; + float FPS = 0.0f; + unsigned char RGB[3] = {255, 255, 255}; }; // Value format: X, Y, MaxDigits?, Direction?, FillWithZero? @@ -41,10 +42,9 @@ struct NumericValue unsigned char RGB[3]; }; -// Value format: X, Y, AnchorPointX, AnchorPointY +// Value format: X, Y, AnchorPointX, AnchorPointY, FrameTime struct SpriteValue { - int numOfFrames; double X, Y; float AnchorPointX, AnchorPointY; float FrameTime; diff --git a/Game/src/Scenes/GameplayScene.cpp b/Game/src/Scenes/GameplayScene.cpp index cde9b389..dbcdc5e3 100644 --- a/Game/src/Scenes/GameplayScene.cpp +++ b/Game/src/Scenes/GameplayScene.cpp @@ -1,6 +1,7 @@ #include "GameplayScene.h" #include +#include #include #include #include @@ -22,1071 +23,1648 @@ #include "../Engine/NoteImageCacheManager.hpp" #include "../Engine/SkinConfig.hpp" #include "../Engine/SkinManager.hpp" +#include "../Engine/VideoPlayer.hpp" #include "../EnvironmentSetup.hpp" +#include "../Data/Util/Util.hpp" #include "../GameScenes.h" #define AUTOPLAY_TEXT u8"Game currently on autoplay!" +#define GAMEINFO_TEXT u8"O2Clone Build Date: " __DATE__ " " __TIME__ -struct MissInfo -{ - int type; - float beat; - float hit_beat; - float time; +struct MissInfo { + int type; + float beat; + float hit_beat; + float time; }; -bool CheckSkinComponent(std::filesystem::path x) -{ - return std::filesystem::exists(x); +bool CheckSkinComponent(std::filesystem::path x) { + return std::filesystem::exists(x); } -GameplayScene::GameplayScene() : Scene::Scene() -{ - m_keyLighting = {}; - m_keyButtons = {}; - m_keyState = {}; - m_game = nullptr; - m_drawJam = false; - m_wiggleTime = 0; - m_wiggleOffset = 0; +GameplayScene::GameplayScene() : Scene::Scene() { + m_keyLighting = {}; + m_keyButtons = {}; + m_keyState = {}; + m_game = nullptr; + m_drawJam = false; + m_counter = 0.0; + lifeFillDuration = 0.0; } -void GameplayScene::Update(double delta) -{ - if (m_resourceFucked) { - if (!m_ended) { - if (EnvironmentSetup::GetInt("Key") >= 0) { - m_ended = true; - SceneManager::ChangeScene(GameScene::SONGSELECT); - } else { - if (MsgBox::GetResult("GameplayError") == 4) { - m_ended = true; - SceneManager::GetInstance()->StopGame(); - } - } - } +GameplayScene::~GameplayScene() = default; - return; +void GameplayScene::Update(double delta) { + if (m_resourceFucked) { + if (!m_ended) { + if (EnvironmentSetup::GetInt("Key") >= 0) { + m_ended = true; + SceneManager::ChangeScene(GameScene::SONGSELECT); + } else { + if (MsgBox::GetResult("GameplayError") == 4) { + m_ended = true; + SceneManager::GetInstance()->StopGame(); + } + } } - if (!m_starting) { - m_starting = true; - m_game->Start(); - } + return; + } - if (m_game->GetState() == GameState::PosGame && !m_ended) { - m_ended = true; - SceneManager::DisplayFade(100, [] { - SceneManager::ChangeScene(GameScene::RESULT); - }); - } + if (!m_starting) { + EnvironmentSetup::SetInt("FillStart", 1); + m_starting = true; + lifeFillDuration = 0.0; + } - int difficulty = EnvironmentSetup::GetInt("Difficulty"); - if (difficulty >= 1 && m_starting) { - float health = m_game->GetScoreManager()->GetLife(); + if (m_starting && m_game->GetState() == GameState::NotGame) { + lifeFillDuration += delta; + if (lifeFillDuration > 1.50 && EnvironmentSetup::GetInt("FillStart") == 1) { + EnvironmentSetup::SetInt("FillStart", 0); + } + if (lifeFillDuration > 2.50) { + m_game->Start(); + } + } - if (health <= 0) { - m_game->Stop(); - } + if (m_game->GetState() == GameState::PosGame && !m_ended) { + m_counter += delta; + m_drawExitButton = false; + m_doExit = false; + if (m_counter > 2.0) { + m_ended = true; + m_counter = 0.0; // Reset + SceneManager::DisplayFade( + 100, [] { SceneManager::ChangeScene(GameScene::RESULT); }); + } + } + + if (m_game->GetState() == GameState::Fail && !m_ended) { + m_ended = true; + m_counter = 0.0; // Reset + SceneManager::DisplayFade( + 100, [] { SceneManager::ChangeScene(GameScene::RESULT); }); + } + + int difficulty = EnvironmentSetup::GetInt("Difficulty"); + float health = m_game->GetScoreManager()->GetLife(); + if (difficulty >= 1 && m_starting) { + if (health <= 0) { + m_game->Fail(); + EnvironmentSetup::SetInt("NowPlaying", 0); + EnvironmentSetup::SetInt("Failed", 1); } + } else { + if (health <= 0) { + EnvironmentSetup::SetInt("Failed", 1); + } + } - if (m_doExit && !m_ended) { - m_ended = true; + if (m_doExit && !m_ended) { + m_ended = true; - auto scores = m_game->GetScoreManager()->GetScore(); + auto scores = m_game->GetScoreManager()->GetScore(); - if (std::get<1>(scores) != 0 || std::get<2>(scores) != 0 || std::get<3>(scores) != 0 || std::get<4>(scores) != 0) { - SceneManager::DisplayFade(100, [] { - SceneManager::ChangeScene(GameScene::RESULT); - }); - } else { - SceneManager::DisplayFade(100, [] { - SceneManager::ChangeScene(GameScene::SONGSELECT); - }); - } + if (std::get<1>(scores) != 0 || std::get<2>(scores) != 0 || + std::get<3>(scores) != 0 || std::get<4>(scores) != 0) { + if (m_game->GetState() == GameState::PosGame) { + EnvironmentSetup::SetInt("Failed", 0); + } else { + EnvironmentSetup::SetInt("Failed", 1); + } + SceneManager::DisplayFade( + 100, [] { SceneManager::ChangeScene(GameScene::RESULT); }); + } else { + SceneManager::DisplayFade( + 100, [] { SceneManager::ChangeScene(GameScene::SONGSELECT); }); } + } - m_exitButtonFunc->Input(delta); - m_game->Update(delta); + m_exitButtonFunc->Input(delta); + m_game->Update(delta); } -void GameplayScene::Render(double delta) -{ - if (m_resourceFucked) { - return; - } +void GameplayScene::Render(double delta) { + if (m_resourceFucked) { + return; + } - ImguiUtil::NewFrame(); + ImguiUtil::NewFrame(); - bool is_flhd_enabled = m_laneHideImage.get() != nullptr; + int arena = EnvironmentSetup::GetInt("Arena"); - int arena = EnvironmentSetup::GetInt("Arena"); + Chart *chart = (Chart *)EnvironmentSetup::GetObj("SONG"); + bool isGameActive = m_game && (m_game->GetState() == GameState::Playing || m_game->GetState() == GameState::PosGame); + bool hasActiveVisuals = isGameActive && (m_videoPlayer && m_videoPlayer->IsValid()); - if (arena != -1) { - m_PlayBG->Draw(); - } else { + if (!hasActiveVisuals) { + if (EnvironmentSetup::GetInt("Background") == 1) { auto songBG = (Texture2D *)EnvironmentSetup::GetObj("SongBackground"); if (songBG) { - songBG->Draw(); + int dim = EnvironmentSetup::GetInt("BackgroundDim"); + if (dim < 0) dim = 0; + if (dim > 100) dim = 100; + int colorVal = (int)(255.0f * (100.0f - dim) / 100.0f); + songBG->TintColor = Color3::FromRGB(colorVal, colorVal, colorVal); + songBG->Draw(); + } + } else if (EnvironmentSetup::GetInt("Background") == 2) { + // Do nothing + } else { + if (m_PlayBG) { + m_PlayBG->Draw(); } + } + } + + if (m_videoPlayer && m_videoPlayer->IsValid() && chart && isGameActive) { + m_videoPlayer->Update(m_game->GetGameAudioPosition() - chart->m_videoOffset); + m_videoPlayer->Render(); + } + + m_Playfield->Draw(); + m_PlayFooter->Draw(); + + if (m_targetBar && m_game) { + int maxFrames = m_targetBar->GetFrameCount(); + if (maxFrames > 0) { + double bpm = m_game->GetCurrentBPM(); + if (bpm > 0.0) { + m_targetBar->SetIndex(m_game->GetBPMAnimationIndex(maxFrames)); + } } - - m_Playfooter->Draw(); - m_Playfield->Draw(); - if (!is_flhd_enabled) { - m_targetBar->Draw(delta); + m_targetBar->Draw(delta); + m_targetBar->AlphaBlend = true; + } else if (m_targetBar) { + m_targetBar->Draw(delta); + m_targetBar->AlphaBlend = true; + } + + for (auto &[lane, pressed] : m_keyState) { + if (pressed) { + m_keyLighting[lane]->AlphaBlend = true; + m_keyLighting[lane]->Draw(delta); + m_keyDowns[lane]->Draw(delta); + } else { + m_keyButtons[lane]->Draw(delta); } - - for (auto &[lane, pressed] : m_keyState) { - if (pressed) { - m_keyLighting[lane]->AlphaBlend = true; - m_keyLighting[lane]->Draw(); - m_keyButtons[lane]->Draw(); - } + } + + m_game->Render(delta); + + // Draw Mods + if (m_noteMod) { + m_noteMod->Draw(); + } + if (m_visualMod) { + m_visualMod->Draw(); + m_laneHideImage->Draw(); + } + + auto scores = m_game->GetScoreManager()->GetScore(); + m_scoreNum->DrawNumber(std::get<0>(scores)); + + // Draw stats + { + m_statsNum->Position = m_statsPos[0]; + m_statsNum->DrawNumber(std::get<1>(scores)); + m_statsNum->Position = m_statsPos[1]; + m_statsNum->DrawNumber(std::get<2>(scores)); + m_statsNum->Position = m_statsPos[2]; + m_statsNum->DrawNumber(std::get<3>(scores)); + m_statsNum->Position = m_statsPos[3]; + m_statsNum->DrawNumber(std::get<4>(scores)); + m_statsNum->Position = m_statsPos[4]; + m_statsNum->DrawNumber(std::get<8>(scores)); + } + + int numOfPills = m_game->GetScoreManager()->GetPills(); + for (int i = 0; i < numOfPills; i++) { + m_pills[i]->Draw(); + } + + if (m_lifeBar && m_game) { + int maxFrames = m_lifeBar->GetFrameCount(); + if (maxFrames > 0) { + double bpm = m_game->GetCurrentBPM(); + if (bpm > 0.0) { + m_lifeBar->SetIndex(m_game->GetBPMAnimationIndex(maxFrames)); + } } + } - m_game->Render(delta); + bool fillstart = EnvironmentSetup::GetInt("FillStart") == 1; - if (is_flhd_enabled) { - m_laneHideImage->Draw(); - m_targetBar->Draw(delta); + if (fillstart) { + float progress = 0.0f; + if (lifeFillDuration > 0.500) { + progress = static_cast((lifeFillDuration - 0.500) / 1.0); } + if (progress > 1.0f) + progress = 1.0f; - auto scores = m_game->GetScoreManager()->GetScore(); - m_scoreNum->DrawNumber(std::get<0>(scores)); - - // Draw stats - { - m_statsNum->Position = m_statsPos[0]; - m_statsNum->DrawNumber(std::get<1>(scores)); - m_statsNum->Position = m_statsPos[1]; - m_statsNum->DrawNumber(std::get<2>(scores)); - m_statsNum->Position = m_statsPos[2]; - m_statsNum->DrawNumber(std::get<3>(scores)); - m_statsNum->Position = m_statsPos[3]; - m_statsNum->DrawNumber(std::get<4>(scores)); - m_statsNum->Position = m_statsPos[4]; - m_statsNum->DrawNumber(std::get<8>(scores)); - } + float fillRatio = progress; + float alpha = 1.0f - fillRatio; - int numOfPills = m_game->GetScoreManager()->GetPills(); - for (int i = 0; i < numOfPills; i++) { - m_pills[i]->Draw(); - } - - auto curLifeTex = m_lifeBar->GetTexture(); // Move lifebar to here so it will not overlapping + auto curLifeTex = m_lifeBar->GetTexture(); curLifeTex->CalculateSize(); Rect rc = {}; - rc.left = (int)curLifeTex->AbsolutePosition.X; - rc.top = (int)curLifeTex->AbsolutePosition.Y; - rc.right = rc.left + (int)curLifeTex->AbsoluteSize.X; - rc.bottom = rc.top + (int)curLifeTex->AbsoluteSize.Y + 5; // Need to add + value because wiggle effect =w= - float alpha = (float)(kMaxLife - m_game->GetScoreManager()->GetLife()) / kMaxLife; - - // Add wiggle effect - float yOffset = 0.0f; - // Wiggle effect after the first second - yOffset = sinf((float)m_game->GetElapsedTime() * 75.0f) * 5.0f; - - int topCur = (int)::round((1.0f - alpha) * rc.top + alpha * rc.bottom); - rc.top = topCur + static_cast(::round(yOffset)); - if (rc.top >= rc.bottom) { - rc.top = rc.bottom - 1; - } + rc.left = static_cast(curLifeTex->AbsolutePosition.X); + rc.top = static_cast(curLifeTex->AbsolutePosition.Y + + curLifeTex->AbsoluteSize.Y * (1.0f - fillRatio)); + rc.right = static_cast(rc.left + curLifeTex->AbsoluteSize.X); + rc.bottom = static_cast(rc.top + curLifeTex->AbsoluteSize.Y); m_lifeBar->Draw(delta, &rc); + } else { - if (m_drawJudge && m_judgement[m_judgeIndex] != nullptr) { - m_judgement[m_judgeIndex]->Size = UDim2::fromScale(m_judgeSize, m_judgeSize); - m_judgement[m_judgeIndex]->AnchorPoint = { 0.5, 0.5 }; - m_judgement[m_judgeIndex]->Draw(); + float alpha = + (float)(kMaxLife - m_game->GetScoreManager()->GetLife()) / kMaxLife; - m_judgeSize = std::clamp(m_judgeSize + (delta * 6), 0.5, 1.0); // Nice - if ((m_judgeTimer += delta) > 0.60) { - m_drawJudge = false; - } - } + auto curLifeTex = m_lifeBar->GetTexture(); + curLifeTex->CalculateSize(); - if (m_drawJam) { - if (std::get<5>(scores) > 0) { - m_jamNum->DrawNumber(std::get<5>(scores)); - m_jamLogo->Draw(delta); - } + float offset = + 10.0f + + (static_cast(rand()) / static_cast(RAND_MAX)) * 5.0f; - if ((m_jamTimer += delta) > 0.60) { - m_drawJam = false; - } - } + Rect rc = {}; + rc.left = static_cast(curLifeTex->AbsolutePosition.X); + rc.top = static_cast(curLifeTex->AbsolutePosition.Y); + rc.right = static_cast(rc.left + curLifeTex->AbsoluteSize.X); + rc.bottom = static_cast(rc.top + curLifeTex->AbsoluteSize.Y); - if (m_drawCombo && std::get<7>(scores) > 0) { - m_amplitude = 30.0; - m_wiggleTime = 60 * m_comboTimer; + double wiggle = + sinf(static_cast(m_game->GetGameFrame()) * 60.0f) * offset; - double decrement = 6.0; // Calculate the decrement for each frame - double totalDecrement = decrement * m_wiggleTime; // Calculate the total decrement based on the current frame/time + int maxTop = static_cast(curLifeTex->AbsolutePosition.Y); + int topCur = (int)::round((1.0f - alpha) * maxTop + alpha * rc.bottom); - double currentAmplitude = m_amplitude - totalDecrement; // Adjust the amplitude based on the total decrement + rc.top = topCur + static_cast(::round(wiggle)); - if (currentAmplitude < 0.0) { - currentAmplitude = 0.0; // Stopper like this due compiler issues - } + if (rc.top < maxTop) { + rc.top = maxTop; + } + if (rc.top >= rc.bottom) { + rc.top = rc.bottom - 1; + } - if (m_comboTimer > 1.0) { // Check if the previous animation hasn't finished - m_comboTimer = 0.0; // Reset the timer - m_amplitude += decrement; // Increase the starting amplitude for the next animation - } + m_lifeBar->Draw(delta, &rc); + } - m_comboLogo->Position2 = UDim2::fromOffset(0, currentAmplitude / 3.0); - m_comboLogo->Draw(delta); + if (m_drawJudge) { + if (m_judgementSprite.find(m_judgeIndex) != m_judgementSprite.end() && m_judgementSprite[m_judgeIndex] != nullptr) { + m_judgementSprite[m_judgeIndex]->AnchorPoint = {0.5, 0.5}; + m_judgementSprite[m_judgeIndex]->DrawOnce(delta); - m_comboNum->Position2 = UDim2::fromOffset(0, currentAmplitude); - m_comboNum->DrawNumber(std::get<7>(scores)); + if ((m_judgeTimer += delta) > 0.60) { + m_drawJudge = false; + } + } else if (m_judgement[m_judgeIndex] != nullptr) { + m_judgement[m_judgeIndex]->Size = + UDim2::fromScale(m_judgeSize, m_judgeSize); + m_judgement[m_judgeIndex]->AnchorPoint = {0.5, 0.5}; + m_judgement[m_judgeIndex]->Draw(); - m_comboTimer += delta; - if (m_comboTimer > 1.0) { - m_comboTimer = 0.0; - m_drawCombo = false; + m_judgeSize = std::clamp(m_judgeSize + (delta * 6), 0.4, 1.0); // Nice + if ((m_judgeTimer += delta) > 0.60) { + m_drawJudge = false; } } + } - if (m_drawLN && std::get<9>(scores) > 0) { // Same Animation logic like DrawCombo - m_amplitude = 5.0; - m_wiggleTime = 60 * m_lnTimer; - - double decrement = 1.0; - double totalDecrement = decrement * m_wiggleTime; - - double currentAmplitude = m_amplitude - totalDecrement; + if (m_drawJam) { + if (std::get<5>(scores) > 0) { + m_jamNum->DrawNumber(std::get<5>(scores)); + m_jamLogo->DrawStop(delta); // Example for DrawStop + } - if (currentAmplitude < 0.0) { - currentAmplitude = 0.0; - } + if ((m_jamTimer += delta) > 0.60) { + m_drawJam = false; + } + } - if (m_lnTimer > 1.0) { - m_lnTimer = 0.0; - m_amplitude += decrement; - } + if (m_drawCombo && + std::get<7>(scores) > 0) { // O2Jam Replication by Albert Frengki! + const double positionStart = 30.0; - m_lnLogo->Position2 = UDim2::fromOffset(0, currentAmplitude); - m_lnLogo->Draw(delta); + // Animate over 0.10 seconds + double t = std::clamp(m_comboTimer / 0.10, 0.0, 1.0); - m_lnComboNum->Position2 = UDim2::fromOffset(0, currentAmplitude); - m_lnComboNum->DrawNumber(std::get<9>(scores)); + // Ease-Out Cubic curve for a fast, clean stop without bouncing + double t_m_1 = t - 1.0; + double easeOut = 1.0 + (t_m_1 * t_m_1 * t_m_1); - m_lnTimer += delta; + double currentPosition = positionStart * (1.0 - easeOut); - if (m_lnTimer > 1.0) { - m_lnTimer = 0.0; - m_drawLN = false; - m_lnLogo->Reset(); - } - } + m_comboLogo->Position2 = UDim2::fromOffset(0, currentPosition / 3.0); + m_comboNum->Position2 = UDim2::fromOffset(0, currentPosition); - float gaugeVal = (float)m_game->GetScoreManager()->GetJamGauge() / kMaxJamGauge; - if (gaugeVal > 0) { - m_jamGauge->CalculateSize(); + m_comboLogo->Draw(delta); + m_comboNum->DrawNumber(std::get<7>(scores)); - int lerp; - if (m_jamGauge->AbsoluteSize.Y > m_jamGauge->AbsoluteSize.X) { - // Fill from bottom to top - lerp = static_cast(std::lerp(0.0, m_jamGauge->AbsoluteSize.Y, (double)gaugeVal)); - Rect rc = { - (int)m_jamGauge->AbsolutePosition.X, - (int)(m_jamGauge->AbsolutePosition.Y + m_jamGauge->AbsoluteSize.Y - lerp), - (int)(m_jamGauge->AbsolutePosition.X + m_jamGauge->AbsoluteSize.X), - (int)(m_jamGauge->AbsolutePosition.Y + m_jamGauge->AbsoluteSize.Y) - }; + m_comboTimer += delta; - m_jamGauge->Draw(&rc); - } else { - // Fill from left to right - lerp = static_cast(std::lerp(0.0, m_jamGauge->AbsoluteSize.X, gaugeVal)); - Rect rc = { - (int)m_jamGauge->AbsolutePosition.X, - (int)m_jamGauge->AbsolutePosition.Y, - (int)(m_jamGauge->AbsolutePosition.X + lerp), - (int)(m_jamGauge->AbsolutePosition.Y + m_jamGauge->AbsoluteSize.Y) - }; - - m_jamGauge->Draw(&rc); - } + if (m_comboTimer >= 1.0) { + m_comboTimer = 0.0; + m_drawCombo = false; } + } - float currentProgress = (float)m_game->GetAudioPosition() / (float)m_game->GetAudioLength(); - if (currentProgress > 0) { - m_waveGage->CalculateSize(); + if (m_drawLN && std::get<9>(scores) > 0) { + const double positionStart = 5.0; - int min = 0, max = (int)m_waveGage->AbsoluteSize.X; - int lerp = (int)std::lerp(min, max, currentProgress); + // Animate over 0.10 seconds + double t = std::clamp(m_lnTimer / 0.10, 0.0, 1.0); - Rect rc = { - (int)m_waveGage->AbsolutePosition.X, - (int)m_waveGage->AbsolutePosition.Y, - (int)(m_waveGage->AbsolutePosition.X + lerp), - (int)(m_waveGage->AbsolutePosition.Y + m_waveGage->AbsoluteSize.Y) - }; + // Ease-Out Cubic curve for a fast, clean stop without bouncing + double t_m_1 = t - 1.0; + double easeOut = 1.0 + (t_m_1 * t_m_1 * t_m_1); - m_waveGage->Draw(&rc); - } + double currentPosition = positionStart * (1.0 - easeOut); - int PlayTime = std::clamp(m_game->GetPlayTime(), 0, INT_MAX); - int currentMinutes = PlayTime / 60; - int currentSeconds = PlayTime % 60; + m_lnLogo->Position2 = UDim2::fromOffset(0, currentPosition); + m_lnLogo->Draw(delta); - m_minuteNum->SetValue(currentMinutes); - m_minuteNum->DrawNumber(currentMinutes); - m_secondNum->SetValue(currentSeconds); - m_secondNum->DrawNumber(currentSeconds); + m_lnComboNum->Position2 = UDim2::fromOffset(0, currentPosition); + m_lnComboNum->DrawNumber(std::get<9>(scores)); - for (int i = 0; i < 7; i++) { - m_hitEffect[i]->Draw(delta); + m_lnTimer += delta; - if (m_drawHold[i]) { - m_holdEffect[i]->Draw(delta); - } + if (m_lnTimer > 1.0) { + m_lnTimer = 0.0; + m_drawLN = false; + } + } + + float gaugeVal = + (float)m_game->GetScoreManager()->GetJamGauge() / kMaxJamGauge; + if (gaugeVal > 0) { + m_jamGauge->CalculateSize(); + + int lerp; + if (m_jamGauge->AbsoluteSize.Y > m_jamGauge->AbsoluteSize.X) { + // Fill from bottom to top + lerp = static_cast( + std::lerp(0.0, m_jamGauge->AbsoluteSize.Y, (double)gaugeVal)); + Rect rc = { + (int)m_jamGauge->AbsolutePosition.X, + (int)(m_jamGauge->AbsolutePosition.Y + m_jamGauge->AbsoluteSize.Y - + lerp), + (int)(m_jamGauge->AbsolutePosition.X + m_jamGauge->AbsoluteSize.X), + (int)(m_jamGauge->AbsolutePosition.Y + m_jamGauge->AbsoluteSize.Y)}; + + m_jamGauge->Draw(&rc); + } else { + // Fill from left to right + lerp = static_cast( + std::lerp(0.0, m_jamGauge->AbsoluteSize.X, gaugeVal)); + Rect rc = { + (int)m_jamGauge->AbsolutePosition.X, + (int)m_jamGauge->AbsolutePosition.Y, + (int)(m_jamGauge->AbsolutePosition.X + lerp), + (int)(m_jamGauge->AbsolutePosition.Y + m_jamGauge->AbsoluteSize.Y)}; + + m_jamGauge->Draw(&rc); } + } + + float currentProgress = + (float)m_game->GetAudioPosition() / (float)m_game->GetAudioLength(); + if (currentProgress > 0) { + m_waveGage->CalculateSize(); + + int min = 0, max = (int)m_waveGage->AbsoluteSize.X; + int lerp = (int)std::lerp(min, max, currentProgress); + + Rect rc = { + (int)m_waveGage->AbsolutePosition.X, + (int)m_waveGage->AbsolutePosition.Y, + (int)(m_waveGage->AbsolutePosition.X + lerp), + (int)(m_waveGage->AbsolutePosition.Y + m_waveGage->AbsoluteSize.Y)}; + + m_waveGage->Draw(&rc); + } + + // Fix if playtime sometimes slighly double draw + int PlayTime = 0; + int currentMinutes = 0 / 60; + int currentSeconds = 0 % 60; + + bool isPlaying = EnvironmentSetup::GetInt("NowPlaying") == 1; + if (isPlaying) // get timer if on play state + { + PlayTime = std::clamp(m_game->GetPlayTime(), 0, INT_MAX); + currentMinutes = PlayTime / 60; + currentSeconds = PlayTime % 60; + } else { // get timer but this while game ended or failed + int lastPlayTime = PlayTime; + currentMinutes = lastPlayTime / 60; + currentSeconds = lastPlayTime % 60; + } + + m_minuteNum->DrawNumber(currentMinutes); + m_secondNum->DrawNumber(currentSeconds); + + for (int i = 0; i < 7; i++) { + if (EnvironmentSetup::GetInt("NowPlaying") == 1) { + if (m_drawHit[i]) { + m_hitEffect[i]->DrawOnce(delta); + } + + if (m_drawHold[i]) { + m_holdEffect[i]->Draw(delta); + } + } else { + m_hitEffect[i]->SetIndex(m_hitEffect[i]->GetFrameCount() - 1); - if (m_drawExitButton) { - m_exitBtn->Draw(); + if (m_drawHold[i]) { + m_holdEffect[i]->SetIndex(m_holdEffect[i]->GetFrameCount() - 1); + } } + } - m_title->Draw(m_game->GetTitle()); + if (m_drawExitButton) { + m_exitBtn->Draw(); + } - if (m_autoPlay) { - m_autoText->Position = m_autoTextPos; - m_autoText->Draw(AUTOPLAY_TEXT); + m_title->Draw(m_game->GetTitle()); - m_autoTextPos.X.Offset -= delta * 50.0; - if (m_autoTextPos.X.Offset < (-m_autoTextSize + 20)) { - m_autoTextPos = UDim2::fromOffset(GameWindow::GetInstance()->GetBufferWidth(), 50); - } - } + m_gameInfo->Position = m_gameInfoPos; + m_gameInfo->Draw(GAMEINFO_TEXT); - int idx = 2; - try { - idx = std::stoi(Configuration::Load("Game", "GuideLine")); - } catch (const std::invalid_argument &) { - idx = 2; + if (m_autoPlay) { + m_autoText->Position = m_autoTextPos; + m_autoText->Draw(AUTOPLAY_TEXT); + + m_autoTextPos.X.Offset -= delta * 30.0; + if (m_autoTextPos.X.Offset < (-m_autoTextSize + 30)) { + m_autoTextPos = + UDim2::fromOffset(GameWindow::GetInstance()->GetBufferWidth(), 60); } + } - m_game->SetGuideLineIndex(idx); + int idx = 2; + try { + idx = std::stoi(Configuration::Load("Game", "GuideLine")); + } catch (const std::invalid_argument &) { + idx = 2; + } + + m_game->SetGuideLineIndex(idx); } -void GameplayScene::Input(double delta) -{ - if (m_resourceFucked) - return; +void GameplayScene::Input(double delta) { + if (m_resourceFucked) + return; - m_game->Input(delta); + m_game->Input(delta); } -void GameplayScene::OnKeyDown(const KeyState &state) -{ - if (m_resourceFucked) - return; +void GameplayScene::OnKeyDown(const KeyState &state) { + if (m_resourceFucked) + return; - m_game->OnKeyDown(state); + m_game->OnKeyDown(state); } -void GameplayScene::OnKeyUp(const KeyState &state) -{ - if (m_resourceFucked) - return; +void GameplayScene::OnKeyUp(const KeyState &state) { + if (m_resourceFucked) + return; - m_game->OnKeyUp(state); + m_game->OnKeyUp(state); } -void GameplayScene::OnMouseDown(const MouseState &state) -{ -} +void GameplayScene::OnMouseDown(const MouseState &state) {} -bool GameplayScene::Attach() -{ - SkinManager::GetInstance()->ReloadSkin(); +bool GameplayScene::Attach() { + SkinManager::GetInstance()->ReloadSkin(); - m_ended = false; - m_starting = false; - m_doExit = false; - m_drawExitButton = false; - m_resourceFucked = false; - m_drawJudge = false; - m_autoPlay = EnvironmentSetup::GetInt("Autoplay") == 1; + m_ended = false; + m_starting = false; + m_doExit = false; + m_drawExitButton = false; + m_resourceFucked = false; + m_drawJudge = false; + lifeFillDuration = 0.0; + m_autoPlay = EnvironmentSetup::GetInt("Autoplay") == 1; - try { - auto manager = SkinManager::GetInstance(); + try { + auto manager = SkinManager::GetInstance(); - int LaneOffset = 5; - int HitPos = 480; + int LaneOffset = 5; + int HitPos = 480; - try { - LaneOffset = std::stoi(manager->GetSkinProp("Game", "LaneOffset", "5")); - HitPos = std::stoi(manager->GetSkinProp("Game", "HitPos", "480")); - } catch (const std::invalid_argument &) { - throw std::runtime_error("Invalid parameter on Skin::Game::LaneOffset or Skin::Game::HitPos"); - } + try { + LaneOffset = std::stoi(manager->GetSkinProp("Game", "LaneOffset", "5")); + HitPos = std::stoi(manager->GetSkinProp("Game", "HitPos", "480")); + } catch (const std::invalid_argument &) { + throw std::runtime_error( + "Invalid parameter on Skin::Game::LaneOffset or Skin::Game::HitPos"); + } - int arena = EnvironmentSetup::GetInt("Arena"); - auto skinPath = manager->GetPath(); // Move to above, make it easiest - auto playingPath = skinPath / "Playing"; - auto arenaPath = playingPath / "Arena"; + int arena = EnvironmentSetup::GetInt("Arena"); + auto skinPath = manager->GetPath(); // Move to above, make it easiest + auto playingPath = skinPath / "Playing"; + auto arenaPath = playingPath / "Arena"; - if (arena == 0 || arena == -1) { - std::random_device dev; - std::mt19937 rng(dev()); + if (arena == 0 || arena == -1) { + std::random_device dev; + std::mt19937 rng(dev()); - std::uniform_int_distribution<> dist(1, 12); + int max_arena = SkinManager::GetInstance()->GetArenas().size() - 1; + if (max_arena < 1) + max_arena = 1; - arena = dist(rng); - } + std::uniform_int_distribution<> dist(1, max_arena); - // HACK: arena -1 is Music Arena - arenaPath /= std::to_string(arena); - EnvironmentSetup::SetInt("CurrentArena", arena); + arena = dist(rng); + } - if (!std::filesystem::exists(arenaPath)) { - throw std::runtime_error("Arena " + std::to_string(arena) + " is missing from folder: " + (playingPath / "Arena").string()); - } - manager->SetKeyCount(7); - manager->Arena_SetIndex(arena != -1 ? arena : 0); + // HACK: arena -1 is Music Arena + auto numberedArenaPath = arenaPath / std::to_string(arena); + if (std::filesystem::exists(numberedArenaPath)) { + arenaPath = numberedArenaPath; + } - for (int i = 0; i < 7; i++) { - m_keyState[i] = false; - m_drawHit[i] = false; - m_drawHold[i] = false; - } + EnvironmentSetup::SetInt("CurrentArena", arena); - m_title = std::make_unique(13); - auto TitlePos = manager->GetPosition(SkinGroup::Playing, "Title"); // conf.GetPosition("Title"); - auto RectPos = manager->GetRect(SkinGroup::Playing, "Title"); // conf.GetRect("Title"); - m_title->Position = UDim2::fromOffset(TitlePos[0].X, TitlePos[0].Y); - m_title->AnchorPoint = { TitlePos[0].AnchorPointX, TitlePos[0].AnchorPointY }; - m_title->Clip = { RectPos[0].X, RectPos[0].Y, RectPos[0].Width, RectPos[0].Height }; + if (!std::filesystem::exists(arenaPath)) { + throw std::runtime_error( + "Arena " + std::to_string(arena) + + " is missing from folder: " + (playingPath / "Arena").string()); + } + manager->SetKeyCount(7); + // Arena Index logic is removed since Arena.ini is merged into Playing.ini - m_autoText = std::make_unique(13); - m_autoTextSize = m_autoText->CalculateSize(AUTOPLAY_TEXT); + for (int i = 0; i < 7; i++) { + m_keyState[i] = false; + m_drawHit[i] = false; + m_drawHold[i] = false; + } - m_autoTextPos = UDim2::fromOffset(GameWindow::GetInstance()->GetBufferWidth(), 50); + m_title = std::make_unique(13); + auto TitlePos = manager->GetPosition(SkinGroup::Playing, + "Title"); // conf.GetPosition("Title"); + auto RectPos = + manager->GetRect(SkinGroup::Playing, "Title"); // conf.GetRect("Title"); + m_title->Position = UDim2::fromOffset(TitlePos[0].X, TitlePos[0].Y); + m_title->AnchorPoint = {TitlePos[0].AnchorPointX, TitlePos[0].AnchorPointY}; + m_title->Clip = {RectPos[0].X, RectPos[0].Y, RectPos[0].Width, + RectPos[0].Height}; + + m_autoText = std::make_unique(12); + m_autoTextSize = m_autoText->CalculateSize(AUTOPLAY_TEXT); + m_autoTextPos = + UDim2::fromOffset(GameWindow::GetInstance()->GetBufferWidth(), 50); + + m_gameInfo = std::make_unique(8); + m_gameInfoSize = m_gameInfo->CalculateSize(GAMEINFO_TEXT); + m_gameInfoPos = + UDim2::fromOffset(/*GameWindow::GetInstance()->GetBufferWidth()*/ 0, + GameWindow::GetInstance()->GetBufferHeight() - 10); + + std::filesystem::path playBgPath = arenaPath / "PlayingBG.png"; + bool hasBg = true; + if (!std::filesystem::exists(playBgPath)) { + Chart *chartCheck = (Chart *)EnvironmentSetup::GetObj("SONG"); + if (chartCheck && !chartCheck->m_backgroundFile.empty()) { + playBgPath = chartCheck->m_beatmapDirectory / chartCheck->m_backgroundFile; + if (!std::filesystem::exists(playBgPath)) { + hasBg = false; + } + } else { + hasBg = false; + } + } - m_PlayBG = std::make_unique(arenaPath / ("PlayingBG.png")); - auto PlayBGPos = manager->Arena_GetPosition("PlayingBG"); // arena_conf.GetPosition("PlayingBG"); + if (hasBg) { + m_PlayBG = std::make_unique(playBgPath); + auto PlayBGPos = manager->GetPosition(SkinGroup::Playing, "PlayingBG"); m_PlayBG->Position = UDim2::fromOffset(PlayBGPos[0].X, PlayBGPos[0].Y); - m_PlayBG->AnchorPoint = { PlayBGPos[0].AnchorPointX, PlayBGPos[0].AnchorPointY }; + m_PlayBG->AnchorPoint = {PlayBGPos[0].AnchorPointX, + PlayBGPos[0].AnchorPointY}; + } else { + m_PlayBG = nullptr; + } + + auto conKeyLight = manager->GetPosition( + SkinGroup::Playing, "KeyLighting"); // conf.GetPosition("KeyLighting"); + auto conKeyButton = manager->GetPosition( + SkinGroup::Playing, "KeyButton"); // conf.GetPosition("KeyButton"); - auto conKeyLight = manager->GetPosition(SkinGroup::Playing, "KeyLighting"); // conf.GetPosition("KeyLighting"); - auto conKeyButton = manager->GetPosition(SkinGroup::Playing, "KeyButton"); // conf.GetPosition("KeyButton"); + if (conKeyLight.size() < 7 || conKeyButton.size() < 7) { + throw std::runtime_error( + "Playing.ini : Positions : KeyLighting#KeyButton : Not enough " + "positions! (count < 7)"); + } - if (conKeyLight.size() < 7 || conKeyButton.size() < 7) { - throw std::runtime_error("Playing.ini : Positions : KeyLighting#KeyButton : Not enough positions! (count < 7)"); + auto playfieldPos = manager->GetPosition( + SkinGroup::Playing, "Playfield"); // conf.GetPosition("Playfield"); + m_Playfield = std::make_unique(playingPath / "Playfield.png"); + m_Playfield->Position = + UDim2::fromOffset(playfieldPos[0].X, playfieldPos[0].Y); + m_Playfield->AnchorPoint = {playfieldPos[0].AnchorPointX, + playfieldPos[0].AnchorPointY}; + + std::string imageFlipStr = manager->GetImageMapping(SkinGroup::Playing, "ImageFlip"); + std::vector imageFlip; + if (!imageFlipStr.empty()) { + auto splits = splitString(imageFlipStr, '|'); + for (auto& s : splits) { + imageFlip.push_back(s == "1" || s == "true" || s == "TRUE"); } + } else { + imageFlip = {false, false, false, false, false, false, false}; + } - auto playfieldPos = manager->GetPosition(SkinGroup::Playing, "Playfield"); // conf.GetPosition("Playfield"); - m_Playfield = std::make_unique(playingPath / "Playfield.png"); - m_Playfield->Position = UDim2::fromOffset(playfieldPos[0].X, playfieldPos[0].Y); - m_Playfield->AnchorPoint = { playfieldPos[0].AnchorPointX, playfieldPos[0].AnchorPointY }; + std::string keyMap[7] = { "1", "2", "1", "S", "1", "2", "1" }; + for (int i = 0; i < 7; i++) { + std::string btnBase = manager->GetImageMapping(SkinGroup::Playing, "KeyButton" + std::to_string(i)); + if (btnBase.empty()) btnBase = "KeyButton" + keyMap[i]; - for (int i = 0; i < 7; i++) { - m_keyLighting[i] = std::move(std::make_unique(playingPath / ("KeyLighting" + std::to_string(i) + ".png"))); - m_keyButtons[i] = std::move(std::make_unique(playingPath / ("KeyButton" + std::to_string(i) + ".png"))); + std::string dwnBase = manager->GetImageMapping(SkinGroup::Playing, "KeyDown" + std::to_string(i)); + if (dwnBase.empty()) dwnBase = "KeyDown" + keyMap[i]; - m_keyLighting[i]->Position = UDim2::fromOffset(conKeyLight[i].X, conKeyLight[i].Y); - m_keyButtons[i]->Position = UDim2::fromOffset(conKeyButton[i].X, conKeyButton[i].Y); - } + std::string lgtBase = manager->GetImageMapping(SkinGroup::Playing, "KeyLighting" + std::to_string(i)); + if (lgtBase.empty()) lgtBase = "KeyLighting" + keyMap[i]; - std::vector numComboPaths = {}; - for (int i = 0; i < 10; i++) { - auto filePath = arenaPath / ("ComboNum" + std::to_string(i) + ".png"); + std::vector btnPaths; + std::vector dwnPaths; + std::vector lgtPaths; - if (!CheckSkinComponent(filePath)) { - std::cout << "Missing: " << filePath.filename() << std::endl; + if (std::filesystem::exists(playingPath / (btnBase + "-0.png"))) { + int frame = 0; + while (std::filesystem::exists(playingPath / (btnBase + "-" + std::to_string(frame) + ".png"))) { + btnPaths.push_back(playingPath / (btnBase + "-" + std::to_string(frame) + ".png")); + frame++; + } + } else { + btnPaths.push_back(playingPath / (btnBase + ".png")); + } - throw std::runtime_error("Failed to load Integer Images 0-9, please check your skin folder."); + if (std::filesystem::exists(playingPath / (dwnBase + "-0.png"))) { + int frame = 0; + while (std::filesystem::exists(playingPath / (dwnBase + "-" + std::to_string(frame) + ".png"))) { + dwnPaths.push_back(playingPath / (dwnBase + "-" + std::to_string(frame) + ".png")); + frame++; } + } else if (std::filesystem::exists(playingPath / (dwnBase + ".png"))) { + dwnPaths.push_back(playingPath / (dwnBase + ".png")); + } else { + dwnPaths = btnPaths; // Fallback to KeyButton if KeyDown doesn't exist + } - numComboPaths.emplace_back(filePath); + if (std::filesystem::exists(playingPath / (lgtBase + "-0.png"))) { + int frame = 0; + while (std::filesystem::exists(playingPath / (lgtBase + "-" + std::to_string(frame) + ".png"))) { + lgtPaths.push_back(playingPath / (lgtBase + "-" + std::to_string(frame) + ".png")); + frame++; + } + } else { + lgtPaths.push_back(playingPath / (lgtBase + ".png")); } - m_comboNum = std::make_unique(numComboPaths); - auto numPos = manager->Arena_GetNumeric("Combo").front(); // arena_conf.GetNumeric("Combo").front(); + m_keyButtons[i] = std::make_unique(btnPaths, 0.016f); + m_keyDowns[i] = std::make_unique(dwnPaths, 0.016f); + m_keyLighting[i] = std::make_unique(lgtPaths, 0.016f); - m_comboNum->Position = UDim2::fromOffset(numPos.X, numPos.Y); - m_comboNum->NumberPosition = IntToPos(numPos.Direction); - m_comboNum->MaxDigits = numPos.MaxDigit; - m_comboNum->FillWithZeros = numPos.FillWithZero; - m_comboNum->AlphaBlend = true; + if (i < imageFlip.size() && imageFlip[i]) { + m_keyButtons[i]->FlipX = true; + m_keyDowns[i]->FlipX = true; + m_keyLighting[i]->FlipX = true; + } - std::vector numJamPaths = {}; - for (int i = 0; i < 10; i++) { - auto filePath = playingPath / ("JamNum" + std::to_string(i) + ".png"); + m_keyLighting[i]->Position = UDim2::fromOffset(conKeyLight[i].X, conKeyLight[i].Y); + m_keyButtons[i]->Position = UDim2::fromOffset(conKeyButton[i].X, conKeyButton[i].Y); + + // Try to get KeyDown position, fallback to KeyButton position if not found + try { + auto conKeyDown = manager->GetPosition(SkinGroup::Playing, "KeyDown"); + m_keyDowns[i]->Position = UDim2::fromOffset(conKeyDown[i].X, conKeyDown[i].Y); + } catch (const std::runtime_error&) { + m_keyDowns[i]->Position = m_keyButtons[i]->Position; + } + } - if (!CheckSkinComponent(filePath)) { - std::cout << "Missing: " << filePath.filename() << std::endl; + std::vector numComboPaths = {}; + for (int i = 0; i < 10; i++) { + auto filePath = arenaPath / ("ComboNum" + std::to_string(i) + ".png"); - throw std::runtime_error("Failed to load Integer Images 0-9, please check your skin folder."); - } + if (!CheckSkinComponent(filePath)) { + std::cout << "Missing: " << filePath.filename() << std::endl; - numJamPaths.emplace_back(filePath); - } + throw std::runtime_error("Failed to load Integer Images 0-9, please " + "check your skin folder."); + } - m_jamNum = std::make_unique(numJamPaths); - numPos = manager->GetNumeric(SkinGroup::Playing, "Jam").front(); + numComboPaths.emplace_back(filePath); + } - m_jamNum->Position = UDim2::fromOffset(numPos.X, numPos.Y); - m_jamNum->NumberPosition = IntToPos(numPos.Direction); - m_jamNum->MaxDigits = numPos.MaxDigit; - m_jamNum->FillWithZeros = numPos.FillWithZero; + m_comboNum = std::make_unique(numComboPaths); + auto numPos = manager->GetNumeric(SkinGroup::Playing, "Combo").front(); - std::vector numScorePaths = {}; - for (int i = 0; i < 10; i++) { - auto filePath = playingPath / ("ScoreNum" + std::to_string(i) + ".png"); + m_comboNum->Position = UDim2::fromOffset(numPos.X, numPos.Y); + m_comboNum->NumberPosition = IntToPos(numPos.Direction); + m_comboNum->MaxDigits = numPos.MaxDigit; + m_comboNum->FillWithZeros = numPos.FillWithZero; + m_comboNum->AlphaBlend = true; - if (!CheckSkinComponent(filePath)) { - std::cout << "Missing: " << filePath.filename() << std::endl; + std::vector numJamPaths = {}; + for (int i = 0; i < 10; i++) { + auto filePath = playingPath / ("JamNum" + std::to_string(i) + ".png"); - throw std::runtime_error("Failed to load Integer Images 0-9, please check your skin folder."); - } + if (!CheckSkinComponent(filePath)) { + std::cout << "Missing: " << filePath.filename() << std::endl; - numScorePaths.emplace_back(filePath); - } + throw std::runtime_error("Failed to load Integer Images 0-9, please " + "check your skin folder."); + } - m_scoreNum = std::make_unique(numScorePaths); - numPos = manager->GetNumeric(SkinGroup::Playing, "Score").front(); // conf.GetNumeric("Score").front(); + numJamPaths.emplace_back(filePath); + } - m_scoreNum->Position = UDim2::fromOffset(numPos.X, numPos.Y); - m_scoreNum->NumberPosition = IntToPos(numPos.Direction); - m_scoreNum->MaxDigits = numPos.MaxDigit; - m_scoreNum->FillWithZeros = numPos.FillWithZero; + m_jamNum = std::make_unique(numJamPaths); + numPos = manager->GetNumeric(SkinGroup::Playing, "Jam").front(); - std::vector judgeFileName = { "Miss", "Bad", "Good", "Cool" }; - auto judgePos = manager->Arena_GetPosition("Judge"); // arena_conf.GetPosition("Judge"); - if (judgePos.size() < 4) { - throw std::runtime_error("Playing.ini : Positions : Judge : Not enough positions! (count < 4)"); - } + m_jamNum->Position = UDim2::fromOffset(numPos.X, numPos.Y); + m_jamNum->NumberPosition = IntToPos(numPos.Direction); + m_jamNum->MaxDigits = numPos.MaxDigit; + m_jamNum->FillWithZeros = numPos.FillWithZero; - for (int i = 0; i < 4; i++) { - auto filePath = arenaPath / ("Judge" + judgeFileName[i] + ".png"); + std::vector numScorePaths = {}; + for (int i = 0; i < 10; i++) { + auto filePath = playingPath / ("ScoreNum" + std::to_string(i) + ".png"); - if (!CheckSkinComponent(filePath)) { - throw std::runtime_error("Failed to load Judge image!"); - } + if (!CheckSkinComponent(filePath)) { + std::cout << "Missing: " << filePath.filename() << std::endl; - m_judgement[i] = std::move(std::make_unique(filePath)); - m_judgement[i]->Position = UDim2::fromOffset(judgePos[i].X, judgePos[i].Y); - m_judgement[i]->AlphaBlend = true; - } + throw std::runtime_error("Failed to load Integer Images 0-9, please " + "check your skin folder."); + } - m_jamGauge = std::make_unique(playingPath / "JamGauge.png"); - auto gaugePos = manager->GetPosition(SkinGroup::Playing, "JamGauge"); // conf.GetPosition("JamGauge"); - if (gaugePos.size() < 1) { - throw std::runtime_error("Playing.ini : Positions : JamGauge : Position Not defined!"); - } + numScorePaths.emplace_back(filePath); + } - m_jamGauge->Position = UDim2::fromOffset(gaugePos[0].X, gaugePos[0].Y); - m_jamGauge->AnchorPoint = { gaugePos[0].AnchorPointX, gaugePos[0].AnchorPointY }; + m_scoreNum = std::make_unique(numScorePaths); + numPos = manager->GetNumeric(SkinGroup::Playing, "Score") + .front(); // conf.GetNumeric("Score").front(); + + m_scoreNum->Position = UDim2::fromOffset(numPos.X, numPos.Y); + m_scoreNum->NumberPosition = IntToPos(numPos.Direction); + m_scoreNum->MaxDigits = numPos.MaxDigit; + m_scoreNum->FillWithZeros = numPos.FillWithZero; + + std::vector judgeFileName = {"Miss", "Bad", "Good", "Cool"}; + auto judgePos = + manager->GetPosition(SkinGroup::Playing, "Judge"); + if (judgePos.size() < 1) { + throw std::runtime_error("Playing.ini : Positions : Judge : Not enough " + "arguments, expected 1 or 4"); + } - auto jamLogoPos = manager->GetSprite(SkinGroup::Playing, "JamLogo"); // conf.GetSprite("JamLogo"); - std::vector jamLogoFileName = {}; - for (int i = 0; i < jamLogoPos.numOfFrames; i++) { - auto filePath = playingPath / ("JamLogo" + std::to_string(i) + ".png"); + for (int i = 0; i < 4; i++) { + auto filePathBase = arenaPath / ("Judge" + judgeFileName[i]); + + int posIndex = (judgePos.size() >= 4) ? i : 0; + + if (std::filesystem::exists(filePathBase.string() + "0.png") || std::filesystem::exists(filePathBase.string() + "-0.png")) { + // Load Animated Sequence + std::vector animPaths; + int frame = 0; + + while (std::filesystem::exists(filePathBase.string() + std::to_string(frame) + ".png") || + std::filesystem::exists(filePathBase.string() + "-" + std::to_string(frame) + ".png")) { + + if (std::filesystem::exists(filePathBase.string() + std::to_string(frame) + ".png")) { + animPaths.push_back(filePathBase.string() + std::to_string(frame) + ".png"); + } else { + animPaths.push_back(filePathBase.string() + "-" + std::to_string(frame) + ".png"); + } + frame++; + } + + float frameTime = 0.02f; + if (judgePos[posIndex].FPS > 0.0f) { + frameTime = 1.0f / judgePos[posIndex].FPS; + } + + m_judgementSprite[i] = std::make_unique(animPaths, frameTime); + m_judgementSprite[i]->Position = UDim2::fromOffset(judgePos[posIndex].X, judgePos[posIndex].Y); + m_judgementSprite[i]->AlphaBlend = true; + } else { + auto filePath = arenaPath / ("Judge" + judgeFileName[i] + ".png"); + + if (!std::filesystem::exists(filePath)) { + throw std::runtime_error("Failed to load Judge image!"); + } + + m_judgement[i] = std::move(std::make_unique(filePath)); + m_judgement[i]->Position = + UDim2::fromOffset(judgePos[posIndex].X, judgePos[posIndex].Y); + m_judgement[i]->AlphaBlend = true; + } + } - if (!CheckSkinComponent(filePath)) { - std::cout << "Missing: " << filePath.filename() << std::endl; + m_jamGauge = std::make_unique(playingPath / "JamGauge.png"); + auto gaugePos = manager->GetPosition( + SkinGroup::Playing, "JamGauge"); // conf.GetPosition("JamGauge"); + if (gaugePos.size() < 1) { + throw std::runtime_error( + "Playing.ini : Positions : JamGauge : Position Not defined!"); + } - throw std::runtime_error("Failed to load Jam Logo image!"); - } + m_jamGauge->Position = UDim2::fromOffset(gaugePos[0].X, gaugePos[0].Y); + m_jamGauge->AnchorPoint = {gaugePos[0].AnchorPointX, + gaugePos[0].AnchorPointY}; + + auto jamLogoPos = manager->GetSprite( + SkinGroup::Playing, "JamLogo"); // conf.GetSprite("JamLogo"); + std::vector jamLogoFileName = {}; + if (std::filesystem::exists(playingPath / "JamLogo-0.png")) { + int frame = 0; + while (std::filesystem::exists(playingPath / ("JamLogo-" + std::to_string(frame) + ".png"))) { + jamLogoFileName.emplace_back(playingPath / ("JamLogo-" + std::to_string(frame) + ".png")); + frame++; + } + } else if (std::filesystem::exists(playingPath / "JamLogo0.png")) { + int frame = 0; + while (std::filesystem::exists(playingPath / ("JamLogo" + std::to_string(frame) + ".png"))) { + jamLogoFileName.emplace_back(playingPath / ("JamLogo" + std::to_string(frame) + ".png")); + frame++; + } + } else if (std::filesystem::exists(playingPath / "JamLogo.png")) { + jamLogoFileName.emplace_back(playingPath / "JamLogo.png"); + } else { + throw std::runtime_error("Failed to load Jam Logo image!"); + } - jamLogoFileName.emplace_back(filePath); + m_jamLogo = std::make_unique(jamLogoFileName, 0.25f); + m_jamLogo->Position = UDim2::fromOffset(jamLogoPos.X, jamLogoPos.Y); + m_jamLogo->AnchorPoint = {(double)jamLogoPos.AnchorPointX, + (double)jamLogoPos.AnchorPointY}; + m_jamLogo->SetFPS(jamLogoPos.FrameTime); + + auto lifeBarPos = manager->GetSprite( + SkinGroup::Playing, "LifeBar"); // conf.GetSprite("LifeBar"); + std::vector lifeBarFileName = {}; + if (std::filesystem::exists(playingPath / "LifeBar-0.png")) { + int frame = 0; + while (std::filesystem::exists(playingPath / ("LifeBar-" + std::to_string(frame) + ".png"))) { + lifeBarFileName.emplace_back(playingPath / ("LifeBar-" + std::to_string(frame) + ".png")); + frame++; + } + } else if (std::filesystem::exists(playingPath / "LifeBar0.png")) { + int frame = 0; + while (std::filesystem::exists(playingPath / ("LifeBar" + std::to_string(frame) + ".png"))) { + lifeBarFileName.emplace_back(playingPath / ("LifeBar" + std::to_string(frame) + ".png")); + frame++; } + } else if (std::filesystem::exists(playingPath / "LifeBar.png")) { + lifeBarFileName.emplace_back(playingPath / "LifeBar.png"); + } else { + throw std::runtime_error("Failed to load Life Bar image!"); + } - m_jamLogo = std::make_unique(jamLogoFileName, 0.25f); - m_jamLogo->Position = UDim2::fromOffset(jamLogoPos.X, jamLogoPos.Y); - m_jamLogo->AnchorPoint = { (double)jamLogoPos.AnchorPointX, (double)jamLogoPos.AnchorPointY }; - m_jamLogo->SetFPS(jamLogoPos.FrameTime); + m_lifeBar = std::make_unique(lifeBarFileName, 0.15f); + m_lifeBar->Position = UDim2::fromOffset(lifeBarPos.X, lifeBarPos.Y); + m_lifeBar->AnchorPoint = {lifeBarPos.AnchorPointX, lifeBarPos.AnchorPointY}; + m_lifeBar->SetFPS(0.0); - auto lifeBarPos = manager->GetSprite(SkinGroup::Playing, "LifeBar"); // conf.GetSprite("LifeBar"); - std::vector lifeBarFileName = {}; - for (int i = 0; i < lifeBarPos.numOfFrames; i++) { - auto filePath = playingPath / ("LifeBar" + std::to_string(i) + ".png"); + std::vector lnComboFileName = {}; + for (int i = 0; i < 10; i++) { + auto filePath = + playingPath / ("LongNoteNum" + std::to_string(i) + ".png"); - if (!CheckSkinComponent(filePath)) { - std::cout << "Missing: " << filePath.filename() << std::endl; - throw std::runtime_error("Failed to load Life Bar image!"); - } + if (!CheckSkinComponent(filePath)) { + std::cout << "Missing: " << filePath.filename() << std::endl; + throw std::runtime_error("Failed to load Long Note Combo image!"); + } - lifeBarFileName.emplace_back(filePath); - } + lnComboFileName.emplace_back(filePath); + } - m_lifeBar = std::make_unique(lifeBarFileName, 0.15f); - m_lifeBar->Position = UDim2::fromOffset(lifeBarPos.X, lifeBarPos.Y); - m_lifeBar->AnchorPoint = { lifeBarPos.AnchorPointX, lifeBarPos.AnchorPointY }; - m_lifeBar->SetFPS(lifeBarPos.FrameTime); + std::vector statsNumFileName = {}; + for (int i = 0; i < 10; i++) { + auto filePath = playingPath / ("StatsNum" + std::to_string(i) + ".png"); - std::vector lnComboFileName = {}; - for (int i = 0; i < 10; i++) { - auto filePath = playingPath / ("LongNoteNum" + std::to_string(i) + ".png"); + if (!CheckSkinComponent(filePath)) { + std::cout << "Missing: " << filePath.filename() << std::endl; + throw std::runtime_error("Failed to load Stats Number image!"); + } - if (!CheckSkinComponent(filePath)) { - std::cout << "Missing: " << filePath.filename() << std::endl; - throw std::runtime_error("Failed to load Long Note Combo image!"); - } + statsNumFileName.emplace_back(filePath); + } - lnComboFileName.emplace_back(filePath); - } + m_statsNum = std::make_unique(statsNumFileName); + auto statsNumPos = manager->GetNumeric( + SkinGroup::Playing, "Stats"); // conf.GetNumeric("Stats"); + if (statsNumPos.size() < 5) { + throw std::runtime_error( + "Playing.ini : Numerics : Stats : Not enough positions! (count < 5)"); + } - std::vector statsNumFileName = {}; - for (int i = 0; i < 10; i++) { - auto filePath = playingPath / ("StatsNum" + std::to_string(i) + ".png"); + m_statsNum->NumberPosition = IntToPos(statsNumPos[0].Direction); + m_statsNum->MaxDigits = statsNumPos[0].MaxDigit; + m_statsNum->FillWithZeros = statsNumPos[0].FillWithZero; + m_statsNum->NumberPosition = IntToPos(statsNumPos[0].Direction); + m_statsNum->AnchorPoint = {0, .5}; - if (!CheckSkinComponent(filePath)) { - std::cout << "Missing: " << filePath.filename() << std::endl; - throw std::runtime_error("Failed to load Stats Number image!"); - } + // COOL: 0 -> MAXCOMBO: 4 + for (int i = 0; i < 5; i++) { + m_statsPos[i] = UDim2::fromOffset(statsNumPos[i].X, statsNumPos[i].Y); + } - statsNumFileName.emplace_back(filePath); - } + m_lnComboNum = std::make_unique(lnComboFileName); + auto lnComboPos = manager->GetNumeric( + SkinGroup::Playing, + "LongNoteCombo"); // conf.GetNumeric("LongNoteCombo"); + if (lnComboPos.size() < 1) { + throw std::runtime_error( + "Playing.ini : Numerics : LongNoteCombo : Position Not defined!"); + } - m_statsNum = std::make_unique(statsNumFileName); - auto statsNumPos = manager->GetNumeric(SkinGroup::Playing, "Stats"); // conf.GetNumeric("Stats"); - if (statsNumPos.size() < 5) { - throw std::runtime_error("Playing.ini : Numerics : Stats : Not enough positions! (count < 5)"); - } + auto btnExitPos = manager->GetPosition( + SkinGroup::Playing, "ExitButton"); // conf.GetPosition("ExitButton"); + auto btnExitRect = + manager->GetRect(SkinGroup::Playing, "Exit"); // conf.GetRect("Exit"); - m_statsNum->NumberPosition = IntToPos(statsNumPos[0].Direction); - m_statsNum->MaxDigits = statsNumPos[0].MaxDigit; - m_statsNum->FillWithZeros = statsNumPos[0].FillWithZero; - m_statsNum->NumberPosition = IntToPos(statsNumPos[0].Direction); - m_statsNum->AnchorPoint = { 0, .5 }; + if (btnExitPos.size() < 1 || btnExitRect.size() < 1) { + throw std::runtime_error( + "Playing.ini : Positions|Rect : Exit : Not defined!"); + } - // COOL: 0 -> MAXCOMBO: 4 - for (int i = 0; i < 5; i++) { - m_statsPos[i] = UDim2::fromOffset(statsNumPos[i].X, statsNumPos[i].Y); + m_exitBtn = std::make_unique(playingPath / "Exit.png"); + m_exitBtn->Position = UDim2::fromOffset( + btnExitRect[0].X, + btnExitRect[0].Y); // Fix Exit not functional with Playing.ini + m_exitBtn->AnchorPoint = {btnExitPos[0].AnchorPointX, + btnExitPos[0].AnchorPointY}; + + auto OnButtonHover = [&](int state) { m_drawExitButton = state; }; + + auto OnButtonClick = [&]() { m_doExit = true; }; + + m_exitButtonFunc = + std::make_unique