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 @@
+
+
+
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'));