diff --git a/.github/actions/setup-haxe/action.yml b/.github/actions/setup-haxe/action.yml new file mode 100644 index 000000000..54db9bf79 --- /dev/null +++ b/.github/actions/setup-haxe/action.yml @@ -0,0 +1,135 @@ +name: setup-haxeshit +description: "sets up haxe shit, using HMM!" + +inputs: + haxe: + description: 'Version of haxe to install' + required: true + default: '4.3.4' + hxcpp-cache: + description: 'Whether to use a shared hxcpp compile cache' + required: true + default: 'true' + hxcpp-cache-path: + description: 'Path to create hxcpp cache in' + required: true + default: ${{ runner.temp }}/hxcpp_cache + targets: + description: 'Targets we plan to compile to. Installs native dependencies needed.' + required: true + +runs: + using: "composite" + steps: + + - name: Setup timers + shell: bash + run: | + echo "TIMER_HAXE=$(date +%s)" >> "$GITHUB_ENV" + + - name: Install Haxe + uses: funkincrew/ci-haxe@v3.1.0 + with: + haxe-version: ${{ inputs.haxe }} + + - name: Install native dependencies + if: ${{ runner.os == 'Linux' }} + shell: bash + run: | + ls -lah /usr/lib/x86_64-linux-gnu/ + apt-get update + apt-get install -y \ + g++ \ + libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \ + libgl-dev libgl1-mesa-dev \ + libasound2-dev + ln -s /usr/lib/x86_64-linux-gnu/libffi.so.8 /usr/lib/x86_64-linux-gnu/libffi.so.6 || true + - name: Install linux-specific dependencies + if: ${{ runner.os == 'Linux' && contains(inputs.targets, 'linux') }} + shell: bash + run: | + apt-get install -y libvlc-dev libvlccore-dev + + - name: Config haxelib + shell: bash + run: | + echo "TIMER_HAXELIB=$(date +%s)" >> "$GITHUB_ENV" + haxelib --debug --never install haxelib 4.1.0 --global + haxelib --debug --never deleterepo || true + haxelib --debug --never newrepo + echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV" + haxelib --debug --never git haxelib https://github.com/HaxeFoundation/haxelib.git master + haxelib --debug --global install hmm + echo "TIMER_DEPS=$(date +%s)" >> "$GITHUB_ENV" + + - name: Restore cached dependencies + id: cache-hmm + uses: actions/cache@v4 + with: + path: .haxelib + key: haxe-hmm-${{ runner.os }}-${{ hashFiles('**/hmm.json') }} + + - if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }} + name: Install dependencies + shell: bash + run: | + haxelib --debug --global run hmm install + echo "TIMER_DONE=$(date +%s)" >> "$GITHUB_ENV" + + # by default use a shared hxcpp cache + - if: ${{ inputs.hxcpp-cache == 'true' }} + name: Restore hxcpp cache + uses: actions/cache@v4 + with: + path: ${{ inputs.hxcpp-cache-path }} + key: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }} + restore-keys: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }} + # export env for it to reuse in builds + - if: ${{ inputs.hxcpp-cache == 'true' }} + name: Persist env for hxcpp cache + shell: bash + run: | + echo "HXCPP_COMPILE_CACHE=${{ inputs.hxcpp-cache-path }}" >> "$GITHUB_ENV" + echo 'HXCPP_CACHE_MB="4096"' >> "$GITHUB_ENV" + + # if it's explicitly disabled, still cache export/ since that then contains the builds + - if: ${{ inputs.hxcpp-cache != 'true' }} + name: Restore export cache + uses: actions/cache@v4 + with: + path: ${{ inputs.hxcpp-cache-path }} + key: haxe-export-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }} + restore-keys: haxe-export-${{ runner.os }}-${{ github.ref_name }} + + - name: Print debug info + shell: bash + run: | + cat << EOF + runner: + kernel: $(uname -a) + haxe: + version: $(haxe -version) + which: $(which haxe) + haxepath: $HAXEPATH + took: $((TIMER_HAXELIB - TIMER_HAXE))s + haxelib: + version: $(haxelib version) + which: $(which haxelib) + local: + config: $(haxelib config) + path: $(haxelib path haxelib || true) + global + config: $(haxelib config --global) + path: $(haxelib path haxelib --global || true) + system + version: $(haxelib --system version) + local: + config: $(haxelib --system config) + global: + config: $(haxelib --system config --global) + took: $((TIMER_DEPS - TIMER_HAXELIB))s + deps: + took: $((TIMER_DONE - TIMER_DEPS))s + hxcpp_cache: | + $(haxelib run hxcpp cache list || true) + EOF diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml deleted file mode 100644 index 236d29944..000000000 --- a/.github/actions/setup-haxeshit/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: setup-haxeshit -description: "sets up haxe shit, using HMM!" -runs: - using: "composite" - steps: - - name: Install Haxe lol - uses: funkincrew/ci-haxe@v3.1.0 - with: - haxe-version: 4.3.3 - - name: Config haxelib - run: | - haxelib --never install haxelib 4.1.0 --global - haxelib --never deleterepo || true - haxelib --never newrepo - echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV" - haxelib --never git haxelib https://github.com/HaxeFoundation/haxelib.git master - shell: bash - - name: Gather debug info - run: | - cat << EOF >> "$GITHUB_STEP_SUMMARY" - ## haxe - - version: \`$(haxe -version)\` - - exe: \`$(which haxe)\` - ## haxelib - - version: \`$(haxelib version)\` - - exe: \`$(which haxelib)\` - - path: \`$HAXEPATH\` - ### local - - config: \`$(haxelib config)\` - - path: \`$(haxelib path haxelib || true)\` - ### global - - config: \`$(haxelib config --global)\` - - path: \`$(haxelib path haxelib --global || true)\` - ### system - - version: \`$(haxelib --system version)\` - - local: \`$(haxelib --system config)\` - - global: \`$(haxelib --system config --global)\` - EOF - shell: bash - - name: Install hmm - # hmm only supports global installs - run: | - haxelib --global install hmm - shell: bash - - name: Restore cached dependencies - id: cache-hmm - uses: actions/cache@v4 - with: - path: .haxelib - key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }} - - if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }} - name: hmm install - run: | - haxelib --global run hmm install - shell: bash diff --git a/.github/actions/upload-itch/action.yml b/.github/actions/upload-itch/action.yml index 2f7d3027d..fb049efc9 100644 --- a/.github/actions/upload-itch/action.yml +++ b/.github/actions/upload-itch/action.yml @@ -1,44 +1,124 @@ name: upload-itch description: "installs Butler, and uploads to itch.io!" + inputs: butler-key: description: "Butler API secret key" required: true + itch-repo: + description: "Where to upload the game to" + required: true + default: "ninja-muffin24/funkin-secret" build-dir: description: "Directory of the game build" - required: true + required: false target: - description: "Target (html5, win, linux, mac)" + description: "Target (html5, windows, linux, macos)" required: true + runs: using: "composite" steps: - - name: Install butler Windows - if: runner.os == 'Windows' - run: | - curl -L -o butler.zip https://broth.itch.ovh/butler/windows-amd64/LATEST/archive/default - 7z x butler.zip - ./butler -v - shell: bash - - name: Install butler Mac - if: runner.os == 'macOS' - run: | - curl -L -o butler.zip https://broth.itch.ovh/butler/darwin-amd64/LATEST/archive/default - unzip butler.zip - ./butler -V - shell: bash - - name: Install butler Linux - if: runner.os == 'Linux' - run: | - curl -L -o butler.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default - unzip butler.zip - chmod +x butler - ./butler -V - shell: bash - - name: Upload game to itch.io - env: - BUTLER_API_KEY: ${{inputs.butler-key}} - run: | - ./butler login - ./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME} - shell: bash + + # RUNNER_OS = Windows | macOS | Linux + # TARGET_BUILD = windows | macos | linux + # TARGET_ITCH = win | macos | linux + # TARGET_BUTLER_DOWNLOAD = windows-amd64 | darwin-amd64 | linux-amd64 + - name: Setup variables + shell: bash + run: | + TARGET_OS=${{ inputs.target }} + RUNNER=${RUNNER_OS@L} + TARGET=${TARGET_OS@L} + case "$TARGET" in + "windows") + TARGET_ITCH=win + ;; + *) + TARGET_ITCH=$TARGET + ;; + esac + case "$RUNNER" in + "macos") + OS_NODE=darwin + ;; + *) + OS_NODE=$RUNNER + ;; + esac + BUTLER_PATH=$RUNNER_TEMP/butler + + echo BUILD_DIR="export/release/$TARGET/bin" >> "$GITHUB_ENV" + echo ITCH_TAG=${{ inputs.itch-repo }}:$TARGET_ITCH-$GITHUB_REF_NAME >> "$GITHUB_ENV" + echo OS_AND_ARCH=$OS_NODE-amd64 >> "$GITHUB_ENV" + echo BUTLER_API_KEY=${{ inputs.butler-key }} >> "$GITHUB_ENV" + echo BUTLER_INSTALL_PATH=$BUTLER_PATH >> "$GITHUB_ENV" + echo TIMER_BUTLER=$(date +%s) >> "$GITHUB_ENV" + echo TARGET_ITCH=$TARGET_ITCH >> "$GITHUB_ENV" + + echo "$BUTLER_PATH" >> "$GITHUB_PATH" + + - name: Get latest butler version + shell: bash + run: | + LATEST=$(curl -sfL https://broth.itch.ovh/butler/$OS_AND_ARCH/LATEST) + echo BUTLER_LATEST=$LATEST >> "$GITHUB_ENV" + + command -v butler \ + && echo BUTLER_CURRENT=$(butler -V 2>&1 | cut -d ',' -f 1) >> "$GITHUB_ENV" \ + || echo BUTLER_CURRENT=none >> "$GITHUB_ENV" + + - name: Try to get butler from cache + id: cache-butler + uses: actions/cache@v4 + with: + path: ${{ env.BUTLER_INSTALL_PATH }} + key: butler-${{ runner.os }}-${{ env.BUTLER_LATEST }} + + - if: steps.cache-butler.outputs.cache-hit == 'true' + name: Make sure butler is executable + shell: bash + run: | + chmod +x $BUTLER_INSTALL_PATH/butler + + - if: steps.cache-butler.outputs.cache-hit != 'true' + name: Install butler + shell: bash + run: | + mkdir -p $BUTLER_INSTALL_PATH + cd $BUTLER_INSTALL_PATH + + curl -L -o butler.zip https://broth.itch.ovh/butler/$OS_AND_ARCH/LATEST/archive/default + unzip butler.zip + chmod +x butler + + - name: Upload game to itch.io + shell: bash + run: | + echo "TIMER_UPLOAD=$(date +%s)" >> "$GITHUB_ENV" + butler login + butler push $BUILD_DIR $ITCH_TAG + echo "TIMER_DONE=$(date +%s)" >> "$GITHUB_ENV" + + - name: Print debug info + shell: bash + run: | + cat << EOF + butler: + version: $( + if [[ "$BUTLER_CURRENT" == "$BUTLER_LATEST" ]] + then + echo $BUTLER_CURRENT + else + echo $BUTLER_CURRENT -> $BUTLER_LATEST + fi + ) + install: + took: $(($TIMER_UPLOAD-$TIMER_BUTLER))s + upload: + tag: $TARGET_ITCH/$GITHUB_REF_NAME + took: $(($TIMER_DONE-$TIMER_UPLOAD))s + EOF + cat << EOF >> "$GITHUB_STEP_SUMMARY" + ### open in launcher: [$TARGET_ITCH/$GITHUB_REF_NAME](https://run.funkin.me/$TARGET_ITCH/$GITHUB_REF_NAME) + EOF diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml new file mode 100644 index 000000000..15c9e5582 --- /dev/null +++ b/.github/workflows/build-docker-image.yml @@ -0,0 +1,53 @@ +name: Create and publish Docker image + +on: + workflow_dispatch: + push: + paths: + - '**/Dockerfile' + - '.github/workflows/build-docker-image.yml' + +jobs: + build-and-push-image: + runs-on: build-set + permissions: + contents: read + packages: write + + steps: + - name: Get checkout token + uses: actions/create-github-app-token@v1 + id: app_token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PEM }} + owner: ${{ github.repository_owner }} + + - name: Checkout repo + uses: funkincrew/ci-checkout@v6 + with: + submodules: false + token: ${{ steps.app_token.outputs.token }} + + - name: Log into GitHub Container Registry + uses: docker/login-action@v3.1.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5.3.0 + with: + context: ./build + push: true + tags: | + ghcr.io/funkincrew/build-dependencies:latest + ghcr.io/funkincrew/build-dependencies:${{ github.sha }} + labels: | + org.opencontainers.image.description=precooked haxe build-dependencies + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.title=${{ github.repository_owner }}/build-dependencies + org.opencontainers.image.url=https://github.com/${{ github.repository }} + org.opencontainers.image.version=${{ github.sha }} diff --git a/.github/workflows/build-game.yml b/.github/workflows/build-game.yml new file mode 100644 index 000000000..3bfea20f2 --- /dev/null +++ b/.github/workflows/build-game.yml @@ -0,0 +1,125 @@ +name: Build and Upload nightly game builds + +on: + workflow_dispatch: + push: + paths-ignore: + - '**/Dockerfile' + - '.github/workflows/build-docker-image.yml' + +jobs: + + build-game-on-host: + strategy: + matrix: + include: + - target: windows + - target: macos + runs-on: + - ${{ matrix.target }} + defaults: + run: + shell: bash + + steps: + - name: Make git happy + if: ${{ matrix.target == 'macos' }} + run: | + git config --global --add safe.directory $GITHUB_WORKSPACE + + - name: Get checkout token + uses: actions/create-github-app-token@v1 + id: app_token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PEM }} + owner: ${{ github.repository_owner }} + + - name: Checkout repo + uses: funkincrew/ci-checkout@v6 + with: + submodules: 'recursive' + token: ${{ steps.app_token.outputs.token }} + + - name: Setup build environment + uses: ./.github/actions/setup-haxe + + - name: Build game + if: ${{ matrix.target == 'windows' }} + run: | + haxelib run lime build windows -v -release -DGITHUB_BUILD + timeout-minutes: 120 + - name: Build game + if: ${{ matrix.target != 'windows' }} + run: | + haxelib run lime build ${{ matrix.target }} -v -release --times -DGITHUB_BUILD + timeout-minutes: 120 + + - name: Upload build artifacts + uses: ./.github/actions/upload-itch + with: + butler-key: ${{ secrets.BUTLER_API_KEY}} + target: ${{ matrix.target }} + + build-game-in-container: + runs-on: build-set + container: ghcr.io/funkincrew/build-dependencies:latest + strategy: + matrix: + include: + - target: linux + - target: html5 + defaults: + run: + shell: bash + + steps: + - name: Get checkout token + uses: actions/create-github-app-token@v1 + id: app_token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.APP_PEM }} + owner: ${{ github.repository_owner }} + + - name: Checkout repo + uses: funkincrew/ci-checkout@v6 + with: + submodules: 'recursive' + token: ${{ steps.app_token.outputs.token }} + + - name: Config haxelib + run: | + haxelib --never newrepo + echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV" + + - name: Restore cached dependencies + id: cache-hmm + uses: actions/cache@v4 + with: + path: .haxelib + key: haxe-hmm-${{ runner.os }}-${{ hashFiles('**/hmm.json') }} + + - if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }} + name: Install dependencies + run: | + haxelib --global run hmm install + + - if: ${{ matrix.target != 'html5' }} + name: Restore hxcpp cache + uses: actions/cache@v4 + with: + path: /usr/share/hxcpp + key: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }}-${{ github.sha }} + restore-keys: haxe-hxcpp-${{ runner.os }}-${{ github.ref_name }} + + - name: Build game + run: | + haxelib run lime build ${{ matrix.target }} -v -release --times -DGITHUB_BUILD + timeout-minutes: 120 + + - name: Upload build artifacts + uses: ./.github/actions/upload-itch + with: + butler-key: ${{ secrets.BUTLER_API_KEY}} + target: ${{ matrix.target }} diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml deleted file mode 100644 index 4d674f025..000000000 --- a/.github/workflows/build-shit.yml +++ /dev/null @@ -1,136 +0,0 @@ -name: build-upload -on: - workflow_dispatch: - push: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - create-nightly-html5: - runs-on: [self-hosted, linux] - container: ubuntu:23.10 - steps: - - name: Install tools missing in container - run: | - apt update - apt install -y sudo git curl unzip - - name: Fix git config on posix runner - # this can't be {{ github.workspace }} because that's not docker-aware - run: | - git config --global --add safe.directory $GITHUB_WORKSPACE - - name: Get checkout token - uses: actions/create-github-app-token@v1 - id: app_token - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PEM }} - owner: ${{ github.repository_owner }} - - name: Checkout repo - uses: funkincrew/ci-checkout@v6 - with: - submodules: 'recursive' - token: ${{ steps.app_token.outputs.token }} - - name: Install Haxe, dependencies - uses: ./.github/actions/setup-haxeshit - - name: Install native dependencies - run: | - apt install -y \ - libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \ - libgl-dev libgl1-mesa-dev \ - libasound2-dev - - name: Build game - run: | - haxelib run lime build html5 -release --times -DGITHUB_BUILD - - name: Upload build artifacts - uses: ./.github/actions/upload-itch - with: - butler-key: ${{ secrets.BUTLER_API_KEY}} - build-dir: export/release/html5/bin - target: html5 - create-nightly-win: - runs-on: [self-hosted, windows] - defaults: - run: - shell: bash - steps: - - name: Get checkout token - uses: actions/create-github-app-token@v1 - id: app_token - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PEM }} - owner: ${{ github.repository_owner }} - - name: Checkout repo - uses: funkincrew/ci-checkout@v6 - with: - submodules: 'recursive' - token: ${{ steps.app_token.outputs.token }} - - name: Install Haxe, dependencies - uses: ./.github/actions/setup-haxeshit - - name: Setup build cache - run: | - mkdir -p ${{ runner.temp }}/hxcpp_cache - - name: Restore build cache - id: cache-build-win - uses: actions/cache@v4 - with: - path: | - export - ${{ runner.temp }}/hxcpp_cache - key: ${{ runner.os }}-build-win-${{ github.ref_name }} - - name: Build game - run: | - haxelib run lime build windows -v -release -DGITHUB_BUILD - env: - HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache" - - name: Upload build artifacts - uses: ./.github/actions/upload-itch - with: - butler-key: ${{ secrets.BUTLER_API_KEY }} - build-dir: export/release/windows/bin - target: win - create-nightly-mac: - runs-on: [self-hosted, macos] - steps: - - name: Fix git config on posix runner - # this can't be {{ github.workspace }} because that's not docker-aware - run: | - git config --global --add safe.directory $GITHUB_WORKSPACE - - name: Get checkout token - uses: actions/create-github-app-token@v1 - id: app_token - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_PEM }} - owner: ${{ github.repository_owner }} - - name: Checkout repo - uses: funkincrew/ci-checkout@v6 - with: - submodules: 'recursive' - token: ${{ steps.app_token.outputs.token }} - - name: Install Haxe, dependencies - uses: ./.github/actions/setup-haxeshit - - name: Setup build cache - run: | - mkdir -p ${{ runner.temp }}/hxcpp_cache - - name: Restore build cache - id: cache-build-win - uses: actions/cache@v4 - with: - path: | - export - ${{ runner.temp }}/hxcpp_cache - key: ${{ runner.os }}-build-mac-${{ github.ref_name }} - - name: Build game - run: | - haxelib run lime build macos -release --times -DGITHUB_BUILD - env: - HXCPP_COMPILE_CACHE: "${{ runner.temp }}/hxcpp_cache" - - name: Upload build artifacts - uses: ./.github/actions/upload-itch - with: - butler-key: ${{ secrets.BUTLER_API_KEY}} - build-dir: export/release/macos/bin - target: macos diff --git a/.github/workflows/cancel-merged-branches.yml b/.github/workflows/cancel-merged-branches.yml index 84e3bedc9..f66f9647b 100644 --- a/.github/workflows/cancel-merged-branches.yml +++ b/.github/workflows/cancel-merged-branches.yml @@ -1,35 +1,38 @@ -name: cancel-merged-branches +name: Cancel queued workflows on PR merge + on: pull_request: types: - closed jobs: + cancel_stuff: if: github.event.pull_request.merged == true - runs-on: ubuntu-latest + runs-on: build-set permissions: actions: write + steps: - - uses: actions/github-script@v7 - id: cancel-runs - with: - result-encoding: string - retries: 3 - script: | - let branch_workflows = await github.rest.actions.listWorkflowRuns({ + - name: Cancel queued workflows for ${{ github.event.pull_request.head.ref }} + uses: actions/github-script@v7 + with: + result-encoding: string + retries: 3 + script: | + let branch_workflows = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: "build-shit.yml", + status: "queued", + branch: "${{ github.event.pull_request.head.ref }}" + }); + let runs = branch_workflows.data.workflow_runs; + runs.forEach((run) => { + github.rest.actions.cancelWorkflowRun({ owner: context.repo.owner, repo: context.repo.repo, - workflow_id: "build-shit.yml", - status: "queued", - branch: "${{ github.event.pull_request.head.ref }}" + run_id: run.id }); - let runs = branch_workflows.data.workflow_runs; - runs.forEach((run) => { - github.rest.actions.cancelWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: run.id - }); - }); - console.log(runs); + }); + console.log(runs); diff --git a/.vscode/settings.json b/.vscode/settings.json index 13a1862d2..c28bebeab 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -96,6 +96,11 @@ "target": "windows", "args": ["-debug", "-DFORCE_DEBUG_VERSION"] }, + { + "label": "Linux / Debug", + "target": "linux", + "args": ["-debug", "-DFORCE_DEBUG_VERSION"] + }, { "label": "HashLink / Debug", "target": "hl", @@ -130,6 +135,11 @@ "-DFORCE_DEBUG_VERSION" ] }, + { + "label": "Windows / Debug (Straight to Play - 2hot)", + "target": "windows", + "args": ["-debug", "-DSONG=2hot", "-DFORCE_DEBUG_VERSION"] + }, { "label": "HashLink / Debug (Straight to Play - Bopeebo Normal)", "target": "hl", diff --git a/Project.xml b/Project.xml index 8ba14e7dc..db338d32a 100644 --- a/Project.xml +++ b/Project.xml @@ -45,6 +45,7 @@ +
@@ -58,10 +59,13 @@ +
+ + diff --git a/assets b/assets index 1a7a0b6cc..069c9bf45 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 1a7a0b6cc60dc8131f1651caa7abef0c1944a10c +Subproject commit 069c9bf45f197ebe0b38483d11bb30c15bbb5eca diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 000000000..88b44f7a6 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,185 @@ +FROM ubuntu:mantic + +ARG haxe_version=4.3.4 +ARG haxelib_version=4.1.0 +ARG neko_version=2.3.0 + +# prepare runner +ENV GITHUB_HOME="/github/home" + +RUN <> /etc/apt/apt.conf.d/10apt-autoremove +APT::Get::AutomaticRemove "0"; +APT::Get::HideAutoRemove "1"; +EOC + +echo <> /etc/apt/apt.conf.d/80retries +"APT::Acquire::Retries \"10\";" +EOC + +echo <> /etc/apt/apt.conf.d/90assumeyes +"APT::Get::Assume-Yes \"true\";" +EOC +EOF + +# Prepare apt-fast +RUN <> /etc/gitconfig +[safe] + directory = * +EOC + +ssh-keyscan -t rsa,ecdsa,ed25519 github.com >> /etc/ssh/ssh_known_hosts +ssh-keyscan -t rsa,ecdsa,ed25519 ravy.dev >> /etc/ssh/ssh_known_hosts +EOF + +# Haxe native dependencies +RUN < // already paused before we lost focus. if (_lostFocus && !_alreadyPaused) { + trace('Resuming audio (${this._label}) on focus!'); resume(); } else { - trace('Not resuming audio on focus!'); + trace('Not resuming audio (${this._label}) on focus!'); } _lostFocus = false; } @@ -402,6 +403,12 @@ class FunkinSound extends FlxSound implements ICloneable sound.group = FlxG.sound.defaultSoundGroup; sound.persist = true; + // Make sure to add the sound to the list. + // If it's already in, it won't get re-added. + // If it's not in the list (it gets removed by FunkinSound.playMusic()), + // it will get re-added (then if this was called by playMusic(), removed again) + FlxG.sound.list.add(sound); + // Call onLoad() because the sound already loaded if (onLoad != null && sound._sound != null) onLoad(); diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx index 0857678d0..d76c26153 100644 --- a/source/funkin/input/Controls.hx +++ b/source/funkin/input/Controls.hx @@ -67,7 +67,7 @@ class Controls extends FlxActionSet var _volume_down = new FlxActionDigital(Action.VOLUME_DOWN); var _volume_mute = new FlxActionDigital(Action.VOLUME_MUTE); - var byName:Map = new Map(); + var byName:Map = new Map(); public var gamepadsAdded:Array = []; public var keyboardScheme = KeyboardScheme.None; @@ -75,122 +75,142 @@ class Controls extends FlxActionSet public var UI_UP(get, never):Bool; inline function get_UI_UP() - return _ui_up.check(); + return _ui_up.checkPressed(); public var UI_LEFT(get, never):Bool; inline function get_UI_LEFT() - return _ui_left.check(); + return _ui_left.checkPressed(); public var UI_RIGHT(get, never):Bool; inline function get_UI_RIGHT() - return _ui_right.check(); + return _ui_right.checkPressed(); public var UI_DOWN(get, never):Bool; inline function get_UI_DOWN() - return _ui_down.check(); + return _ui_down.checkPressed(); public var UI_UP_P(get, never):Bool; inline function get_UI_UP_P() - return _ui_upP.check(); + return _ui_up.checkJustPressed(); public var UI_LEFT_P(get, never):Bool; inline function get_UI_LEFT_P() - return _ui_leftP.check(); + return _ui_left.checkJustPressed(); public var UI_RIGHT_P(get, never):Bool; inline function get_UI_RIGHT_P() - return _ui_rightP.check(); + return _ui_right.checkJustPressed(); public var UI_DOWN_P(get, never):Bool; inline function get_UI_DOWN_P() - return _ui_downP.check(); + return _ui_down.checkJustPressed(); public var UI_UP_R(get, never):Bool; inline function get_UI_UP_R() - return _ui_upR.check(); + return _ui_up.checkJustReleased(); public var UI_LEFT_R(get, never):Bool; inline function get_UI_LEFT_R() - return _ui_leftR.check(); + return _ui_left.checkJustReleased(); public var UI_RIGHT_R(get, never):Bool; inline function get_UI_RIGHT_R() - return _ui_rightR.check(); + return _ui_right.checkJustReleased(); public var UI_DOWN_R(get, never):Bool; inline function get_UI_DOWN_R() - return _ui_downR.check(); + return _ui_down.checkJustReleased(); + + public var UI_UP_GAMEPAD(get, never):Bool; + + inline function get_UI_UP_GAMEPAD() + return _ui_up.checkPressedGamepad(); + + public var UI_LEFT_GAMEPAD(get, never):Bool; + + inline function get_UI_LEFT_GAMEPAD() + return _ui_left.checkPressedGamepad(); + + public var UI_RIGHT_GAMEPAD(get, never):Bool; + + inline function get_UI_RIGHT_GAMEPAD() + return _ui_right.checkPressedGamepad(); + + public var UI_DOWN_GAMEPAD(get, never):Bool; + + inline function get_UI_DOWN_GAMEPAD() + return _ui_down.checkPressedGamepad(); public var NOTE_UP(get, never):Bool; inline function get_NOTE_UP() - return _note_up.check(); + return _note_up.checkPressed(); public var NOTE_LEFT(get, never):Bool; inline function get_NOTE_LEFT() - return _note_left.check(); + return _note_left.checkPressed(); public var NOTE_RIGHT(get, never):Bool; inline function get_NOTE_RIGHT() - return _note_right.check(); + return _note_right.checkPressed(); public var NOTE_DOWN(get, never):Bool; inline function get_NOTE_DOWN() - return _note_down.check(); + return _note_down.checkPressed(); public var NOTE_UP_P(get, never):Bool; inline function get_NOTE_UP_P() - return _note_upP.check(); + return _note_up.checkJustPressed(); public var NOTE_LEFT_P(get, never):Bool; inline function get_NOTE_LEFT_P() - return _note_leftP.check(); + return _note_left.checkJustPressed(); public var NOTE_RIGHT_P(get, never):Bool; inline function get_NOTE_RIGHT_P() - return _note_rightP.check(); + return _note_right.checkJustPressed(); public var NOTE_DOWN_P(get, never):Bool; inline function get_NOTE_DOWN_P() - return _note_downP.check(); + return _note_down.checkJustPressed(); public var NOTE_UP_R(get, never):Bool; inline function get_NOTE_UP_R() - return _note_upR.check(); + return _note_up.checkJustReleased(); public var NOTE_LEFT_R(get, never):Bool; inline function get_NOTE_LEFT_R() - return _note_leftR.check(); + return _note_left.checkJustReleased(); public var NOTE_RIGHT_R(get, never):Bool; inline function get_NOTE_RIGHT_R() - return _note_rightR.check(); + return _note_right.checkJustReleased(); public var NOTE_DOWN_R(get, never):Bool; inline function get_NOTE_DOWN_R() - return _note_downR.check(); + return _note_down.checkJustReleased(); public var ACCEPT(get, never):Bool; @@ -260,26 +280,10 @@ class Controls extends FlxActionSet add(_ui_left); add(_ui_right); add(_ui_down); - add(_ui_upP); - add(_ui_leftP); - add(_ui_rightP); - add(_ui_downP); - add(_ui_upR); - add(_ui_leftR); - add(_ui_rightR); - add(_ui_downR); add(_note_up); add(_note_left); add(_note_right); add(_note_down); - add(_note_upP); - add(_note_leftP); - add(_note_rightP); - add(_note_downP); - add(_note_upR); - add(_note_leftR); - add(_note_rightR); - add(_note_downR); add(_accept); add(_back); add(_pause); @@ -293,8 +297,16 @@ class Controls extends FlxActionSet add(_volume_down); add(_volume_mute); - for (action in digitalActions) - byName[action.name] = action; + for (action in digitalActions) { + if (Std.isOfType(action, FunkinAction)) { + var funkinAction:FunkinAction = cast action; + byName[funkinAction.name] = funkinAction; + if (funkinAction.namePressed != null) + byName[funkinAction.namePressed] = funkinAction; + if (funkinAction.nameReleased != null) + byName[funkinAction.nameReleased] = funkinAction; + } + } if (scheme == null) scheme = None; @@ -307,14 +319,17 @@ class Controls extends FlxActionSet super.update(); } - // inline - public function checkByName(name:Action):Bool + public function check(name:Action, trigger:FlxInputState = JUST_PRESSED, gamepadOnly:Bool = false):Bool { #if debug if (!byName.exists(name)) throw 'Invalid name: $name'; #end - return byName[name].check(); + var action = byName[name]; + if (gamepadOnly) + return action.checkFiltered(trigger, GAMEPAD); + else + return action.checkFiltered(trigger); } public function getKeysForAction(name:Action):Array { @@ -405,36 +420,36 @@ class Controls extends FlxActionSet { case UI_UP: func(_ui_up, PRESSED); - func(_ui_upP, JUST_PRESSED); - func(_ui_upR, JUST_RELEASED); + func(_ui_up, JUST_PRESSED); + func(_ui_up, JUST_RELEASED); case UI_LEFT: func(_ui_left, PRESSED); - func(_ui_leftP, JUST_PRESSED); - func(_ui_leftR, JUST_RELEASED); + func(_ui_left, JUST_PRESSED); + func(_ui_left, JUST_RELEASED); case UI_RIGHT: func(_ui_right, PRESSED); - func(_ui_rightP, JUST_PRESSED); - func(_ui_rightR, JUST_RELEASED); + func(_ui_right, JUST_PRESSED); + func(_ui_right, JUST_RELEASED); case UI_DOWN: func(_ui_down, PRESSED); - func(_ui_downP, JUST_PRESSED); - func(_ui_downR, JUST_RELEASED); + func(_ui_down, JUST_PRESSED); + func(_ui_down, JUST_RELEASED); case NOTE_UP: func(_note_up, PRESSED); - func(_note_upP, JUST_PRESSED); - func(_note_upR, JUST_RELEASED); + func(_note_up, JUST_PRESSED); + func(_note_up, JUST_RELEASED); case NOTE_LEFT: func(_note_left, PRESSED); - func(_note_leftP, JUST_PRESSED); - func(_note_leftR, JUST_RELEASED); + func(_note_left, JUST_PRESSED); + func(_note_left, JUST_RELEASED); case NOTE_RIGHT: func(_note_right, PRESSED); - func(_note_rightP, JUST_PRESSED); - func(_note_rightR, JUST_RELEASED); + func(_note_right, JUST_PRESSED); + func(_note_right, JUST_RELEASED); case NOTE_DOWN: func(_note_down, PRESSED); - func(_note_downP, JUST_PRESSED); - func(_note_downR, JUST_RELEASED); + func(_note_down, JUST_PRESSED); + func(_note_down, JUST_RELEASED); case ACCEPT: func(_accept, JUST_PRESSED); case BACK: @@ -1042,6 +1057,173 @@ typedef Swipes = ?curTouchPos:FlxPoint }; +/** + * An FlxActionDigital with additional functionality, including: + * - Combining `pressed` and `released` inputs into one action. + * - Filtering by input method (`KEYBOARD`, `MOUSE`, `GAMEPAD`, etc). + */ +class FunkinAction extends FlxActionDigital { + public var namePressed(default, null):Null; + public var nameReleased(default, null):Null; + + var cache:Map = []; + + public function new(?name:String = "", ?namePressed:String, ?nameReleased:String) + { + super(name); + + this.namePressed = namePressed; + this.nameReleased = nameReleased; + } + + /** + * Input checks default to whether the input was just pressed, on any input device. + */ + public override function check():Bool { + return checkFiltered(JUST_PRESSED); + } + + /** + * Check whether the input is currently being held. + */ + public function checkPressed():Bool { + return checkFiltered(PRESSED); + } + + /** + * Check whether the input is currently being held, and was not held last frame. + */ + public function checkJustPressed():Bool { + return checkFiltered(JUST_PRESSED); + } + + /** + * Check whether the input is not currently being held. + */ + public function checkReleased():Bool { + return checkFiltered(RELEASED); + } + + /** + * Check whether the input is not currently being held, and was held last frame. + */ + public function checkJustReleased():Bool { + return checkFiltered(JUST_RELEASED); + } + + /** + * Check whether the input is currently being held by a gamepad device. + */ + public function checkPressedGamepad():Bool { + return checkFiltered(PRESSED, GAMEPAD); + } + + /** + * Check whether the input is currently being held by a gamepad device, and was not held last frame. + */ + public function checkJustPressedGamepad():Bool { + return checkFiltered(JUST_PRESSED, GAMEPAD); + } + + /** + * Check whether the input is not currently being held by a gamepad device. + */ + public function checkReleasedGamepad():Bool { + return checkFiltered(RELEASED, GAMEPAD); + } + + /** + * Check whether the input is not currently being held by a gamepad device, and was held last frame. + */ + public function checkJustReleasedGamepad():Bool { + return checkFiltered(JUST_RELEASED, GAMEPAD); + } + + public function checkMultiFiltered(?filterTriggers:Array, ?filterDevices:Array):Bool { + if (filterTriggers == null) { + filterTriggers = [PRESSED, JUST_PRESSED]; + } + if (filterDevices == null) { + filterDevices = []; + } + + // Perform checkFiltered for each combination. + for (i in filterTriggers) { + if (filterDevices.length == 0) { + if (checkFiltered(i)) { + return true; + } + } else { + for (j in filterDevices) { + if (checkFiltered(i, j)) { + return true; + } + } + } + } + return false; + } + + /** + * Performs the functionality of `FlxActionDigital.check()`, but with optional filters. + * @param action The action to check for. + * @param filterTrigger Optionally filter by trigger condition (`JUST_PRESSED`, `PRESSED`, `JUST_RELEASED`, `RELEASED`). + * @param filterDevice Optionally filter by device (`KEYBOARD`, `MOUSE`, `GAMEPAD`, `OTHER`). + */ + public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool { + // The normal + + // Make sure we only update the inputs once per frame. + var key = '${filterTrigger}:${filterDevice}'; + var cacheEntry = cache.get(key); + + if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks) { + return cacheEntry.value; + } + // Use a for loop instead so we can remove inputs while iterating. + + // We don't return early because we need to call check() on ALL inputs. + var result = false; + var len = inputs != null ? inputs.length : 0; + for (i in 0...len) + { + var j = len - i - 1; + var input = inputs[j]; + + // Filter out dead inputs. + if (input.destroyed) + { + inputs.splice(j, 1); + continue; + } + + // Update the input. + input.update(); + + // Check whether the input is the right trigger. + if (filterTrigger != null && input.trigger != filterTrigger) { + continue; + } + + // Check whether the input is the right device. + if (filterDevice != null && input.device != filterDevice) { + continue; + } + + // Check whether the input has triggered. + if (input.check(this)) + { + result = true; + } + } + + // We need to cache this result. + cache.set(key, {timestamp: FlxG.game.ticks, value: result}); + + return result; + } +} + class FlxActionInputDigitalMobileSwipeGameplay extends FlxActionInputDigital { var touchMap:Map = new Map(); @@ -1229,8 +1411,7 @@ enum Control DEBUG_STAGE; } -enum -abstract Action(String) to String from String +enum abstract Action(String) to String from String { // NOTE var NOTE_UP = "note_up"; diff --git a/source/funkin/input/TurboActionHandler.hx b/source/funkin/input/TurboActionHandler.hx new file mode 100644 index 000000000..9425db8cd --- /dev/null +++ b/source/funkin/input/TurboActionHandler.hx @@ -0,0 +1,111 @@ +package funkin.input; + +import flixel.input.keyboard.FlxKey; +import flixel.FlxBasic; +import funkin.input.Controls; +import funkin.input.Controls.Action; + +/** + * Handles repeating behavior when holding down a control action. + * + * When the `action` is pressed, `activated` will be true for the first frame, + * then wait `delay` seconds before becoming true for one frame every `interval` seconds. + * + * Example: Pressing Ctrl+Z will undo, while holding Ctrl+Z will start to undo repeatedly. + */ +class TurboActionHandler extends FlxBasic +{ + /** + * Default delay before repeating. + */ + static inline final DEFAULT_DELAY:Float = 0.4; + + /** + * Default interval between repeats. + */ + static inline final DEFAULT_INTERVAL:Float = 0.1; + + /** + * Whether the action for this handler is pressed. + */ + public var pressed(get, never):Bool; + + /** + * Whether the action for this handler is pressed, + * and the handler is ready to repeat. + */ + public var activated(default, null):Bool = false; + + /** + * The Funkin Controls handler. + */ + var controls(get, never):Controls; + + function get_controls():Controls + { + return PlayerSettings.player1.controls; + } + + var action:Action; + + var delay:Float; + var interval:Float; + var gamepadOnly:Bool; + + var pressedTime:Float = 0; + + function new(action:Action, delay:Float = DEFAULT_DELAY, interval:Float = DEFAULT_INTERVAL, gamepadOnly:Bool = false) + { + super(); + this.action = action; + this.delay = delay; + this.interval = interval; + this.gamepadOnly = gamepadOnly; + } + + function get_pressed():Bool + { + return controls.check(action, PRESSED, gamepadOnly); + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (pressed) + { + if (pressedTime == 0) + { + activated = true; + } + else if (pressedTime >= (delay + interval)) + { + activated = true; + pressedTime -= interval; + } + else + { + activated = false; + } + pressedTime += elapsed; + } + else + { + pressedTime = 0; + activated = false; + } + } + + /** + * Builds a TurboActionHandler that monitors from a single key. + * @param inputKey The key to monitor. + * @param delay How long to wait before repeating. + * @param repeatDelay How long to wait between repeats. + * @return A TurboActionHandler + */ + public static overload inline extern function build(action:Action, ?delay:Float = DEFAULT_DELAY, ?interval:Float = DEFAULT_INTERVAL, + ?gamepadOnly:Bool = false):TurboActionHandler + { + return new TurboActionHandler(action, delay, interval); + } +} diff --git a/source/funkin/input/TurboButtonHandler.hx b/source/funkin/input/TurboButtonHandler.hx new file mode 100644 index 000000000..63c2a294b --- /dev/null +++ b/source/funkin/input/TurboButtonHandler.hx @@ -0,0 +1,127 @@ +package funkin.input; + +import flixel.input.gamepad.FlxGamepadInputID; +import flixel.input.gamepad.FlxGamepad; +import flixel.FlxBasic; + +/** + * Handles repeating behavior when holding down a gamepad button or button combination. + * + * When the `inputs` are pressed, `activated` will be true for the first frame, + * then wait `delay` seconds before becoming true for one frame every `interval` seconds. + * + * Example: Pressing Ctrl+Z will undo, while holding Ctrl+Z will start to undo repeatedly. + */ +class TurboButtonHandler extends FlxBasic +{ + /** + * Default delay before repeating. + */ + static inline final DEFAULT_DELAY:Float = 0.4; + + /** + * Default interval between repeats. + */ + static inline final DEFAULT_INTERVAL:Float = 0.1; + + /** + * Whether all of the keys for this handler are pressed. + */ + public var allPressed(get, never):Bool; + + /** + * Whether all of the keys for this handler are activated, + * and the handler is ready to repeat. + */ + public var activated(default, null):Bool = false; + + var inputs:Array; + var delay:Float; + var interval:Float; + var targetGamepad:FlxGamepad; + + var allPressedTime:Float = 0; + + function new(inputs:Array, delay:Float = DEFAULT_DELAY, interval:Float = DEFAULT_INTERVAL, ?targetGamepad:FlxGamepad) + { + super(); + this.inputs = inputs; + this.delay = delay; + this.interval = interval; + this.targetGamepad = targetGamepad ?? FlxG.gamepads.firstActive; + } + + function get_allPressed():Bool + { + if (targetGamepad == null) return false; + if (inputs == null || inputs.length == 0) return false; + if (inputs.length == 1) return targetGamepad.anyPressed(inputs); + + // Check if ANY keys are unpressed + for (input in inputs) + { + if (!targetGamepad.anyPressed([input])) return false; + } + return true; + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + // Try to find a gamepad if we don't have one + if (targetGamepad == null) + { + targetGamepad = FlxG.gamepads.firstActive; + } + + if (allPressed) + { + if (allPressedTime == 0) + { + activated = true; + } + else if (allPressedTime >= (delay + interval)) + { + activated = true; + allPressedTime -= interval; + } + else + { + activated = false; + } + allPressedTime += elapsed; + } + else + { + allPressedTime = 0; + activated = false; + } + } + + /** + * Builds a TurboButtonHandler that monitors from a single input. + * @param input The input to monitor. + * @param delay How long to wait before repeating. + * @param repeatDelay How long to wait between repeats. + * @return A TurboKeyHandler + */ + public static overload inline extern function build(input:FlxGamepadInputID, ?delay:Float = DEFAULT_DELAY, + ?interval:Float = DEFAULT_INTERVAL):TurboButtonHandler + { + return new TurboButtonHandler([input], delay, interval); + } + + /** + * Builds a TurboKeyHandler that monitors a key combination. + * @param inputs The combination of inputs to monitor. + * @param delay How long to wait before repeating. + * @param repeatDelay How long to wait between repeats. + * @return A TurboKeyHandler + */ + public static overload inline extern function build(inputs:Array, ?delay:Float = DEFAULT_DELAY, + ?interval:Float = DEFAULT_INTERVAL):TurboButtonHandler + { + return new TurboButtonHandler(inputs, delay, interval); + } +} diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 78f660d3f..62860ee0f 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -240,8 +240,8 @@ class PolymodHandler { return { assetLibraryPaths: [ - 'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'tutorial' => 'tutorial', 'week1' => 'week1', 'week2' => 'week2', - 'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1', + 'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'videos' => 'videos', 'tutorial' => 'tutorial', 'week1' => 'week1', + 'week2' => 'week2', 'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1', ], coreAssetRedirect: CORE_FOLDER, } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 486eb742c..2e023dccd 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -834,9 +834,12 @@ class PlayState extends MusicBeatSubState inputSpitter = []; // Reset music properly. - FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; - FlxG.sound.music.pitch = playbackRate; - FlxG.sound.music.pause(); + if (FlxG.sound.music != null) + { + FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; + FlxG.sound.music.pitch = playbackRate; + FlxG.sound.music.pause(); + } if (!overrideMusic) { @@ -852,7 +855,7 @@ class PlayState extends MusicBeatSubState vocals.pause(); vocals.time = 0; - FlxG.sound.music.volume = 1; + if (FlxG.sound.music != null) FlxG.sound.music.volume = 1; vocals.volume = 1; vocals.playerVolume = 1; vocals.opponentVolume = 1; @@ -1548,10 +1551,11 @@ class PlayState extends MusicBeatSubState function loadStage(id:String):Void { currentStage = StageRegistry.instance.fetchEntry(id); - currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory. if (currentStage != null) { + currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory. + // Actually create and position the sprites. var event:ScriptEvent = new ScriptEvent(CREATE, false); ScriptEventDispatcher.callEvent(currentStage, event); @@ -2388,13 +2392,6 @@ class PlayState extends MusicBeatSubState // Display the combo meter and add the calculation to the score. popUpScore(note, event.score, event.judgement, event.healthChange); - - if (note.isHoldNote && note.holdNoteSprite != null) - { - playerStrumline.playNoteHoldCover(note.holdNoteSprite); - } - - vocals.playerVolume = 1; } /** @@ -2676,6 +2673,13 @@ class PlayState extends MusicBeatSubState } comboPopUps.displayRating(daRating); if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo); + + if (daNote.isHoldNote && daNote.holdNoteSprite != null) + { + playerStrumline.playNoteHoldCover(daNote.holdNoteSprite); + } + + vocals.playerVolume = 1; } /** @@ -2786,7 +2790,7 @@ class PlayState extends MusicBeatSubState // adds current song data into the tallies for the level (story levels) Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel); - if (Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data)) + if (!isPracticeMode && !isBotPlayMode && Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data)) { Save.instance.setSongScore(currentSong.id, currentDifficulty, data); #if newgrounds @@ -3072,18 +3076,18 @@ class PlayState extends MusicBeatSubState title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), scoreData: { - score: songScore, + score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore, tallies: { - sick: Highscore.tallies.sick, - good: Highscore.tallies.good, - bad: Highscore.tallies.bad, - shit: Highscore.tallies.shit, - missed: Highscore.tallies.missed, - combo: Highscore.tallies.combo, - maxCombo: Highscore.tallies.maxCombo, - totalNotesHit: Highscore.tallies.totalNotesHit, - totalNotes: Highscore.tallies.totalNotes, + sick: talliesToUse.sick, + good: talliesToUse.good, + bad: talliesToUse.bad, + shit: talliesToUse.shit, + missed: talliesToUse.missed, + combo: talliesToUse.combo, + maxCombo: talliesToUse.maxCombo, + totalNotesHit: talliesToUse.totalNotesHit, + totalNotes: talliesToUse.totalNotes, }, accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, }, diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index c05257338..591020955 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -80,34 +80,34 @@ class ResultState extends MusicBeatSubState bgFlash.visible = false; add(bgFlash); - var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared")); - bfGfExcellent.visible = false; - add(bfGfExcellent); + // var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared")); + // bfGfExcellent.visible = false; + // add(bfGfExcellent); + // + // var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared")); + // bfPerfect.visible = false; + // add(bfPerfect); + // + // var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared")); + // bfSHIT.visible = false; + // add(bfSHIT); + // + // bfGfExcellent.anim.onComplete = () -> { + // bfGfExcellent.anim.curFrame = 28; + // bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce! + // }; + // + // bfPerfect.anim.onComplete = () -> { + // bfPerfect.anim.curFrame = 136; + // bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! + // }; + // + // bfSHIT.anim.onComplete = () -> { + // bfSHIT.anim.curFrame = 150; + // bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce! + // }; - var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared")); - bfPerfect.visible = false; - add(bfPerfect); - - var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared")); - bfSHIT.visible = false; - add(bfSHIT); - - bfGfExcellent.anim.onComplete = () -> { - bfGfExcellent.anim.curFrame = 28; - bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce! - }; - - bfPerfect.anim.onComplete = () -> { - bfPerfect.anim.curFrame = 136; - bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! - }; - - bfSHIT.anim.onComplete = () -> { - bfSHIT.anim.curFrame = 150; - bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce! - }; - - var gf:FlxSprite = FunkinSprite.createSparrow(500, 300, 'resultScreen/resultGirlfriendGOOD'); + var gf:FlxSprite = FunkinSprite.createSparrow(625, 325, 'resultScreen/resultGirlfriendGOOD'); gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false); gf.visible = false; gf.animation.finishCallback = _ -> { @@ -268,9 +268,9 @@ class ResultState extends MusicBeatSubState switch (resultsVariation) { - case SHIT: - bfSHIT.visible = true; - bfSHIT.playAnimation(""); + // case SHIT: + // bfSHIT.visible = true; + // bfSHIT.playAnimation(""); case NORMAL: boyfriend.animation.play('fall'); @@ -292,9 +292,9 @@ class ResultState extends MusicBeatSubState gf.animation.play('clap', true); gf.visible = true; }); - case PERFECT: - bfPerfect.visible = true; - bfPerfect.playAnimation(""); + // case PERFECT: + // bfPerfect.visible = true; + // bfPerfect.playAnimation(""); // bfGfExcellent.visible = true; // bfGfExcellent.playAnimation(""); diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx index f1dadf3e2..ed58b92b5 100644 --- a/source/funkin/play/character/AnimateAtlasCharacter.hx +++ b/source/funkin/play/character/AnimateAtlasCharacter.hx @@ -192,6 +192,7 @@ class AnimateAtlasCharacter extends BaseCharacter if (!this.mainSprite.hasAnimation(prefix)) { FlxG.log.warn('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}'); + trace('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}'); continue; } animations.set(anim.name, anim); diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx index 0c05bc876..0939dae38 100644 --- a/source/funkin/play/cutscene/VideoCutscene.hx +++ b/source/funkin/play/cutscene/VideoCutscene.hx @@ -67,8 +67,13 @@ class VideoCutscene if (!openfl.Assets.exists(filePath)) { // Display a popup. - lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video'); - return; + // lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video'); + // return; + + // TODO: After moving videos to their own library, + // this function ALWAYS FAILS on web, but the video still plays. + // I think that's due to a weird quirk with how OpenFL libraries work. + trace('Video file does not exist: ${filePath}'); } var rawFilePath = Paths.stripLibrary(filePath); diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index af2730ddd..bfbda2a02 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -693,7 +693,7 @@ class Save trace("[SAVE] Checking for legacy save data..."); var legacySave:FlxSave = new FlxSave(); legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY); - if (legacySave?.data == null) + if (legacySave.isEmpty()) { trace("[SAVE] No legacy save data found."); return null; diff --git a/source/funkin/ui/credits/CreditsDataHandler.hx b/source/funkin/ui/credits/CreditsDataHandler.hx index 86afdafd1..2240ec50e 100644 --- a/source/funkin/ui/credits/CreditsDataHandler.hx +++ b/source/funkin/ui/credits/CreditsDataHandler.hx @@ -57,10 +57,6 @@ class CreditsDataHandler {line: 'KawaiSprite'}, {line: 'evilsk8r'}, ] - }, - { - header: 'Kickstarter Backers', - appendBackers: true } ] }; @@ -68,11 +64,11 @@ class CreditsDataHandler public static function fetchBackerEntries():Array { - // TODO: Replace this with a web request. + // TODO: Implement a web request. // We can't just grab the current Kickstarter data and include it in builds, // because we don't want to deadname people who haven't logged into the portal yet. // It can be async and paginated for performance! - return ['See the list of backers at $BACKER_PUBLIC_URL.']; + return []; } #if HARDCODED_CREDITS @@ -99,12 +95,19 @@ class CreditsDataHandler static function fetchCreditsData():funkin.data.JsonFile { + #if !macro var rawJson:String = openfl.Assets.getText(CREDITS_DATA_PATH).trim(); return { fileName: CREDITS_DATA_PATH, contents: rawJson }; + #else + return { + fileName: CREDITS_DATA_PATH, + contents: null + }; + #end } static function parseCreditsData(file:JsonFile):Null diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 78222570a..93907bdda 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -8,6 +8,7 @@ import flixel.FlxSprite; import flixel.FlxSubState; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxSpriteGroup; +import flixel.input.gamepad.FlxGamepadInputID; import flixel.input.keyboard.FlxKey; import flixel.input.mouse.FlxMouseEvent; import flixel.math.FlxMath; @@ -40,6 +41,8 @@ import funkin.data.stage.StageData; import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinSprite; import funkin.input.Cursor; +import funkin.input.TurboActionHandler; +import funkin.input.TurboButtonHandler; import funkin.input.TurboKeyHandler; import funkin.modding.events.ScriptEvent; import funkin.play.character.BaseCharacter.CharacterType; @@ -74,6 +77,7 @@ import funkin.ui.debug.charting.commands.SetItemSelectionCommand; import funkin.ui.debug.charting.components.ChartEditorEventSprite; import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite; import funkin.ui.debug.charting.components.ChartEditorMeasureTicks; +import funkin.ui.debug.charting.components.ChartEditorMeasureTicks; import funkin.ui.debug.charting.components.ChartEditorNotePreview; import funkin.ui.debug.charting.components.ChartEditorNoteSprite; import funkin.ui.debug.charting.components.ChartEditorPlaybarHead; @@ -401,8 +405,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState renderedSelectionSquares.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); // Offset the selection box start position, if we are dragging. if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; - // Update the note preview viewport box. + + // Update the note preview. setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); + refreshNotePreviewPlayheadPosition(); + // Update the measure tick display. if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0; return this.scrollPositionInPixels; @@ -463,6 +470,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Move the playhead sprite to the correct position. gridPlayhead.y = this.playheadPositionInPixels + GRID_INITIAL_Y_POS; + updatePlayheadGhostHoldNotes(); + refreshNotePreviewPlayheadPosition(); + return this.playheadPositionInPixels; } @@ -769,6 +779,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return currentPlaceNoteData = value; } + /** + * The SongNoteData which is currently being placed, for each column. + * `null` if the user isn't currently placing a note. + * As the user moves down, we will update this note's sustain length, and finalize the note when they release. + */ + var currentLiveInputPlaceNoteData:Array = []; + // Note Movement /** @@ -799,6 +816,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var dragLengthCurrent:Float = 0; + /** + * The current length of the hold note we are placing with the playhead, in steps. + * Play a sound when this value changes. + */ + var playheadDragLengthCurrent:Array = []; + /** * Flip-flop to alternate between two stretching sounds. */ @@ -1071,6 +1094,66 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var pageDownKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.PAGEDOWN); + /** + * Variable used to track how long the user has been holding up on the dpad. + */ + var dpadUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_UP); + + /** + * Variable used to track how long the user has been holding down on the dpad. + */ + var dpadDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_DOWN); + + /** + * Variable used to track how long the user has been holding left on the dpad. + */ + var dpadLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_LEFT); + + /** + * Variable used to track how long the user has been holding right on the dpad. + */ + var dpadRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_RIGHT); + + /** + * Variable used to track how long the user has been holding up on the left stick. + */ + var leftStickUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_UP); + + /** + * Variable used to track how long the user has been holding down on the left stick. + */ + var leftStickDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_DOWN); + + /** + * Variable used to track how long the user has been holding left on the left stick. + */ + var leftStickLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_LEFT); + + /** + * Variable used to track how long the user has been holding right on the left stick. + */ + var leftStickRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_RIGHT); + + /** + * Variable used to track how long the user has been holding up on the right stick. + */ + var rightStickUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_UP); + + /** + * Variable used to track how long the user has been holding down on the right stick. + */ + var rightStickDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_DOWN); + + /** + * Variable used to track how long the user has been holding left on the right stick. + */ + var rightStickLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_LEFT); + + /** + * Variable used to track how long the user has been holding right on the right stick. + */ + var rightStickRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_RIGHT); + /** * AUDIO AND SOUND DATA */ @@ -1949,10 +2032,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var gridGhostNote:Null = null; /** - * A sprite used to indicate the note that will be placed on click. + * A sprite used to indicate the hold note that will be placed on click. */ var gridGhostHoldNote:Null = null; + /** + * A sprite used to indicate the hold note that will be placed on button release. + */ + var gridPlayheadGhostHoldNotes:Array = []; + /** * A sprite used to indicate the event that will be placed on click. */ @@ -1970,6 +2058,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var notePreviewViewport:Null = null; + /** + * The thin sprite used for representing the playhead on the note preview. + * We move this up and down to represent the current position. + */ + var notePreviewPlayhead:Null = null; + /** * The rectangular sprite used for rendering the selection box. * Uses a 9-slice to stretch the selection box to the correct size without warping. @@ -2349,7 +2443,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState gridGhostHoldNote = new ChartEditorHoldNoteSprite(this); gridGhostHoldNote.alpha = 0.6; - gridGhostHoldNote.noteData = new SongNoteData(0, 0, 0, ""); + gridGhostHoldNote.noteData = null; gridGhostHoldNote.visible = false; add(gridGhostHoldNote); gridGhostHoldNote.zIndex = 11; @@ -2423,6 +2517,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState add(notePreviewViewport); notePreviewViewport.zIndex = 30; + notePreviewPlayhead = new FlxSprite().makeGraphic(2, 2, 0xFFFF0000); + notePreviewPlayhead.scrollFactor.set(0, 0); + notePreviewPlayhead.scale.set(notePreview.width / 2, 0.5); // Setting width does nothing. + notePreviewPlayhead.updateHitbox(); + notePreviewPlayhead.x = notePreview.x; + notePreviewPlayhead.y = notePreview.y; + add(notePreviewPlayhead); + notePreviewPlayhead.zIndex = 31; + setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); } @@ -2519,6 +2622,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } + function refreshNotePreviewPlayheadPosition():Void + { + if (notePreviewPlayhead == null) return; + + notePreviewPlayhead.y = notePreview.y + (notePreview.height * ((scrollPositionInPixels + playheadPositionInPixels) / songLengthInPixels)); + } + /** * Builds the group that will hold all the notes. */ @@ -3015,6 +3125,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ function setupTurboKeyHandlers():Void { + // Keyboard shortcuts add(undoKeyHandler); add(redoKeyHandler); add(upKeyHandler); @@ -3023,6 +3134,20 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState add(sKeyHandler); add(pageUpKeyHandler); add(pageDownKeyHandler); + + // Gamepad inputs + add(dpadUpGamepadHandler); + add(dpadDownGamepadHandler); + add(dpadLeftGamepadHandler); + add(dpadRightGamepadHandler); + add(leftStickUpGamepadHandler); + add(leftStickDownGamepadHandler); + add(leftStickLeftGamepadHandler); + add(leftStickRightGamepadHandler); + add(rightStickUpGamepadHandler); + add(rightStickDownGamepadHandler); + add(rightStickLeftGamepadHandler); + add(rightStickRightGamepadHandler); } /** @@ -3709,32 +3834,56 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Up Arrow = Scroll Up if (upKeyHandler.activated && currentLiveInputStyle == None) { - scrollAmount = -GRID_SIZE * 0.25 * 25.0; + scrollAmount = -GRID_SIZE * 4; shouldPause = true; } // Down Arrow = Scroll Down if (downKeyHandler.activated && currentLiveInputStyle == None) { - scrollAmount = GRID_SIZE * 0.25 * 25.0; + scrollAmount = GRID_SIZE * 4; shouldPause = true; } // W = Scroll Up (doesn't work with Ctrl+Scroll) if (wKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL) { - scrollAmount = -GRID_SIZE * 0.25 * 25.0; + scrollAmount = -GRID_SIZE * 4; shouldPause = true; } // S = Scroll Down (doesn't work with Ctrl+Scroll) if (sKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL) { - scrollAmount = GRID_SIZE * 0.25 * 25.0; + scrollAmount = GRID_SIZE * 4; shouldPause = true; } - // PAGE UP = Jump up to nearest measure - if (pageUpKeyHandler.activated) + // GAMEPAD LEFT STICK UP = Scroll Up by 1 note snap + if (leftStickUpGamepadHandler.activated) { + scrollAmount = -GRID_SIZE * noteSnapRatio; + shouldPause = true; + } + // GAMEPAD LEFT STICK DOWN = Scroll Down by 1 note snap + if (leftStickDownGamepadHandler.activated) + { + scrollAmount = GRID_SIZE * noteSnapRatio; + shouldPause = true; + } + + // GAMEPAD RIGHT STICK UP = Scroll Up by 1 note snap (playhead only) + if (rightStickUpGamepadHandler.activated) + { + playheadAmount = -GRID_SIZE * noteSnapRatio; + shouldPause = true; + } + // GAMEPAD RIGHT STICK DOWN = Scroll Down by 1 note snap (playhead only) + if (rightStickDownGamepadHandler.activated) + { + playheadAmount = GRID_SIZE * noteSnapRatio; + shouldPause = true; + } + + var funcJumpUp = (playheadOnly:Bool) -> { var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var targetScrollPosition:Float = Math.floor(playheadPos / measureHeight) * measureHeight; @@ -3744,20 +3893,37 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure; } - scrollAmount = targetScrollPosition - playheadPos; + if (playheadOnly) + { + playheadAmount = targetScrollPosition - playheadPos; + } + else + { + scrollAmount = targetScrollPosition - playheadPos; + } + } + + // PAGE UP = Jump up to nearest measure + // GAMEPAD LEFT STICK LEFT = Jump up to nearest measure + if (pageUpKeyHandler.activated || leftStickLeftGamepadHandler.activated) + { + funcJumpUp(false); + shouldPause = true; + } + if (rightStickLeftGamepadHandler.activated) + { + funcJumpUp(true); shouldPause = true; } if (playbarButtonPressed == 'playbarBack') { playbarButtonPressed = ''; - scrollAmount = -GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; + funcJumpUp(false); shouldPause = true; } - // PAGE DOWN = Jump down to nearest measure - if (pageDownKeyHandler.activated) - { + var funcJumpDown = (playheadOnly:Bool) -> { var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var targetScrollPosition:Float = Math.ceil(playheadPos / measureHeight) * measureHeight; @@ -3767,26 +3933,46 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure; } - scrollAmount = targetScrollPosition - playheadPos; + if (playheadOnly) + { + playheadAmount = targetScrollPosition - playheadPos; + } + else + { + scrollAmount = targetScrollPosition - playheadPos; + } + } + + // PAGE DOWN = Jump down to nearest measure + // GAMEPAD LEFT STICK RIGHT = Jump down to nearest measure + if (pageDownKeyHandler.activated || leftStickRightGamepadHandler.activated) + { + funcJumpDown(false); + shouldPause = true; + } + if (rightStickRightGamepadHandler.activated) + { + funcJumpDown(true); shouldPause = true; } if (playbarButtonPressed == 'playbarForward') { playbarButtonPressed = ''; - scrollAmount = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; + funcJumpDown(false); shouldPause = true; } // SHIFT + Scroll = Scroll Fast - if (FlxG.keys.pressed.SHIFT) + // GAMEPAD LEFT STICK CLICK + Scroll = Scroll Fast + if (FlxG.keys.pressed.SHIFT || (FlxG.gamepads.firstActive?.pressed?.LEFT_STICK_CLICK ?? false)) { scrollAmount *= 2; } // CONTROL + Scroll = Scroll Precise if (FlxG.keys.pressed.CONTROL) { - scrollAmount /= 10; + scrollAmount /= 4; } // Alt + Drag = Scroll but move the playhead the same amount. @@ -4380,9 +4566,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } gridGhostHoldNote.visible = true; - gridGhostHoldNote.noteData = currentPlaceNoteData; - gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection(); - + gridGhostHoldNote.noteData = gridGhostNote.noteData; + gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection(); gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true); gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes); @@ -4943,37 +5128,57 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function handlePlayhead():Void { - // Place notes at the playhead. + // Place notes at the playhead with the keyboard. switch (currentLiveInputStyle) { case ChartEditorLiveInputStyle.WASDKeys: if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4); + if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4); if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5); + if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5); if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6); + if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6); if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7); + if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7); if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0); + if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0); if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1); + if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1); if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2); + if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2); if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3); + if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3); case ChartEditorLiveInputStyle.NumberKeys: // Flipped because Dad is on the left but represents data 0-3. if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4); + if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4); if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5); + if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5); if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6); + if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6); if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7); + if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7); if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0); + if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0); if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1); if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2); + if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2); if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3); + if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3); case ChartEditorLiveInputStyle.None: // Do nothing. } + + updatePlayheadGhostHoldNotes(); } function placeNoteAtPlayhead(column:Int):Void { + // SHIFT + press or LEFT_SHOULDER + press to remove notes instead of placing them. + var removeNoteInstead:Bool = FlxG.keys.pressed.SHIFT || (FlxG.gamepads.firstActive?.pressed?.LEFT_SHOULDER ?? false); + var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); @@ -4984,14 +5189,136 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState playheadPosSnappedMs + Conductor.instance.stepLengthMs * noteSnapRatio); notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]); - if (notesAtPos.length == 0) + if (notesAtPos.length == 0 && !removeNoteInstead) { + trace('Placing note. ${column}'); var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); + currentLiveInputPlaceNoteData[column] = newNoteData; + } + else if (removeNoteInstead) + { + trace('Removing existing note at position. ${column}'); + performCommand(new RemoveNotesCommand(notesAtPos)); } else { - trace('Already a note there.'); + trace('Already a note there. ${column}'); + } + } + + function updatePlayheadGhostHoldNotes():Void + { + // Ensure all the ghost hold notes exist. + while (gridPlayheadGhostHoldNotes.length < (STRUMLINE_SIZE * 2)) + { + var ghost = new ChartEditorHoldNoteSprite(this); + ghost.alpha = 0.6; + ghost.noteData = null; + ghost.visible = false; + ghost.zIndex = 11; + add(ghost); // Don't add to `renderedHoldNotes` because then it will get killed every frame. + + gridPlayheadGhostHoldNotes.push(ghost); + refresh(); + } + + // Update playhead ghost hold notes. + for (column in 0...gridPlayheadGhostHoldNotes.length) + { + var targetNoteData = currentLiveInputPlaceNoteData[column]; + var ghostHold = gridPlayheadGhostHoldNotes[column]; + + if (targetNoteData == null && ghostHold.noteData != null) + { + // Remove the ghost hold note. + ghostHold.noteData = null; + } + + if (targetNoteData != null && ghostHold.noteData == null) + { + // Readd the new ghost hold note. + ghostHold.noteData = targetNoteData.clone(); + ghostHold.noteDirection = ghostHold.noteData.getDirection(); + ghostHold.visible = true; + ghostHold.alpha = 0.6; + ghostHold.setHeightDirectly(0); + ghostHold.updateHoldNotePosition(renderedHoldNotes); + } + + if (ghostHold.noteData == null) + { + ghostHold.visible = false; + ghostHold.setHeightDirectly(0); + playheadDragLengthCurrent[column] = 0; + continue; + } + + var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; + var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; + var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); + var playheadPosSnappedMs:Float = playheadPosStep * Conductor.instance.stepLengthMs * noteSnapRatio; + + var newNoteLength:Float = playheadPosSnappedMs - ghostHold.noteData.time; + trace('newNoteLength: ${newNoteLength}'); + + if (newNoteLength > 0) + { + ghostHold.noteData.length = newNoteLength; + var targetNoteLengthSteps:Float = ghostHold.noteData.getStepLength(true); + var targetNoteLengthStepsInt:Int = Std.int(Math.floor(targetNoteLengthSteps)); + var targetNoteLengthPixels:Float = targetNoteLengthSteps * GRID_SIZE; + + if (playheadDragLengthCurrent[column] != targetNoteLengthStepsInt) + { + stretchySounds = !stretchySounds; + this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI')); + playheadDragLengthCurrent[column] = targetNoteLengthStepsInt; + } + ghostHold.visible = true; + ghostHold.alpha = 0.6; + ghostHold.setHeightDirectly(targetNoteLengthPixels, true); + ghostHold.updateHoldNotePosition(renderedHoldNotes); + trace('lerpLength: ${ghostHold.fullSustainLength}'); + trace('position: ${ghostHold.x}, ${ghostHold.y}'); + } + else + { + ghostHold.visible = false; + ghostHold.setHeightDirectly(0); + playheadDragLengthCurrent[column] = 0; + continue; + } + } + } + + function finishPlaceNoteAtPlayhead(column:Int):Void + { + if (currentLiveInputPlaceNoteData[column] == null) return; + + var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; + var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; + var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); + var playheadPosSnappedMs:Float = playheadPosStep * Conductor.instance.stepLengthMs * noteSnapRatio; + + var newNoteLength:Float = playheadPosSnappedMs - currentLiveInputPlaceNoteData[column].time; + trace('finishPlace newNoteLength: ${newNoteLength}'); + + if (newNoteLength < Conductor.instance.stepLengthMs) + { + // Don't extend the note if it's too short. + trace('Not extending note. ${column}'); + currentLiveInputPlaceNoteData[column] = null; + gridPlayheadGhostHoldNotes[column].noteData = null; + } + else + { + // Extend the note to the playhead position. + trace('Extending note. ${column}'); + this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI')); + performCommand(new ExtendNoteLengthCommand(currentLiveInputPlaceNoteData[column], newNoteLength)); + currentLiveInputPlaceNoteData[column] = null; + gridPlayheadGhostHoldNotes[column].noteData = null; } } diff --git a/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx index b4d913607..30f4280d2 100644 --- a/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx @@ -20,6 +20,8 @@ class RemoveEventsCommand implements ChartEditorCommand public function execute(state:ChartEditorState):Void { + if (events.length == 0) return; + state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); state.currentEventSelection = []; @@ -34,6 +36,8 @@ class RemoveEventsCommand implements ChartEditorCommand public function undo(state:ChartEditorState):Void { + if (events.length == 0) return; + for (event in events) { state.currentSongChartEventData.push(event); diff --git a/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx index 69317aff4..1cc61f233 100644 --- a/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx @@ -23,6 +23,8 @@ class RemoveItemsCommand implements ChartEditorCommand public function execute(state:ChartEditorState):Void { + if ((notes.length + events.length) == 0) return; + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); @@ -40,6 +42,8 @@ class RemoveItemsCommand implements ChartEditorCommand public function undo(state:ChartEditorState):Void { + if ((notes.length + events.length) == 0) return; + for (note in notes) { state.currentSongChartNoteData.push(note); diff --git a/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx index 4811f831d..18ad6e04d 100644 --- a/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx +++ b/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx @@ -20,6 +20,8 @@ class RemoveNotesCommand implements ChartEditorCommand public function execute(state:ChartEditorState):Void { + if (notes.length == 0) return; + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentNoteSelection = []; state.currentEventSelection = []; @@ -35,6 +37,8 @@ class RemoveNotesCommand implements ChartEditorCommand public function undo(state:ChartEditorState):Void { + if (notes.length == 0) return; + for (note in notes) { state.currentSongChartNoteData.push(note); diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx index c7f7747c0..aeb6dd0e4 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx @@ -54,11 +54,16 @@ class ChartEditorHoldNoteSprite extends SustainTrail * Set the height directly, to a value in pixels. * @param h The desired height in pixels. */ - public function setHeightDirectly(h:Float, ?lerp:Bool = false) + public function setHeightDirectly(h:Float, lerp:Bool = false) { - if (lerp != null && lerp) sustainLength = FlxMath.lerp(sustainLength, h / (getScrollSpeed() * Constants.PIXELS_PER_MS), 0.25); + if (lerp) + { + sustainLength = FlxMath.lerp(sustainLength, h / (getScrollSpeed() * Constants.PIXELS_PER_MS), 0.25); + } else + { sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS); + } fullSustainLength = sustainLength; } diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx index cd403c6f8..98f5a47aa 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx @@ -117,12 +117,6 @@ class ChartEditorNoteSprite extends FlxSprite { noteFrameCollection.pushFrame(frame); } - var frameCollectionNormal2:FlxAtlasFrames = Paths.getSparrowAtlas('NoteHoldNormal'); - - for (frame in frameCollectionNormal2.frames) - { - noteFrameCollection.pushFrame(frame); - } // Pixel notes var graphicPixel = FlxG.bitmap.add(Paths.image('weeb/pixelUI/arrows-pixels', 'week6'), false, null); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorGamepadHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorGamepadHandler.hx new file mode 100644 index 000000000..70383d3fd --- /dev/null +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorGamepadHandler.hx @@ -0,0 +1,193 @@ +package funkin.ui.debug.charting.handlers; + +import haxe.ui.focus.FocusManager; +import flixel.input.gamepad.FlxGamepad; +import haxe.ui.actions.ActionManager; +import haxe.ui.actions.IActionInputSource; +import haxe.ui.actions.ActionType; + +/** + * Yes, we're that crazy. Gamepad support for the chart editor. + */ +// @:nullSafety + +@:access(funkin.ui.debug.charting.ChartEditorState) +class ChartEditorGamepadHandler +{ + public static function handleGamepadControls(chartEditorState:ChartEditorState) + { + if (FlxG.gamepads.firstActive != null) handleGamepad(chartEditorState, FlxG.gamepads.firstActive); + } + + /** + * Handle context-generic binds for the gamepad. + * @param chartEditorState The chart editor state. + * @param gamepad The gamepad to handle. + */ + static function handleGamepad(chartEditorState:ChartEditorState, gamepad:FlxGamepad):Void + { + if (chartEditorState.isHaxeUIFocused) + { + ChartEditorGamepadActionInputSource.instance.handleGamepad(gamepad); + } + else + { + handleGamepadLiveInputs(chartEditorState, gamepad); + + if (gamepad.justPressed.RIGHT_SHOULDER) + { + trace('Gamepad: Right shoulder pressed, toggling audio playback.'); + chartEditorState.toggleAudioPlayback(); + } + + if (gamepad.justPressed.START) + { + var minimal = gamepad.pressed.LEFT_SHOULDER; + chartEditorState.hideAllToolboxes(); + trace('Gamepad: Start pressed, opening playtest (minimal: ${minimal})'); + chartEditorState.testSongInPlayState(minimal); + } + + if (gamepad.justPressed.BACK && !gamepad.pressed.LEFT_SHOULDER) + { + trace('Gamepad: Back pressed, focusing on HaxeUI menu.'); + // FocusManager.instance.focus = chartEditorState.menubarMenuFile; + } + else if (gamepad.justPressed.BACK && gamepad.pressed.LEFT_SHOULDER) + { + trace('Gamepad: Back pressed, unfocusing on HaxeUI menu.'); + FocusManager.instance.focus = null; + } + } + + if (gamepad.justPressed.GUIDE) + { + trace('Gamepad: Guide pressed, quitting chart editor.'); + chartEditorState.quitChartEditor(); + } + } + + static function handleGamepadLiveInputs(chartEditorState:ChartEditorState, gamepad:FlxGamepad):Void + { + // Place notes at the playhead with the gamepad. + // Disable when we are interacting with HaxeUI. + if (!(chartEditorState.isHaxeUIFocused || chartEditorState.isHaxeUIDialogOpen)) + { + if (gamepad.justPressed.DPAD_LEFT) chartEditorState.placeNoteAtPlayhead(4); + if (gamepad.justReleased.DPAD_LEFT) chartEditorState.finishPlaceNoteAtPlayhead(4); + if (gamepad.justPressed.DPAD_DOWN) chartEditorState.placeNoteAtPlayhead(5); + if (gamepad.justReleased.DPAD_DOWN) chartEditorState.finishPlaceNoteAtPlayhead(5); + if (gamepad.justPressed.DPAD_UP) chartEditorState.placeNoteAtPlayhead(6); + if (gamepad.justReleased.DPAD_UP) chartEditorState.finishPlaceNoteAtPlayhead(6); + if (gamepad.justPressed.DPAD_RIGHT) chartEditorState.placeNoteAtPlayhead(7); + if (gamepad.justReleased.DPAD_RIGHT) chartEditorState.finishPlaceNoteAtPlayhead(7); + + if (gamepad.justPressed.X) chartEditorState.placeNoteAtPlayhead(0); + if (gamepad.justReleased.X) chartEditorState.finishPlaceNoteAtPlayhead(0); + if (gamepad.justPressed.A) chartEditorState.placeNoteAtPlayhead(1); + if (gamepad.justReleased.A) chartEditorState.finishPlaceNoteAtPlayhead(1); + if (gamepad.justPressed.Y) chartEditorState.placeNoteAtPlayhead(2); + if (gamepad.justReleased.Y) chartEditorState.finishPlaceNoteAtPlayhead(2); + if (gamepad.justPressed.B) chartEditorState.placeNoteAtPlayhead(3); + if (gamepad.justReleased.B) chartEditorState.finishPlaceNoteAtPlayhead(3); + } + } +} + +class ChartEditorGamepadActionInputSource implements IActionInputSource +{ + public static var instance:ChartEditorGamepadActionInputSource = new ChartEditorGamepadActionInputSource(); + + public function new() {} + + public function start():Void {} + + /** + * Handle HaxeUI-specific binds for the gamepad. + * Only called when the HaxeUI menu is focused. + * @param chartEditorState The chart editor state. + * @param gamepad The gamepad to handle. + */ + public function handleGamepad(gamepad:FlxGamepad):Void + { + if (gamepad.justPressed.DPAD_LEFT) + { + trace('Gamepad: DPAD_LEFT pressed, moving left.'); + ActionManager.instance.actionStart(ActionType.LEFT, this); + } + else if (gamepad.justReleased.DPAD_LEFT) + { + ActionManager.instance.actionEnd(ActionType.LEFT, this); + } + + if (gamepad.justPressed.DPAD_RIGHT) + { + trace('Gamepad: DPAD_RIGHT pressed, moving right.'); + ActionManager.instance.actionStart(ActionType.RIGHT, this); + } + else if (gamepad.justReleased.DPAD_RIGHT) + { + ActionManager.instance.actionEnd(ActionType.RIGHT, this); + } + + if (gamepad.justPressed.DPAD_UP) + { + trace('Gamepad: DPAD_UP pressed, moving up.'); + ActionManager.instance.actionStart(ActionType.UP, this); + } + else if (gamepad.justReleased.DPAD_UP) + { + ActionManager.instance.actionEnd(ActionType.UP, this); + } + + if (gamepad.justPressed.DPAD_DOWN) + { + trace('Gamepad: DPAD_DOWN pressed, moving down.'); + ActionManager.instance.actionStart(ActionType.DOWN, this); + } + else if (gamepad.justReleased.DPAD_DOWN) + { + ActionManager.instance.actionEnd(ActionType.DOWN, this); + } + + if (gamepad.justPressed.A) + { + trace('Gamepad: A pressed, confirmingg.'); + ActionManager.instance.actionStart(ActionType.CONFIRM, this); + } + else if (gamepad.justReleased.A) + { + ActionManager.instance.actionEnd(ActionType.CONFIRM, this); + } + + if (gamepad.justPressed.B) + { + trace('Gamepad: B pressed, cancelling.'); + ActionManager.instance.actionStart(ActionType.CANCEL, this); + } + else if (gamepad.justReleased.B) + { + ActionManager.instance.actionEnd(ActionType.CANCEL, this); + } + + if (gamepad.justPressed.LEFT_TRIGGER) + { + trace('Gamepad: LEFT_TRIGGER pressed, moving to previous item.'); + ActionManager.instance.actionStart(ActionType.PREVIOUS, this); + } + else if (gamepad.justReleased.LEFT_TRIGGER) + { + ActionManager.instance.actionEnd(ActionType.PREVIOUS, this); + } + + if (gamepad.justPressed.RIGHT_TRIGGER) + { + trace('Gamepad: RIGHT_TRIGGER pressed, moving to next item.'); + ActionManager.instance.actionStart(ActionType.NEXT, this); + } + else if (gamepad.justReleased.RIGHT_TRIGGER) + { + ActionManager.instance.actionEnd(ActionType.NEXT, this); + } + } +} diff --git a/source/funkin/ui/debug/charting/import.hx b/source/funkin/ui/debug/charting/import.hx index b0569e3bb..2c3d59ef7 100644 --- a/source/funkin/ui/debug/charting/import.hx +++ b/source/funkin/ui/debug/charting/import.hx @@ -5,6 +5,7 @@ package funkin.ui.debug.charting; using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler; using funkin.ui.debug.charting.handlers.ChartEditorContextMenuHandler; using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler; +using funkin.ui.debug.charting.handlers.ChartEditorGamepadHandler; using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler; using funkin.ui.debug.charting.handlers.ChartEditorNotificationHandler; using funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler; diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx index 189e04973..6b963a242 100644 --- a/source/funkin/ui/freeplay/AlbumRoll.hx +++ b/source/funkin/ui/freeplay/AlbumRoll.hx @@ -37,8 +37,8 @@ class AlbumRoll extends FlxSpriteGroup } var newAlbumArt:FlxAtlasSprite; - var difficultyStars:DifficultyStars; + // var difficultyStars:DifficultyStars; var _exitMovers:Null; var albumData:Album; @@ -65,9 +65,9 @@ class AlbumRoll extends FlxSpriteGroup add(newAlbumArt); - difficultyStars = new DifficultyStars(140, 39); - difficultyStars.stars.visible = false; - add(difficultyStars); + // difficultyStars = new DifficultyStars(140, 39); + // difficultyStars.stars.visible = false; + // add(difficultyStars); } function onAlbumFinish(animName:String):Void @@ -86,7 +86,7 @@ class AlbumRoll extends FlxSpriteGroup { if (albumId == null) { - difficultyStars.stars.visible = false; + // difficultyStars.stars.visible = false; return; } @@ -132,13 +132,6 @@ class AlbumRoll extends FlxSpriteGroup speed: 0.4, wait: 0 }); - - exitMovers.set([difficultyStars], - { - x: FlxG.width * 1.2, - speed: 0.2, - wait: 0.3 - }); } var titleTimer:Null = null; @@ -151,10 +144,10 @@ class AlbumRoll extends FlxSpriteGroup newAlbumArt.visible = true; newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false); - difficultyStars.stars.visible = false; + // difficultyStars.stars.visible = false; new FlxTimer().start(0.75, function(_) { // showTitle(); - showStars(); + // showStars(); }); } @@ -163,18 +156,16 @@ class AlbumRoll extends FlxSpriteGroup newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false); } - public function setDifficultyStars(?difficulty:Int):Void - { - if (difficulty == null) return; - - difficultyStars.difficulty = difficulty; - } - - /** - * Make the album stars visible. - */ - public function showStars():Void - { - difficultyStars.stars.visible = false; // true; - } + // public function setDifficultyStars(?difficulty:Int):Void + // { + // if (difficulty == null) return; + // difficultyStars.difficulty = difficulty; + // } + // /** + // * Make the album stars visible. + // */ + // public function showStars():Void + // { + // difficultyStars.stars.visible = false; // true; + // } } diff --git a/source/funkin/ui/freeplay/DifficultyStars.hx b/source/funkin/ui/freeplay/DifficultyStars.hx deleted file mode 100644 index 51526bcbe..000000000 --- a/source/funkin/ui/freeplay/DifficultyStars.hx +++ /dev/null @@ -1,106 +0,0 @@ -package funkin.ui.freeplay; - -import flixel.group.FlxSpriteGroup; -import funkin.graphics.adobeanimate.FlxAtlasSprite; -import funkin.graphics.shaders.HSVShader; - -class DifficultyStars extends FlxSpriteGroup -{ - /** - * Internal handler var for difficulty... ranges from 0... to 15 - * 0 is 1 star... 15 is 0 stars! - */ - var curDifficulty(default, set):Int = 0; - - /** - * Range between 0 and 15 - */ - public var difficulty(default, set):Int = 1; - - public var stars:FlxAtlasSprite; - - var flames:FreeplayFlames; - - var hsvShader:HSVShader; - - public function new(x:Float, y:Float) - { - super(x, y); - - hsvShader = new HSVShader(); - - flames = new FreeplayFlames(0, 0); - add(flames); - - stars = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/freeplayStars")); - stars.anim.play("diff stars"); - add(stars); - - stars.shader = hsvShader; - - for (memb in flames.members) - memb.shader = hsvShader; - } - - override function update(elapsed:Float):Void - { - super.update(elapsed); - - // "loops" the current animation - // for clarity, the animation file looks like - // frame : stars - // 0-99: 1 star - // 100-199: 2 stars - // ...... - // 1300-1499: 15 stars - // 1500 : 0 stars - if (curDifficulty < 15 && stars.anim.curFrame >= (curDifficulty + 1) * 100) - { - stars.anim.play("diff stars", true, false, curDifficulty * 100); - } - } - - function set_difficulty(value:Int):Int - { - difficulty = value; - - if (difficulty <= 0) - { - difficulty = 0; - curDifficulty = 15; - } - else if (difficulty <= 15) - { - difficulty = value; - curDifficulty = difficulty - 1; - } - else - { - difficulty = 15; - curDifficulty = difficulty - 1; - } - - if (difficulty > 10) flames.flameCount = difficulty - 10; - else - flames.flameCount = 0; - - return difficulty; - } - - function set_curDifficulty(value:Int):Int - { - curDifficulty = value; - if (curDifficulty == 15) - { - stars.anim.play("diff stars", true, false, 1500); - stars.anim.pause(); - } - else - { - stars.anim.curFrame = Std.int(curDifficulty * 100); - stars.anim.play("diff stars", true, false, curDifficulty * 100); - } - - return curDifficulty; - } -} diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 6fdc7e309..0bafa02ed 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -469,6 +469,10 @@ class FreeplayState extends MusicBeatSubState albumRoll.playIntro(); + new FlxTimer().start(0.75, function(_) { + // albumRoll.showTitle(); + }); + FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut}); var diffSelLeft:DifficultySelector = new DifficultySelector(20, grpDifficulties.y - 10, false, controls); @@ -634,14 +638,7 @@ class FreeplayState extends MusicBeatSubState funnyMenu.favIcon.visible = tempSongs[i].isFav; funnyMenu.hsvShader = hsvShader; - if (i < 8) - { - funnyMenu.initJumpIn(Math.min(i, 4), force); - } - else - { - funnyMenu.forcePosition(); - } + funnyMenu.forcePosition(); grpCapsules.add(funnyMenu); } @@ -1039,9 +1036,6 @@ class FreeplayState extends MusicBeatSubState } } - // Set the difficulty star count on the right. - albumRoll.setDifficultyStars(daSong?.songRating); - // Set the album graphic and play the animation if relevant. var newAlbumId:String = daSong?.albumId; if (albumRoll.albumId != newAlbumId) @@ -1161,10 +1155,6 @@ class FreeplayState extends MusicBeatSubState { currentDifficulty = rememberedDifficulty; } - - // Set the difficulty star count on the right. - var daSong:Null = grpCapsules.members[curSelected]?.songData; - albumRoll.setDifficultyStars(daSong?.songRating ?? 0); } function changeSelection(change:Int = 0):Void diff --git a/source/funkin/ui/haxeui/FlxGamepadActionInputSource.hx b/source/funkin/ui/haxeui/FlxGamepadActionInputSource.hx new file mode 100644 index 000000000..9c2901d16 --- /dev/null +++ b/source/funkin/ui/haxeui/FlxGamepadActionInputSource.hx @@ -0,0 +1,53 @@ +package funkin.ui.haxeui; + +import flixel.FlxBasic; +import flixel.input.gamepad.FlxGamepad; +import haxe.ui.actions.IActionInputSource; + +/** + * Receives button presses from the Flixel gamepad and emits HaxeUI events. + */ +class FlxGamepadActionInputSource extends FlxBasic +{ + public static var instance(get, null):FlxGamepadActionInputSource; + + static function get_instance():FlxGamepadActionInputSource + { + if (instance == null) instance = new FlxGamepadActionInputSource(); + return instance; + } + + public function new() + { + super(); + } + + public function start():Void + { + FlxG.plugins.addPlugin(this); + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (FlxG.gamepads.firstActive != null) + { + updateGamepad(elapsed, FlxG.gamepads.firstActive); + } + } + + function updateGamepad(elapsed:Float, gamepad:FlxGamepad):Void + { + if (gamepad.justPressed.BACK) + { + // + } + } + + public override function destroy():Void + { + super.destroy(); + FlxG.plugins.remove(this); + } +} diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index 0e444782c..0a164cf86 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -1,5 +1,6 @@ package funkin.ui.mainmenu; +import funkin.graphics.FunkinSprite; import flixel.addons.transition.FlxTransitionableState; import funkin.ui.debug.DebugMenuSubState; import flixel.FlxObject; @@ -67,7 +68,7 @@ class MainMenuState extends MusicBeatState camFollow = new FlxObject(0, 0, 1, 1); add(camFollow); - magenta = new FlxSprite(Paths.image('menuDesat')); + magenta = new FlxSprite(Paths.image('menuBGMagenta')); magenta.scrollFactor.x = bg.scrollFactor.x; magenta.scrollFactor.y = bg.scrollFactor.y; magenta.setGraphicSize(Std.int(bg.width)); @@ -75,7 +76,6 @@ class MainMenuState extends MusicBeatState magenta.x = bg.x; magenta.y = bg.y; magenta.visible = false; - magenta.color = 0xFFfd719b; // TODO: Why doesn't this line compile I'm going fucking feral diff --git a/source/funkin/ui/options/OptionsState.hx b/source/funkin/ui/options/OptionsState.hx index 0f33a0780..a00b28dbb 100644 --- a/source/funkin/ui/options/OptionsState.hx +++ b/source/funkin/ui/options/OptionsState.hx @@ -23,8 +23,7 @@ class OptionsState extends MusicBeatState override function create() { - var menuBG = new FlxSprite().loadGraphic(Paths.image('menuDesat')); - menuBG.color = 0xFFea71fd; + var menuBG = new FlxSprite().loadGraphic(Paths.image('menuBGBlue')); menuBG.setGraphicSize(Std.int(menuBG.width * 1.1)); menuBG.updateHitbox(); menuBG.screenCenter(); diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index af8798ae2..347190993 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -281,7 +281,6 @@ class LoadingState extends MusicBeatSubState { // TODO: This section is a hack! Redo this later when we have a proper asset caching system. FunkinSprite.preparePurgeCache(); - FunkinSprite.cacheTexture(Paths.image('combo')); FunkinSprite.cacheTexture(Paths.image('healthBar')); FunkinSprite.cacheTexture(Paths.image('menuDesat')); FunkinSprite.cacheTexture(Paths.image('combo'));