diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3fba46b..c07e82a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: - name: Move Assets/Songs folders and bass.dll x64 working-directory: ${{github.workspace}}/build_x64/Encore - run: move Assets Release/Assets ; move Songs Release/Songs ; move bass.dll Release/bass.dll ; move bassopus.dll Release/bassopus.dll ; move discord_game_sdk.dll Release/discord_game_sdk.dll + run: move Assets Release/Assets ; move Songs Release/Songs ; move bass.dll Release/bass.dll ; move bassopus.dll Release/bassopus.dll ; move discord_game_sdk.dll Release/discord_game_sdk.dll ; xcopy /E /I /Y ffmpeg\bin\*.dll Release\ - uses: actions/upload-artifact@v4 with: @@ -56,7 +56,7 @@ jobs: - name: Move Assets/Songs folders and bass.dll x86 working-directory: ${{github.workspace}}/build_x86/Encore - run: move Assets Release/Assets ; move Songs Release/Songs ; move bass.dll Release/bass.dll ; move bassopus.dll Release/bassopus.dll ; move discord_game_sdk.dll Release/discord_game_sdk.dll + run: move Assets Release/Assets ; move Songs Release/Songs ; move bass.dll Release/bass.dll ; move bassopus.dll Release/bassopus.dll ; move discord_game_sdk.dll Release/discord_game_sdk.dll ; xcopy /E /I /Y ffmpeg\bin\*.dll Release\ - uses: actions/upload-artifact@v4 with: @@ -89,7 +89,19 @@ jobs: - name: Remove CMake files x64 working-directory: ${{github.workspace}}/build_linux_x64/Encore - run: rm -r -f CMakeFiles Makefile cmake_install.cmake + run: | + # Remove CMake files + rm -r -f CMakeFiles Makefile cmake_install.cmake + # Copy FFmpeg .so files directly to the current directory + if [ -d "ffmpeg/lib" ]; then + for lib in libavcodec libavformat libavutil libswscale libswresample libavfilter libavdevice; do + find ffmpeg/lib -name "${lib}.so.[0-9]*" ! -name "*.100" ! -name "*.102" -exec cp {} . \; 2>/dev/null || true + done + fi + # Remove the ffmpeg directory to ensure it's not included + rm -rf ffmpeg + # Remove the ffmpeg directory to ensure it's not included + rm -rf ffmpeg # - name: Configure CMake x86 # # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. diff --git a/CHARTS.md b/CHARTS.md index 614b9d9..ed55392 100644 --- a/CHARTS.md +++ b/CHARTS.md @@ -12,7 +12,8 @@ A fully fleshed out `info.json` file will look similar to the following: { "title": "Through the Fire and Flames", "artist": "DragonForce", - "preview_start_time": 41727, + "preview_start_time": 41727, + "video_start_time": 1400, "release_year": "2006", "source": "rb3dlc", "album": "Inhuman Rampage", diff --git a/Encore/CMakeLists.txt b/Encore/CMakeLists.txt index d5eb360..b8810ca 100644 --- a/Encore/CMakeLists.txt +++ b/Encore/CMakeLists.txt @@ -1,4 +1,4 @@ -# CMakeList.txt : CMake project for Encore, include source and define +# CMakeList.txt : CMake project for Encore, include source and define # project specific logic here. # @@ -42,6 +42,22 @@ if (NOT json_FOUND) endif() endif() +if(WIN32) +FetchContent_Declare( +zlib +GIT_REPOSITORY https://github.com/madler/zlib.git +GIT_TAG v1.3.1 +) +set(ZLIB_BUILD_EXAMPLES OFF CACHE BOOL "Z" FORCE) +FetchContent_MakeAvailable(zlib) +endif() + +include(${CMAKE_CURRENT_SOURCE_DIR}/scripts/setup-ffmpeg.cmake) +setup_ffmpeg() +find_ffmpeg_libraries() + + + option(SUPPORT_FILEFORMAT_JPG "Support loading JPG as textures" ON) # Add all subdirectories of src @@ -53,48 +69,71 @@ if (WIN32) else() add_executable(Encore ${SRC_FILES} ${INC_FILES}) endif() -# Temp player file file(COPY "Songs" DESTINATION ${CMAKE_BINARY_DIR}/Encore) file(COPY "Assets" DESTINATION ${CMAKE_BINARY_DIR}/Encore) -# Set the include directory for the executable -target_include_directories(Encore PRIVATE "include" "src") + +target_include_directories(Encore PRIVATE "include" "src" ${FFMPEG_INCLUDE_DIR}) if(WIN32) if (CMAKE_SIZEOF_VOID_P EQUAL 4) find_library(DISCORD_GAME_SDK NAMES discord_game_sdk PATHS "lib/discord-rpc/windows/x86/") - file(COPY "lib/discord-rpc/windows/x86/discord_game_sdk.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/discord-rpc/windows/x86/discord_game_sdk.dll") + file(COPY "lib/discord-rpc/windows/x86/discord_game_sdk.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() find_library(BASS NAMES bass PATHS "lib/bass/windows/x86/") - file(COPY "lib/bass/windows/x86/bass.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/bass/windows/x86/bass.dll") + file(COPY "lib/bass/windows/x86/bass.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() find_library(BASSOPUS NAMES bassopus PATHS "lib/bass/windows/x86/") - file(COPY "lib/bass/windows/x86/bassopus.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/bass/windows/x86/bassopus.dll") + file(COPY "lib/bass/windows/x86/bassopus.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() endif() if (CMAKE_SIZEOF_VOID_P EQUAL 8) find_library(DISCORD_GAME_SDK NAMES discord_game_sdk PATHS "lib/discord-rpc/windows/x64/") - file(COPY "lib/discord-rpc/windows/x64/discord_game_sdk.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/discord-rpc/windows/x64/discord_game_sdk.dll") + file(COPY "lib/discord-rpc/windows/x64/discord_game_sdk.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() find_library(BASS NAMES bass PATHS "lib/bass/windows/x64/") - file(COPY "lib/bass/windows/x64/bass.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/bass/windows/x64/bass.dll") + file(COPY "lib/bass/windows/x64/bass.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() find_library(BASSOPUS NAMES bassopus PATHS "lib/bass/windows/x64/") - file(COPY "lib/bass/windows/x64/bassopus.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/bass/windows/x64/bassopus.dll") + file(COPY "lib/bass/windows/x64/bassopus.dll" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() endif() endif() if(UNIX AND NOT APPLE) if (CMAKE_SIZEOF_VOID_P EQUAL 8) find_library(DISCORD_GAME_SDK NAMES discord_game_sdk PATHS "lib/discord-rpc/linux/x64") - file(COPY "lib/discord-rpc/linux/x64/libdiscord_game_sdk.so" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/discord-rpc/linux/x64/libdiscord_game_sdk.so") + file(COPY "lib/discord-rpc/linux/x64/libdiscord_game_sdk.so" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() find_library(BASS NAMES bass PATHS "lib/bass/linux/x86_64") - file(COPY "lib/bass/linux/x86_64/libbass.so" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/bass/linux/x86_64/libbass.so") + file(COPY "lib/bass/linux/x86_64/libbass.so" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() find_library(BASSOPUS NAMES bassopus PATHS "lib/bass/linux/x86_64") - file(COPY "lib/bass/linux/x86_64/libbassopus.so" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/bass/linux/x86_64/libbassopus.so") + file(COPY "lib/bass/linux/x86_64/libbassopus.so" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath='$ORIGIN'") endif() endif() if(APPLE) find_library(DISCORD_GAME_SDK NAMES discord_game_sdk PATHS "lib/discord-rpc/macos") - file(COPY "lib/discord-rpc/macos/libdiscord_game_sdk.dylib" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/discord-rpc/macos/libdiscord_game_sdk.dylib") + file(COPY "lib/discord-rpc/macos/libdiscord_game_sdk.dylib" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() find_library(BASS NAMES bass PATHS "lib/bass/macos") - file(COPY "lib/bass/macos/libbass.dylib" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/bass/macos/libbass.dylib") + file(COPY "lib/bass/macos/libbass.dylib" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() find_library(BASSOPUS NAMES bassopus PATHS "lib/bass/macos") - file(COPY "lib/bass/macos/libbassopus.dylib" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/bass/macos/libbassopus.dylib") + file(COPY "lib/bass/macos/libbassopus.dylib" DESTINATION ${CMAKE_BINARY_DIR}/Encore) + endif() endif() # Get the current working branch @@ -128,5 +167,74 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE set_property(TARGET Encore PROPERTY CXX_STANDARD 20) -target_link_libraries(Encore raylib nlohmann_json::nlohmann_json ${BASS} ${BASSOPUS} ${DISCORD_GAME_SDK}) + +set(ENCORE_LIBRARIES +raylib +nlohmann_json::nlohmann_json +${BASS} +${BASSOPUS} +${DISCORD_GAME_SDK} +${FFMPEG_LIBRARIES} +) +if(UNIX AND NOT APPLE) +find_package(ZLIB REQUIRED) +list(APPEND ENCORE_LIBRARIES ZLIB::ZLIB) +endif() +if(APPLE) +find_package(ZLIB REQUIRED) +list(APPEND ENCORE_LIBRARIES ZLIB::ZLIB) + + +find_library(COREFOUNDATION_FRAMEWORK CoreFoundation) +find_library(COREMEDIA_FRAMEWORK CoreMedia) +find_library(COREVIDEO_FRAMEWORK CoreVideo) +find_library(AUDIOTOOLBOX_FRAMEWORK AudioToolbox) +find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox) +find_library(SECURITY_FRAMEWORK Security) +find_library(APPKIT_FRAMEWORK AppKit) +find_library(IOKIT_FRAMEWORK IOKit) + +list(APPEND ENCORE_LIBRARIES +${COREFOUNDATION_FRAMEWORK} +${COREMEDIA_FRAMEWORK} +${COREVIDEO_FRAMEWORK} +${AUDIOTOOLBOX_FRAMEWORK} +${VIDEOTOOLBOX_FRAMEWORK} +${SECURITY_FRAMEWORK} +${APPKIT_FRAMEWORK} +${IOKIT_FRAMEWORK} +) + +list(APPEND ENCORE_LIBRARIES "-liconv" "-lbz2" "-llzma") +endif() +if(WIN32) +list(APPEND ENCORE_LIBRARIES zlibstatic bcrypt) +endif() +if(UNIX AND NOT APPLE) +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--no-fatal-warnings") +endif() +if(WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 4) +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SAFESEH:NO") +endif() + +target_link_libraries(Encore PRIVATE ${ENCORE_LIBRARIES}) + +if(UNIX AND NOT APPLE) +set_target_properties(Encore PROPERTIES +BUILD_WITH_INSTALL_RPATH TRUE +INSTALL_RPATH "$ORIGIN" +INSTALL_RPATH_USE_LINK_PATH FALSE +) +endif() + +# Set up runtime output directories +# Set up runtime output directories - all platforms use the same structure +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/Encore) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/Encore) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/Encore) + +# Set up post-build steps +setup_all_post_build(Encore ${CMAKE_BINARY_DIR}/Encore) + + diff --git a/Encore/lib/discord-rpc/windows/x64/discord_game_sdk.dll.lib b/Encore/lib/discord-rpc/windows/x64/discord_game_sdk.lib similarity index 100% rename from Encore/lib/discord-rpc/windows/x64/discord_game_sdk.dll.lib rename to Encore/lib/discord-rpc/windows/x64/discord_game_sdk.lib diff --git a/Encore/lib/discord-rpc/windows/x86/discord_game_sdk.dll.lib b/Encore/lib/discord-rpc/windows/x86/discord_game_sdk.lib similarity index 100% rename from Encore/lib/discord-rpc/windows/x86/discord_game_sdk.dll.lib rename to Encore/lib/discord-rpc/windows/x86/discord_game_sdk.lib diff --git a/Encore/scripts/copy-ffmpeg-libs-macos.cmake b/Encore/scripts/copy-ffmpeg-libs-macos.cmake new file mode 100644 index 0000000..de003a4 --- /dev/null +++ b/Encore/scripts/copy-ffmpeg-libs-macos.cmake @@ -0,0 +1,21 @@ + +set(FFMPEG_LIB_DIR "$ENV{FFMPEG_LIB_DIR}") +set(TARGET_DIR "$ENV{TARGET_DIR}") + +if(FFMPEG_LIB_DIR AND TARGET_DIR) + file(GLOB FFMPEG_DYLIB_FILES "${FFMPEG_LIB_DIR}/*.dylib") + + if(FFMPEG_DYLIB_FILES) + list(LENGTH FFMPEG_DYLIB_FILES NUM_FILES) + message(STATUS "Copying ${NUM_FILES} FFmpeg .dylib files...") + foreach(DYLIB_FILE ${FFMPEG_DYLIB_FILES}) + get_filename_component(FILENAME ${DYLIB_FILE} NAME) + message(STATUS " Copying ${FILENAME}") + file(COPY ${DYLIB_FILE} DESTINATION ${TARGET_DIR}) + endforeach() + else() + message(STATUS "No FFmpeg .dylib files found in ${FFMPEG_LIB_DIR}") + endif() +else() + message(STATUS "FFMPEG_LIB_DIR or TARGET_DIR not set") +endif() \ No newline at end of file diff --git a/Encore/scripts/copy-ffmpeg-libs.cmake b/Encore/scripts/copy-ffmpeg-libs.cmake new file mode 100644 index 0000000..87f505e --- /dev/null +++ b/Encore/scripts/copy-ffmpeg-libs.cmake @@ -0,0 +1,177 @@ +set(FFMPEG_LIB_DIR "$ENV{FFMPEG_LIB_DIR}") +set(TARGET_DIR "$ENV{TARGET_DIR}") + +if(FFMPEG_LIB_DIR AND TARGET_DIR) + + message(STATUS "Checking FFmpeg lib directory: ${FFMPEG_LIB_DIR}") + if(EXISTS ${FFMPEG_LIB_DIR}) + file(GLOB ALL_LIB_FILES "${FFMPEG_LIB_DIR}/*") + message(STATUS "Available files in FFmpeg lib directory:") + foreach(FILE ${ALL_LIB_FILES}) + get_filename_component(FILENAME ${FILE} NAME) + message(STATUS " - ${FILENAME}") + endforeach() + + set(FFMPEG_LIBS "avcodec" "avformat" "avutil" "swscale" "swresample" "avfilter" "avdevice") + set(COPIED_COUNT 0) + + foreach(LIB_BASE ${FFMPEG_LIBS}) + file(GLOB VERSIONED_FILES "${FFMPEG_LIB_DIR}/lib${LIB_BASE}.so.[0-9]*") + foreach(VERSIONED_FILE ${VERSIONED_FILES}) + get_filename_component(LIB_NAME ${VERSIONED_FILE} NAME) + if(NOT LIB_NAME MATCHES "\\.100$" AND NOT LIB_NAME MATCHES "\\.102$") + message(STATUS " Copying versioned file: ${LIB_NAME}") + configure_file(${VERSIONED_FILE} ${TARGET_DIR}/${LIB_NAME} COPYONLY) + + if(EXISTS "${TARGET_DIR}/${LIB_NAME}") + message(STATUS " ✓ Successfully copied to ${TARGET_DIR}/${LIB_NAME}") + math(EXPR COPIED_COUNT "${COPIED_COUNT} + 1") + else() + message(STATUS " ✗ Failed to copy ${LIB_NAME}") + endif() + else() + message(STATUS " Skipping .100 file: ${LIB_NAME}") + endif() + endforeach() + + endforeach() + else() + message(STATUS "FFmpeg lib directory does not exist!") + # Try fallback to local FFmpeg + set(SOURCE_DIR "$ENV{SOURCE_DIR}") + set(LOCAL_FFMPEG_LIB_DIR "${SOURCE_DIR}/lib/ffmpeg/linux/lib") + message(STATUS "Trying fallback local FFmpeg libraries from: ${LOCAL_FFMPEG_LIB_DIR}") + + if(EXISTS ${LOCAL_FFMPEG_LIB_DIR}) + file(GLOB LOCAL_SO_FILES "${LOCAL_FFMPEG_LIB_DIR}/*.so*") + message(STATUS "Local FFmpeg files found:") + foreach(FILE ${LOCAL_SO_FILES}) + get_filename_component(FILENAME ${FILE} NAME) + message(STATUS " - ${FILENAME}") + endforeach() + + set(COPIED_COUNT 0) + foreach(LIB_FILE ${LOCAL_SO_FILES}) + get_filename_component(LIB_NAME ${LIB_FILE} NAME) + + if(LIB_NAME MATCHES "^lib.*\\.so.*$") + message(STATUS " Copying local file: ${LIB_NAME}") + configure_file(${LIB_FILE} ${TARGET_DIR}/${LIB_NAME} COPYONLY) + + if(EXISTS "${TARGET_DIR}/${LIB_NAME}") + message(STATUS " ✓ Successfully copied to ${TARGET_DIR}/${LIB_NAME}") + math(EXPR COPIED_COUNT "${COPIED_COUNT} + 1") + else() + message(STATUS " ✗ Failed to copy ${LIB_NAME}") + endif() + else() + message(STATUS " Skipping: ${LIB_NAME} (not a library file)") + endif() + endforeach() + else() + message(FATAL_ERROR "Neither downloaded nor local FFmpeg lib directory found") + endif() + endif() + + + message(STATUS "Final files in target directory:") + file(GLOB TARGET_FILES "${TARGET_DIR}/lib*.so*") + foreach(FILE ${TARGET_FILES}) + get_filename_component(FILENAME ${FILE} NAME) + if(IS_SYMLINK ${FILE}) + message(STATUS " - ${FILENAME} (symlink)") + else() + message(STATUS " - ${FILENAME}") + endif() + endforeach() + + + + + + find_program(PATCHELF_PROGRAM patchelf) + find_program(CHRPATH_PROGRAM chrpath) + + if(PATCHELF_PROGRAM) + message(STATUS "Fixing RPATH and dependencies on copied libraries...") + file(GLOB COPIED_LIBS "${TARGET_DIR}/lib*.so*") + foreach(LIB_FILE ${COPIED_LIBS}) + get_filename_component(LIB_NAME ${LIB_FILE} NAME) + + + if(NOT IS_SYMLINK ${LIB_FILE}) + message(STATUS " Processing ${LIB_NAME}") + + + execute_process( + COMMAND ${PATCHELF_PROGRAM} --set-rpath '$ORIGIN' ${LIB_FILE} + RESULT_VARIABLE RPATH_RESULT + OUTPUT_QUIET + ERROR_QUIET + ) + + if(RPATH_RESULT EQUAL 0) + message(STATUS " ✓ RPATH set successfully") + else() + message(STATUS " Warning: Failed to set RPATH") + endif() + + + + endif() + endforeach() + elseif(CHRPATH_PROGRAM) + message(STATUS "Using chrpath to fix RPATH...") + file(GLOB COPIED_LIBS "${TARGET_DIR}/lib*.so.[0-9]*") + foreach(LIB_FILE ${COPIED_LIBS}) + get_filename_component(LIB_NAME ${LIB_FILE} NAME) + message(STATUS " Setting RPATH on ${LIB_NAME}") + + execute_process( + COMMAND ${CHRPATH_PROGRAM} -r '$ORIGIN' ${LIB_FILE} + RESULT_VARIABLE RPATH_RESULT + OUTPUT_QUIET + ERROR_QUIET + ) + + if(RPATH_RESULT EQUAL 0) + message(STATUS " ✓ RPATH set successfully") + else() + message(STATUS " Warning: Failed to set RPATH") + endif() + endforeach() + else() + message(STATUS "Neither patchelf nor chrpath found") + message(STATUS "Install patchelf for better library compatibility") + + + message(STATUS "Creating LD_LIBRARY_PATH wrapper script...") + set(WRAPPER_SCRIPT "${TARGET_DIR}/run_encore.sh") + file(WRITE ${WRAPPER_SCRIPT} "#!/bin/bash\n") + file(APPEND ${WRAPPER_SCRIPT} "export LD_LIBRARY_PATH=\"$(dirname \"$0\"):$LD_LIBRARY_PATH\"\n") + file(APPEND ${WRAPPER_SCRIPT} "exec \"$(dirname \"$0\")/Encore\" \"$@\"\n") + + execute_process( + COMMAND chmod +x ${WRAPPER_SCRIPT} + RESULT_VARIABLE CHMOD_RESULT + ) + + if(CHMOD_RESULT EQUAL 0) + message(STATUS "✓ Created wrapper script: run_encore.sh") + message(STATUS " Use ./run_encore.sh instead of ./Encore to run the application") + endif() + endif() + + + + message(STATUS "Final files in target directory:") + file(GLOB TARGET_SO_FILES "${TARGET_DIR}/lib*.so*") + foreach(FILE ${TARGET_SO_FILES}) + get_filename_component(FILENAME ${FILE} NAME) + message(STATUS " - ${FILENAME}") + endforeach() + + message(STATUS "Copied ${COPIED_COUNT} FFmpeg .so files and created symlinks") +else() + message(STATUS "FFMPEG_LIB_DIR or TARGET_DIR not set") +endif() \ No newline at end of file diff --git a/Encore/scripts/setup-ffmpeg.cmake b/Encore/scripts/setup-ffmpeg.cmake new file mode 100644 index 0000000..bd5bce1 --- /dev/null +++ b/Encore/scripts/setup-ffmpeg.cmake @@ -0,0 +1,345 @@ + +function(setup_ffmpeg) + set(FFMPEG_VERSION "6.0") + + if(WIN32) + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(FFMPEG_URL "https://github.com/JaydenzKoci/ffmpeg/releases/download/release/ffmpeg-windows-x64.zip") + set(PLATFORM_NAME "Windows x64") + else() + set(FFMPEG_URL "https://github.com/JaydenzKoci/ffmpeg/releases/download/release/ffmpeg-windows-x86.zip") + set(PLATFORM_NAME "Windows x86") + endif() + set(LIB_PREFIX "") + set(LIB_SUFFIX ".lib") + set(SHARED_SUFFIX ".dll") + set(USE_DOWNLOAD TRUE) + elseif(APPLE) + set(FFMPEG_URL "https://github.com/JaydenzKoci/ffmpeg/releases/download/release/ffmpeg-macos.zip") + set(PLATFORM_NAME "macOS") + set(LIB_PREFIX "lib") + set(LIB_SUFFIX ".dylib") + set(SHARED_SUFFIX ".dylib") + set(USE_DOWNLOAD TRUE) + elseif(UNIX) + set(FFMPEG_URL "https://github.com/JaydenzKoci/ffmpeg/releases/download/release/ffmpeg-linux-x64.zip") + set(PLATFORM_NAME "Linux x64") + set(LIB_PREFIX "lib") + set(LIB_SUFFIX ".so") + set(SHARED_SUFFIX ".so") + set(USE_DOWNLOAD TRUE) + endif() + + message(STATUS "Setting up FFmpeg for ${PLATFORM_NAME}") + + if(USE_DOWNLOAD) + set(FFMPEG_INSTALL_DIR "${CMAKE_CURRENT_BINARY_DIR}/ffmpeg" PARENT_SCOPE) + + include(FetchContent) + FetchContent_Declare( + ffmpeg_prebuilt + URL ${FFMPEG_URL} + DOWNLOAD_EXTRACT_TIMESTAMP OFF + ) + + FetchContent_GetProperties(ffmpeg_prebuilt) + if(NOT ffmpeg_prebuilt_POPULATED) + message(STATUS "Downloading FFmpeg prebuilt binaries from ${FFMPEG_URL}...") + FetchContent_MakeAvailable(ffmpeg_prebuilt) + + file(COPY ${ffmpeg_prebuilt_SOURCE_DIR}/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/ffmpeg) + message(STATUS "FFmpeg setup complete for ${PLATFORM_NAME}") + endif() + + set(FFMPEG_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/ffmpeg/include" PARENT_SCOPE) + set(FFMPEG_LIB_DIR "${CMAKE_CURRENT_BINARY_DIR}/ffmpeg/lib" PARENT_SCOPE) + set(FFMPEG_BIN_DIR "${CMAKE_CURRENT_BINARY_DIR}/ffmpeg/bin" PARENT_SCOPE) + else() + if(APPLE) + set(FFMPEG_LOCAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/lib/ffmpeg/macos") + elseif(UNIX) + set(FFMPEG_LOCAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/lib/ffmpeg/linux") + else() + set(FFMPEG_LOCAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/lib/ffmpeg/windows") + endif() + + # Check if local FFmpeg exists + if(NOT EXISTS "${FFMPEG_LOCAL_DIR}") + message(FATAL_ERROR "FFmpeg not found at ${FFMPEG_LOCAL_DIR}. Please ensure FFmpeg is installed in the lib folder with the expected structure.") + endif() + + message(STATUS "Using local FFmpeg from: ${FFMPEG_LOCAL_DIR}") + + # Set the output variables to point to local FFmpeg + set(FFMPEG_INCLUDE_DIR "${FFMPEG_LOCAL_DIR}/include" PARENT_SCOPE) + set(FFMPEG_LIB_DIR "${FFMPEG_LOCAL_DIR}/lib" PARENT_SCOPE) + set(FFMPEG_BIN_DIR "${FFMPEG_LOCAL_DIR}/bin" PARENT_SCOPE) + endif() + set(FFMPEG_LIB_PREFIX "${LIB_PREFIX}" PARENT_SCOPE) + set(FFMPEG_LIB_SUFFIX "${LIB_SUFFIX}" PARENT_SCOPE) + set(FFMPEG_SHARED_SUFFIX "${SHARED_SUFFIX}" PARENT_SCOPE) + +endfunction() + +function(find_ffmpeg_libraries) + if(APPLE) + # For macOS, search in the specified paths first, then allow default paths + find_library(AVFORMAT_LIB + NAMES ${FFMPEG_LIB_PREFIX}avformat${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + PATH_SUFFIXES lib + ) + find_library(AVCODEC_LIB + NAMES ${FFMPEG_LIB_PREFIX}avcodec${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + PATH_SUFFIXES lib + ) + find_library(AVUTIL_LIB + NAMES ${FFMPEG_LIB_PREFIX}avutil${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + PATH_SUFFIXES lib + ) + find_library(SWSCALE_LIB + NAMES ${FFMPEG_LIB_PREFIX}swscale${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + PATH_SUFFIXES lib + ) + find_library(SWRESAMPLE_LIB + NAMES ${FFMPEG_LIB_PREFIX}swresample${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + PATH_SUFFIXES lib + ) + find_library(AVFILTER_LIB + NAMES ${FFMPEG_LIB_PREFIX}avfilter${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + PATH_SUFFIXES lib + ) + find_library(AVDEVICE_LIB + NAMES ${FFMPEG_LIB_PREFIX}avdevice${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + PATH_SUFFIXES lib + ) + find_library(POSTPROC_LIB + NAMES ${FFMPEG_LIB_PREFIX}postproc${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + PATH_SUFFIXES lib + ) + else() + find_library(AVFORMAT_LIB + NAMES ${FFMPEG_LIB_PREFIX}avformat${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + NO_DEFAULT_PATH + ) + find_library(AVCODEC_LIB + NAMES ${FFMPEG_LIB_PREFIX}avcodec${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + NO_DEFAULT_PATH + ) + find_library(AVUTIL_LIB + NAMES ${FFMPEG_LIB_PREFIX}avutil${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + NO_DEFAULT_PATH + ) + find_library(SWSCALE_LIB + NAMES ${FFMPEG_LIB_PREFIX}swscale${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + NO_DEFAULT_PATH + ) + find_library(SWRESAMPLE_LIB + NAMES ${FFMPEG_LIB_PREFIX}swresample${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + NO_DEFAULT_PATH + ) + find_library(AVFILTER_LIB + NAMES ${FFMPEG_LIB_PREFIX}avfilter${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + NO_DEFAULT_PATH + ) + find_library(AVDEVICE_LIB + NAMES ${FFMPEG_LIB_PREFIX}avdevice${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + NO_DEFAULT_PATH + ) + find_library(POSTPROC_LIB + NAMES ${FFMPEG_LIB_PREFIX}postproc${FFMPEG_LIB_SUFFIX} + PATHS ${FFMPEG_LIB_DIR} + NO_DEFAULT_PATH + ) + endif() + + # Debug output for macOS + if(APPLE) + message(STATUS "FFmpeg library search results:") + message(STATUS " AVFORMAT_LIB: ${AVFORMAT_LIB}") + message(STATUS " AVCODEC_LIB: ${AVCODEC_LIB}") + message(STATUS " AVUTIL_LIB: ${AVUTIL_LIB}") + message(STATUS " SWSCALE_LIB: ${SWSCALE_LIB}") + message(STATUS " FFMPEG_LIB_DIR: ${FFMPEG_LIB_DIR}") + message(STATUS " FFMPEG_LIB_PREFIX: ${FFMPEG_LIB_PREFIX}") + message(STATUS " FFMPEG_LIB_SUFFIX: ${FFMPEG_LIB_SUFFIX}") + + # Debug: List what's actually in the lib directory + if(EXISTS ${FFMPEG_LIB_DIR}) + file(GLOB LIB_FILES "${FFMPEG_LIB_DIR}/*") + message(STATUS "Files in FFMPEG_LIB_DIR:") + foreach(FILE ${LIB_FILES}) + message(STATUS " ${FILE}") + endforeach() + else() + message(STATUS "FFMPEG_LIB_DIR does not exist: ${FFMPEG_LIB_DIR}") + endif() + endif() + + if(NOT AVFORMAT_LIB OR NOT AVCODEC_LIB OR NOT AVUTIL_LIB OR NOT SWSCALE_LIB) + if(APPLE) + # Try using pkg-config as fallback for macOS + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + message(STATUS "Trying pkg-config as fallback...") + pkg_check_modules(PC_AVFORMAT QUIET libavformat) + pkg_check_modules(PC_AVCODEC QUIET libavcodec) + pkg_check_modules(PC_AVUTIL QUIET libavutil) + pkg_check_modules(PC_SWSCALE QUIET libswscale) + pkg_check_modules(PC_SWRESAMPLE QUIET libswresample) + pkg_check_modules(PC_AVFILTER QUIET libavfilter) + pkg_check_modules(PC_AVDEVICE QUIET libavdevice) + + if(PC_AVFORMAT_FOUND AND PC_AVCODEC_FOUND AND PC_AVUTIL_FOUND AND PC_SWSCALE_FOUND) + set(AVFORMAT_LIB ${PC_AVFORMAT_LIBRARIES}) + set(AVCODEC_LIB ${PC_AVCODEC_LIBRARIES}) + set(AVUTIL_LIB ${PC_AVUTIL_LIBRARIES}) + set(SWSCALE_LIB ${PC_SWSCALE_LIBRARIES}) + if(PC_SWRESAMPLE_FOUND) + set(SWRESAMPLE_LIB ${PC_SWRESAMPLE_LIBRARIES}) + endif() + if(PC_AVFILTER_FOUND) + set(AVFILTER_LIB ${PC_AVFILTER_LIBRARIES}) + endif() + if(PC_AVDEVICE_FOUND) + set(AVDEVICE_LIB ${PC_AVDEVICE_LIBRARIES}) + endif() + message(STATUS "Found FFmpeg libraries via pkg-config") + endif() + endif() + endif() + + if(NOT AVFORMAT_LIB OR NOT AVCODEC_LIB OR NOT AVUTIL_LIB OR NOT SWSCALE_LIB) + message(STATUS "Final FFmpeg library search results:") + message(STATUS " AVFORMAT_LIB: ${AVFORMAT_LIB}") + message(STATUS " AVCODEC_LIB: ${AVCODEC_LIB}") + message(STATUS " AVUTIL_LIB: ${AVUTIL_LIB}") + message(STATUS " SWSCALE_LIB: ${SWSCALE_LIB}") + message(STATUS " FFMPEG_LIB_DIR: ${FFMPEG_LIB_DIR}") + message(STATUS " FFMPEG_LIB_PREFIX: ${FFMPEG_LIB_PREFIX}") + message(STATUS " FFMPEG_LIB_SUFFIX: ${FFMPEG_LIB_SUFFIX}") + message(FATAL_ERROR "Required FFmpeg libraries not found!") + endif() + endif() + + set(FFMPEG_LIBRARIES_LIST) + + if(AVDEVICE_LIB) + list(APPEND FFMPEG_LIBRARIES_LIST ${AVDEVICE_LIB}) + endif() + if(AVFILTER_LIB) + list(APPEND FFMPEG_LIBRARIES_LIST ${AVFILTER_LIB}) + endif() + list(APPEND FFMPEG_LIBRARIES_LIST ${AVFORMAT_LIB}) + list(APPEND FFMPEG_LIBRARIES_LIST ${AVCODEC_LIB}) + if(POSTPROC_LIB) + list(APPEND FFMPEG_LIBRARIES_LIST ${POSTPROC_LIB}) + endif() + if(SWRESAMPLE_LIB) + list(APPEND FFMPEG_LIBRARIES_LIST ${SWRESAMPLE_LIB}) + endif() + list(APPEND FFMPEG_LIBRARIES_LIST ${SWSCALE_LIB}) + list(APPEND FFMPEG_LIBRARIES_LIST ${AVUTIL_LIB}) + + set(FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES_LIST} PARENT_SCOPE) + + message(STATUS "Found FFmpeg libraries:") + message(STATUS " AVFORMAT: ${AVFORMAT_LIB}") + message(STATUS " AVCODEC: ${AVCODEC_LIB}") + message(STATUS " AVUTIL: ${AVUTIL_LIB}") + message(STATUS " SWSCALE: ${SWSCALE_LIB}") + if(SWRESAMPLE_LIB) + message(STATUS " SWRESAMPLE: ${SWRESAMPLE_LIB}") + endif() + if(AVFILTER_LIB) + message(STATUS " AVFILTER: ${AVFILTER_LIB}") + endif() + if(AVDEVICE_LIB) + message(STATUS " AVDEVICE: ${AVDEVICE_LIB}") + endif() + if(POSTPROC_LIB) + message(STATUS " POSTPROC: ${POSTPROC_LIB}") + endif() +endfunction() + +function(copy_ffmpeg_binaries TARGET_DIR) + if(WIN32) + file(GLOB FFMPEG_BINARIES "${FFMPEG_BIN_DIR}/*${FFMPEG_SHARED_SUFFIX}") + if(FFMPEG_BINARIES) + file(COPY ${FFMPEG_BINARIES} DESTINATION ${TARGET_DIR}) + list(LENGTH FFMPEG_BINARIES DLL_COUNT) + message(STATUS "Copied ${DLL_COUNT} FFmpeg DLLs to ${TARGET_DIR}") + + foreach(DLL ${FFMPEG_BINARIES}) + get_filename_component(DLL_NAME ${DLL} NAME) + message(STATUS " - ${DLL_NAME}") + endforeach() + else() + message(WARNING "No FFmpeg DLLs found in ${FFMPEG_BIN_DIR}") + endif() + elseif(UNIX) + file(GLOB FFMPEG_BINARIES "${FFMPEG_LIB_DIR}/*${FFMPEG_SHARED_SUFFIX}*") + if(FFMPEG_BINARIES) + file(COPY ${FFMPEG_BINARIES} DESTINATION ${TARGET_DIR}) + message(STATUS "Copied FFmpeg shared libraries to ${TARGET_DIR}") + endif() + endif() +endfunction() + +function(setup_all_post_build TARGET_NAME TARGET_DIR) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "Copying assets and dependencies..." + COMMAND ${CMAKE_COMMAND} -E make_directory "${TARGET_DIR}" + ) + + # Copy assets (Songs and Assets folders) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/Songs" + "${TARGET_DIR}/Songs" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/Assets" + "${TARGET_DIR}/Assets" + ) + + if(WIN32) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "Copying Windows-specific libraries..." + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${FFMPEG_BIN_DIR}" + "${TARGET_DIR}" + COMMENT "Copying Windows dependencies to output directory" + ) + elseif(UNIX AND NOT APPLE) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "Copying FFmpeg .so files..." + COMMAND ${CMAKE_COMMAND} -E env FFMPEG_LIB_DIR=${FFMPEG_LIB_DIR} TARGET_DIR=${TARGET_DIR} SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/scripts/copy-ffmpeg-libs.cmake + COMMENT "Copying Linux dependencies to output directory" + ) + elseif(APPLE) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "Copying FFmpeg libraries from ${FFMPEG_LIB_DIR}..." + COMMAND ${CMAKE_COMMAND} -E env FFMPEG_LIB_DIR=${FFMPEG_LIB_DIR} TARGET_DIR=${TARGET_DIR} ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/scripts/copy-ffmpeg-libs-macos.cmake + COMMENT "Copying macOS dependencies to output directory" + ) + endif() +endfunction() + +# Legacy function for backward compatibility +function(setup_ffmpeg_post_build TARGET_NAME TARGET_DIR) + setup_all_post_build(${TARGET_NAME} ${TARGET_DIR}) +endfunction() \ No newline at end of file diff --git a/Encore/src/gameplay/video.cpp b/Encore/src/gameplay/video.cpp new file mode 100644 index 0000000..e8c3a75 --- /dev/null +++ b/Encore/src/gameplay/video.cpp @@ -0,0 +1,5 @@ +// +// Created by Jaydenz on 9/4/2025. +// + +#include "video.h" \ No newline at end of file diff --git a/Encore/src/gameplay/video.h b/Encore/src/gameplay/video.h new file mode 100644 index 0000000..04fac0f --- /dev/null +++ b/Encore/src/gameplay/video.h @@ -0,0 +1,391 @@ +#ifndef VIDEO_STREAM_PLAYER_H +#define VIDEO_STREAM_PLAYER_H + +#include "raylib.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "settings.h" + +namespace Encore::RhythmEngine { +// Check if FFmpeg is available +#ifdef __has_include + #if __has_include() + #define FFMPEG_AVAILABLE 1 + #else + #define FFMPEG_AVAILABLE 0 + #endif +#else + #define FFMPEG_AVAILABLE 1 // Assume available if __has_include not supported +#endif + +#if FFMPEG_AVAILABLE +extern "C" { +#include +#include +#include +#include +} +#endif + +#if FFMPEG_AVAILABLE +class VideoStream { +public: + VideoStream() = default; + ~VideoStream() { + Unload(); + } + + bool Load(const std::filesystem::path& videoPath) { + if (isLoaded) { + TraceLog(LOG_WARNING, "VIDEO: A video is already loaded. Call Unload() first."); + return true; + } + + const std::string pathStr = videoPath.string(); + + if (!std::filesystem::exists(videoPath)) { + TraceLog(LOG_INFO, "VIDEO: No video file found at: %s", pathStr.c_str()); + return false; + } + + if (avformat_open_input(&pFormatCtx, pathStr.c_str(), NULL, NULL) != 0) { + TraceLog(LOG_ERROR, "FFMPEG: Couldn't open video file: %s", pathStr.c_str()); + return false; + } + + if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { + TraceLog(LOG_ERROR, "FFMPEG: Couldn't find stream information."); + avformat_close_input(&pFormatCtx); + return false; + } + + videoStreamIndex = -1; + for (unsigned int i = 0; i < pFormatCtx->nb_streams; i++) { + if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + videoStreamIndex = i; + break; + } + } + if (videoStreamIndex == -1) { + TraceLog(LOG_ERROR, "FFMPEG: Didn't find a video stream."); + avformat_close_input(&pFormatCtx); + return false; + } + + AVCodecParameters* pCodecPar = pFormatCtx->streams[videoStreamIndex]->codecpar; + const AVCodec* pCodec = avcodec_find_decoder(pCodecPar->codec_id); + if (pCodec == NULL) { + TraceLog(LOG_ERROR, "FFMPEG: Unsupported codec!"); + avformat_close_input(&pFormatCtx); + return false; + } + + pCodecCtx = avcodec_alloc_context3(pCodec); + if (avcodec_parameters_to_context(pCodecCtx, pCodecPar) < 0) { + TraceLog(LOG_ERROR, "FFMPEG: Couldn't copy codec context."); + avcodec_free_context(&pCodecCtx); + avformat_close_input(&pFormatCtx); + return false; + } + + if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) { + TraceLog(LOG_ERROR, "FFMPEG: Could not open codec."); + avcodec_free_context(&pCodecCtx); + avformat_close_input(&pFormatCtx); + return false; + } + + width = pCodecCtx->width; + height = pCodecCtx->height; + AVRational time_base = pFormatCtx->streams[videoStreamIndex]->avg_frame_rate; + fps = (time_base.num > 0 && time_base.den > 0) ? static_cast(time_base.num) / time_base.den : 30.0; + + display_width = width; + display_height = height; + if (height > 500) { + float aspect = (float)width / (float)height; + display_height = 500; + display_width = (int)(display_height * aspect); + } + + sws_ctx = sws_getContext(width, height, pCodecCtx->pix_fmt, display_width, display_height, AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL); + if (sws_ctx == NULL) { + TraceLog(LOG_ERROR, "FFMPEG: Could not initialize the conversion context."); + Unload(); + return false; + } + + Image blankImage = GenImageColor(display_width, display_height, BLANK); + displayTexture = LoadTextureFromImage(blankImage); + UnloadImage(blankImage); + SetTextureFilter(displayTexture, TEXTURE_FILTER_BILINEAR); + + isLoaded = true; + stopDecoder = false; + decoderThread = std::thread(&VideoStream::DecodeLoop, this); + + TraceLog(LOG_INFO, "VIDEO: Stream loaded successfully (%dx%d @ %f fps), downscaled to %dx%d", width, height, fps, display_width, display_height); + return true; + } + + void Unload() { + if (!isLoaded) return; + + isPlaying = false; + stopDecoder = true; + frameQueueCond.notify_all(); + if (decoderThread.joinable()) { + decoderThread.join(); + } + + if(sws_ctx) sws_freeContext(sws_ctx); + if(pCodecCtx) avcodec_free_context(&pCodecCtx); + if(pFormatCtx) avformat_close_input(&pFormatCtx); + + std::lock_guard lock(queueMutex); + while(!frameQueue.empty()){ + av_frame_free(&frameQueue.front()); + frameQueue.pop(); + } + + if (displayTexture.id > 0) UnloadTexture(displayTexture); + + pFormatCtx = nullptr; pCodecCtx = nullptr; sws_ctx = nullptr; + displayTexture = { 0 }; + isLoaded = false; + } + + void Update() { + if (!isLoaded) return; + + if (delayedStart) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - playStartTime).count(); + if (elapsed >= startDelayMs) { + isPlaying = true; + delayedStart = false; + } else { + return; + } + } + + if (!isPlaying) return; + + frameTimer += GetFrameTime(); + double frameDuration = (fps > 0) ? (1.0 / fps) : 0.033; + + if (frameTimer >= frameDuration) { + frameTimer -= frameDuration; + + std::unique_lock lock(queueMutex); + if (!frameQueue.empty()) { + AVFrame* frame = frameQueue.front(); + frameQueue.pop(); + lock.unlock(); + frameQueueCond.notify_one(); + + uint8_t* frameBuffer[4] = { nullptr }; + int linesize[4] = { 0 }; + av_image_alloc(frameBuffer, linesize, display_width, display_height, AV_PIX_FMT_RGBA, 1); + sws_scale(sws_ctx, (uint8_t const* const*)frame->data, frame->linesize, 0, height, frameBuffer, linesize); + UpdateTexture(displayTexture, frameBuffer[0]); + av_freep(&frameBuffer[0]); + av_frame_free(&frame); + } + } + } + + void Draw(int posX = 0, int posY = 0, Color tint = WHITE) { + if (isLoaded && displayTexture.id > 0) { + float screenWidth = (float)GetScreenWidth(); + float screenHeight = (float)GetScreenHeight(); + float screenAspect = screenWidth / screenHeight; + float videoAspect = (float)display_width / (float)display_height; + + Rectangle sourceRec = { 0.0f, 0.0f, (float)display_width, (float)display_height }; + Rectangle destRec; + + if (screenAspect > videoAspect) { + float scale = screenWidth / (float)display_width; + float scaledHeight = (float)display_height * scale; + destRec = { 0, (screenHeight - scaledHeight) / 2.0f, screenWidth, scaledHeight }; + } else { + float scale = screenHeight / (float)display_height; + float scaledWidth = (float)display_width * scale; + destRec = { (screenWidth - scaledWidth) / 2.0f, 0, scaledWidth, screenHeight }; + } + + DrawTexturePro(displayTexture, sourceRec, destRec, {0, 0}, 0.0f, tint); + } + } + + void Play() { + if (isLoaded) { + isPlaying = true; + startDelayMs = 0.0; + playStartTime = std::chrono::steady_clock::now(); + } + } + void PlayWithDelay(double delayMs) { + if (isLoaded) { + startDelayMs = delayMs; + playStartTime = std::chrono::steady_clock::now(); + isPlaying = false; + delayedStart = true; + } + } + void Pause() { + if (isLoaded) { + isPlaying = false; + delayedStart = false; + } + } + void Resume() { + if (isLoaded) { + if (startDelayMs > 0.0) { + PlayWithDelay(startDelayMs); + } else { + Play(); + } + } + } + void Stop() { + if (isLoaded) { + isPlaying = false; + delayedStart = false; + std::lock_guard lock(seekMutex); + seekRequest = true; + seekToTime = 0.0; + frameQueueCond.notify_all(); + } + } + + bool IsLoaded() const { return isLoaded; } + +private: + void DecodeLoop() { + AVPacket* packet = av_packet_alloc(); + const size_t maxQueueSize = 30; + + while (!stopDecoder) { + { + std::lock_guard lock(seekMutex); + if (seekRequest) { + // Convert time to timestamp + int64_t timestamp = (int64_t)(seekToTime * AV_TIME_BASE); + if (pFormatCtx->streams[videoStreamIndex]->time_base.den != 0) { + timestamp = av_rescale_q(timestamp, AV_TIME_BASE_Q, pFormatCtx->streams[videoStreamIndex]->time_base); + } + + av_seek_frame(pFormatCtx, videoStreamIndex, timestamp, AVSEEK_FLAG_BACKWARD); + avcodec_flush_buffers(pCodecCtx); + + std::lock_guard queueLock(queueMutex); + while(!frameQueue.empty()){ + av_frame_free(&frameQueue.front()); + frameQueue.pop(); + } + seekRequest = false; + frameQueueCond.notify_all(); + } + } + + if (!isPlaying) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; + } + + { + std::unique_lock lock(queueMutex); + frameQueueCond.wait(lock, [this, maxQueueSize] { return frameQueue.size() < maxQueueSize || stopDecoder; }); + } + if (stopDecoder) break; + + if (av_read_frame(pFormatCtx, packet) >= 0) { + if (packet->stream_index == videoStreamIndex) { + if (avcodec_send_packet(pCodecCtx, packet) == 0) { + while (true) { + AVFrame* decoded_frame = av_frame_alloc(); + int ret = avcodec_receive_frame(pCodecCtx, decoded_frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + av_frame_free(&decoded_frame); + break; + } else if (ret < 0) { + TraceLog(LOG_ERROR, "FFMPEG: Error during decoding."); + av_frame_free(&decoded_frame); + break; + } + + std::lock_guard lock(queueMutex); + frameQueue.push(decoded_frame); + } + } + } + av_packet_unref(packet); + } else { + std::lock_guard lock(seekMutex); + seekRequest = true; + } + } + av_packet_free(&packet); + } + + AVFormatContext *pFormatCtx = nullptr; + AVCodecContext *pCodecCtx = nullptr; + SwsContext *sws_ctx = nullptr; + int videoStreamIndex = -1; + + Texture2D displayTexture = { 0 }; + + std::thread decoderThread; + std::queue frameQueue; + std::mutex queueMutex; + std::condition_variable frameQueueCond; + std::mutex seekMutex; + std::atomic seekRequest{false}; + std::atomic stopDecoder{false}; + double seekToTime = 0.0; + double startDelayMs = 0.0; + std::chrono::steady_clock::time_point playStartTime; + std::atomic delayedStart{false}; + + int width = 0, height = 0; + int display_width = 0, display_height = 0; + double fps = 30.0; + double frameTimer = 0.0; + std::atomic isLoaded{false}; + std::atomic isPlaying{false}; +}; + +#else +class VideoStream { +public: + VideoStream() = default; + ~VideoStream() = default; + + bool Load(const std::filesystem::path& videoPath) { + TraceLog(LOG_INFO, "VIDEO: FFmpeg not available, video playback disabled"); + return false; + } + void Unload() {} + void Update() {} + void Draw(int posX = 0, int posY = 0, Color tint = WHITE) {} + void Play() {} + void PlayWithDelay(double delayMs) {} + void Pause() {} + void Resume() {} + void Stop() {} + bool IsLoaded() const { return false; } +}; +#endif +} +#endif // VIDEO_H \ No newline at end of file diff --git a/Encore/src/menus/GameplayMenu.cpp b/Encore/src/menus/GameplayMenu.cpp index fbf4741..6951233 100644 --- a/Encore/src/menus/GameplayMenu.cpp +++ b/Encore/src/menus/GameplayMenu.cpp @@ -22,9 +22,13 @@ #include "settings-old.h" #include "settings.h" #include "timingvalues.h" +#include "gameplay/video.h" +#include #include +Encore::RhythmEngine::VideoStream backgroundVideo; + GameplayMenu::GameplayMenu() {} GameplayMenu::~GameplayMenu() {} @@ -501,8 +505,15 @@ void GameplayMenu::Draw() { // OvershellRenderer osr; double curTime = GetTime(); - // IMAGE BACKGROUNDS?????? ClearBackground(BLACK); + backgroundVideo.Update(); + + if (backgroundVideo.IsLoaded()) { + backgroundVideo.Draw(0, 0, WHITE); + } else { + GameMenu::DrawAlbumArtBackground(TheSongList.curSong->albumArtBlur); + } + unsigned char BackgroundColor = 0; // if (ThePlayerManager.BandStats->PlayersInOverdrive > 0) { // BackgroundColor = BeatToCharViaTickThing(TheSongTime.GetCurrentTick(), 0, 8, 960); @@ -516,7 +527,6 @@ void GameplayMenu::Draw() { } std::array grybo = { GREEN, RED, YELLOW, BLUE, ORANGE }; std::array orybg = { ORANGE, RED, YELLOW, BLUE, GREEN }; - GameMenu::DrawAlbumArtBackground(TheSongList.curSong->albumArtBlur); DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color { 0, 0, 0, 128 }); DrawRectangle( 0, 0, GetScreenWidth(), GetScreenHeight(), Color { 255, 255, 255, BackgroundColor } @@ -978,24 +988,127 @@ void GameplayMenu::Draw() { //} } } - } else { - if (chart->at(0).empty()) { - continue; - } - // if the size of the chart is bigger than the selected "range" - int NotePoolStart = - std::distance(chart->at(0).begin(), chart->CurrentNoteIterators.at(0)); - int NotePoolEnd = NOTE_POOL_SIZE - + std::distance(chart->at(0).begin(), chart->CurrentNoteIterators.at(0)); - NotePoolEnd = - chart->at(0).size() > NotePoolEnd ? NotePoolEnd : chart->at(0).size(); - int NotePoolSize = NotePoolEnd - NotePoolStart; - - if (!chart->rolls.empty()) { - for (auto &roll : chart->rolls) { + } else { + if (chart->at(0).empty()) { + continue; + } + int NotePoolStart = + std::distance(chart->at(0).begin(), chart->CurrentNoteIterators.at(0)); + int NotePoolEnd = NOTE_POOL_SIZE + + std::distance(chart->at(0).begin(), chart->CurrentNoteIterators.at(0)); + NotePoolEnd = + chart->at(0).size() > NotePoolEnd ? NotePoolEnd : chart->at(0).size(); + int NotePoolSize = NotePoolEnd - NotePoolStart; + + if (!chart->rolls.empty()) { + for (auto &roll : chart->rolls) { + int ScrollPos = -1 + * GetNotePos( + roll.StartSec, + TheSongTime.GetElapsedTime(), + FakeStrikeline / 2, + FakeStrikeline + ); + + int NoteLength = -1 + * GetNotePos( + roll.StartSec + roll.EndSec, + TheSongTime.GetElapsedTime(), + FakeStrikeline / 2, + FakeStrikeline + ); + + int ScrollEndPos = ScrollPos - NoteLength; + uint8_t x = roll.lane; + while (x) { + uint8_t y = x & ~(x - 1); + int pos = TrackLeft; + Color color = GREEN; + if (y == Encore::RhythmEngine::PlasticFrets[1]) { + pos += NoteXWidth; + color = RED; + } else if (y == Encore::RhythmEngine::PlasticFrets[2]) { + pos += NoteXWidth * 2; + color = YELLOW; + } else if (y == Encore::RhythmEngine::PlasticFrets[3]) { + pos += NoteXWidth * 3; + color = BLUE; + } else if (y == Encore::RhythmEngine::PlasticFrets[4]) { + pos += NoteXWidth * 4; + color = ORANGE; + } + color = ColorBrightness(color, -0.75); + DrawRectangle(pos, NoteLength, NoteXWidth, ScrollEndPos, color); + x &= (x - 1); + } + } + } + if (!chart->trills.empty()) { + for (auto &trill : chart->trills) { + int ScrollPos = -1 + * GetNotePos( + trill.StartSec, + TheSongTime.GetElapsedTime(), + FakeStrikeline / 2, + FakeStrikeline + ); + + int NoteLength = -1 + * GetNotePos( + trill.StartSec + trill.EndSec, + TheSongTime.GetElapsedTime(), + FakeStrikeline / 2, + FakeStrikeline + ); + + int ScrollEndPos = ScrollPos - NoteLength; + int pos = TrackLeft; + Color color = GREEN; + if (trill.lane1 == Encore::RhythmEngine::PlasticFrets[1]) { + pos += NoteXWidth; + color = RED; + } else if (trill.lane1 == Encore::RhythmEngine::PlasticFrets[2]) { + pos += NoteXWidth * 2; + color = YELLOW; + } else if (trill.lane1 == Encore::RhythmEngine::PlasticFrets[3]) { + pos += NoteXWidth * 3; + color = BLUE; + } else if (trill.lane1 == Encore::RhythmEngine::PlasticFrets[4]) { + pos += NoteXWidth * 4; + color = ORANGE; + } + color = ColorBrightness(color, -0.5); + DrawRectangle(pos, NoteLength, NoteXWidth, ScrollEndPos, color); + color = GREEN; + pos = TrackLeft; + if (trill.lane2 == Encore::RhythmEngine::PlasticFrets[1]) { + pos += NoteXWidth; + color = RED; + } else if (trill.lane2 == Encore::RhythmEngine::PlasticFrets[2]) { + pos += NoteXWidth * 2; + color = YELLOW; + } else if (trill.lane2 == Encore::RhythmEngine::PlasticFrets[3]) { + pos += NoteXWidth * 3; + color = BLUE; + } else if (trill.lane2 == Encore::RhythmEngine::PlasticFrets[4]) { + pos += NoteXWidth * 4; + color = ORANGE; + } + color = ColorBrightness(color, -0.75); + DrawRectangle(pos, NoteLength, NoteXWidth, ScrollEndPos, color); + } + } + // because i have to do bounds checks myself + // std::span NotePool { chart->CurrentNoteIterators.at(0), + // chart->CurrentNoteIterators.at(0) + NotePoolSize }; for (auto ¬e : + // NotePool) { + + for (int curNote = NotePoolStart; curNote < NotePoolEnd; curNote++) { + auto ¬e = chart->at(0).at(curNote); + // basic miss check, only delays for showing misses int ScrollPos = -1 * GetNotePos( - roll.StartSec, + note.StartSeconds, TheSongTime.GetElapsedTime(), FakeStrikeline / 2, FakeStrikeline @@ -1003,14 +1116,26 @@ void GameplayMenu::Draw() { int NoteLength = -1 * GetNotePos( - roll.StartSec + roll.EndSec, + note.StartSeconds + note.LengthSeconds, TheSongTime.GetElapsedTime(), FakeStrikeline / 2, FakeStrikeline ); int ScrollEndPos = ScrollPos - NoteLength; - uint8_t x = roll.lane; + bool sust = false; + int sustLength = ScrollPos - NoteHeight; + if (note.LengthTicks == 0) { + NoteLength = ScrollPos - NoteHeight; + ScrollEndPos = NoteHeight; + } else { + sust = true; + } + int ScrollStartPos = ScrollPos; + + uint8_t x = note.Lane; + + DrawRectangle(0, NoteXWidth, NoteXWidth, NoteXWidth * 2, GREEN); while (x) { uint8_t y = x & ~(x - 1); int pos = TrackLeft; @@ -1028,17 +1153,30 @@ void GameplayMenu::Draw() { pos += NoteXWidth * 4; color = ORANGE; } - color = ColorBrightness(color, -0.75); - DrawRectangle(pos, NoteLength, NoteXWidth, ScrollEndPos, color); + if (note.NotePassed) + color = MAROON; + // DrawRectangle(pos, NoteLength, NoteXWidth, ScrollEndPos, color); + if (sust) { + DrawRectangle(pos + (NoteXWidth/4), NoteLength, NoteXWidth/2, ScrollEndPos, color); + } + DrawRectangle(pos, sustLength, NoteXWidth, NoteHeight, color); + if (note.NoteType == 1) { + DrawRectangle( + pos + 5, + sustLength + 5, + NoteXWidth - 10, + NoteHeight - 10, + WHITE + ); + } x &= (x - 1); } } - } - if (!chart->trills.empty()) { - for (auto &trill : chart->trills) { + if (chart->HeldNotePointers.at(0)) { + auto ¬e = chart->HeldNotePointers.at(0); int ScrollPos = -1 * GetNotePos( - trill.StartSec, + note->StartSeconds, TheSongTime.GetElapsedTime(), FakeStrikeline / 2, FakeStrikeline @@ -1046,177 +1184,48 @@ void GameplayMenu::Draw() { int NoteLength = -1 * GetNotePos( - trill.StartSec + trill.EndSec, + note->StartSeconds + note->LengthSeconds, TheSongTime.GetElapsedTime(), FakeStrikeline / 2, FakeStrikeline ); + int ScrollEndPos = FakeStrikeline - NoteLength; + int ScrollStartPos = ScrollPos; - int ScrollEndPos = ScrollPos - NoteLength; - int pos = TrackLeft; - Color color = GREEN; - if (trill.lane1 == Encore::RhythmEngine::PlasticFrets[1]) { - pos += NoteXWidth; - color = RED; - } else if (trill.lane1 == Encore::RhythmEngine::PlasticFrets[2]) { - pos += NoteXWidth * 2; - color = YELLOW; - } else if (trill.lane1 == Encore::RhythmEngine::PlasticFrets[3]) { - pos += NoteXWidth * 3; - color = BLUE; - } else if (trill.lane1 == Encore::RhythmEngine::PlasticFrets[4]) { - pos += NoteXWidth * 4; - color = ORANGE; - } - color = ColorBrightness(color, -0.5); - DrawRectangle(pos, NoteLength, NoteXWidth, ScrollEndPos, color); - color = GREEN; - pos = TrackLeft; - if (trill.lane2 == Encore::RhythmEngine::PlasticFrets[1]) { - pos += NoteXWidth; - color = RED; - } else if (trill.lane2 == Encore::RhythmEngine::PlasticFrets[2]) { - pos += NoteXWidth * 2; - color = YELLOW; - } else if (trill.lane2 == Encore::RhythmEngine::PlasticFrets[3]) { - pos += NoteXWidth * 3; - color = BLUE; - } else if (trill.lane2 == Encore::RhythmEngine::PlasticFrets[4]) { - pos += NoteXWidth * 4; - color = ORANGE; - } - color = ColorBrightness(color, -0.75); - DrawRectangle(pos, NoteLength, NoteXWidth, ScrollEndPos, color); - } - } - // because i have to do bounds checks myself - // std::span NotePool { chart->CurrentNoteIterators.at(0), - // chart->CurrentNoteIterators.at(0) + NotePoolSize }; for (auto ¬e : - // NotePool) { - - for (int curNote = NotePoolStart; curNote < NotePoolEnd; curNote++) { - auto ¬e = chart->at(0).at(curNote); - // basic miss check, only delays for showing misses - int ScrollPos = -1 - * GetNotePos( - note.StartSeconds, - TheSongTime.GetElapsedTime(), - FakeStrikeline / 2, - FakeStrikeline - ); - - int NoteLength = -1 - * GetNotePos( - note.StartSeconds + note.LengthSeconds, - TheSongTime.GetElapsedTime(), - FakeStrikeline / 2, - FakeStrikeline - ); - - int ScrollEndPos = ScrollPos - NoteLength; - bool sust = false; - int sustLength = ScrollPos - NoteHeight; - if (note.LengthTicks == 0) { - NoteLength = ScrollPos - NoteHeight; - ScrollEndPos = NoteHeight; - } else { - sust = true; - } - int ScrollStartPos = ScrollPos; - - uint8_t x = note.Lane; - - DrawRectangle(0, NoteXWidth, NoteXWidth, NoteXWidth * 2, GREEN); - while (x) { - uint8_t y = x & ~(x - 1); - int pos = TrackLeft; - Color color = GREEN; - if (y == Encore::RhythmEngine::PlasticFrets[1]) { - pos += NoteXWidth; - color = RED; - } else if (y == Encore::RhythmEngine::PlasticFrets[2]) { - pos += NoteXWidth * 2; - color = YELLOW; - } else if (y == Encore::RhythmEngine::PlasticFrets[3]) { - pos += NoteXWidth * 3; - color = BLUE; - } else if (y == Encore::RhythmEngine::PlasticFrets[4]) { - pos += NoteXWidth * 4; - color = ORANGE; - } - if (note.NotePassed) - color = MAROON; - // DrawRectangle(pos, NoteLength, NoteXWidth, ScrollEndPos, color); - if (sust) { - DrawRectangle(pos + (NoteXWidth/4), NoteLength, NoteXWidth/2, ScrollEndPos, color); - } - DrawRectangle(pos, sustLength, NoteXWidth, NoteHeight, color); - if (note.NoteType == 1) { - DrawRectangle( - pos + 5, - sustLength + 5, - NoteXWidth - 10, - NoteHeight - 10, - WHITE - ); - } - x &= (x - 1); - } - } - if (chart->HeldNotePointers.at(0)) { - auto ¬e = chart->HeldNotePointers.at(0); - int ScrollPos = -1 - * GetNotePos( - note->StartSeconds, - TheSongTime.GetElapsedTime(), - FakeStrikeline / 2, - FakeStrikeline - ); - - int NoteLength = -1 - * GetNotePos( - note->StartSeconds + note->LengthSeconds, - TheSongTime.GetElapsedTime(), - FakeStrikeline / 2, - FakeStrikeline - ); - int ScrollEndPos = FakeStrikeline - NoteLength; - int ScrollStartPos = ScrollPos; - - uint8_t x = note->Lane; - while (x) { - uint8_t y = x & ~(x - 1); - int pos = TrackLeft; - Color color = GREEN; - if (y == Encore::RhythmEngine::PlasticFrets[1]) { - pos += NoteXWidth; - color = RED; - } else if (y == Encore::RhythmEngine::PlasticFrets[2]) { - pos += NoteXWidth * 2; - color = YELLOW; - } else if (y == Encore::RhythmEngine::PlasticFrets[3]) { - pos += NoteXWidth * 3; - color = BLUE; - } else if (y == Encore::RhythmEngine::PlasticFrets[4]) { - pos += NoteXWidth * 4; - color = ORANGE; + uint8_t x = note->Lane; + while (x) { + uint8_t y = x & ~(x - 1); + int pos = TrackLeft; + Color color = GREEN; + if (y == Encore::RhythmEngine::PlasticFrets[1]) { + pos += NoteXWidth; + color = RED; + } else if (y == Encore::RhythmEngine::PlasticFrets[2]) { + pos += NoteXWidth * 2; + color = YELLOW; + } else if (y == Encore::RhythmEngine::PlasticFrets[3]) { + pos += NoteXWidth * 3; + color = BLUE; + } else if (y == Encore::RhythmEngine::PlasticFrets[4]) { + pos += NoteXWidth * 4; + color = ORANGE; + } + if (note->NotePassed) + color = MAROON; + DrawRectangle(pos + (NoteXWidth / 4), NoteLength, NoteXWidth/2, ScrollEndPos, color); + //if (note->NoteType == 1) { + // DrawRectangle( + // pos + 5, + // NoteLength + 5, + // NoteXWidth - 10, + // ScrollEndPos - 10, + // WHITE + // ); + //} + x &= (x - 1); } - if (note->NotePassed) - color = MAROON; - DrawRectangle(pos + (NoteXWidth / 4), NoteLength, NoteXWidth/2, ScrollEndPos, color); - //if (note->NoteType == 1) { - // DrawRectangle( - // pos + 5, - // NoteLength + 5, - // NoteXWidth - 10, - // ScrollEndPos - 10, - // WHITE - // ); - //} - x &= (x - 1); } } - } bool maxmult = player.engine->stats->SixMultiplier ? player.engine->stats->multNoOD() >= 5 : player.engine->stats->multNoOD() == 4; @@ -1300,7 +1309,7 @@ void GameplayMenu::Draw() { 10, WHITE ); - } + } if (player.engine.get()->stats.get()->strumState == Encore::RhythmEngine::StrumState::DownStrum) { DrawRectangle( @@ -1310,7 +1319,7 @@ void GameplayMenu::Draw() { 10, WHITE ); - } + } if (player.engine.get()->stats.get()->AudioMuted) { int InstrumentNum = @@ -1379,6 +1388,7 @@ void GameplayMenu::Draw() { TheGameRenderer.LowerHighway(); } if (TheSongTime.SongComplete()) { + TheGameRenderer.backgroundVideo.Unload(); TheSongList.curSong->LoadAlbumArt(); TheGameRenderer.midiLoaded = false; TheGameRenderer.highwayInAnimation = false; @@ -1602,8 +1612,8 @@ void GameplayMenu::Draw() { 35, -SongNameWidth ); - } else if (curTime > TheSongTime.GetStartTime() + 7.5 + SongNameDuration) - SongNameAlpha = 0; + } else if (curTime > TheSongTime.GetStartTime() + 7.5 + SongNameDuration) + SongNameAlpha = 0; if (curTime > TheSongTime.GetStartTime() + 7.75 && curTime < TheSongTime.GetStartTime() + 7.75 + SongNameDuration) { @@ -1627,7 +1637,7 @@ void GameplayMenu::Draw() { 35, -SongArtistWidth ); - } + } if (curTime > TheSongTime.GetStartTime() + 8 && curTime < TheSongTime.GetStartTime() + 8 + SongNameDuration) { double timeSinceStart = GetTime() - (TheSongTime.GetStartTime() + 8); @@ -1659,7 +1669,7 @@ void GameplayMenu::Draw() { 35, -SongExtrasWidth ); - } + } if (curTime < TheSongTime.GetStartTime() + 7.75 + SongNameDuration) { DrawRectangleGradientH( 0, @@ -1732,6 +1742,7 @@ void GameplayMenu::Draw() { if (GuiButton(ResumeBox, "Resume")) { TheAudioManager.unpauseStreams(); TheSongTime.Resume(); + TheGameRenderer.backgroundVideo.Resume(); ThePlayerManager.BandStats->Paused = false; for (int playerNum = 0; playerNum < ThePlayerManager.PlayersActive; playerNum++) { @@ -1739,6 +1750,7 @@ void GameplayMenu::Draw() { } } if (GuiButton(RestartBox, "Restart")) { + TheGameRenderer.backgroundVideo.Stop(); TheSongTime.Reset(); TheGameRenderer.highwayInAnimation = false; TheGameRenderer.highwayInEndAnim = false; @@ -1762,10 +1774,7 @@ void GameplayMenu::Draw() { ThePlayerManager.BandStats->Paused = false; } if (GuiButton(QuitBox, "Back to Music Library")) { - // notes = - // TheSongList.curSong->parts[instrument]->charts[diff].notes.size(); - // notes = TheSongList.curSong->parts[instrument]->charts[diff]; - + TheGameRenderer.backgroundVideo.Unload(); TheSongList.curSong->LoadAlbumArt(); // ThePlayerManager.BandStats->ResetBandGameplayStats(); // TheGameRenderer.midiLoaded = false; @@ -1867,133 +1876,6 @@ void GameplayMenu::Draw() { */ GameMenu::DrawFPS(u.LeftSide, u.hpct(0.0025f) + u.hinpct(0.025f)); GameMenu::DrawVersion(); - /* - if (!ThePlayerManager.BandStats->Multiplayer - && ThePlayerManager.GetActivePlayer(0).stats->Health <= 0) { - TheSongList.curSong->LoadAlbumArt(); - ThePlayerManager.BandStats->ResetBandGameplayStats(); - TheGameRenderer.midiLoaded = false; - TheSongTime.Reset(); - - TheAudioManager.unloadStreams(); - TheGameRenderer.highwayInAnimation = false; - TheGameRenderer.highwayInEndAnim = false; - TheGameRenderer.songPlaying = false; - - TheSongList.curSong->parts[ThePlayerManager.GetActivePlayer(0).Instrument] - ->charts[ThePlayerManager.GetActivePlayer(0).Difficulty] - .resetNotes(); - ThePlayerManager.GetActivePlayer(0).stats->Quit = true; - TheMenuManager.SwitchScreen(RESULTS); - } - // if (!TheGameRenderer.bot) - // DrawTextEx(assets.rubikBold, TextFormat("%s", player.FC ? "FC" : ""), - // {5, GetScreenHeight() - u.hinpct(0.05f)}, u.hinpct(0.04), 0, - // GOLD); - // if (TheGameRenderer.bot) - // DrawTextEx(assets.rubikBold, "BOT", - // {5, GetScreenHeight() - u.hinpct(0.05f)}, u.hinpct(0.04), 0, - // SKYBLUE); - // if (!TheGameRenderer.bot) - /* - GuiProgressBar( - Rectangle { 0, - (float)GetScreenHeight() - u.hinpct(0.005f), - (float)GetScreenWidth(), - u.hinpct(0.01f) }, - "", - "", - &floatSongLength, - 0, - (float)songLength - ); - - std::string ScriptDisplayString = ""; - lua.script_file("scripts/testing.lua"); - ScriptDisplayString = lua["TextDisplay"]; - DrawTextEx(assets.rubikBold, ScriptDisplayString.c_str(), - {5, GetScreenHeight() - u.hinpct(0.1f)}, - u.hinpct(0.04), 0, GOLD); - - - if (ThePlayerManager.PlayersActive) { - DrawRectangle( - u.wpct(0.5f) - (u.winpct(0.12f) / 2), - u.hpct(0.02f) - u.winpct(0.01f), - u.winpct(0.12f), - u.winpct(0.065f), - DARKGRAY - ); - for (int fretBox = 0; - fretBox < ThePlayerManager.GetActivePlayer(0).stats->HeldFrets.size(); - fretBox++) { - float leftInputBoxSize = (5 * u.winpct(0.02f)) / 2; - - Color fretColor; - switch (fretBox) { - default: - fretColor = BROWN; - break; - case (0): - fretColor = GREEN; - break; - case (1): - fretColor = RED; - break; - case (2): - fretColor = YELLOW; - break; - case (3): - fretColor = BLUE; - break; - case (4): - fretColor = ORANGE; - break; - } - - DrawRectangle( - u.wpct(0.5f) - leftInputBoxSize + (fretBox * u.winpct(0.02f)), - u.hpct(0.02f), - u.winpct(0.02f), - u.winpct(0.02f), - ThePlayerManager.GetActivePlayer(0).stats->HeldFrets[fretBox] - || - ThePlayerManager.GetActivePlayer(0).stats->HeldFretsAlt[fretBox] ? fretColor : GRAY - ); - } - DrawRectangle( - u.wpct(0.5f) - ((5 * u.winpct(0.02f)) / 2), - u.hpct(0.02f) + u.winpct(0.025f), - u.winpct(0.1f), - u.winpct(0.01f), - ThePlayerManager.GetActivePlayer(0).stats->UpStrum ? WHITE : GRAY - ); - DrawRectangle( - u.wpct(0.5f) - ((5 * u.winpct(0.02f)) / 2), - u.hpct(0.02f) + u.winpct(0.035f), - u.winpct(0.1f), - u.winpct(0.01f), - ThePlayerManager.GetActivePlayer(0).stats->DownStrum ? WHITE : GRAY - ); - } - - DrawTextEx( - assets.rubik, - TextFormat("song time: %f", TheSongTime.GetSongTime()), - { 0, u.hpct(0.5f) }, - u.hinpct(SmallHeader), - 0, - WHITE - ); - DrawTextEx( - assets.rubik, - TextFormat("audio time: %f", TheAudioManager.GetMusicTimePlayed()), - { 0, u.hpct(0.5f + SmallHeader) }, - u.hinpct(SmallHeader), - 0, - WHITE - ); - */ } void GameplayMenu::Load() { @@ -2012,6 +1894,13 @@ void GameplayMenu::Load() { stream.volume = TheGameSettings.avMainVolume * TheGameSettings.avInactiveInstrumentVolume; } + + std::filesystem::path songPath(TheSongList.curSong->songInfoPath); + std::filesystem::path videoPath = songPath.parent_path() / "video.mp4"; + if (backgroundVideo.Load(videoPath)) { + backgroundVideo.Play(); + } + /* if (ThePlayerManager.PlayersActive > 1) { ThePlayerManager.BandStats->Multiplayer = true; diff --git a/Encore/src/menus/SettingsGameplay.cpp b/Encore/src/menus/SettingsGameplay.cpp index a11c0c6..bef347e 100644 --- a/Encore/src/menus/SettingsGameplay.cpp +++ b/Encore/src/menus/SettingsGameplay.cpp @@ -9,6 +9,7 @@ #include "raygui.h" #include "assets.h" #include "settings.h" +#include "settings-old.h" #include "settingsOptionRenderer.h" #include "uiUnits.h" #include "gameplay/enctime.h" @@ -78,7 +79,6 @@ void SettingsGameplay::Draw() { // scan Songs { "Scan Songs", - "TBD" } }; @@ -236,6 +236,8 @@ void SettingsGameplay::Draw() { } } + + if (!isHovering) { selectedIndex = 0; } diff --git a/Encore/src/menus/SongSelectMenu.cpp b/Encore/src/menus/SongSelectMenu.cpp index 844948d..4ecd49f 100644 --- a/Encore/src/menus/SongSelectMenu.cpp +++ b/Encore/src/menus/SongSelectMenu.cpp @@ -9,23 +9,145 @@ #include "MenuManager.h" #include "gameMenu.h" #include "raygui.h" +#include "settings.h" #include "uiUnits.h" #include "gameplay/gameplayRenderer.h" +#include "song/audio.h" #include "song/songlist.h" - +#include "assets.h" +#include +#include +#include +#include + +float EaseInOutQuad(float t) { + t = t < 0.5f ? 2.0f * t * t : 1.0f - powf(-2.0f * t + 2.0f, 2.0f) / 2.0f; + return t; +} SortType currentSortValue = SortType::Title; Color AccentColor = {255, 0, 255, 255}; +SongSelectMenu::~SongSelectMenu() { + Unload(); +} void SongSelectMenu::Load() { - TheSongList.curSong->LoadAlbumArt(); - SetTextureWrap(TheSongList.curSong->albumArtBlur, TEXTURE_WRAP_REPEAT); - SetTextureFilter(TheSongList.curSong->albumArtBlur, TEXTURE_FILTER_ANISOTROPIC_16X); - TheSongList.SongSelectOffset = TheSongList.curSong->songListPos - 5; + if (!IsAudioDeviceReady()) { + InitAudioDevice(); + TraceLog(LOG_INFO, "Initialized audio device"); + } + previewStartTime = 0.0; + phaseStartTime = 0.0; + currentPreviewVolume = 0.0f; + previewState = PreviewState::FadeIn; + pendingSongID = -1; + selectionTime = 0.0; + + if (TheSongList.curSong && !TheSongList.curSong->AlbumArtLoaded) { + try { + TheSongList.curSong->LoadAlbumArt(); + SetTextureWrap(TheSongList.curSong->albumArtBlur, TEXTURE_WRAP_REPEAT); + SetTextureFilter(TheSongList.curSong->albumArtBlur, TEXTURE_FILTER_ANISOTROPIC_16X); + TheSongList.curSong->AlbumArtLoaded = true; + TraceLog(LOG_DEBUG, "Loaded album art for %s", TheSongList.curSong->title.c_str()); + } catch (const std::exception& e) { + TraceLog(LOG_ERROR, "Failed to load album art for %s: %s", TheSongList.curSong->title.c_str(), e.what()); + } + } + + if (TheSongList.curSong) { + TheSongList.SongSelectOffset = TheSongList.curSong->songListPos - 5; + if (TheSongList.SongSelectOffset < 1) TheSongList.SongSelectOffset = 1; + if (TheSongList.SongSelectOffset > TheSongList.listMenuEntries.size() - 10) + TheSongList.SongSelectOffset = TheSongList.listMenuEntries.size() - 10; + } else { + TheSongList.SongSelectOffset = 1; + } + // TheGameRenderer.streamsLoaded = false; // TheGameRenderer.midiLoaded = false; + for (Song& song : TheSongList.songs) { + if (!song.ini) { + song.LoadInfo(song.songInfoPath); + } else { + song.LoadInfoINI(song.songInfoPath); + } + } } +void SongSelectMenu::Unload() { + if (!TheAudioManager.loadedStreams.empty()) { + for (auto& stream : TheAudioManager.loadedStreams) { + TheAudioManager.StopPlayback(stream.handle); + } + TheAudioManager.loadedStreams.clear(); + } +} + + +void SongSelectMenu::UpdatePreviewVolume(double currentTime) { + float targetVolume = TheGameSettings.avMainVolume * TheGameSettings.avMenuMusicVolume; + float t; + + if (TheAudioManager.loadedStreams.empty()) { + currentPreviewVolume = 0.0f; + return; + } + + switch (previewState) { + case PreviewState::FadeIn: + t = (currentTime - phaseStartTime) / fadeDuration; + if (t >= 1.0f) { + currentPreviewVolume = targetVolume; + previewState = PreviewState::Playing; + phaseStartTime = currentTime; + } else { + t = EaseInOutQuad(t); + currentPreviewVolume = t * targetVolume; + } + break; + case PreviewState::Playing: + currentPreviewVolume = targetVolume; + if (currentTime - phaseStartTime >= previewPlayDuration) { + previewState = PreviewState::FadeOut; + phaseStartTime = currentTime; + } + break; + case PreviewState::FadeOut: + t = (currentTime - phaseStartTime) / fadeDuration; + if (t >= 1.0f) { + currentPreviewVolume = 0.0f; + previewState = PreviewState::Pause; + phaseStartTime = currentTime; + } else { + t = EaseInOutQuad(t); + currentPreviewVolume = (1.0f - t) * targetVolume; + } + break; + case PreviewState::Pause: + currentPreviewVolume = 0.0f; + if (currentTime - phaseStartTime >= pauseDuration) { + previewState = PreviewState::FadeIn; + phaseStartTime = currentTime; + if (TheSongList.curSong && !TheAudioManager.loadedStreams.empty()) { + float previewStartTimeSec = TheSongList.curSong->previewStartTime / 1000.0f; + TheAudioManager.seekStreams(previewStartTimeSec); + for (auto& stream : TheAudioManager.loadedStreams) { + TheAudioManager.BeginPlayback(stream.handle); + } + } + } + break; + } + + for (int i = 0; i < TheAudioManager.loadedStreams.size(); i++) { + float volume = currentPreviewVolume; + if (i == PartVocals) volume = 0; + TheAudioManager.SetAudioStreamVolume(TheAudioManager.loadedStreams[i].handle, volume); + } +} + + void SongSelectMenu::KeyboardInputCallback(int key, int scancode, int action, int mods) { if (action == GLFW_PRESS && key == GLFW_KEY_UP) { if (TheSongList.SongSelectOffset <= TheSongList.listMenuEntries.size() && TheSongList.SongSelectOffset >= 1 @@ -49,10 +171,32 @@ void SongSelectMenu::Draw() { double curTime = GetTime(); // -5 -4 -3 -2 -1 0 1 2 3 4 5 6 - Vector2 mouseWheel = GetMouseWheelMoveV(); - int lastIntChosen = (int)mouseWheel.y; - // set to specified height + if (pendingSongID >= 0 && curTime - selectionTime >= 0.75) { + if (pendingSongID < TheSongList.songs.size()) { + try { + TheAudioManager.loadStreams(TheSongList.songs[pendingSongID].stemsPath); + float previewStartTimeSec = TheSongList.songs[pendingSongID].previewStartTime / 1000.0f; + TheAudioManager.seekStreams(previewStartTimeSec); + for (int j = 0; j < TheAudioManager.loadedStreams.size(); j++) { + float volume = 0.0f; + if (j == PartVocals) volume = 0; + TheAudioManager.SetAudioStreamVolume(TheAudioManager.loadedStreams[j].handle, volume); + TheAudioManager.BeginPlayback(TheAudioManager.loadedStreams[j].handle); + } + previewStartTime = curTime; + phaseStartTime = curTime; + currentPreviewVolume = 0.0f; + previewState = PreviewState::FadeIn; + } catch (const std::exception& e) { + TraceLog(LOG_ERROR, "Failed to load preview audio for song %d: %s", pendingSongID, e.what()); + } + } + pendingSongID = -1; + } + + UpdatePreviewVolume(curTime); + Vector2 mouseWheel = GetMouseWheelMoveV(); // Update song select offset based on mouse wheel if (TheSongList.SongSelectOffset <= TheSongList.listMenuEntries.size() && TheSongList.SongSelectOffset >= 1 && TheSongList.listMenuEntries.size() >= 10) { @@ -68,17 +212,21 @@ void SongSelectMenu::Draw() { TheSongList.SongSelectOffset = TheSongList.listMenuEntries.size() - 10; // todo(3drosalia): clean this shit up after changing it + Song SongToDisplayInfo = TheSongList.curSong ? *TheSongList.curSong : Song(); + if (TheSongList.curSong) { + if (TheSongList.curSong->ini) + TheSongList.curSong->LoadInfoINI(TheSongList.curSong->songInfoPath); + else + TheSongList.curSong->LoadInfo(TheSongList.curSong->songInfoPath); + } - Song SongToDisplayInfo; - BeginShaderMode(assets.bgShader); - // todo(3drosalia): this too - SongToDisplayInfo = *TheSongList.curSong; - if (TheSongList.curSong->ini) - TheSongList.curSong->LoadInfoINI(TheSongList.curSong->songInfoPath); - else - TheSongList.curSong->LoadInfo(TheSongList.curSong->songInfoPath); - GameMenu::DrawAlbumArtBackground(TheSongList.curSong->albumArtBlur); - EndShaderMode(); + BeginDrawing(); + ClearBackground(DARKGRAY); + if (TheSongList.curSong && TheSongList.curSong->AlbumArtLoaded) { + BeginShaderMode(assets.bgShader); + GameMenu::DrawAlbumArtBackground(TheSongList.curSong->albumArtBlur); + EndShaderMode(); + } float TopOvershell = u.hpct(0.15f); DrawRectangle( @@ -95,7 +243,6 @@ void SongSelectMenu::Draw() { int AlbumHeight = u.winpct(0.25f); int AlbumOuter = u.hinpct(0.01f); int AlbumInner = u.hinpct(0.005f); - int BorderBetweenAlbumStuff = (u.RightSide - u.LeftSide) - u.winpct(0.25f); DrawTextEx( assets.josefinSansItalic, @@ -167,7 +314,7 @@ void SongSelectMenu::Draw() { WHITE ); } else if (!TheSongList.listMenuEntries[i].hiddenEntry) { - bool isCurSong = i == TheSongList.curSong->songListPos - 1; + bool isCurSong = TheSongList.curSong && i == TheSongList.curSong->songListPos - 1; Font &artistFont = isCurSong ? assets.josefinSansItalic : assets.josefinSansItalic; Song &songi = TheSongList.songs[TheSongList.listMenuEntries[i].songListID]; int songID = TheSongList.listMenuEntries[i].songListID; @@ -194,6 +341,18 @@ void SongSelectMenu::Draw() { "" )) { TheSongList.curSong = &TheSongList.songs[songID]; + + if (!TheAudioManager.loadedStreams.empty()) { + for (auto& stream : TheAudioManager.loadedStreams) { + TheAudioManager.StopPlayback(stream.handle); + } + TheAudioManager.loadedStreams.clear(); + currentPreviewVolume = 0.0f; + previewState = PreviewState::FadeIn; + } + pendingSongID = songID; + selectionTime = curTime; + if (!TheSongList.songs[songID].AlbumArtLoaded) { try { TheSongList.songs[songID].LoadAlbumArt(); @@ -255,7 +414,6 @@ void SongSelectMenu::Draw() { } } - auto SelectedText = WHITE; BeginScissorMode( (int)songXPos + 30 + (int)songTitleWidth, (int)songYPos, @@ -274,6 +432,54 @@ void SongSelectMenu::Draw() { EndScissorMode(); } } + if (TheSongList.SongSelectOffset > 0 && TheSongList.SongSelectOffset < TheSongList.listMenuEntries.size()) { + std::string categoryHeaderText = ""; + int songIndex = TheSongList.SongSelectOffset; + + if (TheSongList.listMenuEntries[songIndex].isHeader && songIndex > 0 && !TheSongList.listMenuEntries[songIndex - 1].isHeader) { + songIndex--; + } else if (!TheSongList.listMenuEntries[songIndex].isHeader) { + } else if (songIndex + 1 < TheSongList.listMenuEntries.size() && !TheSongList.listMenuEntries[songIndex + 1].isHeader) { + songIndex++; + } + + if (songIndex < TheSongList.listMenuEntries.size() && !TheSongList.listMenuEntries[songIndex].isHeader) { + Song& representativeSong = TheSongList.songs[TheSongList.listMenuEntries[songIndex].songListID]; + switch (currentSortValue) { + case SortType::Title: + categoryHeaderText = representativeSong.title.empty() ? "#" : std::string(1, toupper(representativeSong.title[0])); + break; + case SortType::Artist: + categoryHeaderText = representativeSong.artist.empty() ? "#" : std::string(1, toupper(representativeSong.artist[0])); + break; + case SortType::Source: + categoryHeaderText = representativeSong.source.empty() ? "Unknown" : representativeSong.source; + break; + case SortType::Length: + categoryHeaderText = TheSongList.listMenuEntries[TheSongList.SongSelectOffset].headerChar; + break; + case SortType::Year: + categoryHeaderText = representativeSong.releaseYear.empty() ? "Unknown Year" : representativeSong.releaseYear; + break; + default: + categoryHeaderText = ""; + break; + } + } + + if (categoryHeaderText.empty()) { + categoryHeaderText = TheSongList.listMenuEntries[TheSongList.SongSelectOffset].headerChar; + } + + DrawTextEx( + assets.rubikBold, + categoryHeaderText.c_str(), + { u.LeftSide + 5, u.hpct(0.218333f) }, + u.hinpct(0.035f), + 0, + WHITE + ); + } DrawRectangle( AlbumX - AlbumOuter, @@ -298,12 +504,11 @@ void SongSelectMenu::Draw() { ); DrawRectangle(AlbumX - AlbumInner, AlbumY, AlbumHeight, AlbumHeight, BLACK); - // Display song title above the album cover std::string titleText = SongToDisplayInfo.title.empty() ? "Unknown Song" : SongToDisplayInfo.title; float titleFontSize = u.hinpct(0.035f); float titleTextWidth = MeasureTextEx(assets.rubikBold, titleText.c_str(), titleFontSize, 0).x; float titleTextX = AlbumX - AlbumInner + (AlbumHeight / 2.0f) - (titleTextWidth / 2.0f); - float titleTextY = AlbumY - u.hinpct(0.045f); // Same position as current year + float titleTextY = AlbumY - u.hinpct(0.045f); DrawTextEx( assets.rubikBold, titleText.c_str(), @@ -313,127 +518,28 @@ void SongSelectMenu::Draw() { WHITE ); - // Draw album cover - DrawTexturePro( - TheSongList.curSong->albumArt, - Rectangle { 0, - 0, - (float)TheSongList.curSong->albumArt.width, - (float)TheSongList.curSong->albumArt.width }, - Rectangle { (float)AlbumX - AlbumInner, - (float)AlbumY, - (float)AlbumHeight, - (float)AlbumHeight }, - { 0, 0 }, - 0, - WHITE - ); - // TODO: replace this with actual sorting/category hiding - - // Sorting/category header - if (TheSongList.SongSelectOffset > 0) { - std::string SongTitleForCharThingyThatsTemporary = - TheSongList.listMenuEntries[TheSongList.SongSelectOffset].headerChar; - switch (currentSortValue) { - case SortType::Title: { - if (TheSongList.listMenuEntries[TheSongList.SongSelectOffset].isHeader) { - SongTitleForCharThingyThatsTemporary = - TheSongList - .songs[TheSongList.listMenuEntries[TheSongList.SongSelectOffset - 1].songListID] - .title[0]; - } else { - SongTitleForCharThingyThatsTemporary = - TheSongList - .songs[TheSongList.listMenuEntries[TheSongList.SongSelectOffset].songListID] - .title[0]; - } - break; - } - case SortType::Artist: { - if (TheSongList.listMenuEntries[TheSongList.SongSelectOffset].isHeader) { - SongTitleForCharThingyThatsTemporary = - TheSongList - .songs[TheSongList.listMenuEntries[TheSongList.SongSelectOffset - 1].songListID] - .artist[0]; - } else { - SongTitleForCharThingyThatsTemporary = - TheSongList - .songs[TheSongList.listMenuEntries[TheSongList.SongSelectOffset].songListID] - .artist[0]; - } - break; - } - case SortType::Source: { - if (TheSongList.listMenuEntries[TheSongList.SongSelectOffset].isHeader) { - SongTitleForCharThingyThatsTemporary = - TheSongList - .songs[TheSongList.listMenuEntries[TheSongList.SongSelectOffset - 1].songListID] - .source; - } else { - SongTitleForCharThingyThatsTemporary = - TheSongList - .songs[TheSongList.listMenuEntries[TheSongList.SongSelectOffset].songListID] - .source; - } - break; - } - case SortType::Length: { - if (TheSongList.listMenuEntries[TheSongList.SongSelectOffset].isHeader) { - SongTitleForCharThingyThatsTemporary = std::to_string( - TheSongList - .songs[TheSongList.listMenuEntries[TheSongList.SongSelectOffset - 1].songListID] - .length - ); - } else { - SongTitleForCharThingyThatsTemporary = std::to_string( - TheSongList - .songs[TheSongList.listMenuEntries[TheSongList.SongSelectOffset].songListID] - .length - ); - } - break; - } - case SortType::Year: { - if (TheSongList.listMenuEntries[TheSongList.SongSelectOffset].isHeader) { - SongTitleForCharThingyThatsTemporary = - TheSongList - .songs[TheSongList.listMenuEntries[TheSongList.SongSelectOffset - 1].songListID] - .releaseYear[0]; - } else { - SongTitleForCharThingyThatsTemporary = - TheSongList - .songs[TheSongList.listMenuEntries[TheSongList.SongSelectOffset].songListID] - .releaseYear[0]; - } - break; - } - default: - // Handle EnumEnd or unexpected values - SongTitleForCharThingyThatsTemporary = ""; - break; - } - - DrawTextEx( - assets.rubikBold, - SongTitleForCharThingyThatsTemporary.c_str(), - { u.LeftSide + 5, u.hpct(0.218333f) }, - u.hinpct(0.035f), + if (TheSongList.curSong && TheSongList.curSong->AlbumArtLoaded) { + DrawTexturePro( + TheSongList.curSong->albumArt, + Rectangle { 0, + 0, + (float)TheSongList.curSong->albumArt.width, + (float)TheSongList.curSong->albumArt.width }, + Rectangle { (float)AlbumX - AlbumInner, + (float)AlbumY, + (float)AlbumHeight, + (float)AlbumHeight }, + { 0, 0 }, 0, WHITE ); + } else { + DrawRectangle(AlbumX - AlbumInner, AlbumY, AlbumHeight, AlbumHeight, DARKGRAY); } float TextPlacementTB = u.hpct(0.05f); float TextPlacementLR = u.LeftSide; - GameMenu::mhDrawText( - assets.redHatDisplayBlack, - "MUSIC LIBRARY", - { TextPlacementLR, TextPlacementTB }, - u.hinpct(0.125f), - WHITE, - assets.sdfShader, - LEFT - ); + GameMenu::mhDrawText(assets.redHatDisplayBlack, "MUSIC LIBRARY", { TextPlacementLR, TextPlacementTB }, u.hinpct(0.125f), WHITE, assets.sdfShader, LEFT); std::string albumText = SongToDisplayInfo.album.empty() ? "No Album Listed" : SongToDisplayInfo.album; std::string yearText = SongToDisplayInfo.releaseYear.empty() ? "Unknown Year" : SongToDisplayInfo.releaseYear; @@ -442,61 +548,17 @@ void SongSelectMenu::Draw() { float albumTextWidth = MeasureTextEx(assets.rubikBold, albumDisplayText.c_str(), u.hinpct(0.035f), 0).x; float albumNameTextCenter = u.RightSide - u.winpct(0.125f) - AlbumInner; float albumTTop = AlbumY + AlbumHeight + u.hinpct(0.011f); - float albumNameFontSize = albumTextWidth <= u.winpct(0.25f) - ? u.hinpct(0.035f) - : u.winpct(0.23f) / (albumTextWidth / albumTextHeight); - float albumNameLeft = albumNameTextCenter - - (MeasureTextEx(assets.rubikBold, albumDisplayText.c_str(), albumNameFontSize, 0).x / 2); - float albumNameTextTop = albumTextWidth <= u.winpct(0.25f) - ? albumTTop - : albumTTop + ((u.hinpct(0.035f) / 2) - (albumNameFontSize / 2)); - DrawTextEx( - assets.rubikBold, - albumDisplayText.c_str(), - { albumNameLeft, albumNameTextTop }, - albumNameFontSize, - 0, - WHITE - ); + float albumNameFontSize = albumTextWidth <= u.winpct(0.25f) ? u.hinpct(0.035f) : u.winpct(0.23f) / (albumTextWidth / albumTextHeight); + float albumNameLeft = albumNameTextCenter - (MeasureTextEx(assets.rubikBold, albumDisplayText.c_str(), albumNameFontSize, 0).x / 2); + float albumNameTextTop = albumTextWidth <= u.winpct(0.25f) ? albumTTop : albumTTop + ((u.hinpct(0.035f) / 2) - (albumNameFontSize / 2)); + DrawTextEx(assets.rubikBold, albumDisplayText.c_str(), { albumNameLeft, albumNameTextTop }, albumNameFontSize, 0, WHITE); - DrawLine( - u.RightSide - AlbumHeight - AlbumOuter, - AlbumY + AlbumHeight + AlbumOuter + (u.hinpct(0.04f)), - u.RightSide, - AlbumY + AlbumHeight + AlbumOuter + (u.hinpct(0.04f)), - WHITE - ); + DrawLine(u.RightSide - AlbumHeight - AlbumOuter, AlbumY + AlbumHeight + AlbumOuter + (u.hinpct(0.04f)), u.RightSide, AlbumY + AlbumHeight + AlbumOuter + (u.hinpct(0.04f)), WHITE); float DiffTop = AlbumY + AlbumHeight + AlbumOuter + (u.hinpct(0.045f)); - float DiffHeight = u.hinpct(0.035f); - float DiffTextSize = u.hinpct(0.03f); - float DiffDotLeft = - u.RightSide - MeasureTextEx(assets.rubikBold, "OOOOO ", DiffHeight, 0).x; - float ScrollbarLeft = u.RightSide - AlbumHeight - AlbumOuter - (AlbumInner * 2); - float ScrollbarTop = u.hpct(0.208333f); - float ScrollbarHeight = GetScreenHeight() - u.hpct(0.208333f) - u.hpct(0.15f); - GuiSetStyle(SCROLLBAR, BACKGROUND_COLOR, 0x181827FF); - GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, u.hinpct(0.03f)); float IconWidth = float(AlbumHeight - AlbumOuter) / 5.0f; - GameMenu::mhDrawText( - assets.rubikItalic, - "Pad", - { (u.RightSide - AlbumHeight + AlbumInner), DiffTop }, - AlbumOuter * 3, - WHITE, - assets.sdfShader, - LEFT - ); - GameMenu::mhDrawText( - assets.rubikItalic, - "Classic", - { (u.RightSide - AlbumHeight + AlbumInner), - DiffTop + IconWidth + (AlbumOuter * 3) }, - AlbumOuter * 3, - WHITE, - assets.sdfShader, - LEFT - ); + GameMenu::mhDrawText(assets.rubikItalic, "Pad", { (u.RightSide - AlbumHeight + AlbumInner), DiffTop }, AlbumOuter * 3, WHITE, assets.sdfShader, LEFT); + GameMenu::mhDrawText(assets.rubikItalic, "Classic", { (u.RightSide - AlbumHeight + AlbumInner), DiffTop + IconWidth + (AlbumOuter * 3) }, AlbumOuter * 3, WHITE, assets.sdfShader, LEFT); for (int i = 0; i < 10; i++) { bool RowTwo = i < 5; int RowTwoInt = i - 5; @@ -504,67 +566,28 @@ void SongSelectMenu::Draw() { float BoxTopPos = DiffTop + PosTopAddition + float(IconWidth * (RowTwo ? 0 : 1)); float ResetToLeftPos = (float)(RowTwo ? i : RowTwoInt); int asdasd = (float)(RowTwo ? i : RowTwoInt); - float IconLeftPos = - (float)(u.RightSide - AlbumHeight) + IconWidth * ResetToLeftPos; + float IconLeftPos = (float)(u.RightSide - AlbumHeight) + IconWidth * ResetToLeftPos; Rectangle Placement = { IconLeftPos, BoxTopPos, IconWidth, IconWidth }; Color TintColor = WHITE; - if (SongToDisplayInfo.parts[i]->diff == -1) - TintColor = DARKGRAY; - DrawTexturePro( - assets.InstIcons[asdasd], - { 0, - 0, - (float)assets.InstIcons[asdasd].width, - (float)assets.InstIcons[asdasd].height }, - Placement, - { 0, 0 }, - 0, - TintColor - ); - DrawTexturePro( - assets.BaseRingTexture, - { 0, - 0, - (float)assets.BaseRingTexture.width, - (float)assets.BaseRingTexture.height }, - Placement, - { 0, 0 }, - 0, - ColorBrightness(WHITE, 2) - ); - if (SongToDisplayInfo.parts[i]->diff > 0) - DrawTexturePro( - assets.YargRings[SongToDisplayInfo.parts[i]->diff - 1], - { 0, - 0, - (float)assets.YargRings[SongToDisplayInfo.parts[i]->diff - 1].width, - (float)assets.YargRings[SongToDisplayInfo.parts[i]->diff - 1].height }, - Placement, - { 0, 0 }, - 0, - WHITE - ); + if (SongToDisplayInfo.parts[i] && SongToDisplayInfo.parts[i]->diff == -1) TintColor = DARKGRAY; + DrawTexturePro(assets.InstIcons[asdasd], { 0, 0, (float)assets.InstIcons[asdasd].width, (float)assets.InstIcons[asdasd].height }, Placement, { 0, 0 }, 0, TintColor); + DrawTexturePro(assets.BaseRingTexture, { 0, 0, (float)assets.BaseRingTexture.width, (float)assets.BaseRingTexture.height }, Placement, { 0, 0 }, 0, ColorBrightness(WHITE, 2)); + if (SongToDisplayInfo.parts[i] && SongToDisplayInfo.parts[i]->diff > 0) + DrawTexturePro(assets.YargRings[SongToDisplayInfo.parts[i]->diff - 1], { 0, 0, (float)assets.YargRings[SongToDisplayInfo.parts[i]->diff - 1].width, (float)assets.YargRings[SongToDisplayInfo.parts[i]->diff - 1].height }, Placement, { 0, 0 }, 0, WHITE); } GameMenu::DrawBottomOvershell(); - float BottomOvershell = (float)GetScreenHeight() - 120; - - GuiSetStyle( - BUTTON, BASE_COLOR_NORMAL, ColorToInt(ColorBrightness(AccentColor, -0.25)) - ); - if (GuiButton( - Rectangle { u.LeftSide, - GetScreenHeight() - u.hpct(0.1475f), - u.winpct(0.2f), - u.hinpct(0.05f) }, - "Play Song" - )) { - if (!TheSongList.curSong->ini) { - TheSongList.curSong->LoadSong(TheSongList.curSong->songInfoPath); - } else { - TheSongList.curSong->LoadSongIni(TheSongList.curSong->songDir); + GuiSetStyle(BUTTON, BASE_COLOR_NORMAL, ColorToInt(ColorBrightness(AccentColor, -0.25))); + if (GuiButton(Rectangle{ u.LeftSide, GetScreenHeight() - u.hpct(0.1475f), u.winpct(0.2f), u.hinpct(0.05f) }, "Play Song")) { + if (TheSongList.curSong) { + Unload(); + if (!TheSongList.curSong->ini) { + TheSongList.curSong->LoadSong(TheSongList.curSong->songInfoPath); + } else { + TheSongList.curSong->LoadSongIni(TheSongList.curSong->songDir); + } + TheMenuManager.SwitchScreen(READY_UP); } - TheMenuManager.SwitchScreen(READY_UP); } GuiSetStyle(BUTTON, BASE_COLOR_NORMAL, 0x181827FF); @@ -575,20 +598,43 @@ void SongSelectMenu::Draw() { u.hinpct(0.05f) }, "Sort" )) { + int selectedSongIndex = -1; + if (TheSongList.curSong) { + for (size_t i = 0; i < TheSongList.songs.size(); i++) { + if (&TheSongList.songs[i] == TheSongList.curSong) { + selectedSongIndex = i; + break; + } + } + } currentSortValue = NextSortType(currentSortValue); - TheSongList.sortList(currentSortValue, TheSongList.curSong->songListPos); + TheSongList.sortList(currentSortValue, selectedSongIndex); + if (selectedSongIndex >= 0 && selectedSongIndex < TheSongList.songs.size()) { + TheSongList.curSong = &TheSongList.songs[selectedSongIndex]; + TheSongList.SongSelectOffset = TheSongList.curSong->songListPos - 5; + if (TheSongList.SongSelectOffset < 1) TheSongList.SongSelectOffset = 1; + if (TheSongList.SongSelectOffset > TheSongList.listMenuEntries.size() - 10) + TheSongList.SongSelectOffset = TheSongList.listMenuEntries.size() - 10; + if (!TheAudioManager.loadedStreams.empty()) { + for (auto& stream : TheAudioManager.loadedStreams) { + TheAudioManager.StopPlayback(stream.handle); + } + TheAudioManager.loadedStreams.clear(); + currentPreviewVolume = 0.0f; + previewState = PreviewState::FadeIn; + } + pendingSongID = selectedSongIndex; + selectionTime = curTime; + } } - if (GuiButton( - Rectangle { u.LeftSide + u.winpct(0.2f) - 1, - GetScreenHeight() - u.hpct(0.1475f), - u.winpct(0.2f), - u.hinpct(0.05f) }, - "Back" - )) { - for (Song &songi : TheSongList.songs) { - songi.titleXOffset = 0; - songi.artistXOffset = 0; + if (GuiButton(Rectangle{ u.LeftSide + u.winpct(0.2f) - 1, GetScreenHeight() - u.hpct(0.1475f), u.winpct(0.2f), u.hinpct(0.05f) }, "Back")) { + if (!TheAudioManager.loadedStreams.empty()) { + for (auto& stream : TheAudioManager.loadedStreams) { + TheAudioManager.StopPlayback(stream.handle); + } + TheAudioManager.loadedStreams.clear(); } + Unload(); TheMenuManager.SwitchScreen(MAIN_MENU); } DrawOvershell(); diff --git a/Encore/src/menus/SongSelectMenu.h b/Encore/src/menus/SongSelectMenu.h index e9cbdf0..1959ab3 100644 --- a/Encore/src/menus/SongSelectMenu.h +++ b/Encore/src/menus/SongSelectMenu.h @@ -5,17 +5,32 @@ #ifndef SONGSELECTMENU_H #define SONGSELECTMENU_H #include "OvershellMenu.h" +#include +#include class SongSelectMenu : public OvershellMenu { public: SongSelectMenu() = default; - ~SongSelectMenu() override = default; + ~SongSelectMenu() override; void KeyboardInputCallback(int key, int scancode, int action, int mods) override; void ControllerInputCallback(int joypadID, GLFWgamepadstate state) override; void Draw() override; void Load() override; + void Unload(); + void UpdatePreviewVolume(double currentTime); + +private: + double previewStartTime = 0.0; + float currentPreviewVolume = 0.0f; + enum class PreviewState { FadeIn, Playing, FadeOut, Pause } previewState = PreviewState::FadeIn; + const float fadeDuration = 2.5f; + const float previewPlayDuration = 30.0f; + const float pauseDuration = 2.5f; + double phaseStartTime = 0.0; + int pendingSongID = -1; + double selectionTime = 0.0; }; -#endif //SONGSELECTMENU_H +#endif //SONGSELECTMENU_H \ No newline at end of file diff --git a/Encore/src/settings.h b/Encore/src/settings.h index 11762eb..a1f27c6 100644 --- a/Encore/src/settings.h +++ b/Encore/src/settings.h @@ -24,7 +24,12 @@ OPTION(bool, DiscordRichPresence, true) \ OPTION(int, Framerate, 60) \ OPTION(bool, VerticalSync, true) \ - OPTION(bool, BackgroundBeatFlash, true) + OPTION(bool, BackgroundBeatFlash, true) \ + OPTION(bool, HideHitWindow, false) \ + OPTION(bool, ShowHealthBar, true) \ + OPTION(bool, HideFPSCounter, false) \ + OPTION(bool, HideVersionInfo, false) \ + OPTION(int, HUDPosition, 0) namespace Encore { inline void WriteJsonFile(const std::filesystem::path &FileToWrite, const nlohmann::json &JSONobject) { std::ofstream o(FileToWrite, std::ios::out | std::ios::trunc); @@ -55,7 +60,12 @@ namespace Encore { AudioOffset, DiscordRichPresence, SongPaths, - BackgroundBeatFlash + BackgroundBeatFlash, + HideHitWindow, + ShowHealthBar, + HideFPSCounter, + HideVersionInfo, + HUDPosition ); class SettingsInit { diff --git a/Encore/src/song/audio.cpp b/Encore/src/song/audio.cpp index b63edf2..030b55d 100644 --- a/Encore/src/song/audio.cpp +++ b/Encore/src/song/audio.cpp @@ -204,6 +204,12 @@ void Encore::AudioManager::StopPlayback(unsigned int handle) { CHECK_BASS_ERROR2(); } +void Encore::AudioManager::SetAudioStreamPosition(unsigned int handle, double time) { + int positionBytes = BASS_ChannelSeconds2Bytes(handle, time); + BASS_ChannelSetPosition(handle, positionBytes, BASS_POS_BYTE); + CHECK_BASS_ERROR2(); +} + void Encore::AudioManager::loadSample(const std::string &path, const std::string &name) { HSAMPLE sample = BASS_SampleLoad(false, path.c_str(), 0, 0, 1, 0); if (sample) { diff --git a/Encore/src/song/audio.h b/Encore/src/song/audio.h index 9d6fa58..84ec8e1 100644 --- a/Encore/src/song/audio.h +++ b/Encore/src/song/audio.h @@ -35,6 +35,7 @@ namespace Encore { void UpdateAudioStreamVolumes(); AudioStream* GetAudioStreamByInstrument(int instrument); static void SetAudioStreamVolume(unsigned int handle, float volume); + static void SetAudioStreamPosition(unsigned int handle, double time); static void BeginPlayback(unsigned int handle); static void StopPlayback(unsigned int handle); diff --git a/Encore/src/song/song.h b/Encore/src/song/song.h index 2b1c223..e5f8c2f 100644 --- a/Encore/src/song/song.h +++ b/Encore/src/song/song.h @@ -194,7 +194,7 @@ class Song { bool ini = false; smf::MidiFile midiFile; void LoadAudioINI(std::filesystem::path songPath); - + float previewStartTime = 0.0f; void LoadAudio(std::filesystem::path jsonPath) { std::ifstream ifs(jsonPath); @@ -406,6 +406,36 @@ class Song { } } } + if (document.HasMember("preview_start_time") && document["preview_start_time"].IsInt()) { + previewStartTime = static_cast(document["preview_start_time"].GetInt()); + } + } + if (document.HasMember("stems") && document["stems"].IsObject()) { + for (auto &path : document["stems"].GetObject()) { + std::string stem = std::string(path.name.GetString()); + int partIndex = -1; + if (stem == "drums") partIndex = PartDrums; + else if (stem == "bass") partIndex = PartBass; + else if (stem == "lead") partIndex = PartGuitar; + else if (stem == "vocals") partIndex = PartVocals; + else if (stem == "backing") partIndex = 5; + + if (path.value.IsString()) { + std::filesystem::path stemPath = jsonPath.parent_path() / path.value.GetString(); + if (std::filesystem::exists(stemPath)) { + stemsPath.push_back({ stemPath.string(), partIndex }); + } + } else if (path.value.IsArray()) { + for (auto &path2 : path.value.GetArray()) { + if (path2.IsString()) { + std::filesystem::path stemPath = jsonPath.parent_path() / path2.GetString(); + if (std::filesystem::exists(stemPath)) { + stemsPath.push_back({ stemPath.string(), partIndex }); + } + } + } + } + } } if (charters.empty()) { charters.push_back("Unknown Charter"); @@ -744,6 +774,9 @@ class Song { SetTextureFilter(albumArtBlur, TEXTURE_FILTER_TRILINEAR); UnloadImage(albumImage); }; + + // Video background support + double videoStartTime = 0.0; }; #endif // ENCORE_SONG_H \ No newline at end of file diff --git a/Encore/src/song/songlist.cpp b/Encore/src/song/songlist.cpp index b800485..3b5bf72 100644 --- a/Encore/src/song/songlist.cpp +++ b/Encore/src/song/songlist.cpp @@ -3,8 +3,12 @@ #include +#include +#include +#include #include "util/binary.h" +using json = nlohmann::json; // sorting void SongList::Clear() { listMenuEntries.clear(); @@ -86,7 +90,6 @@ void SongList::sortList(SortType sortType) { std::sort(songs.begin(), songs.end(), sortSource); break; case SortType::Length: - std::sort(songs.begin(), songs.end(), sortTitle); std::sort(songs.begin(), songs.end(), sortLen); break; case SortType::Year: @@ -99,7 +102,11 @@ void SongList::sortList(SortType sortType) { } void SongList::sortList(SortType sortType, int &selectedSong) { - Song &curSong = songs[selectedSong]; + Song curSong; + bool hasCurrentSong = selectedSong >= 0 && selectedSong < songs.size(); + if (hasCurrentSong) { + curSong = songs[selectedSong]; + } selectedSong = 0; switch (sortType) { case SortType::Title: @@ -114,7 +121,6 @@ void SongList::sortList(SortType sortType, int &selectedSong) { std::sort(songs.begin(), songs.end(), sortSource); break; case SortType::Length: - std::sort(songs.begin(), songs.end(), sortTitle); std::sort(songs.begin(), songs.end(), sortLen); break; case SortType::Year: @@ -123,10 +129,12 @@ void SongList::sortList(SortType sortType, int &selectedSong) { break; default:; } - for (int i = 0; i < songs.size(); i++) { - if (songs[i].artist == curSong.artist && songs[i].title == curSong.title) { - selectedSong = i; - break; + if (hasCurrentSong) { + for (size_t i = 0; i < songs.size(); i++) { + if (songs[i].artist == curSong.artist && songs[i].title == curSong.title) { + selectedSong = i; + break; + } } } listMenuEntries = GenerateSongEntriesWithHeaders(songs, sortType); @@ -180,21 +188,100 @@ void SongList::ScanSongs(const std::vector &songsFolder) } directoryCount++; - if (std::filesystem::exists(entry.path() / "info.json")) { - Song song; - song.LoadSong(entry.path() / "info.json"); - songs.push_back(std::move(song)); - songCount++; + Song song; + std::filesystem::path infoPath = entry.path() / "info.json"; + if (std::filesystem::exists(infoPath)) { + song.LoadSong(infoPath); + try { + json infoData; + std::ifstream infoFile(infoPath); + infoFile >> infoData; + infoFile.close(); + + if (infoData.contains("source") && infoData["source"].is_string()) { + song.source = infoData["source"].get(); + if (song.source.empty()) { + song.source = "Unknown Source"; + } + } else { + song.source = "Unknown Source"; + } + + if (infoData.contains("release_year") && infoData["release_year"].is_string()) { + song.releaseYear = infoData["release_year"].get(); + if (song.releaseYear.empty()) { + song.releaseYear = "Unknown Year"; + } + } else { + song.releaseYear = "Unknown Year"; + } + + if (infoData.contains("preview_start_time") && infoData["preview_start_time"].is_number_integer()) { + song.previewStartTime = infoData["preview_start_time"].get(); + } else { + song.previewStartTime = 3000; + } + + } catch (const std::exception& e) { + song.source = "Unknown Source"; + song.releaseYear = "Unknown Year"; + song.previewStartTime = 500; + } } else if (std::filesystem::exists(entry.path() / "song.ini")) { - Song song; - song.songInfoPath = (entry.path() / "song.ini").string(); song.songDir = entry.path().string(); song.LoadSongIni(entry.path()); song.ini = true; - songs.push_back(std::move(song)); - songCount++; + if (std::filesystem::exists(infoPath)) { + try { + json infoData; + std::ifstream infoFile(infoPath); + infoFile >> infoData; + infoFile.close(); + + if (infoData.contains("source") && infoData["source"].is_string()) { + song.source = infoData["source"].get(); + if (song.source.empty()) { + song.source = "Unknown Source"; + } + } else { + song.source = "Unknown Source"; + } + + if (infoData.contains("release_year") && infoData["release_year"].is_string()) { + song.releaseYear = infoData["release_year"].get(); + if (song.releaseYear.empty()) { + song.releaseYear = "Unknown Year"; + } + } else { + song.releaseYear = "Unknown Year"; + } + + if (infoData.contains("preview_start_time") && infoData["preview_start_time"].is_number_integer()) { + song.previewStartTime = infoData["preview_start_time"].get(); + } else { + song.previewStartTime = 500; + } + + Encore::EncoreLog(LOG_INFO, TextFormat("CACHE: Read metadata for INI song %s - %s from %s", song.title.c_str(), song.artist.c_str(), infoPath.string().c_str())); + } catch (const std::exception& e) { + Encore::EncoreLog(LOG_ERROR, TextFormat("CACHE: Failed to read metadata for INI song %s - %s from %s: %s", song.title.c_str(), song.artist.c_str(), infoPath.string().c_str(), e.what())); + song.source = "Unknown Source"; + song.releaseYear = "Unknown Year"; + song.previewStartTime = 500; + } + } else { + song.source = "Unknown Source"; + song.releaseYear = "Unknown Year"; + song.previewStartTime = 500; + Encore::EncoreLog(LOG_INFO, TextFormat("CACHE: No info.json for INI song %s - %s, using default metadata", song.title.c_str(), song.artist.c_str())); + } + } else { + continue; } + + songs.push_back(std::move(song)); + songCount++; } } @@ -202,60 +289,56 @@ void SongList::ScanSongs(const std::vector &songsFolder) WriteCache(); } +std::string GetLengthHeader(int length) { + if (length < 60) return "< 1:00"; + if (length < 120) return "1:00-2:00"; + if (length < 180) return "2:00-3:00"; + if (length < 240) return "3:00-4:00"; + if (length < 300) return "4:00-5:00"; + return "5:00+"; +} + std::vector SongList::GenerateSongEntriesWithHeaders( const std::vector &songs, SortType sortType ) { std::vector songEntries; std::string currentHeader = ""; int pos = 0; - for (int i = 0; i < songs.size(); i++) { + for (size_t i = 0; i < songs.size(); i++) { const Song &song = songs[i]; + std::string header; switch (sortType) { case SortType::Title: { std::string title = removeArticle(TextToLower(song.title.c_str())); - if (toupper(title[0]) != currentHeader[0]) { - currentHeader = toupper(title[0]); - songEntries.emplace_back(true, 0, currentHeader, false); - pos++; - } + header = title.empty() ? "#" : std::string(1, toupper(title[0])); break; } case SortType::Artist: { std::string artist = removeArticle(song.artist); - if (artist != currentHeader) { - currentHeader = song.artist; - songEntries.emplace_back(true, 0, currentHeader, false); - pos++; - } + header = artist.empty() ? "#" : artist; break; } case SortType::Source: { std::string source = removeArticle(song.source); - if (source != currentHeader) { - currentHeader = song.source; - songEntries.emplace_back(true, 0, currentHeader, false); - pos++; - } + header = source.empty() ? "Unknown" : source; break; } case SortType::Length: { - if (std::to_string(song.length) != currentHeader) { - currentHeader = std::to_string(song.length); - songEntries.emplace_back(true, 0, currentHeader, false); - pos++; - } + header = GetLengthHeader(song.length); break; } case SortType::Year: { - std::string year = removeArticle(song.releaseYear); - if (year != currentHeader) { - currentHeader = song.releaseYear; - songEntries.emplace_back(true, 0, currentHeader, false); - pos++; - } + header = song.releaseYear.empty() ? "Unknown Year" : song.releaseYear; break; } - default:; + default: + header = "#"; + break; + } + if (header != currentHeader) { + currentHeader = header; + songEntries.emplace_back(true, 0, currentHeader, false); + pos++; } songEntries.emplace_back(false, i, "", false); pos++; @@ -404,4 +487,4 @@ void SongList::LoadCache(const std::vector &songsFolder) // ScanSongs(songsFolder); sortList(SortType::Title); -} +} \ No newline at end of file diff --git a/Encore/src/util/discord.cpp b/Encore/src/util/discord.cpp index 2f07471..321ff1f 100644 --- a/Encore/src/util/discord.cpp +++ b/Encore/src/util/discord.cpp @@ -11,6 +11,14 @@ discord::Core* core{}; void Encore::Discord::Initialize() { +#ifdef __linux__ + // Disable Discord on Linux to prevent loading errors + std::cout << "Discord integration disabled on Linux\n"; + Initialized = false; + core = nullptr; + return; +#endif + auto result = discord::Core::Create(1216298119457804379, DiscordCreateFlags_Default, &core); if (!core) { std::cout << "Failed to instantiate discord core! (err " << static_cast(result) @@ -40,11 +48,13 @@ Encore::Discord::~Discord() { Initialized = false; } void Encore::Discord::Update() { + if (!Initialized || !core) + return; core->RunCallbacks(); } void Encore::Discord::DiscordUpdatePresence(const std::string &title, const std::string &details, int players) { - if (!Initialized) + if (!Initialized || !core) return; discord::Activity activity{}; // activity.SetState(title.c_str()); @@ -80,7 +90,7 @@ std::array PartNames = { void Encore::Discord::DiscordUpdatePresenceSong( const std::string &title, const std::string &details, int instrument, int length ) { - if (!Initialized) + if (!Initialized || !core) return; discord::Activity activity{}; activity.SetDetails((details).c_str());