mirror of
https://github.com/ApfelTeeSaft/FunkinMP.git
synced 2026-08-26 19:23:31 +00:00
base
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,3 @@
|
||||
* text=auto eol=lf
|
||||
*.hxc linguist-language=Haxe
|
||||
*.hxp linguist-language=Haxe
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Bug Report
|
||||
description: Report a bug or an issue in the game.
|
||||
labels: ["type: minor bug", "status: pending triage"]
|
||||
title: "Bug Report: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "# PLEASE READ THE [CONTRIBUTING GUIDE](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md) BEFORE OPENING ISSUES!"
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Issue Checklist
|
||||
description: Be sure to complete these steps to increase the chances of your issue being addressed!
|
||||
options:
|
||||
- label: I have read the [Contributing Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md)
|
||||
- label: I have checked the Issues/Discussions pages to see if my issue has already been reported
|
||||
- label: I have properly titled my issue
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Which platform are you playing on?
|
||||
options:
|
||||
- Newgrounds (Web/HTML5)
|
||||
- Itch.io (Web/HTML5)
|
||||
- Itch.io (Downloadable Build) - Windows
|
||||
- Itch.io (Downloadable Build) - MacOS
|
||||
- Itch.io (Downloadable Build) - Linux
|
||||
- Compiled from GitHub Source Code
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Browser
|
||||
description: (Web/HTML5 users only) Which browser are you playing on?
|
||||
options:
|
||||
- Google Chrome
|
||||
- Microsoft Edge
|
||||
- Firefox
|
||||
- Opera
|
||||
- Safari
|
||||
- Other (Specify in Description field)
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
|
||||
placeholder: ex. 0.6.4
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Describe your bug."
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "### Please do not report issues from other engines. These must be reported in their respective repositories."
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description (include any images, videos, errors, or crash logs)
|
||||
description: Provide as much detail as you can. The better others understand your issue, the more they can help you!
|
||||
placeholder: Describe your issue here...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: What steps can other people take to reliably encounter this issue?
|
||||
placeholder: 1. Open the game...
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Charting Issue
|
||||
description: Report an issue with the placement of notes in the game.
|
||||
labels: ["type: charting issue", "status: pending triage"]
|
||||
title: "Charting Issue: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "# PLEASE READ THE [CONTRIBUTING GUIDE](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md) BEFORE OPENING ISSUES!"
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Issue Checklist
|
||||
description: Be sure to complete these steps to increase the chances of your issue being addressed!
|
||||
options:
|
||||
- label: I have read the [Contributing Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md)
|
||||
- label: I have checked the Issues/Discussions pages to see if my issue has already been reported
|
||||
- label: I have properly titled my issue
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Which platform are you playing on?
|
||||
options:
|
||||
- Newgrounds (Web/HTML5)
|
||||
- Itch.io (Web/HTML5)
|
||||
- Itch.io (Downloadable Build) - Windows
|
||||
- Itch.io (Downloadable Build) - MacOS
|
||||
- Itch.io (Downloadable Build) - Linux
|
||||
- Compiled from GitHub Source Code
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Browser
|
||||
description: (Web/HTML5 users only) Which browser are you playing on?
|
||||
options:
|
||||
- Google Chrome
|
||||
- Microsoft Edge
|
||||
- Firefox
|
||||
- Opera
|
||||
- Safari
|
||||
- Other (Specify in Description field)
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
|
||||
placeholder: ex. 0.6.4
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Describe the charting issue(s)."
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "### Please do not report issues from other engines. These must be reported in their respective repositories."
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Location
|
||||
description: Where did you find the issue(s)? Include the name of the song, the variation, the difficulty, the time/section of the song, and any images or videos.
|
||||
placeholder: ex. Cocoa Erect on Erect/Nightmare difficulties at Section 30
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Why do you believe this is an issue? Is a note at the wrong time, a hold note too short/long, or something else?
|
||||
placeholder: Describe the charting issue(s) here...
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Compiling Help
|
||||
description: Report an issue with compiling the game.
|
||||
labels: ["type: compilation help", "status: pending triage"]
|
||||
title: "Compiling Help: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "# PLEASE READ THE [CONTRIBUTING GUIDE](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md) BEFORE OPENING ISSUES!"
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Issue Checklist
|
||||
description: Be sure to complete these steps to increase the chances of your issue being addressed!
|
||||
options:
|
||||
- label: I have followed the [Compiling Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/COMPILING.md) and the [Troubleshooting Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/TROUBLESHOOTING.md)
|
||||
- label: I have read the [Contributing Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md)
|
||||
- label: I have checked the Issues/Discussions pages to see if my issue has already been reported
|
||||
- label: I have properly titled my issue
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Which platform are you compiling for?
|
||||
options:
|
||||
- Web/HTML5
|
||||
- Desktop (Windows)
|
||||
- Desktop (Mac)
|
||||
- Desktop (Linux)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you compiling? The game version is in the bottom left corner of the main menu or in the project.hxp file.
|
||||
placeholder: ex. 0.6.4
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Describe your compiling issue."
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "### Please do not report issues from other engines. These must be reported in their respective repositories."
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Console Output
|
||||
description: Paste any errors or warnings from the console here.
|
||||
placeholder: Paste output here...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Context
|
||||
description: Provide as much detail as you can. Did you modify any code? Provide any relevant images or videos.
|
||||
placeholder: Describe the context here...
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,83 @@
|
||||
name: Crash Report
|
||||
description: Report a crash that occurred while playing the game.
|
||||
labels: ["type: major bug", "status: pending triage"]
|
||||
title: "Crash Report: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "# PLEASE READ THE [CONTRIBUTING GUIDE](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md) BEFORE OPENING ISSUES!"
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Issue Checklist
|
||||
description: Be sure to complete these steps to increase the chances of your issue being addressed!
|
||||
options:
|
||||
- label: I have read the [Contributing Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md)
|
||||
- label: I have checked the Issues/Discussions pages to see if my issue has already been reported
|
||||
- label: I have properly titled my issue
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Which platform are you playing on?
|
||||
options:
|
||||
- Newgrounds (Web/HTML5)
|
||||
- Itch.io (Web/HTML5)
|
||||
- Itch.io (Downloadable Build) - Windows
|
||||
- Itch.io (Downloadable Build) - MacOS
|
||||
- Itch.io (Downloadable Build) - Linux
|
||||
- Compiled from GitHub Source Code
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Browser
|
||||
description: (Web/HTML5 users only) Which browser are you playing on?
|
||||
options:
|
||||
- Google Chrome
|
||||
- Microsoft Edge
|
||||
- Firefox
|
||||
- Opera
|
||||
- Safari
|
||||
- Other (Specify in Description field)
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
|
||||
placeholder: ex. 0.6.4
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "## Describe the crash."
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "### Please do not report issues from other engines. These must be reported in their respective repositories."
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Include screenshots or videos of the crash happening. Provide as much detail as you can.
|
||||
placeholder: Describe the crash here...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: What steps can other people take to reliably trigger this crash?
|
||||
placeholder: 1. Open the game...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Crash logs
|
||||
description: These can be found in the logs folder where Funkin.exe is.
|
||||
placeholder: Upload your logs here...
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Enhancement
|
||||
description: Suggest a new feature.
|
||||
labels: ["type: enhancement", "status: pending triage"]
|
||||
title: "Enhancement: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "# PLEASE READ THE [CONTRIBUTING GUIDE](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md) BEFORE OPENING ISSUES!"
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Issue Checklist
|
||||
description: Be sure to complete these steps to increase the chances of your suggestion being considered!
|
||||
options:
|
||||
- label: I have read the [Contributing Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md)
|
||||
- label: I have checked the Issues/Discussions pages to see if my enhancement has already been suggested
|
||||
- label: I have properly titled my enhancement
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What is your suggestion, and why should it be implemented?
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,154 @@
|
||||
name: setup-haxeshit
|
||||
description: "sets up haxe shit, using HMM!"
|
||||
|
||||
inputs:
|
||||
haxe:
|
||||
description: 'Version of haxe to install'
|
||||
required: true
|
||||
default: '4.3.6'
|
||||
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
|
||||
gh-token:
|
||||
description: 'GitHub secret for private repos as dependencies'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
|
||||
- name: Setup timers
|
||||
shell: bash
|
||||
run: |
|
||||
echo "TIMER_HAXE=$(date +%s)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install Haxe
|
||||
uses: funkincrew/[email protected]
|
||||
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 libpulse-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 fixrepo --global || true
|
||||
haxelib --debug --never --global install haxelib 4.1.0
|
||||
haxelib --debug --global set haxelib 4.1.0
|
||||
haxelib --global remove haxelib git || true
|
||||
haxelib --global remove hmm || true
|
||||
rm -rf .haxelib
|
||||
haxelib --debug --never --global git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies
|
||||
haxelib --debug --never --global git hmm https://github.com/FunkinCrew/hmm funkin-patches
|
||||
haxelib --debug --never newrepo
|
||||
haxelib version
|
||||
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
|
||||
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: Prep git for dependency install
|
||||
uses: gacts/run-and-post-run@v1
|
||||
with:
|
||||
run: |
|
||||
git config --global --name-only --get-regexp 'url\.https\:\/\/x-access-token:.+@github\.com\/\.insteadOf' \
|
||||
| xargs -I {} git config --global --unset {}
|
||||
|
||||
git config -l --show-scope --show-origin
|
||||
git config --global 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' https://github.com/
|
||||
post: git config --global --unset 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf'
|
||||
|
||||
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}
|
||||
name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
haxelib --global run hmm install -q
|
||||
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
|
||||
@@ -0,0 +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: false
|
||||
target:
|
||||
description: "Target (html5, windows, linux, macos)"
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,22 @@
|
||||
# Add 'size: tiny' to any changes of at most 4 lines
|
||||
'size: tiny':
|
||||
max: 4
|
||||
|
||||
# Add 'size: small' to any changes between 5 and 10 lines
|
||||
'size: small':
|
||||
min: 5
|
||||
max: 10
|
||||
|
||||
# Add 'size: medium' to any changes between 11 and 100 lines
|
||||
'size: medium':
|
||||
min: 11
|
||||
max: 100
|
||||
|
||||
# Add 'size: large' to any changes between 101 and 500 lines
|
||||
'size: large':
|
||||
min: 101
|
||||
max: 500
|
||||
|
||||
# Add 'size: huge' to any changes of more than 500 lines
|
||||
'size: huge':
|
||||
min: 501
|
||||
@@ -0,0 +1,5 @@
|
||||
# Git Hooks
|
||||
These work even on Windows because of Git Bash.
|
||||
|
||||
## Setup
|
||||
`git config core.hooksPath .github/hooks`
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
git submodule update --init --recursive
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
git submodule update --init --recursive
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
if git diff --cached --submodule | grep -q "^+"; then
|
||||
echo "WARNING: You have unpushed changes in submodules."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,157 @@
|
||||
# Configuration for Label Actions - https://github.com/dessant/label-actions
|
||||
|
||||
# Automatically close issues and pull requests when the `status: duplicate` label is applied
|
||||
'status: duplicate':
|
||||
issues:
|
||||
# Post a comment
|
||||
comment: >
|
||||
This issue is a duplicate. Please direct all discussion to the original issue.
|
||||
# Close the issue
|
||||
close: false
|
||||
# Set a close reason
|
||||
# close-reason: 'not planned'
|
||||
# Remove other status labels
|
||||
unlabel:
|
||||
- 'status: accepted'
|
||||
- 'status: bug reproduced'
|
||||
- 'status: cannot reproduce'
|
||||
- 'status: needs clarification'
|
||||
- 'status: needs r&d'
|
||||
- 'status: needs revision'
|
||||
- 'status: pending pull request'
|
||||
- 'status: pending triage'
|
||||
- 'status: planned'
|
||||
- 'status: rejected'
|
||||
- 'status: resolved'
|
||||
- 'status: resolved internally'
|
||||
- 'status: reviewing internally'
|
||||
- 'status: stale'
|
||||
prs:
|
||||
# Post a comment
|
||||
comment: >
|
||||
This pull request is a duplicate. Please direct all discussion to the original pull request.
|
||||
# Close the pull request
|
||||
close: false
|
||||
# Remove other status labels
|
||||
unlabel:
|
||||
- 'status: accepted'
|
||||
- 'status: bug reproduced'
|
||||
- 'status: cannot reproduce'
|
||||
- 'status: needs clarification'
|
||||
- 'status: needs r&d'
|
||||
- 'status: needs revision'
|
||||
- 'status: pending pull request'
|
||||
- 'status: pending triage'
|
||||
- 'status: planned'
|
||||
- 'status: rejected'
|
||||
- 'status: resolved'
|
||||
- 'status: resolved internally'
|
||||
- 'status: reviewing internally'
|
||||
- 'status: stale'
|
||||
|
||||
'status: stale':
|
||||
issues:
|
||||
# Close the issue
|
||||
close: true
|
||||
# Set a close reason
|
||||
close-reason: 'not planned'
|
||||
# Remove other status labels
|
||||
unlabel:
|
||||
- 'status: accepted'
|
||||
- 'status: bug reproduced'
|
||||
- 'status: cannot reproduce'
|
||||
- 'status: duplicate'
|
||||
- 'status: needs clarification'
|
||||
- 'status: needs r&d'
|
||||
- 'status: needs revision'
|
||||
- 'status: pending pull request'
|
||||
- 'status: pending triage'
|
||||
- 'status: planned'
|
||||
- 'status: rejected'
|
||||
- 'status: resolved'
|
||||
- 'status: resolved internally'
|
||||
- 'status: reviewing internally'
|
||||
prs:
|
||||
# Close the pull request
|
||||
close: true
|
||||
# Remove other status labels
|
||||
unlabel:
|
||||
- 'status: accepted'
|
||||
- 'status: bug reproduced'
|
||||
- 'status: cannot reproduce'
|
||||
- 'status: duplicate'
|
||||
- 'status: needs clarification'
|
||||
- 'status: needs r&d'
|
||||
- 'status: needs revision'
|
||||
- 'status: pending pull request'
|
||||
- 'status: pending triage'
|
||||
- 'status: planned'
|
||||
- 'status: rejected'
|
||||
- 'status: resolved'
|
||||
- 'status: resolved internally'
|
||||
- 'status: reviewing internally'
|
||||
|
||||
'status: rejected':
|
||||
issues:
|
||||
# Close the issue
|
||||
close: true
|
||||
# Set a close reason
|
||||
close-reason: 'not planned'
|
||||
# Remove other status labels
|
||||
unlabel:
|
||||
- 'status: accepted'
|
||||
- 'status: bug reproduced'
|
||||
- 'status: cannot reproduce'
|
||||
- 'status: duplicate'
|
||||
- 'status: needs clarification'
|
||||
- 'status: needs r&d'
|
||||
- 'status: needs revision'
|
||||
- 'status: pending pull request'
|
||||
- 'status: pending triage'
|
||||
- 'status: planned'
|
||||
- 'status: resolved'
|
||||
- 'status: resolved internally'
|
||||
- 'status: reviewing internally'
|
||||
- 'status: stale'
|
||||
prs:
|
||||
# Close the pull request
|
||||
close: true
|
||||
# Remove other status labels
|
||||
unlabel:
|
||||
- 'status: accepted'
|
||||
- 'status: bug reproduced'
|
||||
- 'status: cannot reproduce'
|
||||
- 'status: duplicate'
|
||||
- 'status: needs clarification'
|
||||
- 'status: needs r&d'
|
||||
- 'status: needs revision'
|
||||
- 'status: pending pull request'
|
||||
- 'status: pending triage'
|
||||
- 'status: planned'
|
||||
- 'status: resolved'
|
||||
- 'status: resolved internally'
|
||||
- 'status: reviewing internally'
|
||||
- 'status: stale'
|
||||
|
||||
'status: resolved':
|
||||
issues:
|
||||
# Close the issue
|
||||
close: true
|
||||
# Set a close reason
|
||||
close-reason: 'completed'
|
||||
# Remove other status labels
|
||||
unlabel:
|
||||
- 'status: accepted'
|
||||
- 'status: bug reproduced'
|
||||
- 'status: cannot reproduce'
|
||||
- 'status: duplicate'
|
||||
- 'status: needs clarification'
|
||||
- 'status: needs r&d'
|
||||
- 'status: needs revision'
|
||||
- 'status: pending pull request'
|
||||
- 'status: pending triage'
|
||||
- 'status: planned'
|
||||
- 'status: rejected'
|
||||
- 'status: resolved internally'
|
||||
- 'status: reviewing internally'
|
||||
- 'status: stale'
|
||||
@@ -0,0 +1,18 @@
|
||||
# Add Documentation tag to PR's changing markdown files, or anything in the docs folder
|
||||
'pr: documentation':
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- docs/*
|
||||
- '**/*.md'
|
||||
|
||||
# Add Haxe tag to PR's changing haxe code files
|
||||
'pr: haxe':
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '**/*.hx'
|
||||
|
||||
# Add GitHub tag to PR's changing yml files, or anything in the .github folder
|
||||
'pr: github':
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- github/*
|
||||
- '**/*.yml'
|
||||
@@ -0,0 +1,10 @@
|
||||
<!-- Please read the Contributing Guide (https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md) before submitting this PR. -->
|
||||
|
||||
<!-- Does this PR close any issues? If so, link them below. -->
|
||||
## Linked Issues
|
||||
|
||||
<!-- Briefly describe the issue(s) fixed. -->
|
||||
## Description
|
||||
|
||||
<!-- Include any relevant screenshots or videos. -->
|
||||
## Screenshots/Videos
|
||||
@@ -0,0 +1,250 @@
|
||||
name: Build and Upload nightly game builds
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build-defines:
|
||||
type: string
|
||||
description: Build defines to use
|
||||
default: '-DGITHUB_BUILD'
|
||||
save-artifact:
|
||||
type: boolean
|
||||
description: Save the build artifact to Github Actions (sends to itch otherwise)
|
||||
default: false
|
||||
push:
|
||||
|
||||
jobs:
|
||||
|
||||
gather-changes:
|
||||
runs-on: build-set
|
||||
outputs:
|
||||
trigger-build: ${{ steps.should-trigger.outputs.result }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: funkincrew/ci-checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
base: ${{ github.ref }}
|
||||
filters: |
|
||||
docker:
|
||||
- '.github/workflows/build-game.yml'
|
||||
- '**/Dockerfile'
|
||||
- uses: actions/github-script@v7
|
||||
id: should-trigger
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
const { payload } = context
|
||||
const changes = ${{ steps.filter.outputs.changes }}
|
||||
const manual = payload.commits
|
||||
.some(c => c.message.toLowerCase().replace(/\W/g, " ").includes("docker rebuild"))
|
||||
|
||||
console.log({ payload, changes, manual, commits: payload.commits })
|
||||
|
||||
return payload.created
|
||||
|| payload.forced
|
||||
|| changes.includes("docker")
|
||||
|| manual
|
||||
|
||||
gather-tags:
|
||||
runs-on: build-set
|
||||
outputs:
|
||||
primary: ${{ steps.build-tags.outputs.primary }}
|
||||
list: ${{ steps.build-tags.outputs.list }}
|
||||
steps:
|
||||
- name: Gather build tags
|
||||
uses: actions/github-script@v7
|
||||
id: build-tags
|
||||
with:
|
||||
script: |
|
||||
const base = "ghcr.io/funkincrew/build-dependencies"
|
||||
const default_branch = "rewrite/master"
|
||||
|
||||
const ref_path = context.ref.split("/").slice(2)
|
||||
const ref = ref_path.join("/")
|
||||
const ref_tag = ref_path.join("-")
|
||||
|
||||
const tags = [ref_tag, context.sha]
|
||||
|
||||
if (ref === default_branch) tags.push("latest")
|
||||
|
||||
console.log([
|
||||
`ref: ${ref}`,
|
||||
`default_branch: ${default_branch}`,
|
||||
`tags: ${tags.join(", ")}`
|
||||
].join('\n'))
|
||||
|
||||
const tag_list = tags
|
||||
.map(tag => `${base}:${tag}`)
|
||||
.join("\n")
|
||||
|
||||
core.setOutput("primary", ref_tag)
|
||||
core.setOutput("list", tag_list)
|
||||
|
||||
docker-image:
|
||||
needs: [gather-changes, gather-tags]
|
||||
if: needs.gather-changes.outputs.trigger-build == 'true'
|
||||
runs-on: build-set
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: funkincrew/ci-checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
- name: Log into GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./build
|
||||
push: true
|
||||
tags: ${{ needs.gather-tags.outputs.list }}
|
||||
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 }}
|
||||
|
||||
build-game-on-host:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: windows
|
||||
runs-on: windows
|
||||
- target: macos
|
||||
runs-on: macos
|
||||
runs-on:
|
||||
- ${{ matrix.runs-on }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_DEFINES: ${{ github.event.inputs.build-defines || '-DGITHUB_BUILD' }}
|
||||
steps:
|
||||
- name: Make git happy
|
||||
run: |
|
||||
git config --global --replace-all 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 }}
|
||||
persist-credentials: false
|
||||
- name: Setup build environment
|
||||
uses: ./.github/actions/setup-haxe
|
||||
with:
|
||||
gh-token: ${{ steps.app_token.outputs.token }}
|
||||
- name: Setup HXCPP dev commit
|
||||
run: |
|
||||
cd .haxelib/hxcpp/git/tools/hxcpp
|
||||
haxe compile.hxml
|
||||
cd ../../../../..
|
||||
- name: Build game (windows)
|
||||
if: ${{ matrix.target == 'windows' }}
|
||||
run: |
|
||||
haxelib run lime build windows -v -release $BUILD_DEFINES
|
||||
timeout-minutes: 120
|
||||
- name: Build game (unix)
|
||||
if: ${{ matrix.target != 'windows' }}
|
||||
run: |
|
||||
haxelib run lime build ${{ matrix.target }} -v -release --times $BUILD_DEFINES
|
||||
timeout-minutes: 120
|
||||
- name: Save build artifact to Github Actions
|
||||
if: ${{ github.event.inputs.save-artifact }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-${{ matrix.target }}
|
||||
path: export/release/${{matrix.target}}/bin/
|
||||
- name: Upload build artifacts
|
||||
uses: ./.github/actions/upload-itch
|
||||
with:
|
||||
butler-key: ${{ secrets.BUTLER_API_KEY}}
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
build-game-in-container:
|
||||
needs: [gather-tags, docker-image]
|
||||
if: ${{ ! cancelled() }}
|
||||
runs-on: build-set
|
||||
container: ghcr.io/funkincrew/build-dependencies:${{ needs.gather-tags.outputs.primary }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: linux
|
||||
- target: html5
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_DEFINES: ${{ github.event.inputs.build-defines || '-DGITHUB_BUILD' }}
|
||||
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 }}
|
||||
persist-credentials: false
|
||||
- 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: |
|
||||
git config --global 'url.https://x-access-token:${{ steps.app_token.outputs.token }}@github.com/.insteadOf' https://github.com/
|
||||
git config --global advice.detachedHead false
|
||||
haxelib --global run hmm install -q
|
||||
cd .haxelib/hxcpp/git/tools/hxcpp && haxe compile.hxml
|
||||
- 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 $BUILD_DEFINES
|
||||
timeout-minutes: 120
|
||||
- name: Save build artifact to Github Actions
|
||||
if: ${{ github.event.inputs.save-artifact }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-${{ matrix.target }}
|
||||
path: export/release/${{matrix.target}}/bin/
|
||||
- name: Upload build artifacts
|
||||
uses: ./.github/actions/upload-itch
|
||||
with:
|
||||
butler-key: ${{ secrets.BUTLER_API_KEY}}
|
||||
target: ${{ matrix.target }}
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Cancel queued workflows on PR merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
|
||||
cancel_stuff:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: build-set
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
- 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,
|
||||
run_id: run.id
|
||||
});
|
||||
});
|
||||
console.log(runs);
|
||||
@@ -0,0 +1,29 @@
|
||||
# Perform actions when labels are applied to issues, discussions, or pull requests
|
||||
# See .github/label-actions.yml
|
||||
name: 'Label Actions'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
discussion:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/label-actions@v4
|
||||
@@ -0,0 +1,19 @@
|
||||
name: "Pull Request Labeler 2 (Runs on PR creation)"
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
# Apply `status: pending triage` to newly created pull requests
|
||||
apply-pending-triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Apply "status pending triage" to new pull requests
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
labels: "status: pending triage"
|
||||
@@ -0,0 +1,29 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
# Apply labels to pull requests based on which files were edited
|
||||
labeler:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set basic labels
|
||||
uses: actions/labeler@v5
|
||||
with:
|
||||
sync-labels: true
|
||||
# Apply labels to pull requests based on how many lines were edited
|
||||
changed-lines-count-labeler:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
name: An action for automatically labelling pull requests based on the changed lines count
|
||||
steps:
|
||||
- name: Set change count labels
|
||||
uses: vkirilichev/[email protected]
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
configuration-path: .github/changed-lines-count-labeler.yml
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
.DS_STORE
|
||||
.haxelib/
|
||||
.vs/
|
||||
APIStuff.hx
|
||||
dump/
|
||||
export/
|
||||
project/
|
||||
RECOVER_*.fla
|
||||
shitAudio/
|
||||
.build_time
|
||||
.swp
|
||||
NewgroundsCredentials.hx
|
||||
|
||||
# Exclude JS stuff
|
||||
node_modules/
|
||||
package.json
|
||||
package-lock.json
|
||||
.aider*
|
||||
@@ -0,0 +1,6 @@
|
||||
[submodule "assets"]
|
||||
path = assets
|
||||
url = https://github.com/FunkinCrew/Funkin.assets
|
||||
[submodule "art"]
|
||||
path = art
|
||||
url = https://github.com/FunkinCrew/Funkin.art
|
||||
@@ -0,0 +1,13 @@
|
||||
# Ignore artifacts
|
||||
export
|
||||
|
||||
# Ignore all JSONS in the images folder (including FlxAnimate JSONs)
|
||||
assets/preload/images
|
||||
assets/shared/images
|
||||
assets/weekend1/images
|
||||
|
||||
# Don't ignore data files
|
||||
# TODO: These don't work.
|
||||
!assets/preload/data/
|
||||
|
||||
assets/exclude/data/credits.json
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Configuration for JSON file formatting.
|
||||
*/
|
||||
module.exports = {
|
||||
// Line width before Prettier tries to add new lines.
|
||||
printWidth: 80,
|
||||
|
||||
// Indent with 2 spaces.
|
||||
tabs: false,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
|
||||
// Use double quotes.
|
||||
singleQuote: false,
|
||||
quoteProps: "preserve",
|
||||
parser: "json",
|
||||
|
||||
bracketSpacing: true, // Spacing between brackets in object literals.
|
||||
trailingComma: "none", // No trailing commas.
|
||||
semi: false, // No semicolons at ends of statements.
|
||||
};
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"nadako.vshaxe", // Basic Haxe integration
|
||||
"wiggin77.codedox", // Haxe documentation
|
||||
"vshaxe.haxe-checkstyle", // Haxe code style and conventions
|
||||
"vshaxe.hxcpp-debugger", // CPP debugging
|
||||
"openfl.lime-vscode-extension", // Lime integration
|
||||
"esbenp.prettier-vscode", // JSON formatting
|
||||
"redhat.vscode-xml" // XML formatting
|
||||
]
|
||||
}
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Launch in native/CPP on Windows/OSX/Linux
|
||||
"name": "Lime Build+Debug",
|
||||
"type": "lime",
|
||||
"request": "launch"
|
||||
},
|
||||
{
|
||||
// Launch in native/CPP on Windows/OSX/Linux
|
||||
"name": "Lime Debug (No Build)",
|
||||
"type": "lime",
|
||||
"request": "launch",
|
||||
"preLaunchTask": null
|
||||
},
|
||||
{
|
||||
// Launch in browser
|
||||
"name": "HTML5 Debug",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"url": "http://127.0.0.1:3001",
|
||||
"sourceMaps": true,
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"preLaunchTask": "debug: html"
|
||||
},
|
||||
{
|
||||
// Performs a build for debugging macros
|
||||
// Debugging is attached on the compilation rather than the game itself
|
||||
"name": "Haxe Eval",
|
||||
"type": "haxe-eval",
|
||||
"request": "launch"
|
||||
},
|
||||
{
|
||||
// Attaches the debugger to an already running game
|
||||
"name": "HXCPP - Attach",
|
||||
"type": "hxcpp",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+229
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"[haxe]": {
|
||||
// Automatically keep Haxe files formatted.
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.defaultFormatter": "nadako.vshaxe",
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
|
||||
"[json]": {
|
||||
// Automatically keep JSON files formatted.
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
|
||||
"[jsonc]": {
|
||||
// Automatically keep JSONC files formatted.
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"prettier.tabWidth": 2,
|
||||
|
||||
// XML formatting style configuration
|
||||
"xml.format.enabled": true,
|
||||
"xml.format.legacy": false,
|
||||
"xml.format.emptyElements": "collapse",
|
||||
"xml.preferences.quoteStyle": "double",
|
||||
"xml.format.enforceQuoteStyle": "preferred",
|
||||
"xml.format.preserveAttributeLineBreaks": false,
|
||||
"xml.format.preservedNewlines": 0,
|
||||
"xml.format.splitAttributes": false,
|
||||
"xml.format.joinCDATALines": true,
|
||||
"xml.format.preserveEmptyContent": false,
|
||||
"xml.format.joinCommentLines": false,
|
||||
"xml.format.joinContentLines": false,
|
||||
"xml.format.spaceBeforeEmptyCloseTag": true,
|
||||
"xml.format.xsiSchemaLocationSplit": "onPair",
|
||||
"xml.format.splitAttributesIndentSize": 2,
|
||||
"xml.format.closingBracketNewLine": false,
|
||||
"xml.format.preserveSpace": [
|
||||
"xsl:text",
|
||||
"xsl:comment",
|
||||
"xsl:processing-instruction",
|
||||
"literallayout",
|
||||
"programlisting",
|
||||
"screen",
|
||||
"synopsis",
|
||||
"pre",
|
||||
"xd:pre"
|
||||
],
|
||||
"xml.format.maxLineWidth": 0,
|
||||
"xml.format.grammarAwareFormatting": true,
|
||||
|
||||
// Generic file formatting style configuration
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": false,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
|
||||
// Automatically detect indentation.
|
||||
"editor.detectIndentation": true,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2,
|
||||
|
||||
// Automatically enforce Linux style line endings.
|
||||
"files.eol": "\n",
|
||||
|
||||
"haxe.displayPort": "auto",
|
||||
"haxe.enableCompilationServer": true,
|
||||
"haxe.enableServerView": true,
|
||||
"haxe.displayServer": {
|
||||
"arguments": ["-v"]
|
||||
},
|
||||
// Fix file associations for HScript.
|
||||
"files.associations": {
|
||||
"*.hxp": "haxe",
|
||||
"*.hscript": "haxe",
|
||||
"*.haxe": "haxe",
|
||||
"*.hxs": "haxe",
|
||||
"*.hxc": "haxe"
|
||||
},
|
||||
"projectManager.git.baseFolders": ["./"],
|
||||
|
||||
"haxecheckstyle.sourceFolders": ["src", "source"],
|
||||
"haxecheckstyle.externalSourceRoots": [],
|
||||
"haxecheckstyle.configurationFile": "checkstyle.json",
|
||||
"haxecheckstyle.codeSimilarityBufferSize": 100,
|
||||
|
||||
"lime.targetConfigurations": [
|
||||
{
|
||||
"label": "Windows / Debug (Discord)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS", "-DFEATURE_DISCORD_RPC"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (FlxAnimate Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (FlxAnimate Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DANIMATE"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Freeplay)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Straight to Freeplay)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DFREEPLAY"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
|
||||
"target": "windows",
|
||||
"args": [
|
||||
"-debug",
|
||||
"-DSONG=bopeebo -DDIFFICULTY=normal",
|
||||
"-DFEATURE_DEBUG_FUNCTIONS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Play - 2hot)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Conversation Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Conversation Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DDIALOGUE"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Results Screen Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DRESULTS"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Animation Editor)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DANIMDEBUG", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Debug hxvlc)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Straight to Animation Editor)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DANIMDEBUG"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Latency Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Latency Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DLATENCY"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Waveform Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Release (GitHub Actions)",
|
||||
"target": "windows",
|
||||
"args": ["-release", "-DGITHUB_BUILD"]
|
||||
},
|
||||
{
|
||||
"label": "HashLink / Debug (Waveform Test)",
|
||||
"target": "hl",
|
||||
"args": ["-debug", "-DWAVEFORM"]
|
||||
},
|
||||
{
|
||||
"label": "HTML5 / Debug (Watch)",
|
||||
"target": "html5",
|
||||
"args": ["-debug", "-watch", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
}
|
||||
],
|
||||
"lime.buildTypes": [
|
||||
{
|
||||
"label": "Debug",
|
||||
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Debug (Tracy)",
|
||||
"args": ["-debug", "-DFEATURE_DEBUG_TRACY", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Debug (Straight to Chart Editor)",
|
||||
"args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Release",
|
||||
"args": ["-release"]
|
||||
},
|
||||
{
|
||||
"label": "Release (GitHub Actions)",
|
||||
"args": ["-release", "-DGITHUB_BUILD"]
|
||||
}
|
||||
],
|
||||
"cmake.configureOnOpen": false,
|
||||
"coverage-gutters.coverageFileNames": [
|
||||
"lcov.info",
|
||||
"cov.xml",
|
||||
"coverage.xml",
|
||||
"jacoco.xml",
|
||||
"coverage.cobertura.xml"
|
||||
]
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "lime",
|
||||
"command": "test",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+995
@@ -0,0 +1,995 @@
|
||||
# Changelog
|
||||
All notable changes will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
|
||||
## [0.6.4] - 2025-05-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Misses now actually reduce your clear percentage and rank, as Eric intended. ([5fdbd23](https://github.com/FunkinCrew/Funkin/commit/5fdbd23a17b8eaf21582283400be2c6444e8b198)) - by @Lasercar in [#4880](https://github.com/FunkinCrew/Funkin/pull/4880)
|
||||
- The miss rebalance is now reflected on the Freeplay clear percent display. ([f6ac4ca](https://github.com/FunkinCrew/Funkin/commit/f6ac4cad43f4834a86a3fc156d6d39eca2c47c7e)) - by @Lasercar, @Hundrec, and @NotHyper-474 in [#4898](https://github.com/FunkinCrew/Funkin/pull/4898) and [#4923](https://github.com/FunkinCrew/Funkin/pull/4923)
|
||||
|
||||
### Fixed
|
||||
|
||||
- HTML5 builds are no longer literally unplayable. ([6f43438](https://github.com/FunkinCrew/Funkin/commit/6f43438cdbfecf26d4bcf62a0d9ff71e40333ac2)) - by @cyn0x8 in [#4398](https://github.com/FunkinCrew/Funkin/pull/4398)
|
||||
- Medals no longer crash the game when obtained. ([808698a](https://github.com/FunkinCrew/Funkin/commit/808698ace7457bc76a8a52d415d63fa4ee5b9400)) - by @KoloInDaCrib in [#4815](https://github.com/FunkinCrew/Funkin/pull/4815)
|
||||
- The game no longer crashes when reaching the main menu before authenticating with Newgrounds. ([a83a4a5](https://github.com/FunkinCrew/Funkin/commit/a83a4a599c2e407832ef9bbe47bb8b8c07ce712a)) - by @NotHyper-474 in [#4871](https://github.com/FunkinCrew/Funkin/pull/4871)
|
||||
- User preferences and other settings are now properly saved after restarting the game. ([23a9e7f](https://github.com/FunkinCrew/Funkin/commit/23a9e7f944fb703160c2ed7b0bef38c70c5370db)) - by @Lasercar in [#4881](https://github.com/FunkinCrew/Funkin/pull/4881)
|
||||
- Texture atlas sprites no longer loop infinitely, fixing issues with multiple cutscenes. ([f385cf9](https://github.com/FunkinCrew/Funkin/commit/f385cf9fd33e5358427f40dad3fecfed233c1706)) - by @AbnormalPoof in [#4564](https://github.com/FunkinCrew/Funkin/pull/4564)
|
||||
- The Week 3 Pico Mix cutscene no longer breaks with Naughtyness turned off. ([e3aba5c](https://github.com/FunkinCrew/funkin.assets/commit/e3aba5c4faa8e8c705c3e0daf04e30db7ac5a136)) - by @Lasercar in [funkin.assets#169](https://github.com/FunkinCrew/funkin.assets/pull/169)
|
||||
- Adjusted the pixel strumline’s position when Downscroll is enabled to match the regular notestyle. ([ea93ec9](https://github.com/FunkinCrew/Funkin/commit/ea93ec90cb5c5dbd4a5bea64114af58c70cb43f4)) - by @Lasercar in [#4318](https://github.com/FunkinCrew/Funkin/pull/4318)
|
||||
- The Main Menu now correctly remembers your most recently selected menu item. ([05b9d68](https://github.com/FunkinCrew/Funkin/commit/05b9d68645b71c9cc36a5cb0c0e93af6acb36132)) - by @Lasercar and @Hundrec in [#4227](https://github.com/FunkinCrew/Funkin/pull/4227)
|
||||
- Fixed an occasional softlock when returning to the Freeplay menu from a song. ([b15e809](https://github.com/FunkinCrew/Funkin/commit/b15e809c63664e039781592dcbdee4383e73800e)) - by @TechnikTil in [#4665](https://github.com/FunkinCrew/Funkin/pull/4665)
|
||||
- Switching to and from the Freeplay Random capsule now behaves more consistently. ([832bc5b](https://github.com/FunkinCrew/Funkin/commit/832bc5bd83d49a646b9c8cb2b767602c848be908)) - by @VioletSnowLeopard in [#4885](https://github.com/FunkinCrew/Funkin/pull/4885)
|
||||
- Changing Freeplay filters with the Random capsule selected now behaves more consistently. ([edf6889](https://github.com/FunkinCrew/Funkin/commit/edf6889af9e7f68caead9b8139178a36b4b5b2d6)) - by @VioletSnowLeopard in [#4913](https://github.com/FunkinCrew/Funkin/pull/4913)
|
||||
- Switching between identical Freeplay filters no longer makes the songlist invisible. ([8b9775d](https://github.com/FunkinCrew/Funkin/commit/8b9775d91f4f2b5896a637a014b4be770319968e)) - by @VioletSnowLeopard in [#4919](https://github.com/FunkinCrew/Funkin/pull/4919)
|
||||
- The millions place digit of the Freeplay score display now updates properly. ([d9fcaf0](https://github.com/FunkinCrew/Funkin/commit/d9fcaf0e6382b121f7773d23f4864364a1e6a577)) - by @Lasercar in [#4065](https://github.com/FunkinCrew/Funkin/pull/4065)
|
||||
- The Freeplay clear percent display is now more consistently aligned. ([329182e](https://github.com/FunkinCrew/funkin.assets/commit/329182ea1b6839187a1d800b2d009a4c1874479d)) - by @Hundrec in [funkin.assets#37](https://github.com/FunkinCrew/funkin.assets/pull/37)
|
||||
- The Freeplay alternate instrumental selector now disables all other inputs. ([664d9e0](https://github.com/FunkinCrew/Funkin/commit/664d9e0fb3f8b93b1f941fb3b6724c37f5b9e5b5)) - @Lasercar in [#4214](https://github.com/FunkinCrew/Funkin/pull/4214)
|
||||
- The Freeplay alternate instrumental selector no longer becomes offset when changing difficulties. ([664d9e0](https://github.com/FunkinCrew/Funkin/commit/664d9e0fb3f8b93b1f941fb3b6724c37f5b9e5b5)) - @Lasercar in [#4214](https://github.com/FunkinCrew/Funkin/pull/4214)
|
||||
- Exiting and then navigating through the Freeplay alternate instrumental selector no longer crashes the game. ([664d9e0](https://github.com/FunkinCrew/Funkin/commit/664d9e0fb3f8b93b1f941fb3b6724c37f5b9e5b5)) - @Lasercar in [#4214](https://github.com/FunkinCrew/Funkin/pull/4214)
|
||||
- Ranks no longer overwrite themselves in the Freeplay new rank animation. ([664d9e0](https://github.com/FunkinCrew/Funkin/commit/664d9e0fb3f8b93b1f941fb3b6724c37f5b9e5b5)) - @Lasercar in [#4214](https://github.com/FunkinCrew/Funkin/pull/4214)
|
||||
- The debris in Pico’s Great Results animation no longer grows in size. ([c108a7f](https://github.com/FunkinCrew/funkin.assets/commit/c108a7ff0d11bf328e7b232160b8f68c71e21bca)) - by @ThatRozebudDude in [funkin.assets#73](https://github.com/FunkinCrew/funkin.assets/pull/73)
|
||||
- Fixed the song not starting if more than 32 sounds are playing at once. ([31d3718](https://github.com/FunkinCrew/Funkin/commit/31d3718e5c33371cacd495219f9cc3908244bf71)) - by @KoloInDaCrib in [#4352](https://github.com/FunkinCrew/Funkin/pull/4352)
|
||||
- Hot reloading (pressing F5) during dialogue no longer crashes the game. ([3e0dbe2](https://github.com/FunkinCrew/Funkin/commit/3e0dbe2758d68ccf4ded1d08bec247ab05d70829)) - by @KoloInDaCrib in [#4769](https://github.com/FunkinCrew/Funkin/pull/4769)
|
||||
- Restarting a song without either a player or an opponent no longer crashes the game, fixing minimal playtest mode. ([bbc0546](https://github.com/FunkinCrew/Funkin/commit/bbc0546c7450b929dc78ad9271171ea899478e9d)) - by @thesuperpig56 in [#4778](https://github.com/FunkinCrew/Funkin/pull/4778)
|
||||
- The sound tray no longer behaves incorrectly after wiping save data. ([afbb335](https://github.com/FunkinCrew/Funkin/commit/afbb3359938f020707efd92a17419e3658528ea5)) - by @Lasercar in [#4617](https://github.com/FunkinCrew/Funkin/pull/4617)
|
||||
- Opening a chart from the “Open Recent” section no longer crashes the Chart Editor. ([def1b74](https://github.com/FunkinCrew/Funkin/commit/def1b74eb6987272bc360cbf586ddcd1c26c6381)) - by @CrusherNotDrip in [#4936](https://github.com/FunkinCrew/Funkin/pull/4936)
|
||||
- Loading an invalid variation in the Chart Editor no longer crashes the game. ([c5f2a74](https://github.com/FunkinCrew/Funkin/commit/c5f2a744940dbc685b25373fa2dabb548f0b26fa)) - by @NotHyper-474 in [#4391](https://github.com/FunkinCrew/Funkin/pull/4391)
|
||||
- Default difficulties are no longer re-added when saving and loading a .FNFC chart. ([8074d57](https://github.com/FunkinCrew/Funkin/commit/8074d571860aba2c2f6f1c642d8d0e671ff5355e)) - by @Lasercar in [#4216](https://github.com/FunkinCrew/Funkin/pull/4216)
|
||||
- The Chart Editor now resizes properly after entering Fullscreen during a playtest. ([142ea6d](https://github.com/FunkinCrew/Funkin/commit/142ea6db0c53f6f9288109604702c2e7e5bc6f18)) - by @NotHyper-474 in [#4266](https://github.com/FunkinCrew/Funkin/pull/4266)
|
||||
- Added three missing hotkeys to the Chart Editor user guide. ([73ccb9a](https://github.com/FunkinCrew/funkin.assets/commit/73ccb9a80d08f3311f51cf88950692d7c1ea5d60)) - by @NotHyper-474 and @Hundrec in [funkin.assets#83](https://github.com/FunkinCrew/funkin.assets/pull/83)
|
||||
- The “Skip Forward” button is no longer named “Skip Back” in the Chart Editor. ([78f9fb7](https://github.com/FunkinCrew/funkin.assets/commit/78f9fb7cb6eaf7466826f144a0142c008fbff2f8)) - by @Hundrec in [funkin.assets#22](https://github.com/FunkinCrew/funkin.assets/pull/22)
|
||||
- Exporting a character .JSON from the Animation Editor now fills in the file name. ([a8262a8](https://github.com/FunkinCrew/Funkin/commit/a8262a8d3b4d2169327131e21c98f803da4a5ee4)) - by @anysad in [#3090](https://github.com/FunkinCrew/Funkin/pull/3090)
|
||||
- Properly credited MtH as a Charter in Monster’s metadata. ([6a10799](https://github.com/FunkinCrew/funkin.assets/commit/6a10799a40eb2b2deb364bcd3cc4387abe8e8bc4)) - by @ChillyBeanBAM in [funkin.assets#66](https://github.com/FunkinCrew/funkin.assets/pull/66)
|
||||
- Corrected the chances of a Pause Menu easter egg appearing. ([0101bae](https://github.com/FunkinCrew/Funkin/commit/0101bae7ecc800e26efd9590fdfa1305039b4461)) - by @VioletSnowLeopard in [#4358](https://github.com/FunkinCrew/Funkin/pull/4358)
|
||||
- Removed spammy traces for Playable Pico’s animations. ([f87255d](https://github.com/FunkinCrew/funkin.assets/commit/f87255d4bcb7a7ce0dc920aefc36b98a96115c75)) - by @VioletSnowLeopard in [funkin.assets#179](https://github.com/FunkinCrew/funkin.assets/pull/179)
|
||||
- Fixed memory overflowing in crash logs. ([189e028](https://github.com/FunkinCrew/Funkin/commit/189e028442c7fb892ce4dab62a5d08db658f3b2b)) - by @ACrazyTown in [#4589](https://github.com/FunkinCrew/Funkin/pull/4589)
|
||||
|
||||
### Removed
|
||||
|
||||
- The CHANGELOG.md file will no longer be included in builds. Visit the [Funkin' GitHub](https://github.com/FunkinCrew/Funkin/blob/main/CHANGELOG.md) to view the latest changes! ([a7da71c](https://github.com/FunkinCrew/Funkin/commit/a7da71c8a87baa440a0bc6e23d7e9b36c7574e14)) - by @Hundrec and @NotHyper-474 in [#4868](https://github.com/FunkinCrew/Funkin/pull/4868)
|
||||
|
||||
## New Contributors for 0.6.4
|
||||
|
||||
* @thesuperpig56 made their first contribution in [#4778](https://github.com/FunkinCrew/Funkin/pull/4778)
|
||||
* @CrusherNotDrip made their first contribution in [#4936](https://github.com/FunkinCrew/Funkin/pull/4936)
|
||||
* @ChillyBeanBAM made their first contribution in [funkin.assets#66](https://github.com/FunkinCrew/funkin.assets/pull/66)
|
||||
* @ThatRozebudDude made their first contribution in [funkin.assets#73](https://github.com/FunkinCrew/funkin.assets/pull/73)
|
||||
|
||||
|
||||
|
||||
## [0.6.3] - 2025-04-25
|
||||
|
||||
### Added
|
||||
|
||||
- New option in the Preferences menu: VSync!
|
||||
- Set it to Off, On, or Adaptive
|
||||
- Adaptive turns VSync off during FPS drops, which is better if supported by your graphics card.
|
||||
- Doesn't work on Mac, sorry!
|
||||
- Otis now has muzzle flashes when shooting.
|
||||
- Added a little easter egg to one of Pico's Results Screen animations.
|
||||
- Overhauled the sticker system to allow modders to easily add custom sticker sets. ([cdc468b](https://github.com/FunkinCrew/Funkin/commit/cdc468ba15683b3c0f54015e337673c77ddd7962)) - by @AbnormalPoof in [#4003](https://github.com/FunkinCrew/Funkin/pull/4003)
|
||||
- Implemented a macro to optimize registries and entries. ([1497521](https://github.com/FunkinCrew/Funkin/commit/14975212a918adb1a5d96a349ec9e8e63c9fc64b)) - by @lemz1 in [#3694](https://github.com/FunkinCrew/Funkin/pull/3694)
|
||||
- The macro is now used to retrieve base game assets. ([963e2ec](https://github.com/FunkinCrew/Funkin/commit/963e2ecd630fae24dae4206d315618cf4c4be6db)) - by @lemz1 in [#4707](https://github.com/FunkinCrew/Funkin/pull/4707)
|
||||
- The Chart Editor now displays the current beat and step of the playhead. ([83bb3bb](https://github.com/FunkinCrew/Funkin/commit/83bb3bb5074ecb5f50ee21979711efcda9d8017d)) - by @NotHyper-474 in [#4649](https://github.com/FunkinCrew/Funkin/pull/4649)
|
||||
|
||||
### Changed
|
||||
|
||||
- The mod API version is now 0.6.3.
|
||||
- Be sure to update the version in your mods' metadata, then ensure they are still working!
|
||||
- Eric's scoring rebalance update!
|
||||
- Misses (not ghost misses) are now worth -100 points, up from -10.
|
||||
- Hold note trails now grant 20% less health.
|
||||
- Dropping a hold note early now plays the miss animation and deducts health and score.
|
||||
- Made a LOT of charting changes!
|
||||
- Many missing, extra, and mistimed notes have been corrected.
|
||||
- Stacked notes that were preventing full combos have been removed.
|
||||
- Camera events have been adjusted to stay within the boundaries of the stage.
|
||||
- Some songs have been recharted.
|
||||
- The Freeplay menu can now always switch between variations.
|
||||
- The nearest song with an Erect variation will be selected when switching.
|
||||
- Switching difficulties now always plays the capsule jump-in animation.
|
||||
- The "Random" capsule now plays animations consistent with other capsules.
|
||||
- The Eggnog Erect cutscene can now be skipped. ([0303a03](https://github.com/FunkinCrew/funkin.assets/commit/1202651db8ea938fe11b6b734fbf7884d101e6ad)) - by @ShadzXD in [funkin.assets#62](https://github.com/FunkinCrew/funkin.assets/pull/62)
|
||||
- Swapped the positions of “skip” and “restart” items in the dialogue pause menu for consistency. ([1f88a3b](https://github.com/FunkinCrew/Funkin/commit/1f88a3b6e13e9283ad680805deab5e27ba141d96)) - by @VioletSnowLeopard in [#4553](https://github.com/FunkinCrew/Funkin/pull/4553)
|
||||
- The Controls menu now prevents unbinding essential UI controls to safeguard against softlocking save files. ([7eebce4](https://github.com/FunkinCrew/Funkin/commit/7eebce432d7627a1cf0bbd43f22a6f9bcb63ff65)) - by @VioletSnowLeopard in [#4382](https://github.com/FunkinCrew/Funkin/pull/4382)
|
||||
- Renamed “Auto Pause” preference to “Pause on Unfocus” for clarity. ([52be941](https://github.com/FunkinCrew/Funkin/commit/52be941b4503da0ac76918e2482ab1804866f2cf)) - by @JackXson-Real in [#4346](https://github.com/FunkinCrew/Funkin/pull/4346)
|
||||
- Overhauled `FileUtil`, introducing various fixes, new functions, and sandboxing. ([95ade2a](https://github.com/FunkinCrew/Funkin/commit/95ade2a08b7709e8208ec1b3e123bf5b4308ba10)) - by @cyn0x8 in [#3032](https://github.com/FunkinCrew/Funkin/pull/3032)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Story mode medals are now awarded even without scoring a new personal best.
|
||||
- Highscores are now submitted to Newgrounds even without scoring a new personal best.
|
||||
- The Freeplay menu no longer displays songs without Erect variations when returning from an Erect variation song.
|
||||
- Fixed Freeplay DJ animations for Boyfriend and Pico when idling (properly this time).
|
||||
- Alternate instrumentals for Cocoa, Senpai, Roses, and Stress are now locked until their Pico Mix is beaten.
|
||||
- The Roses Pico alternate instrumental is now offset to align with the original song's voices.
|
||||
- Entering Stress or Stress (Pico Mix) a second time no longer crashes the game.
|
||||
- Restarting Stress or Stress (Pico Mix) no longer spawns too many Tankmen in the background.
|
||||
- Pico's game over animation now displays properly on Ugh (Pico Mix) and Guns (Pico Mix).
|
||||
- Tankman's death lines now play for Boyfriend in Ugh, Guns, and Ugh Erect.
|
||||
- Week 6 dialogue no longer plays for all non-default variations when entered through Freeplay.
|
||||
- Nene (Pixel) now plays her knife raising animation when the player has low health.
|
||||
- A-Bot's visualizer is now blank before the song starts.
|
||||
- Debug editor tooltips no longer display behind windows.
|
||||
- Clearing save data no longer crashes the game.
|
||||
- Adding custom parameters to note kinds no longer crashes the Chart Editor. ([0a7bd31](https://github.com/FunkinCrew/Funkin/commit/0a7bd3111f59efbecfe097f8bbcfdefa5ace299d)) - by @Lasercar in [funkin.assets#136](https://github.com/FunkinCrew/funkin.assets/pull/136)
|
||||
- Improved performance in the Freeplay menu before entering a song. ([2b7254f](https://github.com/FunkinCrew/Funkin/commit/2b7254fbd2356e9da91e216f178a88f17874a6eb)) - by @superpowers04 in [#4729](https://github.com/FunkinCrew/Funkin/pull/4729)
|
||||
- Prevented a crash by capping Freeplay and Results screen score displays at their largest possible values (actual scores are not capped). ([51324e9](https://github.com/FunkinCrew/Funkin/commit/51324e9c283c43ca861d3369ba9b3e1db9c89765)) - by @KoloInDaCrib in [#3634](https://github.com/FunkinCrew/Funkin/pull/3634)
|
||||
- The Animation Editor no longer crashes when opened. ([b40b4b0](https://github.com/FunkinCrew/Funkin/commit/b40b4b03692910afceff361cf6ca3298fd787e3c)) - by @sector-a in [#4582](https://github.com/FunkinCrew/Funkin/pull/4582)
|
||||
- Disabled navigation in the Options menu while the Clear Save Data prompt is open. ([855deb5](https://github.com/FunkinCrew/Funkin/commit/855deb58280e850c0ad1581807c70f05d6b6a2cb)) - by @KoloInDaCrib in [#4703](https://github.com/FunkinCrew/Funkin/pull/4703)
|
||||
- Save data is no longer wiped after failing to load the current save. ([068c9fb](https://github.com/FunkinCrew/Funkin/commit/068c9fb43d49ff6ea13e5a73f77a42207954be2c)) - by @KoloInDaCrib in [#4574](https://github.com/FunkinCrew/Funkin/pull/4574)
|
||||
- Selecting a different type of event in the Chart Editor no longer crashes or resets the event to its default values. ([71ed154](https://github.com/FunkinCrew/Funkin/commit/71ed154b81abbd002d78c09c2dada8a2ad3fa73e)) - by @KoloInDaCrib in [#3913](https://github.com/FunkinCrew/Funkin/pull/3913)
|
||||
- Deleting a stacked event in the Chart Editor now properly removes the deleted event’s sprite. ([3ad14ba](https://github.com/FunkinCrew/Funkin/commit/3ad14bac32096e45259997ab9957bec5fecf296f)) - by @NotHyper-474 in [#4724](https://github.com/FunkinCrew/Funkin/pull/4724)
|
||||
- Patched a security vulnerability. ([53dec05](https://github.com/FunkinCrew/Funkin/commit/53dec057bee0a913d60e0c70d45dbb59a58620b0)) - by @nebulazorua in [#4740](https://github.com/FunkinCrew/Funkin/pull/4740)
|
||||
- Added null safety to a bunch of classes.
|
||||
- Even more tiny bug fixes.
|
||||
|
||||
## New Contributors for 0.6.3
|
||||
|
||||
* @JackXson-Real made their first contribution in [#4346](https://github.com/FunkinCrew/Funkin/pull/4346)
|
||||
* @VioletSnowLeopard made their first contribution in [#4382](https://github.com/FunkinCrew/Funkin/pull/4382)
|
||||
* @superpowers04 made their first contribution in [#4729](https://github.com/FunkinCrew/Funkin/pull/4729)
|
||||
* @ShadzXD made their first contribution in [#62](https://github.com/FunkinCrew/Funkin/pull/4729)
|
||||
|
||||
|
||||
|
||||
## [0.6.2] - 2025-03-31
|
||||
|
||||
### Added
|
||||
|
||||
- Updated the 0.6 credits list
|
||||
|
||||
### Fixed
|
||||
|
||||
- Additional shader fix for Stress (Pico Mix) crashing at the end (was the same issue as Senpai Pico shader error, just in a different shaderfile)
|
||||
|
||||
|
||||
|
||||
## [0.6.1] - 2025-03-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- Hopefully Senpai Pico/Erect mix shader isn't brokey
|
||||
- NG API encryption key was added proper, so medals + leaderboards posting should work
|
||||
|
||||
|
||||
|
||||
## [0.6.0] - 2025-03-31
|
||||
The Pit Stop 2 update!
|
||||
|
||||
### Added
|
||||
|
||||
- Added six (!) new playable songs! Check them out in the Freeplay menu for their respective characters.
|
||||
- Cocoa (Pico Mix)
|
||||
- Senpai (Pico Mix)
|
||||
- Roses (Pico Mix)
|
||||
- Stress (Pico Mix)
|
||||
- Darnell Erect
|
||||
- Lit Up (BF Mix)
|
||||
- Senpai (Pico Mix) and Roses (Pico Mix) take place on a new Erect variant of the Week 6 stage!
|
||||
- This stage is now used by Senpai Erect, Roses Erect, and Thorns Erect.
|
||||
- Stress (Pico Mix) takes place on a new Erect variant of the Week 7 stage!
|
||||
- This stage is now used by Ugh Erect, Ugh (Pico Mix), and Guns (Pico Mix).
|
||||
- Newly revamped Newgrounds integration! You can now earn Medals and submit scores to the Leaderboards while logged in on Newgrounds!
|
||||
- Existing medals have been re-enabled, and new medals have been added! Think you're a Rap God?
|
||||
- This feature is also available on desktop, check the options menu to find the prompt to login.
|
||||
- There is no feature to view unlocked medals in-game yet, but the feature is planned.
|
||||
- Reworked the sticker system to allow different sets of stickers to be used for different characters and songs.
|
||||
- Added new stickers which appear when exiting Pico songs!
|
||||
- The new sticker system isn't fully available to mods yet, but we're working on it!
|
||||
- New option in the Preferences menu: Strumline Backgrounds!
|
||||
- Options in the Preferences menu now display an on-screen description when selected.
|
||||
- New song event type in the Chart Editor: Set Health Icon!
|
||||
- This event is now used in Stress (Pico Mix).
|
||||
- The HOME and END keys now jump to the top and bottom of the Freeplay song list, respectively. ([bb974c2](https://github.com/FunkinCrew/Funkin/commit/bb974c264270d10ff503784063e5d77bb352b3f7)) - by @AbnormalPoof in [#4103](https://github.com/FunkinCrew/Funkin/pull/4103)
|
||||
- Added an option to launch the game in fullscreen. ([ee53ccd](https://github.com/FunkinCrew/Funkin/commit/ee53ccd32721e0790adfe82c60d4aca419db0a7f)) - by @AbnormalPoof in [#3738](https://github.com/FunkinCrew/Funkin/pull/3738)
|
||||
- Added on-screen descriptions for each item in the Preferences menu. ([a17b0e8](https://github.com/FunkinCrew/Funkin/commit/a17b0e8b3cc1d56fcdc0b51eaca9fd57cdb5bce0)) - by @anysad in [#3872](https://github.com/FunkinCrew/Funkin/pull/3872)
|
||||
- Added precise scrolling in the Chart Editor using Ctrl-Mouse Wheel. ([0d8e4a5](https://github.com/FunkinCrew/Funkin/commit/0d8e4a53305d6d069454812766300122f3581e31)) - by @ninjamuffin99 in [#3806](https://github.com/FunkinCrew/Funkin/pull/3806)
|
||||
- Added a “None” option to the character selector in the Chart Editor. ([9c2ef02](https://github.com/FunkinCrew/Funkin/commit/9c2ef0236818883ad1275571dac49eab70ca0ea0)) - by @Lasercar in [#4279](https://github.com/FunkinCrew/Funkin/pull/4279)
|
||||
- Added the ability to flip the character horizontally in the Animation Editor by pressing G. ([de02137](https://github.com/FunkinCrew/Funkin/commit/de02137d7c7d1779e85aeda34743f506a5b9cc27)) - by @AbnormalPoof in [#3028](https://github.com/FunkinCrew/Funkin/pull/3028)
|
||||
- Added offsets support for album titles. ([69d8570](https://github.com/FunkinCrew/Funkin/commit/69d8570a9eb06011ed6dd95fcbef83d90f7f8684)) - by @AbnormalPoof in [#3618](https://github.com/FunkinCrew/Funkin/pull/3618)
|
||||
- Added three new properties to stage data: `angle`, `scroll`, and `alpha`. ([ff56b19](https://github.com/FunkinCrew/Funkin/commit/ff56b1948aef42bbb6bb4ede4f9b2012d49ab044)) - by @AbnormalPoof in [#3720](https://github.com/FunkinCrew/Funkin/pull/3720)
|
||||
- Added script events for losing/gaining focus. ([4b127b6](https://github.com/FunkinCrew/Funkin/commit/4b127b64130f6f753d0574ec66a1672322e4bd13)) - by @AbnormalPoof in [#3721](https://github.com/FunkinCrew/Funkin/pull/3721)
|
||||
- Added 10 new functions to `ReflectUtil`. ([6216655](https://github.com/FunkinCrew/Funkin/commit/62166554e7a176245d1a63bd15122033044c4e40)) - by @AbnormalPoof in [#3622](https://github.com/FunkinCrew/Funkin/pull/3622), [#3809](https://github.com/FunkinCrew/Funkin/pull/3809), and [#4019](https://github.com/FunkinCrew/Funkin/pull/4019)
|
||||
- Added `DEBUG_BUILD` value to `Constants` to indicate whether a build has debug functions enabled. ([ad45b72](https://github.com/FunkinCrew/Funkin/commit/ad45b72b1ae8eb73a12dc51bcb59f66cc55e7bbd)) - by @AbnormalPoof in [#3853](https://github.com/FunkinCrew/Funkin/pull/3853)
|
||||
|
||||
### Changed
|
||||
|
||||
- Switched from hxCodec to hxvlc for video playback. This may break a mod or two.
|
||||
- Check the [Funkin Modding Docs](https://funkincrew.github.io/funkin-modding-docs/09-migration/09-02-0.5.0-to-0.6.0.html) for more info on how to update your mods.
|
||||
- Polymod should now ignore `.git` files when loading mods.
|
||||
- The pause menu can now be opened and closed rapidly.
|
||||
- Adjusted difficulty ratings and scroll speeds for many songs.
|
||||
- Chart Editor event fields now allow for values to be as specific as desired.
|
||||
- For example, the Zoom Camera event can now be set to 0.9857.
|
||||
- Lots of improvements to GitHub issue and pull request organization. - by @Hundrec and @AbnormalPoof
|
||||
- Overhauled the Changelog to improve readability and properly credit contributors. ([4383fcf](https://github.com/FunkinCrew/Funkin/commit/4383fcf32c280a1c0ee7b9c80d255611d497cabc)) - by @Hundrec in [#4296](https://github.com/FunkinCrew/Funkin/pull/4296) and [#4298](https://github.com/FunkinCrew/Funkin/pull/4298)
|
||||
- Made various improvements to the screenshot plugin. ([868932c](https://github.com/FunkinCrew/Funkin/commit/868932cd138fad4be5b541cbea3110e30479057b)) - by @Lasercar in [#4082](https://github.com/FunkinCrew/Funkin/pull/4082)
|
||||
- Accept keybinds (Z and Space by default) can now be used to exit the Results screen. ([edb270d](https://github.com/FunkinCrew/Funkin/commit/edb270d15e41784dccbf75639ac731840e80fe23)) - by @JVNpixels in [#3799](https://github.com/FunkinCrew/Funkin/pull/3799)
|
||||
- Reordered UI keybinds in the controls menu for consistency. ([a01bcc3](https://github.com/FunkinCrew/Funkin/commit/a01bcc3da836ec52851ca9de13ef459daf61269a)) - by @lemz1 in [#3027](https://github.com/FunkinCrew/Funkin/pull/3027)
|
||||
- New save files now have default Freeplay controls for gamepads. ([2b7f62e](https://github.com/FunkinCrew/Funkin/commit/2b7f62edd33de5527e259d9e5643f926d35da734)) - by @MrMadera in [#3934](https://github.com/FunkinCrew/Funkin/pull/3934)
|
||||
- Made scrolling smoother in the Chart Editor. ([20d9016](https://github.com/FunkinCrew/Funkin/commit/20d90169845f1e50f849e39f4c5f818359756c78)) - by @ninjamuffin99 in [#3768](https://github.com/FunkinCrew/Funkin/pull/3768)
|
||||
- Mods with missing dependencies are now skipped instead of preventing all mods from loading. ([1c2fb43](https://github.com/FunkinCrew/Funkin/commit/1c2fb43ae16cf40be5ef94c40b047e8e772b1211)) - by @AbnormalPoof in [#3993](https://github.com/FunkinCrew/Funkin/pull/3993)
|
||||
- Slightly improved flexibility for modding note hit animations. ([3aad825](https://github.com/FunkinCrew/Funkin/commit/3aad825f865c4ed87016983d44121e2c1610d332)) - by @TechnikTil in [#3936](https://github.com/FunkinCrew/Funkin/pull/3936)
|
||||
- Introduced several QoL modding changes. ([785c4be](https://github.com/FunkinCrew/Funkin/commit/785c4be88b52dc1b5899013822fc004ba7d9894d)) - by @Kade-github in [#4009](https://github.com/FunkinCrew/Funkin/pull/4009)
|
||||
- Lots of smaller changes.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Shaders no longer create thin seams within atlas sprites.
|
||||
- Completing a song in Practice Mode no longer plays a new rank animation in the Freeplay menu.
|
||||
- Fixed lots of charting issues across many songs.
|
||||
- The Chart Editor grid now properly adjusts to the new BPM after switching variations.
|
||||
- Fixed a few crashes in the Stage Editor.
|
||||
- Fixed a bug where the song would restart from the beginning instead of moving to the Results screen. ([3667c51](https://github.com/FunkinCrew/Funkin/commit/3667c51c1efe14cfe7c810e2f35991f08f50781a)) - by @KoloInDaCrib in [#4309](https://github.com/FunkinCrew/Funkin/pull/4309) and @Lasercar in [#4330](https://github.com/FunkinCrew/Funkin/pull/4330)
|
||||
- Reduced stuttering when resyncing instrumental and voices tracks. ([22d41d2](https://github.com/FunkinCrew/Funkin/commit/22d41d21b88acb7422a0afcda8414682710bd2ed)) - by @TechnikTil in [#3955](https://github.com/FunkinCrew/Funkin/pull/3955)
|
||||
- Songs with only instrumental tracks no longer stutter. ([dfe02ec](https://github.com/FunkinCrew/Funkin/commit/dfe02ec668b61d6308f459c978d12a7487f9dc28)) - by @KoloInDaCrib in [#3861](https://github.com/FunkinCrew/Funkin/pull/3861)
|
||||
- The debug mouse cursor no longer flickers before the Title Screen loads. ([1c12b84](https://github.com/FunkinCrew/Funkin/commit/1c12b8467eca350eb28138473360d5358fa620e2)) - by @sphis-Sinco in [#3881](https://github.com/FunkinCrew/Funkin/pull/3881)
|
||||
- Unbound keys now display as [N/A] instead of crashing the game. ([099c309](https://github.com/FunkinCrew/Funkin/commit/099c309f9babdc1ea99b7dbed3fdccf1e952fc8e)) - by @NotHyper-474 in [#4355](https://github.com/FunkinCrew/Funkin/pull/4355)
|
||||
- Songs can no longer be spam-selected after selecting an instrumental in Freeplay. ([0e0c4ae](https://github.com/FunkinCrew/Funkin/commit/0e0c4aeb7745cfb9479685ccbb635cf3743cddbb)) - by @AbnormalPoof in [#3866](https://github.com/FunkinCrew/Funkin/pull/3866)
|
||||
- The Random capsule can now switch to Erect/Nightmare difficulties in Freeplay. ([a90b911](https://github.com/FunkinCrew/Funkin/commit/a90b911653a1beaba57d64b1f05b840109fec42b)) - by @KoloInDaCrib in [#3838](https://github.com/FunkinCrew/Funkin/pull/3838)
|
||||
- Fixed a rare bug where a song would not register as beaten. ([a3e2373](https://github.com/FunkinCrew/Funkin/commit/a3e23733db104b1ef00cfcff17db3a5d032a4d67)) - by @AbnormalPoof in [#3820](https://github.com/FunkinCrew/Funkin/pull/3820)
|
||||
- The difficulty graphic on the Results screen no longer cuts off incorrectly. ([b13bf05](https://github.com/FunkinCrew/Funkin/commit/b13bf05d16ff2977309e0c7ba3f049c0134e8902)) - by @AbnormalPoof in [#4161](https://github.com/FunkinCrew/Funkin/pull/4161)
|
||||
- Four-digit long Total Notes values in the Results screen no longer overflow to the right. ([91a594c](https://github.com/FunkinCrew/Funkin/commit/91a594cc858ed086cd2146a1ac5d2379c6fdd27a)) - by @Hundrec in [#4356](https://github.com/FunkinCrew/Funkin/pull/4356)
|
||||
- The Character Select screen no longer plays the unlock animation for some locked characters. ([7058126](https://github.com/FunkinCrew/Funkin/commit/7058126e99adb55e43f5f487b007d3efa9f324d5)) - by @AbnormalPoof in [#3748](https://github.com/FunkinCrew/Funkin/pull/3748)
|
||||
- All time signatures in the Chart Editor now display the correct number of beat/step tick lines. ([e570dfb](https://github.com/FunkinCrew/Funkin/commit/e570dfb8e754f9cb29ac2d8fff6e8513bc68b630)) - by @Keoiki in [#2860](https://github.com/FunkinCrew/Funkin/pull/2860)
|
||||
- The Debug menu now opens with the correct camera position. ([090ddd1](https://github.com/FunkinCrew/Funkin/commit/090ddd1f1c2aa48fdb83127b2235041643c99af5)) - by @ninjamuffin99 in [#3769](https://github.com/FunkinCrew/Funkin/pull/3769)
|
||||
- Finally added an outline to the third GF sticker. [9e62572](https://github.com/FunkinCrew/funkin.assets/commit/9e62572ae27dc676c624a81af5c755490eb2dafe) - @M7theguy in [funkin.assets#33](https://github.com/FunkinCrew/funkin.assets/pull/33)
|
||||
- Removed an unused Freeplay class left over from legacy versions. ([abe4ac8](https://github.com/FunkinCrew/Funkin/commit/abe4ac8485539cbebe527a9a75698950232b68d2)) - by @AbnormalPoof in [#4370](https://github.com/FunkinCrew/Funkin/pull/4370)
|
||||
- Blacklisted an additional class for security. ([3492d41](https://github.com/FunkinCrew/Funkin/commit/3492d412c65c7f3fd61e6fc6c9410d8467122ab0)) - by @AbnormalPoof in [#4074](https://github.com/FunkinCrew/Funkin/pull/4074)
|
||||
- Removed an unused class from Polymod blacklist. ([06c12e3](https://github.com/FunkinCrew/Funkin/commit/06c12e36c6bd6df4e2be32a3bec540172e79e162)) - by @AbnormalPoof in [#3729](https://github.com/FunkinCrew/Funkin/pull/3729)
|
||||
- Many additional small bug fixes.
|
||||
|
||||
## New Contributors for 0.6.0
|
||||
|
||||
* @PatoFlamejanteTV made their first contribution in [#3843](https://github.com/FunkinCrew/Funkin/pull/3843)
|
||||
* @sphis-Sinco made their first contribution in [#3881](https://github.com/FunkinCrew/Funkin/pull/3881)
|
||||
* @MrMadera made their first contribution in [#3934](https://github.com/FunkinCrew/Funkin/pull/3934)
|
||||
* @MidyGamy made their first contribution in [#4068](https://github.com/FunkinCrew/Funkin/pull/4068)
|
||||
* @Lasercar made their first contribution in [#4082](https://github.com/FunkinCrew/Funkin/pull/4082)
|
||||
* @MrScottyPieey made their first contribution in [#4085](https://github.com/FunkinCrew/Funkin/pull/4085)
|
||||
* @M7theguy made their first contribution in [funkin.assets#33](https://github.com/FunkinCrew/funkin.assets/pull/33)
|
||||
|
||||
|
||||
|
||||
## [0.5.3] - 2024-10-18
|
||||
This patch resolves a critical issue that could cause user's save data to become corrupted. It is recommended that users switch to this version immediately and avoid using version 0.5.2.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a critical issue in which the Stage Editor theme value could not be parsed by older versions of the game, resulting in all save data being destroyed.
|
||||
- Added a check that prevents save data from being loaded if it is corrupted rather than overwriting it.
|
||||
- Converted `optionsStageEditor.theme`, `optionsChartEditor.theme`, and `optionsChartEditor.chartEditorLiveInputStyle` in the save data from an Enum to a String to fix save data compatibility issues.
|
||||
- In the future, Enum values should not be used in order to prevent incompatibilities caused by introducing new types to the save data that older versions cannot parse.
|
||||
|
||||
|
||||
|
||||
## [0.5.2] - 2024-10-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added InverseDotsShader that emulates flash selections. ([097dbf5](https://github.com/FunkinCrew/Funkin/commit/097dbf5bb4346d431d8ca9f0ec4bc5b5e6f4523f)) - by @ninjamuffin99
|
||||
- Added a new reworked Stage Editor. ([27a0b44](https://github.com/FunkinCrew/Funkin/pull/3482/commits/27a0b4426f86f04362f97e16e2eff580c9402f34)) - by @KoloInDaCrib in [#3482](https://github.com/FunkinCrew/Funkin/pull/3482)
|
||||
- Added the `color` attribute to stage prop JSON data to allow them to be tinted without code. ([27a0b44](https://github.com/FunkinCrew/Funkin/pull/3482/commits/27a0b4426f86f04362f97e16e2eff580c9402f34)) - by @KoloInDaCrib in [#3482](https://github.com/FunkinCrew/Funkin/pull/3482)
|
||||
- Added the `angle` attribute to stage prop JSON data to allow them to be rotated without code. ([27a0b44](https://github.com/FunkinCrew/Funkin/pull/3482/commits/27a0b4426f86f04362f97e16e2eff580c9402f34)) - by @KoloInDaCrib in [#3482](https://github.com/FunkinCrew/Funkin/pull/3482)
|
||||
- Added the `blend` attribute to the stage prop JSON data to allow blend modes to be applied without code. ([27a0b44](https://github.com/FunkinCrew/Funkin/pull/3482/commits/27a0b4426f86f04362f97e16e2eff580c9402f34)) - by @KoloInDaCrib in [#3482](https://github.com/FunkinCrew/Funkin/pull/3482)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Input offsets no longer cause songs to stutter or skip. ([410cfe9](https://github.com/FunkinCrew/Funkin/commit/410cfe972d6df9de4d4d128375cf8380c4f06d92)) - by @KoloInDaCrib in [#3546](https://github.com/FunkinCrew/Funkin/pull/3546)
|
||||
- Exiting the Input Offset menu no longer crashes the game. ([39b1a42](https://github.com/FunkinCrew/Funkin/commit/39b1a42cfeafe2b7be8b66e2fe529e853d9ae197)) - by @lemz1 in [#3493](https://github.com/FunkinCrew/Funkin/pull/3493)
|
||||
- Pico's songs now display properly in the Freeplay Menu. ([1d2bd61](https://github.com/FunkinCrew/Funkin/commit/1d2bd61119e5f418df7f11d7ef2a0fdedee17d3d)) - by @ninjamuffin99 in [#3506](https://github.com/FunkinCrew/Funkin/pull/3506)
|
||||
- Fixed issues with variation/difficulty loading for Freeplay Menu which caused some songs to disappear. ([c0314c8](https://github.com/FunkinCrew/Funkin/commit/c0314c85ecd5116641aff3de8e9153f7fe48e79c)) - by @ninjamuffin99 in [#3506](https://github.com/FunkinCrew/Funkin/pull/3506)
|
||||
- `Song.getFirstValidVariation()` now properly takes into account multiple variation/difficulty inputs. ([d2e2987](https://github.com/FunkinCrew/Funkin/commit/d2e29879fe2acc6febfe0f335f655b741d630c34)) - by @ninjamuffin99 in [#3506](https://github.com/FunkinCrew/Funkin/pull/3506)
|
||||
- Song previews no longer restart when changing difficulties within the same variation. ([903b3fc](https://github.com/FunkinCrew/Funkin/commit/903b3fc59905a70802618a1cd67407722ea956ed)) - by @KoloInDaCrib in [#3587](https://github.com/FunkinCrew/Funkin/pull/3587)
|
||||
- Main menu music no longer cuts out when switching states. ([711e0a6](https://github.com/FunkinCrew/Funkin/commit/711e0a6b7547eb04113e9318dab900f01ad576a5)) - by @EliteMasterEric in [#3530](https://github.com/FunkinCrew/Funkin/pull/3530)
|
||||
- Centered preloader 'fnf' and 'dsp' text so they don't clip anymore. ([165ad60](https://github.com/FunkinCrew/Funkin/commit/165ad6015539a295e9eefdaef291c312e9566b26)) - by @Burgerballs in [#3567](https://github.com/FunkinCrew/Funkin/pull/3567)
|
||||
- FPS setting in options menu no longer flickers when selected. ([b2647fe](https://github.com/FunkinCrew/Funkin/commit/b2647fe09f5281ce7074b26d47bc1524764168ee)) - by @lemz1 in [#3629](https://github.com/FunkinCrew/Funkin/pull/3629)
|
||||
- Volume sound tray is now anti-aliased/smoothed. ([e66290c](https://github.com/FunkinCrew/Funkin/commit/e66290c55f7141402223644f06ec8a69edeee089)) - by @Kn1ghtNight in [#2853](https://github.com/FunkinCrew/Funkin/pull/2853)
|
||||
- Fixed looping animations for modded StrumlineNote sprites. ([bc546e8](https://github.com/FunkinCrew/Funkin/commit/bc546e86aa77ffc795b3f079de5f590289a9c583)) - by @DaWaterMalone in [#3577](https://github.com/FunkinCrew/Funkin/pull/3577)
|
||||
- Stopped allowing inputs after selecting a character in Character Select. ([dbf66ac](https://github.com/FunkinCrew/Funkin/commit/dbf66ac250137262866d75f7c1387645b35d88d0)) - by @ACrazyTown in [#3398](https://github.com/FunkinCrew/Funkin/pull/3398)
|
||||
- The player and girlfriend no longer disappear or overlap themselves in Character Select. ([9324359](https://github.com/FunkinCrew/Funkin/commit/9324359d2fce6a7097077d169a0efcd80e6fefa1)) - by @gamerbross in [#3457](https://github.com/FunkinCrew/Funkin/pull/3457)
|
||||
- The player no longer enters twice after entering Character Select or when spamming buttons. ([30a9887](https://github.com/FunkinCrew/Funkin/commit/30a98871367b494c85934cd3fcfa91eeb774a7d5)) - by @gamerbross in [#3457](https://github.com/FunkinCrew/Funkin/pull/3457)
|
||||
- The wrong girlfriend no longer appears in Character Select. ([9324359](https://github.com/FunkinCrew/Funkin/commit/9324359d2fce6a7097077d169a0efcd80e6fefa1)) - by @gamerbross in [#3457](https://github.com/FunkinCrew/Funkin/pull/3457)
|
||||
- Cursor now updates properly when moving and selecting in Character Select. ([9324359](https://github.com/FunkinCrew/Funkin/commit/9324359d2fce6a7097077d169a0efcd80e6fefa1)) - by @gamerbross in [#3457](https://github.com/FunkinCrew/Funkin/pull/3457)
|
||||
- Cursor now moves properly at lower framerates in Character Select. ([ab5bda3](https://github.com/FunkinCrew/Funkin/commit/ab5bda3ee573a6e03595ec6941e6de38df851889)) - by @ninjamuffin99 in [#3507](https://github.com/FunkinCrew/Funkin/pull/3507)
|
||||
- Exiting the Chart Editor no longer crashes the game. ([f52472a](https://github.com/FunkinCrew/Funkin/commit/f52472a4767388b22cfbab0f5f7860f6e6762856)) - by @EliteMasterEric and @ianharrigan in [#3519](https://github.com/FunkinCrew/Funkin/pull/3519)
|
||||
- The millisecond counter in the Chart Editor playbar is now properly formatted. ([f1b6e6c](https://github.com/FunkinCrew/Funkin/commit/f1b6e6c4e42455e0c2900d738ebc24893f2479a0)) - by @afreetoplaynoob in [#3537](https://github.com/FunkinCrew/Funkin/pull/3537)
|
||||
- Pressing F1 multiple times no longer creates more than one help window in the Chart Editor. ([777978f](https://github.com/FunkinCrew/Funkin/commit/777978f5a544e1b7c89b47dcc365f734eb6d0df1)) - by @amyspark-ng in [#3552](https://github.com/FunkinCrew/Funkin/pull/3552)
|
||||
- The dialog box now shows up in the Animation Editor. ([1fde59f](https://github.com/FunkinCrew/Funkin/commit/1fde59f999eac94eb10fc22094885de2f5310705)) - by @EliteMasterEric in [#3530](https://github.com/FunkinCrew/Funkin/pull/3530)
|
||||
- (debug) No more fullscreening when typing "F" in the flixel debugger console. ([29b6763](https://github.com/FunkinCrew/Funkin/commit/29b6763290df05d42039806f3d142740568c80f0)) - by @ninjamuffin99
|
||||
- Added additional classes to Polymod blacklist for security. ([b0b73c8](https://github.com/FunkinCrew/Funkin/commit/b0b73c83994f33118c6a69550da9ec8ec1c07adc)) - by @EliteMasterEric in [#3558](https://github.com/FunkinCrew/Funkin/pull/3558)
|
||||
|
||||
## New Contributors for 0.5.2
|
||||
|
||||
* @Kn1ghtNight made their first contribution in [#2853](https://github.com/FunkinCrew/Funkin/pull/2853)
|
||||
* @Cartridge-Man made their first contribution in [#3082](https://github.com/FunkinCrew/Funkin/pull/3082)
|
||||
* @afreetoplaynoob made their first contribution in [#3537](https://github.com/FunkinCrew/Funkin/pull/3537)
|
||||
* @amyspark-ng made their first contribution in [#3552](https://github.com/FunkinCrew/Funkin/pull/3552)
|
||||
* @DaWaterMalone made their first contribution in [#3577](https://github.com/FunkinCrew/Funkin/pull/3577)
|
||||
|
||||
|
||||
|
||||
## [0.5.1] - 2024-09-30
|
||||
|
||||
### Added
|
||||
|
||||
- Readded the Merch button to the main menu.
|
||||
- Click it to check out our Makeship campaign!
|
||||
- Added Discord Rich Presence support. People can now see what song you are playing from Discord!
|
||||
- We'll get mod support working for this eventually.
|
||||
- Added an FPS limit option to the Preferences menu.
|
||||
- You can now change how high the game tries to push your frame rate, from as little as 30 to as high as 300.
|
||||
- Added support for the Tracy instrumentation-based profiling tool in development environments. Enable it with the `-DFEATURE_DEBUG_TRACY` compilation flag.
|
||||
- For the people who aren't nerds, this is a tool for tracking down performance issues!
|
||||
- Playable Character data now defines an asset location for an Animate Atlas to display Girlfriend.
|
||||
- This includes the option to display a visualizer, if configured correctly.
|
||||
- Separated the Perfect and Perfect (Gold) animations in the Playable Character data.
|
||||
- Base game just uses the same animation for both, but modders can split the animations up on their custom characters now.
|
||||
- Added a bunch of Flash project files from the Weekend 1 and Playable Pico updates to the `funkin.art` repository.
|
||||
- Added the `flipX` and `flipY` parameters to props in the Stage data. ([community feature by AbnormalPoof](https://github.com/FunkinCrew/Funkin/pull/3474))
|
||||
|
||||
### Changed
|
||||
|
||||
- Pico is no longer unlocked for all players automatically.
|
||||
- You need to beat Weekend 1 in Story Mode in order to unlock him in Character Select.
|
||||
- The game's mod API version check is now more dynamic.
|
||||
- The update accepts mods with API version `0.5.0` as well as `0.5.1`.
|
||||
- Removed some of the more spammy `trace()` calls to improve debugging a bit.
|
||||
- The game now complains if you create a song variation with symbols in its name.
|
||||
- Switched the force crash keybind from Ctrl-Shift-L to Ctrl-Alt-Shift-L.
|
||||
- Added some additional functions to `funkin.Assets` after `openfl.utils.Assets` had to get blacklisted from scripts.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Pico is no longer locked every time the game starts, so you no longer have to watch the unlock animation each game boot.
|
||||
- The animation should now play only once per save file.
|
||||
- The clear % now displays in Freeplay after switching characters.
|
||||
- Character remixes no longer display the base song's highscore in Freeplay.
|
||||
- Freeplay no longer displays the wrong text on capsules.
|
||||
- Freeplay now displays custom songs when switching characters.
|
||||
- Duplicate difficulties from custom variations now display properly in Freeplay.
|
||||
- DadBattle (Pico Mix) now has charts for Normal and Easy difficulties.
|
||||
- DadBattle (Pico Mix) is now properly credited to `TeraVex (ft. Saruky)`.
|
||||
- Spookeez (Pico Mix) is now properly credited to `Six Impala (ft. Saster)`.
|
||||
- The audio track now unmutes if you miss a note just before Pico burps.
|
||||
- Pico now plays out his full burp animation in South (Pico Mix).
|
||||
- Removed a tap note stacked on top of a hold note in Cocoa Erect (Erect difficulty).
|
||||
- Pico Erect can no longer be played with different instrumentals.
|
||||
- The curtains in Week 1 no longer display in front of larger characters.
|
||||
- Boyfriend now plays his death animation properly on the Week 2 Remix stage.
|
||||
- The game no longer stutters when playing on the Week 5 Remix stage.
|
||||
- The "Shit!" judgement no longer displays with anti-aliasing in Week 6.
|
||||
- Spirit's trail in Week 6 now displays correctly.
|
||||
- Pico now plays his shooting animations in Stress.
|
||||
- Characters with high offsets no longer shift over after the player dies or restarts.
|
||||
- Custom note styles no longer sometimes use default values rather than the fallback note style.
|
||||
- Custom note styles no longer randomly fail to fetch information about their fallback note style.
|
||||
- Screenshots and Chart Editor binds no longer display in the controls menu on Web builds (where they are disabled).
|
||||
- Stage Editor bind no longer displays in the controls menu even when the feature is disabled.
|
||||
- Freeplay Character Select keybind no longer displays strangely in the controls menu.
|
||||
- Audio tracks no longer get destroyed if they are flagged as persistent.
|
||||
- Video cutscenes now scale their volume properly.
|
||||
- Results screen audio no longer continues into Freeplay or gameplay.
|
||||
- The Results screen now plays the percentage tick sound when the value changes instead of spamming the sound.
|
||||
- The save data version number is now written to the save data properly.
|
||||
- The example mod can now be loaded.
|
||||
- Pressing F5 to force reload a song no longer occasionally causes the game to crash.
|
||||
- Animations on Animate Atlas characters no longer throw a bunch of warnings in the console.
|
||||
- Entering Blazin' no longer displays a script error.
|
||||
- The Input Offsets menu no longer crashes when entering it before playing a song on web builds.
|
||||
- Setting the input offset or visual offset to high values no longer causes the song to skip.
|
||||
- Classic FocusCamera song events no longer cause the camera to snap in place. ([community fix by nebulazorua](https://github.com/FunkinCrew/Funkin/pull/2331))
|
||||
- Pixel hold note trails in Week 6 are now scaled/positioned correctly. ([community fix by dombomb64](https://github.com/FunkinCrew/Funkin/pull/3351))
|
||||
- Achieving the same rank on a song with a lower clear % no longer overwrites your clear %. ([community fix by lemz1](https://github.com/FunkinCrew/Funkin/pull/3019))
|
||||
- The FPS counter no longer displays if Debug Display is turned off. ([community fix by Lethrial](https://github.com/FunkinCrew/Funkin/pull/3356))
|
||||
- The Chart Editor can now be interacted with properly. ([community fix by Kade-github](https://github.com/FunkinCrew/Funkin/pull/3337))
|
||||
- Selecting the area to the left of the Chart Editor no longer selects some of the player's notes. ([community fix by NotHyper-474](https://github.com/FunkinCrew/Funkin/pull/3093))
|
||||
- Pixel icons now display correctly in the Chart Editor. ([community fix by TechnikTil](https://github.com/FunkinCrew/Funkin/pull/3339))
|
||||
- Audio offsets now interact with the Chart Editor properly. ([community fix by Kade-github](https://github.com/FunkinCrew/Funkin/pull/3384))
|
||||
- Players can no longer crash the game by interacting with Character Select during the unlock sequence. ([community fix by ActualMandM](https://github.com/FunkinCrew/Funkin/pull/3355))
|
||||
- `Stage.addCharacter` now properly assigns the `characterType`. ([community fix by Kade-github](https://github.com/FunkinCrew/Funkin/pull/3357))
|
||||
- Fetching Modules during the `onDestroy` event no longer fails at random. ([community fix by cyn0x8](https://github.com/FunkinCrew/Funkin/pull/3131))
|
||||
- `onSubStateOpenEnd` and `onSubStateCloseEnd` script events are now called consistently. ([community fix by lemz1](https://github.com/FunkinCrew/Funkin/pull/3138))
|
||||
|
||||
## New Contributors for 0.5.1
|
||||
|
||||
* @dombomb64 made their first contribution in [#3351](https://github.com/FunkinCrew/Funkin/pull/3351)
|
||||
* @Lethrial made their first contribution in [#3356](https://github.com/FunkinCrew/Funkin/pull/3356)
|
||||
* @KoloInDaCrib made their first contribution in [#3371](https://github.com/FunkinCrew/Funkin/pull/3371)
|
||||
|
||||
|
||||
|
||||
## [0.5.0] - 2024-09-12
|
||||
The Playable Pico Update!
|
||||
### Added
|
||||
|
||||
- Added a new Character Select screen to switch between playable characters in Freeplay.
|
||||
- Modding isn't 100% there but we're working on it!
|
||||
- Added Pico as a playable character! Unlock him by completing Weekend 1 (if you haven't already done that).
|
||||
- The songs from Weekend 1 have moved; you must now switch to Pico via Character Select screen in Freeplay to access them.
|
||||
- Added 11 new Pico remixes! Access them by selecting Pico in the Character Select screen.
|
||||
- Bopeebo (Pico Mix)
|
||||
- Fresh (Pico Mix)
|
||||
- DadBattle (Pico Mix)
|
||||
- Spookeez (Pico Mix)
|
||||
- South (Pico Mix)
|
||||
- Pico (Pico Mix)
|
||||
- Philly Nice (Pico Mix)
|
||||
- Blammed (Pico Mix)
|
||||
- Eggnog (Pico Mix)
|
||||
- Ugh (Pico Mix)
|
||||
- Guns (Pico Mix)
|
||||
- Added 1 new Boyfriend remix! Access it by completing Weekend 1 as Pico and then selecting Boyfriend in the Character Select screen.
|
||||
- Darnell (BF Mix)
|
||||
- Added 2 new Erect remixes! Access them by switching difficulties on the songs.
|
||||
- Cocoa Erect
|
||||
- Ugh Erect
|
||||
- Implemented support for a new Instrumental Selector in Freeplay.
|
||||
- Beating a Pico remix lets you use that instrumental when playing as Boyfriend.
|
||||
- Added the first batch of Erect Stages! These graphical overhauls of the original stages will be used when playing Erect remixes and Pico remixes:
|
||||
- Week 1 Erect Stage
|
||||
- Week 2 Erect Stage
|
||||
- Week 3 Erect Stage
|
||||
- Week 4 Erect Stage
|
||||
- Week 5 Erect Stage
|
||||
- Weekend 1 Erect Stage
|
||||
- Implemented alternate animations and music for Pico in the Results screen.
|
||||
- These display on Pico remixes, as well as when playing Weekend 1.
|
||||
- Implemented support for scripted Note Kinds. You can use HScript define a different note style to display for these notes as well as custom behavior. (community feature by lemz1)
|
||||
- Implemented support for Numeric and Selector options in the Options menu. ([community feature by FlooferLand](https://github.com/FunkinCrew/Funkin/pull/2942))
|
||||
- Implemented new animations for Tankman and Pico.
|
||||
|
||||
## Changed
|
||||
|
||||
- Girlfriend and Nene now perform previously unused animations when you achieve a large combo, or drop a large combo.
|
||||
- The pixel character icons in the Freeplay menu now display an animation!
|
||||
- Altered how Week 6 displays sprites to make things look more retro.
|
||||
- Character offsets are now independent of the character's scale.
|
||||
- This should resolve issues with offsets when porting characters from older mods.
|
||||
- Pixel character offsets have been modified to compensate.
|
||||
- Reworked the JSON merging system in Polymod; you can now include JSONPatch files under `_merge` in your mod folder to add, modify, or remove values in a JSON without replacing it entirely!
|
||||
- The `danceEvery` property of characters and stage props can now use values with a precision of `0.25`, to play their idle animation up to four times per beat.
|
||||
- Characters now respect the `danceEvery` property. ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2925))
|
||||
- Note style data can now specify custom combo count graphics, judgement graphics, countdown graphics, and countdown audio. ([community feature by anysad](https://github.com/FunkinCrew/Funkin/pull/3020))
|
||||
- These were previously using hardcoded values based on whether the stage was `school` or `schoolEvil`.
|
||||
- The YEAH! events in Tutorial now use chart events rather than being hard-coded. ([community fix by anysad](https://github.com/FunkinCrew/Funkin/pull/3007))
|
||||
- Cutscenes now automatically pause when tabbing out. ([community fix by AbnormalPoof](https://github.com/FunkinCrew/Funkin/pull/2903))
|
||||
- The F5 function now reloads the current song's chart data from disk. ([community feature by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2990))
|
||||
- Made several layout improvements and fixes to the Animation Offsets editor in the Debug menu. ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2820))
|
||||
- Animation offsets no longer directly modify the `x` and `y` position of props, making props work better with tweens. ([community fix by Sword352](https://github.com/FunkinCrew/Funkin/pull/2310))
|
||||
- Fixed a bug where the Back sound would be not played when leaving the Story menu and Options menu. ([community fix by AppleHair](https://github.com/FunkinCrew/Funkin/pull/2986))
|
||||
- The player's Score now displays commas in it. (community fix by loggo)
|
||||
|
||||
## Fixed
|
||||
|
||||
- Songs with no notes no longer crash on the Results screen.
|
||||
- Note inputs are no longer accepted during the Thorns cutscene.
|
||||
- The old icon easter egg now works properly on pixel levels.
|
||||
- The Heart icon is no longer malformed when favoriting a song in Freeplay.
|
||||
- Freeplay songs with no `Normal` difficulty no longer crash the game. ([community fix by AppleHair](https://github.com/FunkinCrew/Funkin/pull/3036) and [gamerbross](https://github.com/FunkinCrew/Funkin/pull/2712))
|
||||
- Songs that aren't valid for the current variation can no longer be selected. ([community fix by AppleHair](https://github.com/FunkinCrew/Funkin/pull/3037))
|
||||
- The Freeplay song preview now respects the instrumental ID specified in the song metadata. ([community fix by AppleHair](https://github.com/FunkinCrew/Funkin/pull/2742))
|
||||
- Modded songs now play previews in the Freeplay menu. ([community fix by KarimAkra](https://github.com/FunkinCrew/Funkin/pull/2724))
|
||||
- The Story Mode menu can now be scrolled using the mouse wheel. ([community fix by JVNpixels](https://github.com/FunkinCrew/Funkin/pull/2873))
|
||||
- Pressing F5 after seeing the sticker transition no longer crashes the game. ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2863))
|
||||
- Pausing the game during a camera zoom no longer zooms the pause menu as well. ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2567))
|
||||
- The song no longer majorly desyncs at times. ([community fix by Burgerballs](https://github.com/FunkinCrew/Funkin/pull/3058))
|
||||
- Pico's death animation no longer displays a faint blue background. ([community fix by doggogit](https://github.com/FunkinCrew/funkin.assets/pull/1))
|
||||
- The game now uses the placeholder health icon as a fallback. ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/3005))
|
||||
- Pressing the Chart Editor keybind while playtesting a chart no longer resets the Chart Editor. ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2739))
|
||||
- The Chart Editor no longer gets stuck creating a hold note when using Live Inputs. ([community fix by gamerbross](https://github.com/FunkinCrew/Funkin/pull/2992))
|
||||
- Tankman's icon now displays in the Chart Editor. ([community fix by Hundrec](https://github.com/FunkinCrew/Funkin/pull/2912))
|
||||
- The Memory Usage counter no longer overflows and displays a negative number. ([community fix by KarimAkra](https://github.com/FunkinCrew/Funkin/pull/2713))
|
||||
- Certain UI elements now flash at a consistent rate. ([community fix by cyn0x8](https://github.com/FunkinCrew/Funkin/pull/2494))
|
||||
- Character graphics can now be placed in week folders. ([community fix by 7oltan](https://github.com/FunkinCrew/Funkin/pull/3035))
|
||||
|
||||
## New Contributors for 0.5.0
|
||||
|
||||
* @Sword352 made their first contribution in [#2310](https://github.com/FunkinCrew/Funkin/pull/2310)
|
||||
* @cyn0x8 made their first contribution in [#2494](https://github.com/FunkinCrew/Funkin/pull/2494)
|
||||
* @KarimAkra made their first contribution in [#2713](https://github.com/FunkinCrew/Funkin/pull/2713)
|
||||
* @tposejank made their first contribution in [#2717](https://github.com/FunkinCrew/Funkin/pull/2717)
|
||||
* @AppleHair made their first contribution in [#2742](https://github.com/FunkinCrew/Funkin/pull/2742)
|
||||
* @JVNpixels made their first contribution in [#2873](https://github.com/FunkinCrew/Funkin/pull/2873)
|
||||
* @Flooferland made their first contribution in [#2942](https://github.com/FunkinCrew/Funkin/pull/2942)
|
||||
* @Punkinator7 made their first contribution in [#2962](https://github.com/FunkinCrew/Funkin/pull/2962)
|
||||
* @anysad made their first contribution in [#3007](https://github.com/FunkinCrew/Funkin/pull/3007)
|
||||
* @7oltan made their first contribution in [#3035](https://github.com/FunkinCrew/Funkin/pull/3035)
|
||||
|
||||
|
||||
|
||||
## [0.4.1] - 2024-06-12
|
||||
|
||||
### Added
|
||||
|
||||
- Pressing ESCAPE on the title screen on desktop now exits the game, allowing you to exit the game while in fullscreen on desktop.
|
||||
- Freeplay menu controls (favoriting and switching categories) are now rebindable from the Options menu, and now have default binds on controllers.
|
||||
|
||||
### Changed
|
||||
|
||||
- Highscores and ranks are now saved separately, fixing an issue where lower ranks would overwrite higher ranks if the player achieved a new highscore.
|
||||
- A-Bot speaker now reacts to the user's volume preference on desktop. ([thanks to M7theguy for the issue report/suggestion](https://github.com/FunkinCrew/Funkin/issues/2744)!)
|
||||
- Freeplay heart icons are now shifted to the right when favoriting a song with no rank.
|
||||
- The `scrollMenu` sound effect now only plays when there's a real change on the Freeplay menu. ([thanks gamerbross for the PR!](https://github.com/FunkinCrew/Funkin/pull/2741))
|
||||
- Applied anti-aliasing to the edge of the Freeplay Dad graphic.
|
||||
- Rearranged some controls in the controls menu.
|
||||
- Made several chart revisions:
|
||||
- Re-enabled custom camera events in Roses (Erect/Nightmare)
|
||||
- Tweaked chart for Lit Up (Hard)
|
||||
- Corrected difficulty ratings for M.I.L.F (Easy/Normal/Hard)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Control binds in the controls menu no longer overlap their names.
|
||||
- Attempting to exit the gameover screen and retry the song at the same time no longer crashes the game. ([thanks DMMaster636 for the PR!](https://github.com/FunkinCrew/Funkin/pull/2709))
|
||||
- Botplay mode now handles the player's animations properly during hold notes. ([thanks Hundrec!](https://github.com/FunkinCrew/Funkin/pull/2683))
|
||||
- Camera movement now pauses when the game is paused. ([thanks Matriculaso!](https://github.com/FunkinCrew/Funkin/pull/2684))
|
||||
- Pico's gameplay sprite no longer appears on the gameover screen when dying from an explosion in 2hot.
|
||||
- Freeplay previews now properly fade in volume during the DJ's idle animation.
|
||||
- DadBattle no longer incorrectly appears as DadBattle Erect when returning to Freeplay on Hard.
|
||||
- 2hot now appears under the "#" category in Freeplay menu.
|
||||
- The Chart Editor no longer crashes when selecting an event with the Event toolbox open.
|
||||
- Improved offsets for Pico and Tankman opponents so they don't slide around as much.
|
||||
- The black "temp" graphic in Freeplay is now correctly sized/masked, now it's identical to the Dad Freeplay graphic.
|
||||
|
||||
## New Contributors for 0.4.1
|
||||
|
||||
* @Hundrec made their first contribution in [#2661](https://github.com/FunkinCrew/Funkin/pull/2661)
|
||||
* @DMMaster636 made their first contribution in [#2709](https://github.com/FunkinCrew/Funkin/pull/2709)
|
||||
* @eltociear made their first contribution in [#2730](https://github.com/FunkinCrew/Funkin/pull/2730)
|
||||
|
||||
|
||||
|
||||
## [0.4.0] - 2024-06-06
|
||||
The Pit Stop 1 update!
|
||||
### Added
|
||||
|
||||
- 2 new Erect remixes, Eggnog and Satin Panties. Check them out from the Freeplay menu!
|
||||
- Major visual improvements to the Results screen, with additional animations and audio based on your performance.
|
||||
- Major visual improvements to the Freeplay screen, with song difficulty ratings and player rank displays.
|
||||
- Freeplay now plays a preview of songs when you hover over them.
|
||||
- Added a Charter field to the chart format, to allow for crediting the creator of a level's chart.
|
||||
- You can see who charted a song from the Pause menu.
|
||||
- Added a new Scroll Speed chart event to change the note speed mid-song. ([thanks Burgerballs!](https://github.com/FunkinCrew/Funkin/pull/2409))
|
||||
|
||||
### Changed
|
||||
|
||||
- Tweaked charts for several songs:
|
||||
- Tutorial (increased the note speed slightly)
|
||||
- Spookeez
|
||||
- Monster
|
||||
- Winter Horrorland
|
||||
- M.I.L.F
|
||||
- Senpai (increased the note speed)
|
||||
- Roses
|
||||
- Thorns (increased the note speed slightly)
|
||||
- Ugh
|
||||
- Stress
|
||||
- Lit Up
|
||||
- Favorite songs marked in Freeplay are now stored between sessions.
|
||||
- The Freeplay easter eggs are now easier to see.
|
||||
- In the event that the game cannot load your save data, it will now perform a backup before clearing it, so that we can try to repair it in the future.
|
||||
- Custom note styles are now properly supported for songs; add new notestyles via JSON, then select it for use from the Chart Editor Metadata toolbox. ([thanks Keoiki!](https://github.com/FunkinCrew/Funkin/pull/2581))
|
||||
- Health icons now support a Winning frame without requiring a spritesheet, simply include a third frame in the icon file. ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2593))
|
||||
- Remember that for more complex behaviors such as animations or transitions, you should use an XML file to define each frame.
|
||||
- Improved the Event Toolbox in the Chart Editor; dropdowns are now bigger, include search field, and display elements in alphabetical order rather than a random order.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Nene's visualizer now plays on Desktop builds.
|
||||
- The game no longer silently fails to load saves on HTML5.
|
||||
- Props on the Story Menu now bop properly.
|
||||
- Additional fixes for the loading bar on HTML5. ([thanks lemz1!](https://github.com/FunkinCrew/Funkin/pull/2553))
|
||||
- Fixed several bugs with the TitleState, including missing music when returning from the Main Menu. ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2539))
|
||||
- The Main Menu camera now properly follows the selected item. ([thanks richTrash21!](https://github.com/FunkinCrew/Funkin/pull/2576))
|
||||
- Changing difficulties in Story Mode now updates the score text. ([thanks sector-a!](https://github.com/FunkinCrew/Funkin/pull/2585))
|
||||
- Fixed a crash in Freeplay caused by a level referencing an invalid song. ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2457))
|
||||
- Pressing the volume keys no longer stops the Toy commercial. ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2540))
|
||||
- Playtesting in the Chart Editor no longer crashes when losing. ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2518))
|
||||
- Hold notes now display properly in the Chart Editor when downscroll is enabled for gameplay. ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2565))
|
||||
- Hold notes are now positioned correctly with downscroll enabled. ([thanks MaybeMaru!](https://github.com/FunkinCrew/Funkin/pull/2488))
|
||||
- Removed a large number of unused imports to optimize builds. ([thanks Ethan-makes-music!](https://github.com/FunkinCrew/Funkin/pull/2624))
|
||||
- Improved debug logging for unscripted stages. ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2603))
|
||||
- Fixed a crash on Linux caused by an old version of hxCodec. ([thanks Noobz4Life!](https://github.com/FunkinCrew/Funkin/pull/2472))
|
||||
- Optimized animation handling for characters. ([thanks richTrash21!](https://github.com/FunkinCrew/Funkin/pull/2493))
|
||||
- The Chart Editor no longer uses an incorrect instrumental on imported Legacy songs. ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2604))
|
||||
- Opening the game from the command line no longer crashes the preloader. ([thanks NotHyper-474!](https://github.com/FunkinCrew/Funkin/pull/2629))
|
||||
- Characters no longer use the wrong scale value sometimes. ([thanks PurSnake!](https://github.com/FunkinCrew/Funkin/pull/2610))
|
||||
- Additional bug fixes and optimizations.
|
||||
|
||||
## New Contributors for 0.4.0
|
||||
|
||||
* @doggogit made their first contribution in [#2325](https://github.com/FunkinCrew/Funkin/pull/2325)
|
||||
* @Noobz4Life made their first contribution in [#2472](https://github.com/FunkinCrew/Funkin/pull/2472)
|
||||
* @MaybeMaru made their first contribution in [#2488](https://github.com/FunkinCrew/Funkin/pull/2488)
|
||||
* @NotHyper-474 made their first contribution in [#2490](https://github.com/FunkinCrew/Funkin/pull/2490)
|
||||
* @richTrash21 made their first contribution in [#2493](https://github.com/FunkinCrew/Funkin/pull/2493)
|
||||
* @TechnikTil made their first contribution in [#2508](https://github.com/FunkinCrew/Funkin/pull/2508)
|
||||
* @SanicBTW made their first contribution in [#2544](https://github.com/FunkinCrew/Funkin/pull/2544)
|
||||
* @EnterTheVoid-x86 made their first contribution in [#2573](https://github.com/FunkinCrew/Funkin/pull/2573)
|
||||
* @Keoiki made their first contribution in [#2581](https://github.com/FunkinCrew/Funkin/pull/2581)
|
||||
* @sector-a made their first contribution in [#2585](https://github.com/FunkinCrew/Funkin/pull/2585)
|
||||
* @PurSnake made their first contribution in [#2610](https://github.com/FunkinCrew/Funkin/pull/2610)
|
||||
* @Ethan-makes-music made their first contribution in [#2624](https://github.com/FunkinCrew/Funkin/pull/2624)
|
||||
* @An-enderman made their first contribution in [#2662](https://github.com/FunkinCrew/Funkin/pull/2662)
|
||||
* @moondroidcoder made their first contribution in [#2701](https://github.com/FunkinCrew/Funkin/pull/2701)
|
||||
|
||||
|
||||
|
||||
## [0.3.3] - 2024-05-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Cleaned up some code in `PlayAnimationSongEvent.hx`. ([thanks Burgerballs!](https://github.com/FunkinCrew/Funkin/pull/2308))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixes for the Loading bar on HTML5. ([thanks lemz1!](https://github.com/FunkinCrew/Funkin/pull/2499))
|
||||
- Don't allow inputs when exiting Freeplay. ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2470))
|
||||
- Fixed mouse wheel scrolling in Freeplay. ([thanks JugieNoob!](https://github.com/FunkinCrew/Funkin/pull/2466))
|
||||
- Health icons, score, and notes now reset properly when re-entering gameplay from gameover. ([thanks ImCodist!](https://github.com/FunkinCrew/Funkin/pull/2390))
|
||||
- Fixed the character selector's hitbox width in the Chart Editor. ([thanks MadBear422!](https://github.com/FunkinCrew/Funkin/pull/2370))
|
||||
- Fixed camera stutter once a wipe transition to the Main Menu completes. ([thanks ImCodist!](https://github.com/FunkinCrew/Funkin/pull/2315))
|
||||
- Hold notes no longer become invisible for a single frame. ([thanks ImCodist!](https://github.com/FunkinCrew/Funkin/pull/2309))
|
||||
- Tweens no longer accumulate on the Title screen when pressing Y multiple times. ([thanks TheGaloXx!](https://github.com/FunkinCrew/Funkin/pull/2300))
|
||||
- Fixed a crash when querying FlxG.state in the crash handler.
|
||||
- Fixed a game over easter egg so you don't accidentally exit it when viewing.
|
||||
- The Freeplay menu can now display 100% clear.
|
||||
- Weekend 1 Pico no longer attempts to retrieve a missing asset.
|
||||
- Fixed an issue where duplicate keybinds would be stored, potentially causing a crash.
|
||||
- Chart debug key now properly returns you to the previous chart editor session if you were playtesting a chart. ([thanks nebulazorua!](https://github.com/FunkinCrew/Funkin/pull/2323))
|
||||
- Fixed a crash on Freeplay found on AMD graphics cards.
|
||||
|
||||
## New Contributors for 0.3.3
|
||||
|
||||
* @Chubercik made their first contribution in [#2297](https://github.com/FunkinCrew/Funkin/pull/2297)
|
||||
* @TheGaloXx made their first contribution in [#2300](https://github.com/FunkinCrew/Funkin/pull/2300)
|
||||
* @Burgerballs made their first contribution in [#2308](https://github.com/FunkinCrew/Funkin/pull/2308)
|
||||
* @ImCodist made their first contribution in [#2309](https://github.com/FunkinCrew/Funkin/pull/2309)
|
||||
* @nebulazorua made their first contribution in [#2323](https://github.com/FunkinCrew/Funkin/pull/2323)
|
||||
* @MadBear422 made their first contribution in [#2370](https://github.com/FunkinCrew/Funkin/pull/2370)
|
||||
* @JugieNoob made their first contribution in [#2466](https://github.com/FunkinCrew/Funkin/pull/2466)
|
||||
* @gamerbross made their first contribution in [#2470](https://github.com/FunkinCrew/Funkin/pull/2470)
|
||||
* @lemz1 made their first contribution in [#2499](https://github.com/FunkinCrew/Funkin/pull/2499)
|
||||
|
||||
|
||||
|
||||
## [0.3.2] - 2024-05-03
|
||||
|
||||
### Added
|
||||
|
||||
- Added `,` and `.` keybinds to the Chart Editor. These place Focus Camera events at the playhead, for the opponent and player respectively.
|
||||
- Implemented a blacklist to prevent mods from calling system functions.
|
||||
- Added a couple utility functions to call useful stuff that got blacklisted.
|
||||
- Added an `onSongLoad` script event which allows for mutation of notes and events.
|
||||
- Added the currently loaded modlist to crash logs.
|
||||
- Added the `visible` attribute to Level JSON data.
|
||||
- Enabled ZIP file system support for Polymod (make sure the metadata is in the root of the ZIP).
|
||||
|
||||
### Changed
|
||||
|
||||
- Songs in the mod folders will display in Freeplay without any extra scripting.
|
||||
- Story levels in the mod folders will display in Story without any extra scripting.
|
||||
- All audio should sound better in HTML5, less muddy.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a typo in the credits folder (`Custcene` -> `Cutscene`)
|
||||
- Health icon transition animations now finish properly instead of looping forever.
|
||||
- Video cutscenes flagged as mid-song no longer crash the game when they finish.
|
||||
- Substate lifecycle events are now dispatched consistently.
|
||||
- Trying to load into the Animation Offsets menu with an invalid character no longer crashes the game.
|
||||
- The preloader no longer spams the logs when it is complete and waiting for user input.
|
||||
- Should definitely have the fix for Freeplay where it stops taking control of the main menu below it.
|
||||
- Changed the code for the Story Mode menu difficulties so that "normal" doesn't overlap the arrows after leaving Weekend 1.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed some unused `.txt` files in the `assets/data` folder.
|
||||
|
||||
|
||||
|
||||
## [0.3.1] - 2024-05-01
|
||||
|
||||
### Changed
|
||||
|
||||
- Ensure the Git commit hash always displays in the log files.
|
||||
- Added whether the local Git repo was modified to the log files.
|
||||
- Removed "PROTOTYPE" text on release builds only (it still shows on debug builds).
|
||||
- Added additional credits and special thanks.
|
||||
- Updated peepo in creds to peepo173.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a crash when retrieving system specs while handing a crash.
|
||||
- Fixed a crash triggered when pausing before the song starts.
|
||||
- Fixed a crash triggered when dying before the song starts.
|
||||
- Fixed a crash triggered when unloading certain graphics.
|
||||
- Pico game over confirm now plays correctly.
|
||||
- When exiting from a song into Freeplay, main menu no longer takes inputs unintentionally (aka issues with merch links opening up when selecting songs).
|
||||
- Arrow keys no longer cause the web browser page to scroll.
|
||||
|
||||
|
||||
|
||||
## [0.3.0] - 2024-04-30
|
||||
The Weekend 1 update!
|
||||
### Added
|
||||
|
||||
- New Story Level: Weekend 1, starring Pico, Darnell, and Nene.
|
||||
- Beat the level in Story Mode to unlock the songs for Freeplay!
|
||||
- 12 new Erect remixes, featuring Kawai Sprite, Saruky, Kohta Takahashi, and Saster.
|
||||
- Unlocked instantly in Freeplay
|
||||
- New visually enhanced Freeplay menu.
|
||||
- Sorting, favorites, and more
|
||||
- New Results screen upon completing any song or story level.
|
||||
- New refactored Chart Editor prototype (accessible via `~` in the main menu or `7` in the Play State, rebindable). (VERY EARLY PROTOTYPE. EXPECT BUGS AND CRASHES)
|
||||
- Implemented a new scripting system using HScript (an interpreted language with Haxe-like syntax) for incredible flexibility.
|
||||
- All character-specific, stage-specific, or song-specific behaviors have been moved to HScript.
|
||||
- New song events system allows for simple customization of camera behavior.
|
||||
- Mods can implement custom song events via HScript, and new built-in song events will come in the future.
|
||||
- New credits menu to list all the dozens of people who contributed.
|
||||
|
||||
### Changed
|
||||
|
||||
- Completely refactored the game's input system for higher reliability and accuracy.
|
||||
- Reworked note rendering to massively reduce lag on larger charts.
|
||||
- Reworks to scoring and health gain.
|
||||
- Dedicated gamepad support with the ability to rebind buttons.
|
||||
- Improvements to video cutscenes and dialogue, allowing them to be easily skipped or restarted.
|
||||
- Updated Polymod by several major versions, allowing for fully dynamic asset replacement and support for scripted classes.
|
||||
- Completely refactored almost every part of the game's code for performance, stability, and extensibility.
|
||||
- This is not the Ludum Dare game held together with sticks and glue you played three years ago.
|
||||
- Characters, stages, songs, story levels, and dialogue are now built from JSON data registries rather than being hardcoded.
|
||||
- All of these also support attaching scripts for custom behavior, more documentation on this soon.
|
||||
- You can forcibly reload the game's JSON data and scripts by pressing F5.
|
||||
- Fully refactored the game's chart file format for extensibility and readability.
|
||||
- You can migrate old charts using the Import FNF Legacy option in the chart editor.
|
||||
- Various visual tweaks and improvements.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 17 quadrillion bugs across hundreds of PRs.
|
||||
|
||||
|
||||
|
||||
## [0.2.8] - 2021-04-18 (note, this one is iffy cuz we slacked wit it lol!)
|
||||
|
||||
### Added
|
||||
|
||||
- TANKMAN! 3 NEW SONGS BY KAWAISPRITE (UGH, GUNS, STRESS)! Charting help by MtH!
|
||||
- Monster added into week 2, FINALLY (Charting help by MtH and ChaoticGamer!)
|
||||
- Can now change song difficulty mid-game.
|
||||
- Shows some song info on pause screen.
|
||||
- Cute little icons onto freeplay menu
|
||||
- Offset files for easier modification of characters
|
||||
|
||||
### Changed
|
||||
|
||||
- ASSET LOADING OVERHAUL, WAY FASTER LOAD TIMES ON WEB!!! (THANKS TO GEOKURELI WOKE KING)
|
||||
- Made difficulty selector on freeplay menu more apparent
|
||||
|
||||
### Fixed
|
||||
|
||||
- That one random note on Bopeebo
|
||||
|
||||
|
||||
|
||||
## [0.2.7.1] - 2021-02-14
|
||||
|
||||
### Added
|
||||
|
||||
- Easter eggs
|
||||
- readme's in desktop versions of the game
|
||||
|
||||
### Changed
|
||||
|
||||
- New icons, old one was placeholder since October woops!
|
||||
- Made the transitions between the story mode levels more seamless.
|
||||
- Offset of the Newgrounds logo on boot screen.
|
||||
- Made the changelog txt so it can be opened easier by normal people who don't have a markdown reader (most normal people);
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed crashes on Week 6 story mode dialogue if spam too fast ([Thanks to Lotusotho for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/357))
|
||||
- Should show intro credits on desktop versions of the game more consistently
|
||||
- Layering on Week 4 songs with GF and the LIMO LOL HOW TF I MISS THIS
|
||||
- Chart's and chart editor now support changeBPM, GOD BLESS MTH FOR THIS ONE I BEEN STRUGGLIN WIT THAT SINCE OCTOBER LMAO ([GOD BLESS MTH](https://github.com/FunkinCrew/Funkin/pull/382))
|
||||
- Fixed sustain note trails ALSO THANKS TO MTH U A REAL ONE ([MTH VERY POWERFUL](https://github.com/FunkinCrew/Funkin/pull/415))
|
||||
- Antialiasing on the skyscraper lights
|
||||
|
||||
|
||||
|
||||
## [0.2.7] - 2021-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- PIXEL DAY UPDATE LOL 1 WEEK LATER
|
||||
- 3 New songs by Kawaisprite!
|
||||
- COOL CUTSCENES
|
||||
- WEEK 6 YOYOYOYOY
|
||||
- Swaggy pixel art by Moawling!
|
||||
|
||||
### Changed
|
||||
|
||||
- Made it so you lose sliiiightly more health when you miss a note.
|
||||
- Removed the default HaxeFlixel pause screen when the game window loses focus, can get screenshots of the game easier hehehe
|
||||
|
||||
### Fixed
|
||||
|
||||
- Idle animation bug with BF christmas and BF hair blow sprites ([Thanks to Injourn for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/237))
|
||||
|
||||
|
||||
|
||||
## [0.2.6] - 2021-01-20
|
||||
|
||||
### Added
|
||||
|
||||
- 3 NEW CHRISTMAS SONGS. 2 BY KAWAISPRITE, 1 BY BASSETFILMS!!!!! BF WITH DRIP! SANTA HANGIN OUT!
|
||||
- Enemy icons change when they you are winning a lot ([Thanks to pahaze for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/138))
|
||||
- Holding CTRL in charting editor places notes on both sides
|
||||
- Q and E changes sustain lengths in note editor
|
||||
- Other charting editor workflow improvements
|
||||
- More hair physics
|
||||
- Heads appear at top of chart editor to help show which side ur charting for
|
||||
|
||||
### Changed
|
||||
|
||||
- Tweaked code relating to inputs, hopefully making notes that are close together more fair to hit
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed APE
|
||||
|
||||
### Fixed
|
||||
|
||||
- Maybe fixed double notes / jump notes. Need to tweak it for balance, but should open things up for cooler charts in the future.
|
||||
- Old Verison popup screen weirdness ([Thanks to gedehari for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/155))
|
||||
- Song no longer loops when finishing the song. ([Thanks Injourn for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/132))
|
||||
- Screen wipe being cut off in the limo/mom stage. Should fill the whole screen now.
|
||||
- Boyfriend animations on hold notes, and pressing on repeating notes should behave differently
|
||||
|
||||
|
||||
|
||||
## [0.2.5] - 2020-12-27
|
||||
|
||||
### Added
|
||||
|
||||
- MOMMY GF, 3 NEW ASS SONGS BY KAWAISPRITE, NEW ART BY PHANTOMARCADE,WOOOOOOAH!!!!
|
||||
- Different icons depending on which character you are against, art by EVILSK8R!!
|
||||
- Autosave to chart editor
|
||||
- Clear section button to note editor
|
||||
- Swap button in note editor
|
||||
- a new boot text or two
|
||||
- automatic check for when you're on an old version of the game!
|
||||
|
||||
### Changed
|
||||
|
||||
- Made Spookeez on Normal easier.
|
||||
- Mouse is now visible in note editor
|
||||
|
||||
### Fixed
|
||||
|
||||
- Crash when playing Week 3 and then playing a non-week 3 song
|
||||
- When pausing music at the start, it doesn't continue the song anyways. ([shoutouts gedehari for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/48))
|
||||
- IDK i think backing out of song menu should play main menu songs again hehe ([shoutouts gedehari for the Pull Request!](https://github.com/FunkinCrew/Funkin/pull/48))
|
||||
|
||||
|
||||
|
||||
## [0.2.4] - 2020-12-11
|
||||
|
||||
### Added
|
||||
|
||||
- 3 NEW SONGS BY KAWAISPRITE. Pico, Philly, and Blammed.
|
||||
- NEW CHARACTER, PICO. Based off the classic Flash game "Pico's School" by Tom Fulp
|
||||
- NEW LEVEL WOW! PHILLY BABEEEE
|
||||
|
||||
### Changed
|
||||
|
||||
- Made it less punishing to ATTEMPT to hit a note and miss, rather than let it pass you
|
||||
|
||||
### Fixed
|
||||
|
||||
- Song desync of you paused and unpaused frequently ([shoutouts SonicBlam](https://github.com/FunkinCrew/Funkin/issues/37))
|
||||
- Animation offsets when GF is scared
|
||||
|
||||
|
||||
|
||||
## [0.2.3] - 2020-12-04
|
||||
|
||||
### Added
|
||||
|
||||
- More intro texts
|
||||
|
||||
### Fixed
|
||||
|
||||
- Exploit where you could potentially give yourself a high score via the debug menu
|
||||
- Issue/bug where you could spam the confirm button on the story menu ([shoutouts lotusotho for the CODE contribution/pull request!](https://github.com/FunkinCrew/Funkin/pull/19))
|
||||
- Glitch where if you never would lose health if you missed a note on a fast song (shoutouts [MrDulfin](https://github.com/FunkinCrew/Funkin/issues/10), [HotSauceBurritos](https://github.com/FunkinCrew/Funkin/issues/13) and [LobsterMango](https://lobstermango.newgrounds.com))
|
||||
- Fixed tiny note bleed over thingies (shoutouts [lotusotho](https://github.com/FunkinCrew/Funkin/pull/24))
|
||||
|
||||
|
||||
|
||||
## [0.2.2] - 2020-11-20
|
||||
|
||||
### Added
|
||||
|
||||
- Music playing on the freeplay menu.
|
||||
- UI sounds on freeplay menu
|
||||
- Score now shows mid-song.
|
||||
- Menu on pause screen! Can resume, and restart song, or go back to main menu.
|
||||
- New music made for pause menu!
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved all the intro texts to its own txt file instead of being hardcoded, this allows for much easier customization. File is in the data folder, called "introText.txt", follow the format in there and you're probably good to go!
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed soft lock when pausing on song finish ([shoutouts gedehari](https://github.com/FunkinCrew/Funkin/issues/15))
|
||||
- Think I fixed issue that led to in-game scores being off by 2 ([shoutouts Mike](https://github.com/FunkinCrew/Funkin/issues/4))
|
||||
- Should have fixed the 1 frame note appearance thing. ([shoutouts Mike](https://github.com/FunkinCrew/Funkin/issues/6))
|
||||
- Cleaned up some charting on South on hard mode
|
||||
- Fixed some animation timings, should feel both better to play, and watch. (shoutouts Dave/Ivan lol)
|
||||
- Animation issue where GF would freak out on the title screen if you returned to it([shoutouts MultiXIII](https://github.com/FunkinCrew/Funkin/issues/12)).
|
||||
|
||||
|
||||
|
||||
## [0.2.1.2] - 2020-11-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- Story mode scores not properly resetting, leading to VERY inflated highscores on the leaderboards. This also requires me to clear the scores that are on the leaderboard right now, sorry!
|
||||
- Difficulty on storymode and in freeplay scores
|
||||
- Hard mode difficulty on campaign levels have been fixed
|
||||
|
||||
|
||||
|
||||
## [0.2.1.1] - 2020-11-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- Week 2 not unlocking properly
|
||||
|
||||
|
||||
|
||||
## [0.2.1] - 2020-11-06
|
||||
|
||||
### Added
|
||||
|
||||
- Scores to the freeplay menu
|
||||
- A few new intro boot messages.
|
||||
- Lightning effect in Spooky stages
|
||||
- Campaign scores, can now compete on scoreboards for campaign!
|
||||
- Can now change difficulties in Freeplay mode
|
||||
|
||||
### Changed
|
||||
|
||||
- Balanced out Normal mode for the harder songs(DadBattle and Spookeez, not South yet). Should be much easier all around.
|
||||
- Put tutorial in it's own 'week', so that if you want to play week 1, you don't have to play the tutorial.
|
||||
|
||||
### Fixed
|
||||
|
||||
- One of the charting bits on South and Spookeez during the intro.
|
||||
|
||||
|
||||
|
||||
## [0.2.0] - 2020-11-01
|
||||
|
||||
### Added
|
||||
|
||||
- Uhh Newgrounds release lolol I always lose track of shit.
|
||||
|
||||
|
||||
|
||||
## [0.1.0] - 2020-10-05
|
||||
|
||||
### Added
|
||||
|
||||
- Uh, everything. This the game's initial gamejam release. We put it out
|
||||
@@ -0,0 +1,32 @@
|
||||
# Code Style Guide
|
||||
|
||||
Code style is enforced using Visual Studio Code extensions.
|
||||
|
||||
## .hx
|
||||
Formatting is handled by the `nadako.vshaxe` extension, which includes Haxe Formatter.
|
||||
Haxe Formatter automatically resolves issues such as indentation style and line breaks, and can be configured in `hxformat.json`.
|
||||
|
||||
Code Quality is handled by the `vshaxe.haxe-checkstyle` extension, which includes Haxe Checkstyle.
|
||||
|
||||
### Haxe Checkstyle Notes
|
||||
* Checks can be escalated to display as different severities in the Problems window.
|
||||
* Checks can be disabled by setting the severity to `IGNORE`.
|
||||
* `IndentationCharacter` checks what is used to indent, `Indentation` checks how deep the indentation is.
|
||||
* `CommentedOutCode` check is in place because old code should be retrieved via Git history.
|
||||
* TODO items: Enable these one-by-one and fix them to improve the overall code quality.
|
||||
- Re-configure `MethodLength`
|
||||
- Re-configure `CyclomaticComplexity`
|
||||
- Re-enable `MagicNumber`
|
||||
- Re-configure `NestedControlFlow`
|
||||
- Re-configure `NestedIfDepth`
|
||||
- Figure out something for `Trace`
|
||||
- Fix bug and enable `DocCommentStyle`
|
||||
|
||||
## .json
|
||||
Formatting is handled by the `esbenp.prettier-vscode` extension, which includes Prettier.
|
||||
Prettier automatically handles formatting of JSON files, and can be configured in `.prettierrc.js`.
|
||||
|
||||
### Prettier Notes
|
||||
* Prettier will automatically attempt to place expressions on a single line if they fit, but will keep them multi-line if they are manually made multi-line.
|
||||
* This means that long single-line objects are automatically expanded, and short multi-line objects aren't automatically collapsed.
|
||||
* You may want to use regex replacement to manually remove the first newline in short multi-line objects to convince Prettier to collapse them.
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
# Friday Night Funkin'
|
||||
|
||||
The Friday Night Funkin' source code is licensed under the Apache 2.0 license: (https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Friday Night Funkin' Copyright 2020-2024 The Funkin' Crew Inc.
|
||||
All Rights Reserved. "Friday Night Funkin'" and the "Friday Night Funkin'" logo are trademarks of The Funkin' Crew Inc.
|
||||
|
||||
You can view the `funkin-assets` license here: (https://github.com/FunkinCrew/funkin.assets/blob/main/LICENSE.md)
|
||||
|
||||
## Apache 2.0 License
|
||||
```
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2020-2024 The Funkin' Crew Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# Friday Night Funkin'
|
||||
|
||||
Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludum Dare 47.
|
||||
|
||||
This game was made with love to Newgrounds and its community. Extra love to Tom Fulp.
|
||||
|
||||
- [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371)
|
||||
- [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin)
|
||||
|
||||
# Getting Started
|
||||
|
||||
**PLEASE USE THE LINKS ABOVE IF YOU JUST WANT TO PLAY THE GAME**
|
||||
|
||||
To learn how to install the necessary dependencies and compile the game from source, please follow our [Compiling Guide](/docs/COMPILING.md).
|
||||
|
||||
# Contributing
|
||||
|
||||
Check out our [Contributing Guide](/docs/CONTRIBUTING.md) to learn how you can actively contribute to the development of Friday Night Funkin'!
|
||||
|
||||
# Modding
|
||||
|
||||
Feel free to start learning to mod the game by reading our [documentation](https://funkincrew.github.io/funkin-modding-docs/) and guide to modding.
|
||||
|
||||
# Credits and Special Thanks
|
||||
|
||||
Full credits can be found in-game, or in the `credits.json` file which is located [here](https://github.com/FunkinCrew/funkin.assets/blob/main/exclude/data/credits.json).
|
||||
|
||||
## Programming
|
||||
- [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer
|
||||
- [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer
|
||||
- [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming
|
||||
- [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming
|
||||
- Our contributors on GitHub
|
||||
|
||||
## Art / Animation / UI
|
||||
- [PhantomArcade3K](https://twitter.com/phantomarcade3k) - Artist and Animator
|
||||
- [Evilsk8r](https://twitter.com/evilsk8r) - Art
|
||||
- [Moawling](https://twitter.com/moawko) - Week 6 Pixel Art
|
||||
- [IvanAlmighty](https://twitter.com/IvanA1mighty) - Misc UI Design
|
||||
|
||||
## Music
|
||||
- [Kawaisprite](https://twitter.com/kawaisprite) - Musician
|
||||
- [BassetFilms](https://twitter.com/Bassetfilms) - Music for "Monster", Additional Character Design
|
||||
|
||||
## Special Thanks
|
||||
- [Tom Fulp](https://twitter.com/tomfulp) - For being a great guy and for Newgrounds
|
||||
- [JohnnyUtah](https://twitter.com/JohnnyUtahNG/) - Voice of Tankman
|
||||
- [L0Litsmonica](https://twitter.com/L0Litsmonica) - Voice of Mommy Mearest
|
||||
@@ -0,0 +1,188 @@
|
||||
FROM ubuntu:noble
|
||||
|
||||
ARG haxe_version=4.3.6
|
||||
ARG haxelib_version=4.1.0
|
||||
ARG neko_version=2.4.0
|
||||
|
||||
# prepare runner
|
||||
ENV GITHUB_HOME="/github/home"
|
||||
|
||||
RUN <<EOF
|
||||
mkdir -p "$GITHUB_HOME"
|
||||
mkdir -p /opt
|
||||
mkdir -p /usr/share/hxcpp
|
||||
mkdir -p /usr/local/bin
|
||||
chmod -R 777 /opt
|
||||
chmod -R 777 /usr/share
|
||||
chmod -R 777 /usr/local/bin
|
||||
EOF
|
||||
|
||||
|
||||
# Prepare Ubuntu
|
||||
# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/configure-environment.sh
|
||||
# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/configure-system.sh
|
||||
RUN <<EOF
|
||||
echo 'vm.max_map_count=262144' | tee -a /etc/sysctl.conf
|
||||
echo 'fs.inotify.max_user_watches=655360' | tee -a /etc/sysctl.conf
|
||||
echo 'fs.inotify.max_user_instances=1280' | tee -a /etc/sysctl.conf
|
||||
EOF
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive"
|
||||
ENV GIT_TERMINAL_PROMPT="0"
|
||||
|
||||
# Prepare APT
|
||||
RUN <<EOF
|
||||
cat <<EOC >> /etc/apt/apt.conf.d/10apt-autoremove
|
||||
APT::Get::AutomaticRemove "0";
|
||||
APT::Get::HideAutoRemove "1";
|
||||
EOC
|
||||
|
||||
echo <<EOC >> /etc/apt/apt.conf.d/80retries
|
||||
"APT::Acquire::Retries \"10\";"
|
||||
EOC
|
||||
|
||||
echo <<EOC >> /etc/apt/apt.conf.d/90assumeyes
|
||||
"APT::Get::Assume-Yes \"true\";"
|
||||
EOC
|
||||
EOF
|
||||
|
||||
# Prepare apt-fast
|
||||
RUN <<EOF
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends software-properties-common
|
||||
add-apt-repository -y ppa:apt-fast/stable
|
||||
apt-get -y install apt-fast
|
||||
echo debconf apt-fast/maxdownloads string 8 | debconf-set-selections
|
||||
echo debconf apt-fast/dlflag boolean true | debconf-set-selections
|
||||
echo debconf apt-fast/aptmanager string apt-get | debconf-set-selections
|
||||
EOF
|
||||
|
||||
# Base packages
|
||||
# https://github.com/actions/runner-images/blob/main/images/ubuntu/toolsets/toolset-2204.json#L114
|
||||
RUN <<EOF
|
||||
apt-fast install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
bzip2 curl g++ gcc make jq tar unzip wget \
|
||||
sudo git openssh-client
|
||||
EOF
|
||||
|
||||
# Prepare git
|
||||
RUN <<EOF
|
||||
cat <<EOC >> /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 <<EOF
|
||||
apt-fast install -y --no-install-recommends \
|
||||
libc6-dev libffi-dev \
|
||||
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
|
||||
libgl-dev libgl1-mesa-dev \
|
||||
libasound2-dev libpulse-dev \
|
||||
libvlc-dev libvlccore-dev
|
||||
EOF
|
||||
|
||||
# Janky libffi.6 fix
|
||||
RUN <<EOF
|
||||
ln -s \
|
||||
/usr/lib/x86_64-linux-gnu/libffi.so.8 \
|
||||
/usr/lib/x86_64-linux-gnu/libffi.so.6 \
|
||||
|| true
|
||||
EOF
|
||||
|
||||
# neko
|
||||
# https://github.com/HaxeFoundation/neko/releases/download/v2-3-0/neko-2.3.0-linux64.tar.gz
|
||||
RUN <<EOF
|
||||
#neko_url=$(curl https://api.github.com/repos/HaxeFoundation/neko/releases -fL \
|
||||
# | jq '.[] | select(.name == "'"$neko_version"'")' \
|
||||
# | jq '.assets[] | select(.name | endswith("linux64.tar.gz"))' \
|
||||
# | jq -r '.browser_download_url')
|
||||
neko_url="https://geo.thei.rs/funkin/neko-2.4.1-linux64.tar.gz"
|
||||
curl -fL "$neko_url" | tar -xz -C /usr/local
|
||||
EOF
|
||||
|
||||
RUN <<EOF
|
||||
neko_path="$(find /usr/local -maxdepth 1 -type d -name 'neko*')"
|
||||
ln -s "$neko_path" /usr/local/neko
|
||||
EOF
|
||||
|
||||
ENV NEKOPATH="/usr/local/neko"
|
||||
ENV LD_LIBRARY_PATH="$NEKOPATH:$LD_LIBRARY_PATH"
|
||||
ENV PATH="$NEKOPATH:$PATH"
|
||||
|
||||
# haxe
|
||||
# https://github.com/HaxeFoundation/haxe/releases/download/4.0.5/haxe-4.0.5-linux64.tar.gz
|
||||
RUN <<EOF
|
||||
#haxe_url=$(curl https://api.github.com/repos/HaxeFoundation/haxe/releases -fL \
|
||||
# | jq '.[] | select(.name == "'"$haxe_version"'")' \
|
||||
# | jq '.assets[] | select(.name | endswith("linux64.tar.gz"))' \
|
||||
# | jq -r '.browser_download_url')
|
||||
haxe_url="https://geo.thei.rs/funkin/haxe-4.3.6-linux64.tar.gz"
|
||||
curl -fL "$haxe_url" | tar -xz -C /usr/local
|
||||
EOF
|
||||
|
||||
RUN <<EOF
|
||||
haxe_path="$(find /usr/local -maxdepth 1 -type d -name 'haxe*')"
|
||||
ln -s "$haxe_path" /usr/local/haxe
|
||||
EOF
|
||||
|
||||
ENV HAXEPATH="/usr/local/haxe"
|
||||
ENV HAXE_STD_PATH="$HAXEPATH/std"
|
||||
ENV PATH="$HAXEPATH:$PATH"
|
||||
|
||||
# haxelib
|
||||
RUN <<EOF
|
||||
HOME=/etc haxelib setup "$HAXEPATH/lib"
|
||||
haxelib --global --never install haxelib $haxelib_version
|
||||
haxelib --global --never git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies
|
||||
haxelib --global --never git hmm https://github.com/FunkinCrew/hmm funkin-patches
|
||||
EOF
|
||||
|
||||
# hxcpp
|
||||
ENV HXCPP_COMPILE_CACHE="/usr/share/hxcpp"
|
||||
ENV HXCPP_CACHE_MB="4096"
|
||||
|
||||
# Clean up
|
||||
# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/cleanup.sh
|
||||
RUN <<EOF
|
||||
rm -r /var/cache/apt/apt-fast
|
||||
apt-get clean
|
||||
if [ -d /var/lib/apt/lists ]; then
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
fi
|
||||
|
||||
if [ -d /tmp ]; then
|
||||
rm -rf /tmp/*
|
||||
fi
|
||||
if [ -d /root/.cache ]; then
|
||||
rm -rf /root/.cache
|
||||
fi
|
||||
|
||||
if command -v journalctl; then
|
||||
journalctl --rotate
|
||||
journalctl --vacuum-time=1s
|
||||
fi
|
||||
if [ -d /var/log ]; then
|
||||
find /var/log -type f -regex ".*\.gz$" -delete
|
||||
find /var/log -type f -regex ".*\.[0-9]$" -delete
|
||||
find /var/log/ -type f -exec cp /dev/null {} \;
|
||||
fi
|
||||
if [ -f /usr/local/bin/invoke_tests ]; then
|
||||
rm -rf /usr/local/bin/invoke_tests
|
||||
fi
|
||||
EOF
|
||||
|
||||
# Print debug info
|
||||
RUN <<EOF
|
||||
echo "/root"
|
||||
ls -la /root
|
||||
cat /root/.haxelib && echo
|
||||
|
||||
id
|
||||
env
|
||||
EOF
|
||||
+668
@@ -0,0 +1,668 @@
|
||||
{
|
||||
"extendsConfigPath": "",
|
||||
"defineCombinations": [],
|
||||
"defaultSeverity": "INFO",
|
||||
"baseDefines": [],
|
||||
"version": 1,
|
||||
"exclude": {},
|
||||
"checks": [
|
||||
{
|
||||
"props": {},
|
||||
"type": "Anonymous"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"spaceBefore": false,
|
||||
"spaceInside": false
|
||||
},
|
||||
"type": "ArrayAccess"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "ArrayLiteral"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"allowSingleArgParens": true,
|
||||
"allowReturn": true,
|
||||
"allowFunction": true,
|
||||
"allowCurlyBody": true
|
||||
},
|
||||
"type": "ArrowFunction"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"avoidIdentifiers": []
|
||||
},
|
||||
"type": "AvoidIdentifier"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "AvoidStarImport"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "AvoidTernaryOperator"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "BlockBreakingConditional"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"format": "^(e|t|ex|[a-z][a-z][a-zA-Z]+)$"
|
||||
},
|
||||
"type": "CatchParameterName"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"thresholdSimilar": 120,
|
||||
"severityIdentical": "WARNING",
|
||||
"thresholdIdentical": 60
|
||||
},
|
||||
"type": "CodeSimilarity"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "CommentedOutCode"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"policy": "aligned",
|
||||
"allowSingleline": true,
|
||||
"severity": "INFO"
|
||||
},
|
||||
"type": "ConditionalCompilation"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"ignoreExtern": true,
|
||||
"format": "^[a-zA-Z0-9]+(?:_[a-zA-Z0-9]+)*$",
|
||||
"tokens": ["INLINE", "NOTINLINE"]
|
||||
},
|
||||
"type": "ConstantName"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"thresholds": [
|
||||
{
|
||||
"complexity": 20,
|
||||
"severity": "WARNING"
|
||||
},
|
||||
{
|
||||
"complexity": 25,
|
||||
"severity": "ERROR"
|
||||
}
|
||||
],
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "CyclomaticComplexity"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "DefaultComesLast"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"lineStyle": "onestar",
|
||||
"startStyle": "twostars",
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "DocCommentStyle"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "Dynamic"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "ERegLiteral"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"tokens": [
|
||||
"CLASS_DEF",
|
||||
"ENUM_DEF",
|
||||
"ABSTRACT_DEF",
|
||||
"TYPEDEF_DEF",
|
||||
"INTERFACE_DEF",
|
||||
"OBJECT_DECL",
|
||||
"FUNCTION",
|
||||
"FOR",
|
||||
"IF",
|
||||
"WHILE",
|
||||
"SWITCH",
|
||||
"TRY",
|
||||
"CATCH"
|
||||
],
|
||||
"option": "empty"
|
||||
},
|
||||
"type": "EmptyBlock"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"requireEmptyLineAfterPackage": true,
|
||||
"requireEmptyLineAfterInterface": true,
|
||||
"requireEmptyLineAfterAbstract": true,
|
||||
"allowEmptyLineAfterSingleLineComment": true,
|
||||
"max": 1,
|
||||
"requireEmptyLineAfterClass": true,
|
||||
"allowEmptyLineAfterMultiLineComment": true
|
||||
},
|
||||
"type": "EmptyLines"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"enforceEmptyPackage": true
|
||||
},
|
||||
"type": "EmptyPackage"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"none": [],
|
||||
"upto": [],
|
||||
"defaultPolicy": "upto",
|
||||
"exact": [],
|
||||
"max": 1,
|
||||
"ignore": [],
|
||||
"atleast": [],
|
||||
"skipSingleLineTypes": true
|
||||
},
|
||||
"type": "ExtendedEmptyLines"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"requireParams": true,
|
||||
"fieldType": "BOTH",
|
||||
"requireReturn": true,
|
||||
"ignoreOverride": true,
|
||||
"tokens": ["ABSTRACT_DEF", "CLASS_DEF", "ENUM_DEF", "INTERFACE_DEF"],
|
||||
"modifier": "PUBLIC",
|
||||
"excludeNames": ["new", "toString"]
|
||||
},
|
||||
"type": "FieldDocComment"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"max": 1000,
|
||||
"ignoreEmptyLines": true,
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "FileLength"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"option": "upperCase"
|
||||
},
|
||||
"type": "HexadecimalLiteral"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"ignoreSetter": true,
|
||||
"ignoreFormat": "^(main|run)$",
|
||||
"ignoreConstructorParameter": true
|
||||
},
|
||||
"type": "HiddenField"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"character": " ",
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "Indentation"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"character": "space",
|
||||
"severity": "INFO",
|
||||
"ignorePattern": "^$"
|
||||
},
|
||||
"type": "IndentationCharacter"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"ignoreReturnAssignments": true,
|
||||
"severity": "WARNING"
|
||||
},
|
||||
"type": "InnerAssignment"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"allowProperties": false,
|
||||
"allowMarkerInterfaces": true
|
||||
},
|
||||
"type": "Interface"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "LeftCurly"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"max": 160,
|
||||
"ignorePattern": "^$"
|
||||
},
|
||||
"type": "LineLength"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"listeners": ["addEventListener", "addListener", "on", "once"],
|
||||
"format": "^_?[a-z][a-zA-Z0-9]*$"
|
||||
},
|
||||
"type": "ListenerName"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"ignoreExtern": true,
|
||||
"format": "^[a-z][a-zA-Z0-9]*$",
|
||||
"tokens": []
|
||||
},
|
||||
"type": "LocalVariableName"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"ignoreNumbers": [-1, 0, 1, 2],
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "MagicNumber"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"format": "^[A-Z][a-zA-Z0-9]*$",
|
||||
"tokens": ["ABSTRACT"]
|
||||
},
|
||||
"type": "MemberName"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"format": "^[_a-zA-Z][a-z][a-zA-Z0-9]*$",
|
||||
"tokens": ["ABSTRACT"]
|
||||
},
|
||||
"type": "MemberName"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"ignoreExtern": true,
|
||||
"format": "^[_a-z][a-zA-Z0-9]*$",
|
||||
"tokens": ["PUBLIC", "PRIVATE", "CLASS", "TYPEDEF"]
|
||||
},
|
||||
"type": "MemberName"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"maxPrivate": 100,
|
||||
"maxPublic": 100,
|
||||
"maxTotal": 100
|
||||
},
|
||||
"type": "MethodCount"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"max": 50,
|
||||
"ignoreEmptyLines": true,
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "MethodLength"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"ignoreExtern": true,
|
||||
"format": "^[a-z][a-zA-Z0-9]*$",
|
||||
"tokens": [],
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "MethodName"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"modifiers": [
|
||||
"PUBLIC_PRIVATE",
|
||||
"STATIC",
|
||||
"MACRO",
|
||||
"OVERRIDE",
|
||||
"INLINE",
|
||||
"DYNAMIC",
|
||||
"FINAL"
|
||||
],
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "ModifierOrder"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"minLength": 2,
|
||||
"ignore": "^\\s+$",
|
||||
"allowDuplicates": 5
|
||||
},
|
||||
"type": "MultipleStringLiterals"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "MultipleVariableDeclarations"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"allowSingleLineStatement": true,
|
||||
"tokens": ["FOR", "ELSE_IF", "WHILE", "DO_WHILE"]
|
||||
},
|
||||
"type": "NeedBraces"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"max": 3,
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "NestedControlFlow"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"max": 1,
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "NestedForDepth"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"max": 4
|
||||
},
|
||||
"type": "NestedIfDepth"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"max": 1
|
||||
},
|
||||
"type": "NestedTryDepth"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"option": "questionMark"
|
||||
},
|
||||
"type": "NullableParameter"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"oldFunctionTypePolicy": "none",
|
||||
"unaryOpPolicy": "none",
|
||||
"intervalOpPolicy": "none",
|
||||
"newFunctionTypePolicy": "around",
|
||||
"ternaryOpPolicy": "around",
|
||||
"boolOpPolicy": "around",
|
||||
"assignOpPolicy": "around",
|
||||
"bitwiseOpPolicy": "around",
|
||||
"arithmeticOpPolicy": "around",
|
||||
"compareOpPolicy": "around",
|
||||
"arrowPolicy": "around",
|
||||
"arrowFunctionPolicy": "around"
|
||||
},
|
||||
"type": "OperatorWhitespace"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"tokens": [
|
||||
"+",
|
||||
"-",
|
||||
"*",
|
||||
"/",
|
||||
"%",
|
||||
">",
|
||||
"<",
|
||||
">=",
|
||||
"<=",
|
||||
"==",
|
||||
"!=",
|
||||
"&",
|
||||
"|",
|
||||
"^",
|
||||
"&&",
|
||||
"||",
|
||||
"<<",
|
||||
">>",
|
||||
">>>",
|
||||
"+=",
|
||||
"-=",
|
||||
"*=",
|
||||
"/=",
|
||||
"%=",
|
||||
"<<=",
|
||||
">>=",
|
||||
">>>=",
|
||||
"|=",
|
||||
"&=",
|
||||
"^=",
|
||||
"...",
|
||||
"=>",
|
||||
"++",
|
||||
"--"
|
||||
],
|
||||
"option": "nl"
|
||||
},
|
||||
"type": "OperatorWrap"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"tokens": ["="],
|
||||
"option": "eol"
|
||||
},
|
||||
"type": "OperatorWrap"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"ignoreExtern": true,
|
||||
"format": "^(_|[a-z][a-zA-Z0-9]*$)",
|
||||
"tokens": []
|
||||
},
|
||||
"type": "ParameterName"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"max": 7,
|
||||
"ignoreOverriddenMethods": false
|
||||
},
|
||||
"type": "ParameterNumber"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"severity": "WARNING"
|
||||
},
|
||||
"type": "PublicAccessor"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"prohibitMeta": false
|
||||
},
|
||||
"type": "RedundantAccessMeta"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"prohibitMeta": false
|
||||
},
|
||||
"type": "RedundantAllowMeta"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"enforcePrivate": false,
|
||||
"enforcePublic": true,
|
||||
"enforcePublicPrivate": false
|
||||
},
|
||||
"type": "RedundantModifier"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"enforceReturnTypeForAnonymous": false,
|
||||
"allowEmptyReturn": true,
|
||||
"enforceReturnType": true
|
||||
},
|
||||
"type": "Return"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"ignoreFormat": "^$",
|
||||
"max": 2,
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "ReturnCount"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "RightCurly"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"commaPolicy": "after",
|
||||
"semicolonPolicy": "after",
|
||||
"allowTrailingComma": false,
|
||||
"dotPolicy": "none"
|
||||
},
|
||||
"type": "SeparatorWhitespace"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"tokens": [","],
|
||||
"option": "eol"
|
||||
},
|
||||
"type": "SeparatorWrap"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "SimplifyBooleanExpression"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "SimplifyBooleanReturn"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"spaceIfCondition": "should",
|
||||
"spaceAroundBinop": true,
|
||||
"spaceForLoop": "should",
|
||||
"ignoreRangeOperator": true,
|
||||
"spaceWhileLoop": "should",
|
||||
"spaceCatch": "should",
|
||||
"spaceSwitchCase": "should",
|
||||
"noSpaceAroundUnop": true
|
||||
},
|
||||
"type": "Spacing"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"policy": "onlySingle",
|
||||
"allowException": true,
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "StringLiteral"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"format": "^\\s*(TODO|FIXME|HACK|XXX|BUG)"
|
||||
},
|
||||
"type": "TODOComment"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"ignorePattern": "^$"
|
||||
},
|
||||
"type": "TabForAligning"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"severity": "IGNORE"
|
||||
},
|
||||
"type": "Trace"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "TrailingWhitespace"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"tokens": ["CLASS_DEF", "ENUM_DEF", "INTERFACE_DEF", "TYPEDEF_DEF"]
|
||||
},
|
||||
"type": "TypeDocComment"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"ignoreExtern": true,
|
||||
"format": "^[A-Z]+[a-zA-Z0-9]*$",
|
||||
"tokens": []
|
||||
},
|
||||
"type": "TypeName"
|
||||
},
|
||||
{
|
||||
"props": {},
|
||||
"type": "UnnecessaryConstructor"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"moduleTypeMap": {},
|
||||
"ignoreModules": []
|
||||
},
|
||||
"type": "UnusedImport"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"severity": "WARNING"
|
||||
},
|
||||
"type": "UnusedLocalVar"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"typeHintPolicy": "enforce_all",
|
||||
"ignoreEnumAbstractValues": true
|
||||
},
|
||||
"type": "VarTypeHint"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"tokens": [",", ";"],
|
||||
"allowTrailingComma": false
|
||||
},
|
||||
"type": "WhitespaceAfter"
|
||||
},
|
||||
{
|
||||
"props": {
|
||||
"tokens": [
|
||||
"=",
|
||||
"+",
|
||||
"-",
|
||||
"*",
|
||||
"/",
|
||||
"%",
|
||||
">",
|
||||
"<",
|
||||
">=",
|
||||
"<=",
|
||||
"==",
|
||||
"!=",
|
||||
"&",
|
||||
"|",
|
||||
"^",
|
||||
"&&",
|
||||
"||",
|
||||
"<<",
|
||||
">>",
|
||||
">>>",
|
||||
"+=",
|
||||
"-=",
|
||||
"*=",
|
||||
"/=",
|
||||
"%=",
|
||||
"<<=",
|
||||
">>=",
|
||||
">>>=",
|
||||
"|=",
|
||||
"&=",
|
||||
"^=",
|
||||
"=>"
|
||||
]
|
||||
},
|
||||
"type": "WhitespaceAround"
|
||||
}
|
||||
],
|
||||
"numberOfCheckerThreads": 5
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[remote.github]
|
||||
owner = "FunkinCrew"
|
||||
repo = "Funkin"
|
||||
|
||||
# To bypass "you have reached your rate limit!", you can input a github token either as an environment variable
|
||||
# or alongside the `git cliff` command like so `git cliff --github-token TOKEN_HERE`
|
||||
# Personally I like to use the github cli `gh` tool to get a token
|
||||
# `git cliff --github-token $(gh auth token)`
|
||||
|
||||
[changelog]
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||
{%- endmacro -%}
|
||||
|
||||
{% if version -%}
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else -%}
|
||||
## [Unreleased]
|
||||
{% endif -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{%- for commit in commits %}
|
||||
- {% if commit.scope %}({{commit.scope}}) {% endif %}{{ commit.message | split(pat="\n") | first | upper_first | trim }} \
|
||||
([{{ commit.id | truncate(length=7, end="") }}]({{self::remote_url()}}/commit/{{ commit.id }})) -\
|
||||
{% if commit.remote.username %} by @{{ commit.remote.username }}
|
||||
{%- elif commit.author.name %} by {{ commit.author.name }}
|
||||
{%- endif -%}
|
||||
{% if commit.remote.pr_number %} in \
|
||||
[#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
|
||||
{%- endif -%}
|
||||
{% if commit.links | length != 0 %}\
|
||||
[#{{ commit.links }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
## New Contributors
|
||||
{%- endif -%}
|
||||
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor %}\n
|
||||
"""
|
||||
|
||||
# remove the leading and trailing whitespace from the templates
|
||||
trim = true
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
# remove issue numbers from commits
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^docs", group = "Changed", scope="docs" },
|
||||
{ message = "^[a|A]dd", group = "Added" },
|
||||
{ message = "^[s|S]upport", group = "Added" },
|
||||
{ message = "^[r|R]emove", group = "Removed" },
|
||||
{ message = "^.*: add", group = "Added" },
|
||||
{ message = "^.*: support", group = "Added" },
|
||||
{ message = "^.*: remove", group = "Removed" },
|
||||
{ message = "^.*: delete", group = "Removed" },
|
||||
{ message = "^test", group = "Fixed" },
|
||||
{ message = "^fix", group = "Fixed" },
|
||||
{ message = "^.*: fix", group = "Fixed" },
|
||||
{ message = "^.*", group = "Changed" },
|
||||
]
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "newest"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Compiling Friday Night Funkin'
|
||||
|
||||
0. Setup
|
||||
- Download Haxe from [Haxe.org](https://haxe.org)
|
||||
- Download Git from [git-scm.com](https://www.git-scm.com)
|
||||
- Do NOT download the repository using the Download ZIP button on GitHub or you may run into errors!
|
||||
- Instead, open a command prompt and do the following steps...
|
||||
1. Run `cd the\directory\you\want\the\source\code\in` to specify which folder the command prompt is working in.
|
||||
- For example, `cd C:\Users\YOURNAME\Documents` would instruct the command prompt to perform the next steps in your Documents folder.
|
||||
2. Run `git clone https://github.com/FunkinCrew/funkin.git` to clone the base repository.
|
||||
3. Run `cd funkin` to enter the cloned repository's directory.
|
||||
4. Run `git submodule update --init --recursive` to download the game's assets.
|
||||
- NOTE: By performing this operation, you are downloading Content which is proprietary and protected by national and international copyright and trademark laws. See [the LICENSE.md file for the Funkin.assets](https://github.com/FunkinCrew/funkin.assets/blob/main/LICENSE.md) repo for more information.
|
||||
5. Run `haxelib --global install hmm` and then `haxelib --global run hmm setup` to install hmm.json
|
||||
6. Run `hmm install` to install all haxelibs of the current branch
|
||||
- If you have issues installing Lime, try utilizing Funkin's patched libraries for hmm and haxelib: `haxelib --global git haxelib https://github.com/FunkinCrew/haxelib.git` and `haxelib --global git hmm https://github.com/FunkinCrew/hmm.git`
|
||||
8. Run `haxelib run lime setup` to set up Lime
|
||||
9. Perform additional platform setup
|
||||
- For Windows, download the [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe)
|
||||
- When prompted, select "Individual Components" and make sure to download the following:
|
||||
- MSVC v143 VS 2022 C++ x64/x86 build tools
|
||||
- Windows 10/11 SDK
|
||||
- Mac: [`lime setup mac` Documentation](https://lime.openfl.org/docs/advanced-setup/macos/)
|
||||
- Linux: [`lime setup linux` Documentation](https://lime.openfl.org/docs/advanced-setup/linux/)
|
||||
- One of Funkin's dependencies uses libVLC, which requires you to install some development packages to be able to compile.
|
||||
Command for Ubuntu/Debian based systems: `sudo apt install libvlc-dev libvlccore-dev libvlccore9`, and for other distros, please refer to [hxvlc's documentation](https://github.com/MAJigsaw77/hxvlc?tab=readme-ov-file#dependencies)
|
||||
- HTML5: Compiles without any extra setup
|
||||
10. If you are targeting for native, you may need to run `lime rebuild <PLATFORM>` and `lime rebuild <PLATFORM> -debug`
|
||||
11. `lime test <PLATFORM>` to build and launch the game for your platform (for example, `lime test windows`)
|
||||
|
||||
## Build Flags
|
||||
|
||||
There are several useful build flags you can add to a build to affect how it works. A full list can be found in `project.hxp`, but here's information on some of them:
|
||||
|
||||
- `-debug` to build the game in debug mode. This automatically enables several useful debug features.
|
||||
- This includes enabling in-game debug functions, disables compile-time optimizations, enabling asset redirection (see below), and enabling the VSCode debug server (which can slow the game on some machines but allows for powerful debugging through breakpoints).
|
||||
- `-DGITHUB_BUILD` will enable in-game debug functions (such as the ability to time travel in a song by pressing `PgUp`/`PgDn`), without enabling the other stuff
|
||||
- `-DFEATURE_POLYMOD_MODS` or `-DNO_FEATURE_POLYMOD_MODS` to forcibly enable or disable modding support.
|
||||
- `-DREDIRECT_ASSETS_FOLDER` or `-DNO_REDIRECT_ASSETS_FOLDER` to forcibly enable or disable asset redirection.
|
||||
- This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game will break if you try to zip it up and send it to someone, so it's disabled for release builds.
|
||||
- `-DFEATURE_DISCORD_RPC` or `-DNO_FEATURE_DISCORD_RPC` to forcibly enable or disable support for Discord Rich Presence.
|
||||
- `-DFEATURE_VIDEO_PLAYBACK` or `-DNO_FEATURE_VIDEO_PLAYBACK` to forcibly enable or disable video cutscene support.
|
||||
- `-DFEATURE_CHART_EDITOR` or `-DNO_FEATURE_CHART_EDITOR` to forcibly enable or disable the chart editor in the Debug menu.
|
||||
- `-DFEATURE_SCREENSHOTS` or `-DNO_FEATURE_SCREENSHOTS` to forcibly enable or disable the screenshots feature.
|
||||
- `-DFEATURE_STAGE_EDITOR` to forcibly enable the experimental stage editor.
|
||||
- `-DFEATURE_GHOST_TAPPING` to forcibly enable an experimental gameplay change to the anti-mash system.
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
If you experience any issues during the compilation process, DO NOT open an issue on GitHub. Instead, check the [Troubleshooting Guide](TROUBLESHOOTING.md) for steps on how to resolve common problems.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Mac Compiling Guide + Considerations
|
||||
|
||||
There's a few extra considerations when compiling FNF for Mac that *we* have to handle when creating a wider release.
|
||||
- [Creating a Universal Binary](#creating-a-universal-binary)
|
||||
- Code-signing
|
||||
- Notarizing
|
||||
|
||||
## Creating a Universal Binary
|
||||
|
||||
Run the `art/macos-universal.sh` script, which automatically compiles release versions of both arm64 and x86 of Funkin. You can also see there for reference of how it's done.
|
||||
@@ -0,0 +1,418 @@
|
||||
# Contributing
|
||||
Welcome to the Contributing Guide!
|
||||
You can contribute to the Funkin' repository by opening issues or pull requests.
|
||||
|
||||
This guide will cover best practices for each type of contribution.
|
||||
|
||||
# Table of Contents
|
||||
<details open>
|
||||
<summary><b>Contents</b></summary>
|
||||
|
||||
[Part 1: Etiquette](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#part-1-etiquette)
|
||||
|
||||
<details>
|
||||
<summary><a href="https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#part-2-issues">Part 2: Issues</a></summary>
|
||||
|
||||
* [Requirements](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#requirements)
|
||||
|
||||
* [Rejected Features](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#rejected-features)
|
||||
|
||||
* [Issue Types](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#issue-types)
|
||||
|
||||
* [Before You Submit...](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#before-you-submit)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary><a href="https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#part-3-pull-requests">Part 3: Pull Requests</a></summary>
|
||||
|
||||
* [Choosing a base branch](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#choosing-a-base-branch)
|
||||
|
||||
* [Merge conflicts and rebasing](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#merge-conflicts-and-rebasing)
|
||||
|
||||
* [Code PRs](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#code-prs)
|
||||
|
||||
* [Documentation PRs](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#documentation-prs)
|
||||
|
||||
* [GitHub PRs](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#github-prs)
|
||||
|
||||
* [funkin.assets PRs](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#funkinassets-prs)
|
||||
|
||||
* [Charting PRs](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#charting-prs)
|
||||
|
||||
</details>
|
||||
|
||||
[Closing](https://github.com/FunkinCrew/Funkin/blob/main/docs/CONTRIBUTING.md#closing)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
# Part 1: Etiquette
|
||||
- Be respectful to one another. We're here to help each other out!
|
||||
- Keep your titles clear and concise. Do not type the whole description into the title.
|
||||
- Do not spam by creating unnecessary issues and pull requests.
|
||||
- Use common sense!
|
||||
|
||||
# Part 2: Issues
|
||||
Issues serve many purposes, from reporting bugs to suggesting new features.
|
||||
This section provides guidelines to follow when [opening an issue](https://github.com/FunkinCrew/Funkin/issues).
|
||||
|
||||
## Requirements
|
||||
Make sure you're playing:
|
||||
- the latest version of the game (currently v0.6.4)
|
||||
- without any mods
|
||||
- on [Newgrounds](https://www.newgrounds.com/portal/view/770371) or downloaded from [itch.io](https://ninja-muffin24.itch.io/funkin)
|
||||
|
||||
## Rejected Features
|
||||
If you want to **suggest a feature**, make sure it hasn't already been rejected.
|
||||
Here's a list of commonly suggested features and the reasons why they won't be added:
|
||||
|
||||
| Feature | Reason |
|
||||
| ------- | ------- |
|
||||
| Combo Break + Accuracy Displays | https://github.com/FunkinCrew/Funkin/pull/2681#issuecomment-2156308982 |
|
||||
| Toggleable Ghost Tapping | https://github.com/FunkinCrew/Funkin/pull/2564#issuecomment-2119701802 |
|
||||
| Perfectly Centered Strumlines | _same as above^_ |
|
||||
| MultiKey, 9k, More than 4 keys, etc. | https://github.com/FunkinCrew/Funkin/issues/4243#issuecomment-2692371969 |
|
||||
| Flashy Combo Milestones | https://github.com/FunkinCrew/Funkin/pull/4700#issuecomment-2798916579 |
|
||||
| Losing Icons for DD and Parents | https://github.com/FunkinCrew/Funkin/issues/3048#issuecomment-2243491536 |
|
||||
| Playable GF / Speaker BF / Speaker Pico | https://github.com/FunkinCrew/Funkin/issues/2953#issuecomment-2216985230 |
|
||||
| New Intro Text Lines | https://github.com/FunkinCrew/Funkin/issues/5031#issuecomment-2855593376 |
|
||||
| Fresh (Chill Mix) as Title Screen Music | https://github.com/FunkinCrew/Funkin/pull/4282#issuecomment-2709334718 |
|
||||
| Difficulty Ratings above 20 | https://github.com/FunkinCrew/Funkin/issues/3075#issuecomment-2368984497 |
|
||||
| Quick Restart Keybind (not R) | https://github.com/FunkinCrew/Funkin/issues/3268#issuecomment-2351095232 |
|
||||
| Countdown after Unpausing Song | https://github.com/FunkinCrew/Funkin/issues/2721#issuecomment-2159330106 |
|
||||
| "Philly Glow" Effect from Psych Engine | https://github.com/FunkinCrew/Funkin/issues/3788#issuecomment-2688966982 |
|
||||
| Importing Charts from Psych Engine (and other mod content) | https://github.com/FunkinCrew/Funkin/issues/2586#issuecomment-2125733327 |
|
||||
| Backwards Compatibility for Modding | https://github.com/FunkinCrew/Funkin/issues/3949#issuecomment-2608391329 |
|
||||
| Lua Support | https://github.com/FunkinCrew/Funkin/issues/2643#issuecomment-2143718093 |
|
||||
|
||||
## Issue Types
|
||||
Choose the issue template that best suits your needs!
|
||||
Here's what each template is designed for:
|
||||
|
||||
### Bug Report ([view list](https://github.com/FunkinCrew/Funkin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22type%3A%20minor%20bug%22))
|
||||
For minor bugs and general issues with the game. Choose this one if none of the others fit your needs.
|
||||
|
||||
### Crash Report ([view list](https://github.com/FunkinCrew/Funkin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22type%3A%20major%20bug%22))
|
||||
For crashes and freezes like the [Null Object Reference](https://github.com/FunkinCrew/Funkin/issues/2209) problem from v0.3.0.
|
||||
|
||||
### Charting Issue ([view list](https://github.com/FunkinCrew/Funkin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22type%3A%20charting%20issue%22))
|
||||
For misplaced notes, wonky camera movements, broken song events, and everything related to the game's charts.
|
||||
|
||||
### Enhancement ([view list](https://github.com/FunkinCrew/Funkin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22type%3A%20enhancement%22))
|
||||
For suggestions to add new features or improve existing ones. We'd love to hear your ideas!
|
||||
|
||||
### Compiling Help (only after reading the [Troubleshooting Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/TROUBLESHOOTING.md))
|
||||
For issues with compiling the game. Legacy versions (before v0.3.0) are not supported.
|
||||
|
||||
> [!TIP]
|
||||
> If none of the above Issue templates suit your inquiry (e.g. Questions or Coding Help), please [open a discussion](https://github.com/FunkinCrew/Funkin/discussions).
|
||||
|
||||
## Before You Submit...
|
||||
Complete the Issue Checklist at the top of your template!
|
||||
|
||||
> [!IMPORTANT]
|
||||
> If you do not complete each step of the Issue Checklist, **your issue may be closed.**
|
||||
|
||||
Be sure to use the search bar on the Issues page to check that your issue hasn't already been reported by someone else.
|
||||
Duplicate issues make it harder to keep track of important issues with the game.
|
||||
|
||||
Also only report one issue or enhancement at a time! If you have multiple bug reports or suggestions, split them up into separate submissions so they can be checked off one by one.
|
||||
|
||||
Once you're sure your issue is unique and specific, feel free to submit it.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **DO NOT CLOSE YOUR ISSUE FOR ANY REASON!** Your issue will be taken care of by a moderator!
|
||||
|
||||
**Thank you for opening issues!**
|
||||
|
||||
# Part 3: Pull Requests
|
||||
Community members are welcome to contribute their changes by [opening pull requests](https://github.com/FunkinCrew/Funkin/pulls).
|
||||
This section covers guidelines for opening and managing pull requests (PRs).
|
||||
|
||||
## Choosing a base branch
|
||||
When creating a branch in your fork, base your branch on either the `main` or `develop` branch depending on the types of changes you want to make.
|
||||
|
||||
> [!CAUTION]
|
||||
> Avoid using your fork's default branch (`main` in this case) for your PR. This is considered an [anti-pattern](https://jmeridth.com/posts/do-not-issue-pull-requests-from-your-master-branch/) by GitHub themselves!
|
||||
> Instead, make a separate branch for your additions (ex. `docs/fix-typo` or `minor-bugfix`).
|
||||
|
||||
Choose the `main` branch if you modify:
|
||||
- Documentation (`.md` files)
|
||||
- GitHub files (`.yml` files or anything in the `.github` folder)
|
||||
|
||||
Choose the `develop` branch if you modify:
|
||||
- Game code (`.hx` files)
|
||||
- Any other type of file
|
||||
|
||||
> [!TIP]
|
||||
> When in doubt, base your branch on the `develop` branch.
|
||||
|
||||
Choosing the right base branch helps keep your commit history clean and avoid merge conflicts.
|
||||
Once you’re satisfied with the changes you’ve made, open a PR and base it on the same branch you previously chose.
|
||||
|
||||
## Maintaining your pull request
|
||||
Keeping your pull request clean and easy to review increases the chance that it will be accepted!
|
||||
|
||||
Our maintenance policy is as follows:
|
||||
- If we require changes to your PR, we will label your PR `status: needs revision`.
|
||||
- We may also leave a comment under your PR specifying what changes you should make.
|
||||
- If you receive a comment, you have 90 days to implement the requested changes.
|
||||
- After this period, your PR will be closed due to inactivity and labeled `status: stale`.
|
||||
- Even after your PR is closed, you may request for us to reopen it. Just be sure to address the issues!
|
||||
|
||||
This policy ensures that PRs awaiting review are up to date and ready to merge.
|
||||
|
||||
### Merge conflicts and rebasing
|
||||
Some game updates introduce significant breaking changes that may create merge conflicts in your PR. To resolve them, you will need to update or rebase your PR.
|
||||
|
||||
Most merge conflicts are small and will only require you to modify a few files to resolve them.
|
||||
However, some changes are so big that your commit history will be a total mess!
|
||||
In this case, you will have to perform a [**rebase**](https://docs.github.com/en/get-started/using-git/about-git-rebase).
|
||||
This process reapplies your changes on top of the updated branch and cleanly resolves the merge conflicts.
|
||||
|
||||
> [!TIP]
|
||||
> If your commit history becomes too long, you can use rebase to `squash` your PR's commits into a single commit.
|
||||
|
||||
## Code PRs
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This guide does not cover compiling. If you have trouble compiling the game, refer to the [Compilation Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/COMPILING.md).
|
||||
|
||||
Code-based PRs make changes such as **fixing bugs** or **implementing new features** in the game.
|
||||
|
||||
This involves modifying one or several of the repository’s `.hx` files, found within the `source/` folder.
|
||||
|
||||
### Codestyle
|
||||
Before submitting your PR, check that your code follows the [Style Guide](https://github.com/FunkinCrew/Funkin/blob/main/docs/style-guide.md). This keeps your code consistent with the rest of the codebase!
|
||||
|
||||
### Code comments
|
||||
Code comments help others understand your changes, so the way you write them is important!
|
||||
Here are some guidelines for writing comments in your code:
|
||||
- Leave comments only when you believe a piece of code warrants explanation. If a piece of code is self-explanatory, it does not need a comment.
|
||||
- Ensure that your comments provide meaningful insight into the function or purpose of the code.
|
||||
- Write your comments in a clear and concise manner.
|
||||
- Only sign your comments with your name when your changes are complex and may require further explanation.
|
||||
|
||||
### Example code comments
|
||||
#### DO NOT:
|
||||
Below are examples of what you SHOULD NOT do when writing code comments.
|
||||
```haxe
|
||||
/**
|
||||
* jumps around the song
|
||||
* works with bpm changes but skipped notes still hurt
|
||||
* @param sections how many sections to jump, negative = backwards
|
||||
*/
|
||||
function changeSection(sections:Int):Void
|
||||
{
|
||||
// Pause the music, as you probably guessed
|
||||
// FlxG.sound.music.pause();
|
||||
|
||||
// Set the target time in steps, I don’t really get how this works though lol - [GitHub username]
|
||||
var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.stepsPerMeasure * sections);
|
||||
var targetTimeMs:Float = Conductor.instance.getStepTimeInMs(targetTimeSteps);
|
||||
|
||||
// Don't go back in time to before the song started, that would probably break a lot of things and cause a bunch of problems!
|
||||
targetTimeMs = Math.max(0, targetTimeMs);
|
||||
|
||||
if (FlxG.sound.music != null) // If the music is not null, set the time to the target time
|
||||
{
|
||||
FlxG.sound.music.time = targetTimeMs;
|
||||
}
|
||||
|
||||
// Handle skipped notes and events and all that jazz
|
||||
handleSkippedNotes();
|
||||
SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition);
|
||||
// regenNoteData(FlxG.sound.music.time);
|
||||
|
||||
Conductor.instance.update(FlxG.sound?.music?.time ?? 0.0);
|
||||
|
||||
// I hate this function - [GitHub username]
|
||||
resyncVocals();
|
||||
}
|
||||
```
|
||||
|
||||
```haxe
|
||||
// End the song when the music is complete.
|
||||
FlxG.sound.music.onComplete = function() {
|
||||
endSong(skipEndingTransition);
|
||||
};
|
||||
// A negative instrumental offset means the song skips the first few milliseconds of the track.
|
||||
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
|
||||
FlxG.sound.music.play(true, Math.max(0, startTimestamp - Conductor.instance.combinedOffset));
|
||||
FlxG.sound.music.pitch = playbackRate;
|
||||
|
||||
// Prevent the volume from being wrong.
|
||||
FlxG.sound.music.volume = 1.0;
|
||||
// IF fadetween is not null we cancel it
|
||||
if (FlxG.sound.music.fadeTween != null) FlxG.sound.music.fadeTween.cancel();
|
||||
|
||||
// Play the vocals
|
||||
trace('Playing vocals...');
|
||||
// Add the vocals
|
||||
add(vocals);
|
||||
// Play the vocals for real this time lol
|
||||
vocals.play();
|
||||
// Set the vocals volume
|
||||
vocals.volume = 1.0;
|
||||
// Set the vocals pitch
|
||||
vocals.pitch = playbackRate;
|
||||
// Set the vocals time to the music time
|
||||
vocals.time = FlxG.sound.music.time;
|
||||
// trace('${FlxG.sound.music.time}');
|
||||
// trace('${vocals.time}');
|
||||
// functionThatWasntHereBeforeThisPRorSomethingIdKLOL();
|
||||
/* testsprite = new FlxSprite(0, 0);
|
||||
testsprite.loadGraphic(Paths.image('test'));
|
||||
testsprite.screenCenter();
|
||||
add(testsprite); */
|
||||
|
||||
// Me too [GitHub username] I hate this function it gave me pain and suffering - Girlfriend
|
||||
resyncVocals();
|
||||
```
|
||||
|
||||
```haxe
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
// PAGEUP: who knows what this does
|
||||
// SHIFT+PAGEUP: There will be dire consequences.
|
||||
if (FlxG.keys.justPressed.PAGEUP) changeSection(FlxG.keys.pressed.SHIFT ? 20 : 2);
|
||||
// PAGEUP: who knows what this does
|
||||
// SHIFT+PAGEUP: There will be dire consequences.
|
||||
if (FlxG.keys.justPressed.PAGEDOWN) changeSection(FlxG.keys.pressed.SHIFT ? -20 : -2);
|
||||
#end
|
||||
```
|
||||
|
||||
#### DO:
|
||||
Below are examples on what you SHOULD do when writing code comments.
|
||||
```haxe
|
||||
/**
|
||||
* Jumps forward or backward a number of sections in the song.
|
||||
* Accounts for BPM changes, does not prevent death from skipped notes.
|
||||
* @param sections The number of sections to jump, negative to go backwards.
|
||||
*/
|
||||
function changeSection(sections:Int):Void
|
||||
{
|
||||
var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.stepsPerMeasure * sections);
|
||||
var targetTimeMs:Float = Conductor.instance.getStepTimeInMs(targetTimeSteps);
|
||||
|
||||
// Don't go back in time to before the song started.
|
||||
targetTimeMs = Math.max(0, targetTimeMs);
|
||||
|
||||
if (FlxG.sound.music != null)
|
||||
{
|
||||
FlxG.sound.music.time = targetTimeMs;
|
||||
}
|
||||
|
||||
handleSkippedNotes();
|
||||
SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition);
|
||||
|
||||
Conductor.instance.update(FlxG.sound?.music?.time ?? 0.0);
|
||||
|
||||
resyncVocals();
|
||||
}
|
||||
```
|
||||
|
||||
```haxe
|
||||
FlxG.sound.music.onComplete = function() {
|
||||
endSong(skipEndingTransition);
|
||||
};
|
||||
// A negative instrumental offset means the song skips the first few milliseconds of the track.
|
||||
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
|
||||
FlxG.sound.music.play(true, Math.max(0, startTimestamp - Conductor.instance.combinedOffset));
|
||||
FlxG.sound.music.pitch = playbackRate;
|
||||
|
||||
// Prevent the volume from being wrong.
|
||||
FlxG.sound.music.volume = 1.0;
|
||||
if (FlxG.sound.music.fadeTween != null) FlxG.sound.music.fadeTween.cancel();
|
||||
|
||||
trace('Playing vocals...');
|
||||
add(vocals);
|
||||
vocals.play();
|
||||
vocals.volume = 1.0;
|
||||
vocals.pitch = playbackRate;
|
||||
vocals.time = FlxG.sound.music.time;
|
||||
resyncVocals();
|
||||
```
|
||||
|
||||
```haxe
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
// PAGEUP: Skip forward two sections.
|
||||
// SHIFT+PAGEUP: Skip forward twenty sections.
|
||||
if (FlxG.keys.justPressed.PAGEUP) changeSection(FlxG.keys.pressed.SHIFT ? 20 : 2);
|
||||
// PAGEDOWN: Skip backward two section. Doesn't replace notes.
|
||||
// SHIFT+PAGEDOWN: Skip backward twenty sections.
|
||||
if (FlxG.keys.justPressed.PAGEDOWN) changeSection(FlxG.keys.pressed.SHIFT ? -20 : -2);
|
||||
#end
|
||||
```
|
||||
|
||||
## Documentation PRs
|
||||
Documentation-based PRs make changes such as **fixing typos** or **adding new information** in documentation files.
|
||||
|
||||
This involves modifying one or several of the repository’s `.md` files, found throughout the repository.
|
||||
|
||||
Make sure your changes are easy to understand and formatted consistently to maximize clarity and readability.
|
||||
|
||||
> [!CAUTION]
|
||||
> DO NOT TOUCH THE `LICENSE.md` FILE, EVEN TO MAKE SMALL CHANGES!
|
||||
|
||||
### Example Documentation PR
|
||||
#### DO NOT:
|
||||
```
|
||||
// The original documentation
|
||||
- `F2`: ***OVERLAY***: Enables the Flixel debug overlay, which has partial support for scripting.
|
||||
- `F3`: ***SCREENSHOT***: Takes a screenshot of the game and saves it to the local `screenshots` directory. Works outside of debug builds too!
|
||||
- `F4`: ***EJECT***: Forcibly switch state to the Main Menu (with no extra transition). Useful if you're stuck in a level and you need to get out!
|
||||
- `F5`: ***HOT RELOAD***: Forcibly reload the game's scripts and data files, then restart the current state. If any files in the `assets` folder have been modified, the game should process the changes for you! NOTE: Known bug, this does not reset song charts or song scripts, but it should reset everything else (such as stage layout data and character animation data).
|
||||
- `CTRL-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace.
|
||||
|
||||
// The new PR additions
|
||||
- `ctrl alt shift e`: No idea what this does
|
||||
- `alt-f4`: closes the game
|
||||
```
|
||||
|
||||
#### DO:
|
||||
```
|
||||
// The original documentation
|
||||
- `F2`: ***OVERLAY***: Enables the Flixel debug overlay, which has partial support for scripting.
|
||||
- `F3`: ***SCREENSHOT***: Takes a screenshot of the game and saves it to the local `screenshots` directory. Works outside of debug builds too!
|
||||
- `F4`: ***EJECT***: Forcibly switch state to the Main Menu (with no extra transition). Useful if you're stuck in a level and you need to get out!
|
||||
- `F5`: ***HOT RELOAD***: Forcibly reload the game's scripts and data files, then restart the current state. If any files in the `assets` folder have been modified, the game should process the changes for you! NOTE: Known bug, this does not reset song charts or song scripts, but it should reset everything else (such as stage layout data and character animation data).
|
||||
- `CTRL-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace.
|
||||
|
||||
// The new PR additions
|
||||
- `CTRL-ALT-SHIFT-E`: ***DUMP SAVE DATA***: Prompts the user to save their save data as a JSON file, so its contents can be viewed. Only available on debug builds.
|
||||
- `ALT-F4`: ***CLOSE GAME***: Closes the game forcibly on Windows.
|
||||
```
|
||||
|
||||
## GitHub PRs
|
||||
GitHub-related PRs make changes such as **tweaking Issue Templates** or **updating the repository’s workflows**.
|
||||
|
||||
This involves modifying one or several of the repository’s `.yml` files, or any other file in the `.github` folder.
|
||||
|
||||
Please test these changes on your fork’s main branch to avoid breaking anything in this repository (e.g. GitHub Actions, issue templates, etc.)!
|
||||
|
||||
## funkin.assets PRs
|
||||
The `assets` submodule has its own repository called [funkin.assets](https://github.com/FunkinCrew/funkin.assets).
|
||||
|
||||
If you only modify files in the `assets` folder, open a PR in the `funkin.assets` repository instead of the main repository.
|
||||
|
||||
If you simultaneously modify files from both repositories, then open two separate PRs and explain the connection in your PR descriptions.
|
||||
|
||||
Be sure to choose `main` as the base branch for `funkin.assets` PRs, as no `develop` branch exists for that repository.
|
||||
|
||||
### Charting PRs
|
||||
Charting PRs make changes such as **adjusting chart metadata**.
|
||||
|
||||
This involves modifying one or several of the `funkin.assets` repository's `*-metadata.json` files, found in the `preload/data/songs/` directory.
|
||||
|
||||
> [!CAUTION]
|
||||
> We **do not** accept changes to the chart `.json` file itself.
|
||||
> If you find an error in the game's charts, please open an issue in the `Funkin` repo, and the Funkin' Crew will review it and make tweaks themselves as necessary.
|
||||
|
||||
These PRs should only be opened in the `funkin.assets` repository.
|
||||
|
||||
# Closing
|
||||
Thank you for reading the Contributing Guide.
|
||||
|
||||
We look forward to seeing your contributions to the game!
|
||||
@@ -0,0 +1,186 @@
|
||||
# .fnfc File Specification
|
||||
|
||||
*Updated 2024-04-29*
|
||||
|
||||
- Manifest version: `1.0.0`
|
||||
- Metadata version: `2.2.2`
|
||||
- Chart data version: `2.0.0`
|
||||
|
||||
## Introduction
|
||||
|
||||
This document describes the structure of the FNFC file format used for saving and loading charts in the Chart Editor for Friday Night Funkin'. It is designed to refactor how the game stores levels, as the original format was clunky and poorly extensible.
|
||||
|
||||
FNFC files are a store of all the required files for crediting and editing charts. This includes any relevant audio files, which is perfect for collaboration between charters.
|
||||
|
||||
## Overview
|
||||
|
||||
Friday Night Funkin' charts utilize a concept called "variations"; these are groups of difficulties which share gameplay data. This is done to prevent significant redundancy; if each chart
|
||||
|
||||
If a difficulty for a song should have the same events as another difficulty, those difficulties should be in the same variation (for example, Normal and Hard). If a particular difficulty for a song should have different song events, characters, stages, or music (including instruments or vocals) from the base variation, they should use a different variation. Difficulties for a song start in the `default` variation.
|
||||
|
||||
An example of this is the Erect and Nightmare difficulties. These are defined in an alternative variation, which allows for those difficulties to utilize different events and music from the base variation.
|
||||
|
||||
The Chart Editor is made aware of which variations are available in the current chart file by querying the `playData.songVariations` key in the `default` variation's metadata file.
|
||||
|
||||
The chart data itself is split into two files. The `<songid>-metadata.json` file should contain all the information the game needs to display a song in menus such as Freeplay, including but not limited to the song name, artist, and BPM. The `<songid>-chart.json` should only contain the note data for each difficulty of that variation, and the song event data for the variation. This allows the game to, when it first loads, parse and cache the metadata for all available songs for use in menus, while keeping the bulky chart file data unloaded until that chart specifically is played.
|
||||
|
||||
Note that the game itself does not store its songs as FNFC files; rather, it stores the `metadata.json`, `chart.json`, and `.ogg` files separately.
|
||||
|
||||
Note also that the files may include some values whose functionality are not yet fully implemented into the game itself.
|
||||
|
||||
## File Contents
|
||||
|
||||
FNFC files are standard ZIP files containing the following:
|
||||
|
||||
- `manifest.json`: This file contains minimal information to minimize work in the parsing of chart files.
|
||||
- `Inst.ogg`: Song instrumental for the default variation.
|
||||
- `Inst-<instid>.ogg`: *(optional)* An alternative instrumental which can be used.
|
||||
- `Voices-<charid>.ogg`: *(optional)* Song vocals for a specific character, for the default variation.
|
||||
- `Voices-<charid>-<variation>.ogg`: *(optional)* Song vocals for a specific character, for an alternate variation.
|
||||
- `<songid>-metadata.json`: Song metadata for the `default` variation.
|
||||
- `<songid>-metadata-<variation>.json`: *(optional)* Song metadata for alternate variations.
|
||||
- `<songid>-chart.json`: Song chart data for the `default` variation.
|
||||
- `<songid>-chart-<variation>.json`: *(optional)* Song chart data for alternate variations.
|
||||
|
||||
## Files
|
||||
|
||||
Note that each component file contains its own separate [Semantic Version](https://semver.org/) number which the game adheres to when parsing. New functionality (with backwards compatibility) should be represented by a `1.x.0` change and breaking changes should be kept to a minimum, and represented by a `x.0.0` change.
|
||||
|
||||
### manifest.json
|
||||
|
||||
`manifest.json` is a JSON-formatted text file, containing a single object with the following keys:
|
||||
|
||||
- `version`: The Semantic Version string for the manifest file.
|
||||
- `songId`: The song ID associated with this chart. Used to allow the Chart Editor to easily determine the proper filenames for the `metadata` and `chart` files.
|
||||
|
||||
#### manifest.json Example
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"version": "1.0.0", // The Semantic Version string.
|
||||
"songId": "dadbattle" // The song ID.
|
||||
}
|
||||
```
|
||||
|
||||
### Inst.ogg
|
||||
|
||||
This is an audio file in the OGG container format with the Vorbis audio codec.
|
||||
|
||||
This file is mandatory. A chart file without an instrumental track is considered invalid.
|
||||
|
||||
This file is used as the default backing track for the song.
|
||||
|
||||
### Inst-<instid>.ogg
|
||||
|
||||
This is an audio file in the OGG container format with the Vorbis audio codec.
|
||||
|
||||
This file is optional if no alternative instrumental is specified in any variation.
|
||||
|
||||
This file is used as an alternate backing track for the song. It is specified using the `playData.characters.instrumental` key in the current variation's metadata file.
|
||||
|
||||
### Voices-<charid>.ogg
|
||||
|
||||
This is an audio file in the OGG container format with the Vorbis audio codec.
|
||||
|
||||
This file is optional. The game will look for the specified file but will ignore if it is missing.
|
||||
|
||||
This file is used for the character vocal track for the song. The game will look for and play the vocal track for the player (using the ID specified by the `playData.characters.player` key in the current variation's metadata file), and the opponent (using the ID specified by the `playData.characters.opponent` key in the current variation's metadata file).
|
||||
|
||||
### Voices-<charid>-<variation>.ogg
|
||||
|
||||
This is an audio file in the OGG container format with the Vorbis audio codec.
|
||||
|
||||
This file is optional. The game will look for the specified file but will ignore if it is missing.
|
||||
|
||||
This file is used for the character vocal track for the song, for the given variation. The game will look for and play the vocal track for the player and the opponent using the same JSON keys as the `Voices-<charid>.ogg` files for the default variation, while also applying the current variation ID.
|
||||
|
||||
### <songid>-metadata.json
|
||||
|
||||
`<songid>-metadata.json` is a JSON-formatted text file, specifying metadata about the default variation for this song. It has the following keys:
|
||||
|
||||
- `version`: The Semantic Version string for the metadata file.
|
||||
- `songName`: The human readable name for the song, as a string.
|
||||
- `artist`: The human readable artist(s) for the song, as a string.
|
||||
- `timeFormat`: The time format. In the future, this will allow chart files to define the timestamps for BPM changes, note data, or event data, in fractional beats and steps, but for now the only supported value is the string `"ms"`.
|
||||
- `timeChanges`: An array of Song Time Change objects. Note that at least one Song Time Change object must be specified, with a timestamp of `0`.
|
||||
- `playData`: A Song Play Data object.
|
||||
- `offsets`: A Song Offset Data object.
|
||||
- `generatedBy`: A string specified when creating a chart. Should only be used for debugging purposes, and not read or used by the game. Custom engines should modify `Constants.hx` to ensure unique values in case of issues with the metadata or chart data.
|
||||
|
||||
The Song Time Change objects have the following keys:
|
||||
- `t`: The timestamp for the BPM change, in milliseconds, as a float.
|
||||
- `bpm`: The new song timing, in beats per minute, as a float.
|
||||
- `n`: *optional* Time signature numerator, as an integer. Defaults to 4. (int). Optional, defaults to `4`.
|
||||
- `d`: *optional* Time signature denominator, as an integer. Should only ever be a power of two. Defaults to `4`.
|
||||
- `bt`: *optional* Beat tuplets. This defines how many steps each beat is divided into. Defaults to `[4, 4, 4, 4]`
|
||||
|
||||
The Song Play Data objects have the following keys:
|
||||
- `album`: The album ID to display in the Freeplay menu, as a string ID.
|
||||
- `previewStart`: The timestamp to begin the audio preview for this track in the Freeplay Menu, in milliseconds, as a float.
|
||||
- `previewEnd`: The timestamp to end the audio preview for this track in the Freeplay Menu, in milliseconds, as a float.
|
||||
- `ratings`: An map object; each key is a difficulty ID and each value is an integer difficulty rating value for display in Freeplay.
|
||||
- `songVariations`: An array of string variation IDs this song has. The game will attempt to read `<songid>-metadata-<variationid>.json` and `<songid>-chart-<variationid>.json` files for each variation ID included in this list.
|
||||
- `difficulties`: An array of string difficulties this song has available to play. Any difficulties in this list will be made available to players in-game, and any difficulties not in this list will be ignored.
|
||||
- `characters`: A Song Character Data object.
|
||||
- `stage`: The stage to use for this chart, as a string ID.
|
||||
- `noteStyle`: The note style to use for this chart, as a string ID.
|
||||
|
||||
The Song Character Data objects have the following keys:
|
||||
- `player`: The player character to use, as a string ID.
|
||||
- `girlfriend`: The girlfriend character to use, as a string ID.
|
||||
- `opponent`: The opponent character to use, as a string ID.
|
||||
- `instrumental`: The instrumental ID to use. Defaults to a blank string to use `Inst.ogg`
|
||||
- `altInstrumentals`:
|
||||
|
||||
The Song Offset Data objects have the following keys:
|
||||
|
||||
#### <songid>-metadata.json Example
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"version": "2.2.2", // Semantic Version string
|
||||
"songName": "DadBattle", // Readable song name
|
||||
"artist": "Kawai Sprite", // Song artist(s)
|
||||
"timeFormat": "ms", // Time format to use
|
||||
"timeChanges": [{ // List of BPM changes. Must have at least one.
|
||||
"t": 0, // BPM change timestamp in milliseconds.
|
||||
"bpm": 180 // The target BPM.
|
||||
}],
|
||||
"playData": {
|
||||
"album": "volume1", // The album to display in Freeplay.
|
||||
"previewStart": 0, // Time (ms) to start preview at
|
||||
"previewEnd": 15000, // Time (ms) to end preview at
|
||||
"ratings": { // Rating data for each difficulty
|
||||
"easy": 1, // Rating for Easy difficulty
|
||||
"normal": 3, // Rating for Normal difficulty
|
||||
"hard": 5 // Rating for Hard difficulty
|
||||
},
|
||||
"songVariations": [ // Available variation files.
|
||||
"erect" // This says dadbattle-metadata-erect.json exists.
|
||||
],
|
||||
"difficulties": [ // Available difficulties.
|
||||
"easy",
|
||||
"normal",
|
||||
"hard"
|
||||
],
|
||||
"characters": { // Characters to use for this variation.
|
||||
"player": "bf", // Boyfriend
|
||||
"girlfriend": "gf", // Girlfriend
|
||||
"opponent": "dad" // Daddy Dearest
|
||||
},
|
||||
"stage": "mainStage", // Week 1 stage.
|
||||
"noteStyle": "funkin" // Default note style.
|
||||
},
|
||||
"generatedBy": "EliteMasterEric (by hand)" // Unique string.
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### <songid>-chart.json
|
||||
Song chart data for the `default` variation.
|
||||
|
||||
### <songid>-metadata-<variation>.json
|
||||
*(optional)* Song metadata for alternate variations.
|
||||
|
||||
### <songid>-chart-<variation>.json
|
||||
*(optional)* Song chart data for alternate variations.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Funkin' Debug Hotkeys
|
||||
|
||||
Most of this functionality is only available on debug builds of the game!
|
||||
|
||||
## Any State
|
||||
- `INSERT`: ***MEMORY GARBAGE COLLECTION***: Immediately perform memory garbage collection.
|
||||
- `F2`: ***OVERLAY***: Enables the Flixel debug overlay, which has partial support for scripting.
|
||||
- `F3`: ***SCREENSHOT***: Takes a screenshot of the game and saves it to the local `screenshots` directory. Works outside of debug builds too!
|
||||
- `F4`: ***EJECT***: Forcibly switch state to the Main Menu (with no extra transition). Useful if you're stuck in a level and you need to get out!
|
||||
- `F5`: ***HOT RELOAD***: Forcibly reload the game's scripts and data files, then restart the current state. If any files in the `assets` folder have been modified, the game should process the changes for you! NOTE: Known bug, this does not reset song charts or song scripts, but it should reset everything else (such as stage layout data and character animation data).
|
||||
- `CTRL-ALT-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace. (Only works in the Main Menu on debug builds).
|
||||
|
||||
- `CTRL-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace.
|
||||
|
||||
## **Play State**
|
||||
- `H`: ***HIDE UI***: Makes the user interface invisible. Works in Pause Menu, great for screenshots.
|
||||
- `1`: ***END SONG***: Immediately ends the song and moves to Results Screen on Freeplay, or next song on Story Mode.
|
||||
- `2`: ***GAIN HEALTH***: Debug function, add 10% to the player's health.
|
||||
- `3`: ***LOSE HEALTH***: Debug function, subtract 5% to the player's health.
|
||||
- `9`: NEATO!
|
||||
- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL***: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead.
|
||||
- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL***: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead.
|
||||
- `CHART Hotkey`: ***OPEN CHART EDITOR***: Open the Chart Editor with the current song loaded.
|
||||
- `STAGE Hotkey`: ***OPEN STAGE EDITOR***: Open the old Stage Editor overlay.
|
||||
|
||||
## **Freeplay State**
|
||||
- `P`: ***SWITCH TO PICO/BF***: Switch to Pico or BF (reopens Freeplay as the other character).
|
||||
- `T`: ***TEST RANK ANIMATION***: Play the Gold Perfect rank animation on the currently selected song.
|
||||
- `SHIFT-ACCEPT`: ***BOTPLAY MODE***: Start a song with Botplay mode enabled.
|
||||
- `CTRL-Switch Song`: Play the first unlocked alternate instrumental as the song preview.
|
||||
|
||||
## **Title State**
|
||||
- `ESCAPE`: ***QUIT***: Quit the game.
|
||||
- `D`: COOL
|
||||
- `UI LEFT`/`UI RIGHT Hotkey`: 2COOL
|
||||
- `Y`: WOAH
|
||||
|
||||
## **Main Menu**
|
||||
- `~`: ***DEBUG***: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu.
|
||||
- `SHIFT-ACCEPT`: ***OPEN FREEPLAY AS PICO***: When selecting Freeplay, open the Freeplay menu with the playable character set to Pico.
|
||||
- `CTRL-ALT-SHIFT-W`: ***ALL ACCESS***: Unlocks all songs in Freeplay. Only available on debug builds.
|
||||
- `CTRL-ALT-SHIFT-M`: ***NO MORE ACCESS***: Re-locks all songs in Freeplay except those unlocked by default. Only available on debug builds.
|
||||
- `CTRL-ALT-SHIFT-R`: ***GREAT SCORE?***: Give the user a hypothetical overridden score, and see if we can maintain that golden P rank. Only available on debug builds.
|
||||
- `CTRL-ALT-SHIFT-P`: ***CHARACTER UNLOCK SCREEN***: Forces the Character Select screen to play Pico's unlocking animation. Only available on debug builds.
|
||||
- `CTRL-ALT-SHIFT-N`: ***CHARACTER NOT SEEN***: Marks all characters as not seen and enables BF's new character unlocked animation in Freeplay. Only available on debug builds.
|
||||
- `CTRL-ALT-SHIFT-E`: ***DUMP SAVE DATA***: Prompts the user to save their save data as a JSON file, so its contents can be viewed. Only available on debug builds.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Tracy Performance Profiling
|
||||
|
||||
In v0.5.1, Funkin' gained support for a powerful instrumentation-based profiler known as Tracy. This development tool allows you to see exactly what the game is doing at any given moment, how long each function is taking to call, and even how memory is allocated and deallocated. This is the most powerful tool in your toolbox for diagnosing and resolving performance issues.
|
||||
|
||||
## How to Use Tracy
|
||||
|
||||
1. Download [Tracy](https://github.com/wolfpld/tracy)
|
||||
2. Build the game with the `FEATURE_DEBUG_TRACY` flag. For example, `lime build windows -debug -DFEATURE_DEBUG_TRACY`
|
||||
3. Start Tracy, click Connect. Tracy will start waiting for Funkin' to start.
|
||||
4. Start the game with Tracy enabled.
|
||||
|
||||

|
||||
@@ -0,0 +1,53 @@
|
||||
# Troubleshooting Common Compilation Issues
|
||||
|
||||
- Any output containing `WARNING` or `(WDeprecated)`
|
||||
- Will not disrupt compilation and can be safely ignored.
|
||||
|
||||
- `This version of hxcpp` ... `Would you like to do this now [y/n]`
|
||||
- Type "y" into the console and press Enter.
|
||||
|
||||
- Weird macro error with a very tall call stack: Restart Visual Studio Code
|
||||
- NOTE: This is caused by Polymod somewhere, and seems to only occur when there is another compile error somewhere in the program. There is a bounty up for it.
|
||||
|
||||
- `Get Thread Context Failed`
|
||||
- Turn off other expensive applications while building.
|
||||
|
||||
- `Type not found: T1`
|
||||
- This is thrown by `json2object`, make sure the data type of `@:default` is correct.
|
||||
- NOTE: `flixel.util.typeLimit.OneOfTwo` isn't supported.
|
||||
|
||||
- `Class lists not properly generated. Try cleaning out your export folder, restarting your IDE, and rebuilding your project.`
|
||||
- This is a bug specific to HTML5. Simply perform the steps listed (don't forget to restart the IDE too).
|
||||
|
||||
- `LINK : fatal error LNK1201: error writing to program database ''; check for insufficient disk space, invalid path, or insufficient privilege`
|
||||
- This error occurs if the PDB file located in your `export` folder is in use or exceeds 4 GB. Try deleting the `export` folder and building again from scratch.
|
||||
|
||||
- `error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)`
|
||||
- This error can happen during cloning as a result of poor network connectivity. A common fix is to run `git config --global http.postBuffer 4096M` in your terminal.
|
||||
|
||||
- Repository is missing an `assets` folder, or `assets` folder is empty.
|
||||
- You did not clone the repository correctly! Copy the path to your `funkin` folder and run `cd the\path\you\copied`. Then follow the compilation guide starting from **Step 4**.
|
||||
|
||||
- Other compilation issues may be caused by installing bad library versions. Try deleting the `.haxelib` folder and following the guide starting from **Step 5**.
|
||||
|
||||
## Lime Related Issues
|
||||
- Segmentation fault and/or crash after `Done mapping time changes: [SongTimeChange(0ms,102bpm)]`
|
||||
- Caused by using official Lime instead of Funkin's fork. Reinstalling Lime should fix it.
|
||||
(NOTE: Make sure you do this via `hmm` (e.g `hmm reinstall -f lime`) to guarantee you get Funkin's version of Lime.)
|
||||
|
||||
- `Uncaught exception - Could not find lime.ndll.` ... `Advanced users may run "lime rebuild cpp" instead.`
|
||||
- If on Linux:
|
||||
- The binaries GLibC version might more recent than the one your system supports. Running the commands below should fix it.
|
||||
```
|
||||
cd .haxelib/lime/git
|
||||
git submodule init
|
||||
git submodule sync
|
||||
git submodule update
|
||||
cd ../../..
|
||||
# Note: The command and packages here might be different depending on your distro.
|
||||
sudo apt install libgl1-mesa-dev libglu1-mesa-dev g++ g++-multilib gcc-multilib libasound2-dev libx11-dev libxext-dev libxi-dev libxrandr-dev libxinerama-dev libpulse-dev
|
||||
lime rebuild cpp -64 -release -clean
|
||||
```
|
||||
|
||||
- The binaries are missing for some reason. You can download pre-built binaries from [Funkin's Lime](https://github.com/FunkinCrew/lime/tree/dev-funkin/ndll).
|
||||
You should copy them to `.haxelib/lime/git/ndll/<PLATFORM>64/`, where `<PLATFORM>` is the current platform you're on, which can be either Windows, Linux or MacOS.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Funkin' Repo Code Style Guide
|
||||
|
||||
This short document is designed to give a run-down on how code should be formatted to maintain a consistent style throughout, making the repo easier to maintain.
|
||||
|
||||
## Notes on IDEs and Extensions
|
||||
|
||||
The Visual Studio Code IDE is highly recommended, as this repo contains various configurations that work with VSCode extensions to automatically style certain things for you.
|
||||
|
||||
VSCode is also the only IDE that has any good tools for Haxe so yeah.
|
||||
|
||||
## Whitespace and Indentation
|
||||
|
||||
The Haxe extension of VSCode will use the repo's `hxformat.json` to tell VSCode how to format the code files of the repo. Enabling VSCode's "Format on Save" feature is recommended.
|
||||
|
||||
## Variable and Function Names
|
||||
|
||||
It is recommended to name variables and functions with descriptive titles, in lowerCamelCase. There is no penalty for giving a variable a longer name as long as it isn't excessive.
|
||||
|
||||
## Code Comments
|
||||
|
||||
The CodeDox extension for VSCode provides extensive support for JavaDoc-style code comments, and these should be used for public functions wherever possible to ensure the usage of each function is clear.
|
||||
|
||||
Example:
|
||||
```
|
||||
/**
|
||||
* Finds the largest deviation from the desired time inside this VoicesGroup.
|
||||
*
|
||||
* @param targetTime The time to check against.
|
||||
* If none is provided, it checks the time of all members against the first member of this VoicesGroup.
|
||||
* @return The largest deviation from the target time found.
|
||||
*/
|
||||
public function checkSyncError(?targetTime:Float):Float
|
||||
```
|
||||
|
||||
## Commenting Unused Code
|
||||
|
||||
Do not comment out sections of code that are unused. Keep these snippets elsewhere or remove them. Older chunks of code can be retrieved by referring to the older Git commits, and having large chunks of commented code makes files longer and more confusing to navigate.
|
||||
|
||||
## License Headers
|
||||
|
||||
Do not include headers specifying code license on individual files in the repo, since the main `LICENSE.md` file covers all of them.
|
||||
|
||||
## Imports
|
||||
|
||||
Imports should be placed in a single group, in alphabetical order, at the top of the code file. The exception is conditional imports (using compile defines), which should be placed at the end of the list (and sorted alphabetically where possible).
|
||||
|
||||
Example:
|
||||
```
|
||||
import haxe.format.JsonParser;
|
||||
import openfl.Assets;
|
||||
import openfl.geom.Matrix;
|
||||
import openfl.geom.Matrix3D;
|
||||
#if sys
|
||||
import funkin.io.FileUtil;
|
||||
import sys.io.File;
|
||||
#end
|
||||
```
|
||||
|
||||
## Argument Formatting
|
||||
|
||||
[Optional arguments](https://haxe.org/manual/types-function-optional-arguments.html) and [default arguments](https://haxe.org/manual/types-function-default-values.html) should be mutually exclusive and not used together!
|
||||
|
||||
For example, `myFunction(?input:Int)` should be used if you want the argument to be a `Null<Int>` whose value is `null` if no value is passed, and `myFunction(input:Int = 0)` should be used if you want the argument to be an `Int`, whose value is `0` if no value is passed.
|
||||
|
||||
Using both at the same time is considered valid by Haxe, but `myFunction(?input:Int = 0)` results in a `Null<Int>` whose value defaults to 0 anyway, so it's never null, but it's annotated as nullable! The biggest consequence of this is that it makes null safety more annoying.
|
||||
@@ -0,0 +1,6 @@
|
||||
# introMod
|
||||
|
||||
This intro mod demonstrates two simple things:
|
||||
|
||||
1. You can replace any game asset simply by placing a modded asset in the right spot.
|
||||
2. You can append to text files simply by placing a text file in the right spot, but under the `_append` directory.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Intro Mod",
|
||||
"description": "An introductory mod.",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "EliteMasterEric"
|
||||
}
|
||||
],
|
||||
"api_version": "0.5.0",
|
||||
"mod_version": "1.0.0",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
awesomes tream--really awesome
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Testing123",
|
||||
"description": "Newgrounds? More like OLDGROUNDS lol.",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "EliteMasterEric"
|
||||
}
|
||||
],
|
||||
"api_version": "0.5.0",
|
||||
"mod_version": "1.0.0",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "FlxPartialSound",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "41f35ddb1eb9d10bc742e6f8b5bcc62f9ef8ad84",
|
||||
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
|
||||
},
|
||||
{
|
||||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "fffb1a74cf08f63dacc2ab09976340563f5b6e6d",
|
||||
"url": "https://github.com/FunkinCrew/flixel"
|
||||
},
|
||||
{
|
||||
"name": "flixel-addons",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "b9118f47f43a66bc0e5fbfcfd9903f0425e918ee",
|
||||
"url": "https://github.com/FunkinCrew/flixel-addons"
|
||||
},
|
||||
{
|
||||
"name": "flixel-text-input",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "951a0103a17bfa55eed86703ce50b4fb0d7590bc",
|
||||
"url": "https://github.com/FunkinCrew/flixel-text-input"
|
||||
},
|
||||
{
|
||||
"name": "flxanimate",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "f8842cea9883d6112a2c854cf93fa72856171428",
|
||||
"url": "https://github.com/FunkinCrew/flxanimate"
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
"type": "haxelib",
|
||||
"version": "3.5.0"
|
||||
},
|
||||
{
|
||||
"name": "funkin.vis",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "1966f8fbbbc509ed90d4b520f3c49c084fc92fd6",
|
||||
"url": "https://github.com/FunkinCrew/funkVis"
|
||||
},
|
||||
{
|
||||
"name": "grig.audio",
|
||||
"type": "git",
|
||||
"dir": "src",
|
||||
"ref": "57f5d47f2533fd0c3dcd025a86cb86c0dfa0b6d2",
|
||||
"url": "https://gitlab.com/haxe-grig/grig.audio.git"
|
||||
},
|
||||
{
|
||||
"name": "hamcrest",
|
||||
"type": "haxelib",
|
||||
"version": "3.0.0"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "07fc7aa8098deaea633b0726d01f83eb4ef8a832",
|
||||
"url": "https://github.com/haxeui/haxeui-core"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "b899a4c7d7318c5ff2b1bb645fbc73728fad1ac9",
|
||||
"url": "https://github.com/haxeui/haxeui-flixel"
|
||||
},
|
||||
{
|
||||
"name": "hscript",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "27c86f9a761c1d16d4433c4cf252eccb7b2e18de",
|
||||
"url": "https://github.com/FunkinCrew/hscript"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "v4.3.75",
|
||||
"url": "https://github.com/HaxeFoundation/hxcpp"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp-debug-server",
|
||||
"type": "git",
|
||||
"dir": "hxcpp-debug-server",
|
||||
"ref": "147294123f983e35f50a966741474438069a7a8f",
|
||||
"url": "https://github.com/FunkinCrew/hxcpp-debugger"
|
||||
},
|
||||
{
|
||||
"name": "hxdiscord_rpc",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "82c47ecc1a454b7dd644e4fcac7e91155f176dec",
|
||||
"url": "https://github.com/FunkinCrew/hxdiscord_rpc"
|
||||
},
|
||||
{
|
||||
"name": "hxjsonast",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "20e72cc68c823496359775ac1f06500e67f189d5",
|
||||
"url": "https://github.com/nadako/hxjsonast/"
|
||||
},
|
||||
{
|
||||
"name": "hxp",
|
||||
"type": "haxelib",
|
||||
"version": "1.3.0"
|
||||
},
|
||||
{
|
||||
"name": "hxvlc",
|
||||
"type": "haxelib",
|
||||
"version": "2.1.2"
|
||||
},
|
||||
{
|
||||
"name": "json2object",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
|
||||
"url": "https://github.com/FunkinCrew/json2object"
|
||||
},
|
||||
{
|
||||
"name": "jsonpatch",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "f9b83215acd586dc28754b4ae7f69d4c06c3b4d3",
|
||||
"url": "https://github.com/EliteMasterEric/jsonpatch"
|
||||
},
|
||||
{
|
||||
"name": "jsonpath",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "7a24193717b36393458c15c0435bb7c4470ecdda",
|
||||
"url": "https://github.com/EliteMasterEric/jsonpath"
|
||||
},
|
||||
{
|
||||
"name": "lime",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "be81ad7e4e1a92c3482bcc009648a4ac892cfa35",
|
||||
"url": "https://github.com/FunkinCrew/lime"
|
||||
},
|
||||
{
|
||||
"name": "mconsole",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "06c0499ed8f80628a0e6e55ffa32c3cbd688a838",
|
||||
"url": "https://github.com/massive-oss/mconsole"
|
||||
},
|
||||
{
|
||||
"name": "mcover",
|
||||
"type": "git",
|
||||
"dir": "src",
|
||||
"ref": "c3c47cd682b0b202a41caee95321989391b617ef",
|
||||
"url": "https://github.com/massive-oss/mcover"
|
||||
},
|
||||
{
|
||||
"name": "mockatoo",
|
||||
"type": "git",
|
||||
"dir": "src",
|
||||
"ref": "13d77a0a8eaf5e789ef5dae6cd33eee812deda36",
|
||||
"url": "https://github.com/FunkinCrew/mockatoo"
|
||||
},
|
||||
{
|
||||
"name": "munit",
|
||||
"type": "git",
|
||||
"dir": "src",
|
||||
"ref": "f61be7f7ba796595f45023ca65164a485aba0e7e",
|
||||
"url": "https://github.com/FunkinCrew/MassiveUnit"
|
||||
},
|
||||
{
|
||||
"name": "newgrounds",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "c8b30832027fa04f48fe6a45fa7903d5b265d56e",
|
||||
"url": "https://github.com/Geokureli/Newgrounds/"
|
||||
},
|
||||
{
|
||||
"name": "openfl",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "d061c936b462f040304ec2bd42d9f59d2e59e285",
|
||||
"url": "https://github.com/FunkinCrew/openfl"
|
||||
},
|
||||
{
|
||||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "0fbdf27fe124549730accd540cec8a183f8652c0",
|
||||
"url": "https://github.com/larsiusprime/polymod"
|
||||
},
|
||||
{
|
||||
"name": "thx.core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "76d87418fadd92eb8e1b61f004cff27d656e53dd",
|
||||
"url": "https://github.com/fponticelli/thx.core"
|
||||
},
|
||||
{
|
||||
"name": "thx.semver",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "bdb191fe7cf745c02a980749906dbf22719e200b",
|
||||
"url": "https://github.com/fponticelli/thx.semver"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"disableFormatting": false,
|
||||
|
||||
"indentation": {
|
||||
"character": " ",
|
||||
"tabWidth": 2,
|
||||
"indentCaseLabels": true
|
||||
},
|
||||
"lineEnds": {
|
||||
"anonFunctionCurly": {
|
||||
"emptyCurly": "break",
|
||||
"leftCurly": "after",
|
||||
"rightCurly": "both"
|
||||
},
|
||||
"leftCurly": "both",
|
||||
"rightCurly": "both"
|
||||
},
|
||||
|
||||
"sameLine": {
|
||||
"ifBody": "same",
|
||||
"ifElse": "next",
|
||||
"doWhile": "next",
|
||||
"tryBody": "next",
|
||||
"tryCatch": "next"
|
||||
},
|
||||
|
||||
"whitespace": {
|
||||
"switchPolicy": "around"
|
||||
}
|
||||
}
|
||||
+1193
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+148
@@ -0,0 +1,148 @@
|
||||
package;
|
||||
|
||||
import flixel.FlxGame;
|
||||
import flixel.FlxState;
|
||||
import funkin.Preferences;
|
||||
import funkin.util.logging.CrashHandler;
|
||||
import funkin.ui.debug.MemoryCounter;
|
||||
import funkin.save.Save;
|
||||
import haxe.ui.Toolkit;
|
||||
import openfl.display.FPS;
|
||||
import openfl.display.Sprite;
|
||||
import openfl.events.Event;
|
||||
import openfl.Lib;
|
||||
import openfl.media.Video;
|
||||
import openfl.net.NetStream;
|
||||
import funkin.util.WindowUtil;
|
||||
|
||||
/**
|
||||
* The main class which initializes HaxeFlixel and starts the game in its initial state.
|
||||
*/
|
||||
class Main extends Sprite
|
||||
{
|
||||
var gameWidth:Int = 1280; // Width of the game in pixels (might be less / more in actual pixels depending on your zoom).
|
||||
var gameHeight:Int = 720; // Height of the game in pixels (might be less / more in actual pixels depending on your zoom).
|
||||
var initialState:Class<FlxState> = funkin.InitState; // The FlxState the game starts with.
|
||||
var zoom:Float = -1; // If -1, zoom is automatically calculated to fit the window dimensions.
|
||||
var skipSplash:Bool = true; // Whether to skip the flixel splash screen that appears in release mode.
|
||||
var startFullscreen:Bool = false; // Whether to start the game in fullscreen on desktop targets
|
||||
|
||||
// You can pretty much ignore everything from here on - your code should go in your states.
|
||||
|
||||
public static function main():Void
|
||||
{
|
||||
// We need to make the crash handler LITERALLY FIRST so nothing EVER gets past it.
|
||||
CrashHandler.initialize();
|
||||
CrashHandler.queryStatus();
|
||||
|
||||
Lib.current.addChild(new Main());
|
||||
}
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
// Initialize custom logging.
|
||||
haxe.Log.trace = funkin.util.logging.AnsiTrace.trace;
|
||||
funkin.util.logging.AnsiTrace.traceBF();
|
||||
|
||||
// Load mods to override assets.
|
||||
// TODO: Replace with loadEnabledMods() once the user can configure the mod list.
|
||||
funkin.modding.PolymodHandler.loadAllMods();
|
||||
|
||||
if (stage != null)
|
||||
{
|
||||
init();
|
||||
}
|
||||
else
|
||||
{
|
||||
addEventListener(Event.ADDED_TO_STAGE, init);
|
||||
}
|
||||
}
|
||||
|
||||
function init(?event:Event):Void
|
||||
{
|
||||
if (hasEventListener(Event.ADDED_TO_STAGE))
|
||||
{
|
||||
removeEventListener(Event.ADDED_TO_STAGE, init);
|
||||
}
|
||||
|
||||
setupGame();
|
||||
}
|
||||
|
||||
var video:Video;
|
||||
var netStream:NetStream;
|
||||
var overlay:Sprite;
|
||||
|
||||
/**
|
||||
* A frame counter displayed at the top left.
|
||||
*/
|
||||
public static var fpsCounter:FPS;
|
||||
|
||||
/**
|
||||
* A RAM counter displayed at the top left.
|
||||
*/
|
||||
public static var memoryCounter:MemoryCounter;
|
||||
|
||||
function setupGame():Void
|
||||
{
|
||||
initHaxeUI();
|
||||
|
||||
// addChild gets called by the user settings code.
|
||||
fpsCounter = new FPS(10, 3, 0xFFFFFF);
|
||||
|
||||
#if !html5
|
||||
// addChild gets called by the user settings code.
|
||||
// TODO: disabled on HTML5 (todo: find another method that works?)
|
||||
memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
|
||||
#end
|
||||
|
||||
// George recommends binding the save before FlxGame is created.
|
||||
Save.load();
|
||||
|
||||
// Don't call anything from the preferences until the save is loaded!
|
||||
#if web
|
||||
// set this variable (which is a function) from the lime version at lime/_internal/backend/html5/HTML5Application.hx
|
||||
// The framerate cap will more thoroughly initialize via Preferences in InitState.hx
|
||||
funkin.Preferences.lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame");
|
||||
#end
|
||||
|
||||
WindowUtil.setVSyncMode(funkin.Preferences.vsyncMode);
|
||||
|
||||
|
||||
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, Preferences.framerate, Preferences.framerate, skipSplash, startFullscreen);
|
||||
|
||||
// FlxG.game._customSoundTray wants just the class, it calls new from
|
||||
// create() in there, which gets called when it's added to the stage
|
||||
// which is why it needs to be added before addChild(game) here
|
||||
@:privateAccess
|
||||
game._customSoundTray = funkin.ui.options.FunkinSoundTray;
|
||||
|
||||
addChild(game);
|
||||
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
|
||||
#end
|
||||
|
||||
#if hxcpp_debug_server
|
||||
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
|
||||
#else
|
||||
trace('hxcpp_debug_server is disabled! This build does not support debugging.');
|
||||
#end
|
||||
}
|
||||
|
||||
function initHaxeUI():Void
|
||||
{
|
||||
// Calling this before any HaxeUI components get used is important:
|
||||
// - It initializes the theme styles.
|
||||
// - It scans the class path and registers any HaxeUI components.
|
||||
Toolkit.init();
|
||||
Toolkit.theme = 'dark'; // don't be cringe
|
||||
// Toolkit.theme = 'light'; // embrace cringe
|
||||
Toolkit.autoScale = false;
|
||||
// Don't focus on UI elements when they first appear.
|
||||
haxe.ui.focus.FocusManager.instance.autoFocus = false;
|
||||
funkin.input.Cursor.registerHaxeUICursors();
|
||||
haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package source; // Yeah, I know...
|
||||
|
||||
import sys.FileSystem;
|
||||
import sys.io.File;
|
||||
|
||||
/**
|
||||
* A script which executes after the game is built.
|
||||
*/
|
||||
class Postbuild
|
||||
{
|
||||
static inline final BUILD_TIME_FILE:String = '.build_time';
|
||||
|
||||
static function main():Void
|
||||
{
|
||||
printBuildTime();
|
||||
}
|
||||
|
||||
static function printBuildTime():Void
|
||||
{
|
||||
// get buildEnd before fs operations since they are blocking
|
||||
var end:Float = Sys.time();
|
||||
if (FileSystem.exists(BUILD_TIME_FILE))
|
||||
{
|
||||
var fi:sys.io.FileInput = File.read(BUILD_TIME_FILE);
|
||||
var start:Float = fi.readDouble();
|
||||
fi.close();
|
||||
|
||||
sys.FileSystem.deleteFile(BUILD_TIME_FILE);
|
||||
|
||||
var buildTime:Float = roundToTwoDecimals(end - start);
|
||||
|
||||
trace('Build took: ${buildTime} seconds');
|
||||
}
|
||||
}
|
||||
|
||||
static function roundToTwoDecimals(value:Float):Float
|
||||
{
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package source; // Yeah, I know...
|
||||
|
||||
import sys.io.File;
|
||||
|
||||
/**
|
||||
* A script which executes before the game is built.
|
||||
*/
|
||||
class Prebuild
|
||||
{
|
||||
static inline final BUILD_TIME_FILE:String = '.build_time';
|
||||
|
||||
static final NG_CREDS_PATH:String = './source/funkin/api/newgrounds/NewgroundsCredentials.hx';
|
||||
|
||||
static final NG_CREDS_TEMPLATE:String = "package funkin.api.newgrounds;
|
||||
|
||||
class NewgroundsCredentials
|
||||
{
|
||||
public static final APP_ID:String = #if API_NG_APP_ID haxe.macro.Compiler.getDefine(\"API_NG_APP_ID\") #else 'INSERT APP ID HERE' #end;
|
||||
public static final ENCRYPTION_KEY:String = #if API_NG_ENC_KEY haxe.macro.Compiler.getDefine(\"API_NG_ENC_KEY\") #else 'INSERT ENCRYPTION KEY HERE' #end;
|
||||
}";
|
||||
|
||||
static function main():Void
|
||||
{
|
||||
trace('Building...');
|
||||
|
||||
saveBuildTime();
|
||||
|
||||
buildCredsFile();
|
||||
}
|
||||
|
||||
static function saveBuildTime():Void
|
||||
{
|
||||
var fo:sys.io.FileOutput = File.write(BUILD_TIME_FILE);
|
||||
var now:Float = Sys.time();
|
||||
fo.writeDouble(now);
|
||||
fo.close();
|
||||
}
|
||||
|
||||
static function buildCredsFile():Void
|
||||
{
|
||||
#if sys
|
||||
if (sys.FileSystem.exists(NG_CREDS_PATH))
|
||||
{
|
||||
trace('NewgroundsCredentials.hx already exists, skipping.');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Creating NewgroundsCredentials.hx...');
|
||||
|
||||
var fileContents:String = NG_CREDS_TEMPLATE;
|
||||
|
||||
sys.io.File.saveContent(NG_CREDS_PATH, fileContents);
|
||||
}
|
||||
#end
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Pulled from Tracey profiler PR
|
||||
* @see https://github.com/HaxeFoundation/haxe/pull/11772
|
||||
*/
|
||||
|
||||
package cpp.vm.tracy;
|
||||
|
||||
#if (!HXCPP_TRACY)
|
||||
#error "This class cannot be used without -D HXCPP_TRACY"
|
||||
#end
|
||||
enum abstract PlotFormatType(cpp.UInt8) from cpp.UInt8 to cpp.UInt8
|
||||
{
|
||||
var Number = 0;
|
||||
var Memory = 1;
|
||||
var Percentage = 2;
|
||||
}
|
||||
|
||||
@:include('hx/TelemetryTracy.h')
|
||||
extern class Native_TracyProfiler
|
||||
{
|
||||
/**
|
||||
Mark a frame. Call this at the end of each frame loop.
|
||||
**/
|
||||
@:native('::__hxcpp_tracy_framemark')
|
||||
public static function frameMark():Void;
|
||||
|
||||
/**
|
||||
Print a message into Tracy's log.
|
||||
**/
|
||||
@:native('::__hxcpp_tracy_message')
|
||||
public static function message(_msg:String, ?_color:Int = 0x000000):Void;
|
||||
|
||||
/**
|
||||
Tracy can collect additional information about the profiled application,
|
||||
which will be available in the trace description.
|
||||
This can include data such as the source repository revision,
|
||||
the application’s environment (dev/prod), etc.
|
||||
**/
|
||||
@:native('::__hxcpp_tracy_message_app_info')
|
||||
public static function messageAppInfo(_info:String):Void;
|
||||
|
||||
/**
|
||||
Plot a named value to tracy. This will generate a graph in the profiler for you.
|
||||
**/
|
||||
@:native('::__hxcpp_tracy_plot')
|
||||
public static function plot(_name:String, _val:cpp.Float32):Void;
|
||||
|
||||
/**
|
||||
Configure how values are plotted and displayed.
|
||||
**/
|
||||
@:native('::__hxcpp_tracy_plot_config')
|
||||
public static function plotConfig(_name:String, _format:PlotFormatType, ?_step:Bool = false, ?_fill:Bool = false, ?_color:Int = 0x000000):Void;
|
||||
|
||||
/**
|
||||
Set a name for the current thread this function is called in. Supply an optional groupHint so threads become grouped in Tracy's UI.
|
||||
**/
|
||||
@:native('::__hxcpp_tracy_set_thread_name_and_group')
|
||||
public static function setThreadName(_name:String, ?_groupHint:Int = 1):Void;
|
||||
|
||||
/**
|
||||
Create a custom named scoped zone in your code.
|
||||
**/
|
||||
@:native('HXCPP_TRACY_ZONE')
|
||||
public static function zoneScoped(_name:String):Void;
|
||||
}
|
||||
|
||||
#if (scriptable || cppia)
|
||||
class Cppia_TracyProfiler
|
||||
{
|
||||
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.frameMark)
|
||||
public static function frameMark()
|
||||
Native_TracyProfiler.frameMark();
|
||||
|
||||
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.message)
|
||||
public static function message(_msg:String, ?_color:Int = 0x000000)
|
||||
Native_TracyProfiler.message(_msg, _color);
|
||||
|
||||
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.messageAppInfo)
|
||||
public static function messageAppInfo(_info:String)
|
||||
Native_TracyProfiler.messageAppInfo(_info);
|
||||
|
||||
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.plot)
|
||||
public static function plot(_name:String, _val:Float)
|
||||
Native_TracyProfiler.plot(_name, _val);
|
||||
|
||||
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.plotConfig)
|
||||
public static function plotConfig(_name:String, _format:PlotFormatType, ?_step:Bool = false, ?_fill:Bool = false, ?_color:Int = 0x000000)
|
||||
Native_TracyProfiler.plotConfig(_name, _format, _step, _fill, _color);
|
||||
|
||||
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.setThreadName)
|
||||
public static function setThreadName(_name:String, ?_groupHint:Int = 1)
|
||||
Native_TracyProfiler.setThreadName(_name, _groupHint);
|
||||
|
||||
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.zoneScoped)
|
||||
public static function zoneScoped(_name:String)
|
||||
Native_TracyProfiler.zoneScoped(_name);
|
||||
}
|
||||
|
||||
typedef TracyProfiler = Cppia_TracyProfiler;
|
||||
#else
|
||||
typedef TracyProfiler = Native_TracyProfiler;
|
||||
#end
|
||||
@@ -0,0 +1,177 @@
|
||||
package funkin;
|
||||
|
||||
import openfl.utils.Future;
|
||||
|
||||
/**
|
||||
* A wrapper around `openfl.utils.Assets` which disallows access to the harmful functions.
|
||||
* Later we'll add Funkin-specific caching to this.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Assets
|
||||
{
|
||||
/**
|
||||
* Get the file system path for an asset
|
||||
* @param path The asset path to load from, relative to the assets folder
|
||||
* @return The path to the asset on the file system
|
||||
*/
|
||||
public static function getPath(path:String):String
|
||||
{
|
||||
return openfl.utils.Assets.getPath(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load bytes from an asset
|
||||
* May cause stutters or throw an error if the asset is not cached
|
||||
* @param path The asset path to load from
|
||||
* @return The byte contents of the file
|
||||
*/
|
||||
public static function getBytes(path:String):haxe.io.Bytes
|
||||
{
|
||||
return openfl.utils.Assets.getBytes(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load bytes from an asset asynchronously
|
||||
* @param path The asset path to load from
|
||||
* @return A future which promises to return the byte contents of the file
|
||||
*/
|
||||
public static function loadBytes(path:String):Future<openfl.utils.ByteArray>
|
||||
{
|
||||
return openfl.utils.Assets.loadBytes(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load text from an asset.
|
||||
* May cause stutters or throw an error if the asset is not cached
|
||||
* @param path The asset path to load from
|
||||
* @return The text contents of the file
|
||||
*/
|
||||
public static function getText(path:String):String
|
||||
{
|
||||
return openfl.utils.Assets.getText(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load text from an asset asynchronously
|
||||
* @param path The asset path to load from
|
||||
* @return A future which promises to return the text contents of the file
|
||||
*/
|
||||
public static function loadText(path:String):Future<String>
|
||||
{
|
||||
return openfl.utils.Assets.loadText(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Sound file from an asset
|
||||
* May cause stutters or throw an error if the asset is not cached
|
||||
* @param path The asset path to load from
|
||||
* @return The loaded sound
|
||||
*/
|
||||
public static function getSound(path:String):openfl.media.Sound
|
||||
{
|
||||
return openfl.utils.Assets.getSound(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Sound file from an asset asynchronously
|
||||
* @param path The asset path to load from
|
||||
* @return A future which promises to return the loaded sound
|
||||
*/
|
||||
public static function loadSound(path:String):Future<openfl.media.Sound>
|
||||
{
|
||||
return openfl.utils.Assets.loadSound(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Sound file from an asset, with optimizations specific to long-duration music
|
||||
* May cause stutters or throw an error if the asset is not cached
|
||||
* @param path The asset path to load from
|
||||
* @return The loaded sound
|
||||
*/
|
||||
public static function getMusic(path:String):openfl.media.Sound
|
||||
{
|
||||
return openfl.utils.Assets.getMusic(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Sound file from an asset asynchronously, with optimizations specific to long-duration music
|
||||
* @param path The asset path to load from
|
||||
* @return A future which promises to return the loaded sound
|
||||
*/
|
||||
public static function loadMusic(path:String):Future<openfl.media.Sound>
|
||||
{
|
||||
return openfl.utils.Assets.loadMusic(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Bitmap from an asset
|
||||
* May cause stutters or throw an error if the asset is not cached
|
||||
* @param path The asset path to load from
|
||||
* @return The loaded Bitmap image
|
||||
*/
|
||||
public static function getBitmapData(path:String):openfl.display.BitmapData
|
||||
{
|
||||
return openfl.utils.Assets.getBitmapData(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Bitmap from an asset asynchronously
|
||||
* @param path The asset path to load from
|
||||
* @return The future which promises to return the loaded Bitmap image
|
||||
*/
|
||||
public static function loadBitmapData(path:String):Future<openfl.display.BitmapData>
|
||||
{
|
||||
return openfl.utils.Assets.loadBitmapData(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given asset of the given type exists.
|
||||
* @param path The asset path to check
|
||||
* @param type The asset type to check
|
||||
* @return Whether the asset exists
|
||||
*/
|
||||
public static function exists(path:String, ?type:openfl.utils.AssetType):Bool
|
||||
{
|
||||
return openfl.utils.Assets.exists(path, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of all assets of the given type
|
||||
* @param type The asset type to check
|
||||
* @return A list of asset paths
|
||||
*/
|
||||
public static function list(?type:openfl.utils.AssetType):Array<String>
|
||||
{
|
||||
return openfl.utils.Assets.list(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a library with the given name exists.
|
||||
* @param name The name to check.
|
||||
* @return Whether or not the library exists.
|
||||
*/
|
||||
public static function hasLibrary(name:String):Bool
|
||||
{
|
||||
return openfl.utils.Assets.hasLibrary(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a library with the given name.
|
||||
* @param name The name of the library to get.
|
||||
* @return The library with the given name.
|
||||
*/
|
||||
public static function getLibrary(name:String):lime.utils.AssetLibrary
|
||||
{
|
||||
return openfl.utils.Assets.getLibrary(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a library with the given name.
|
||||
* @param name The name of the library to load.
|
||||
* @return An `AssetLibary` object.
|
||||
*/
|
||||
public static function loadLibrary(name:String):Future<openfl.utils.AssetLibrary>
|
||||
{
|
||||
return openfl.utils.Assets.loadLibrary(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
package funkin;
|
||||
|
||||
import funkin.util.Constants;
|
||||
import flixel.util.FlxSignal;
|
||||
import flixel.math.FlxMath;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import funkin.save.Save;
|
||||
import haxe.Timer;
|
||||
import flixel.sound.FlxSound;
|
||||
|
||||
/**
|
||||
* A core class which handles musical timing throughout the game,
|
||||
* both in gameplay and in menus.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Conductor
|
||||
{
|
||||
// onBeatHit is called every quarter note
|
||||
// onStepHit is called every sixteenth note
|
||||
// 4/4 = 4 beats per measure = 16 steps per measure
|
||||
// 120 BPM = 120 quarter notes per minute = 2 onBeatHit per second
|
||||
// 120 BPM = 480 sixteenth notes per minute = 8 onStepHit per second
|
||||
// 60 BPM = 60 quarter notes per minute = 1 onBeatHit per second
|
||||
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
||||
// 3/4 = 3 beats per measure = 12 steps per measure
|
||||
// (IDENTICAL TO 4/4 but shorter measure length)
|
||||
// 120 BPM = 120 quarter notes per minute = 2 onBeatHit per second
|
||||
// 120 BPM = 480 sixteenth notes per minute = 8 onStepHit per second
|
||||
// 60 BPM = 60 quarter notes per minute = 1 onBeatHit per second
|
||||
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
||||
// 7/8 = 3.5 beats per measure = 14 steps per measure
|
||||
|
||||
/**
|
||||
* The current instance of the Conductor.
|
||||
* If one doesn't currently exist, a new one will be created.
|
||||
*
|
||||
* You can also do stuff like store a reference to the Conductor and pass it around or temporarily replace it,
|
||||
* or have a second Conductor running at the same time, or other weird stuff like that if you need to.
|
||||
*/
|
||||
public static var instance(get, never):Conductor;
|
||||
|
||||
static var _instance:Null<Conductor> = null;
|
||||
|
||||
/**
|
||||
* Signal fired when the current static Conductor instance advances to a new measure.
|
||||
*/
|
||||
public static var measureHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when THIS Conductor instance advances to a new measure.
|
||||
* TODO: This naming sucks but we can't make a static and instance field with the same name!
|
||||
*/
|
||||
public var onMeasureHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when the current Conductor instance advances to a new beat.
|
||||
*/
|
||||
public static var beatHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when THIS Conductor instance advances to a new beat.
|
||||
* TODO: This naming sucks but we can't make a static and instance field with the same name!
|
||||
*/
|
||||
public var onBeatHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when the current Conductor instance advances to a new step.
|
||||
*/
|
||||
public static var stepHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when THIS Conductor instance advances to a new step.
|
||||
* TODO: This naming sucks but we can't make a static and instance field with the same name!
|
||||
*/
|
||||
public var onStepHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* The list of time changes in the song.
|
||||
* There should be at least one time change (at the beginning of the song) to define the BPM.
|
||||
*/
|
||||
var timeChanges:Array<SongTimeChange> = [];
|
||||
|
||||
/**
|
||||
* The most recent time change for the current song position.
|
||||
*/
|
||||
public var currentTimeChange(default, null):Null<SongTimeChange>;
|
||||
|
||||
/**
|
||||
* The current position in the song in milliseconds.
|
||||
* Update this every frame based on the audio position using `Conductor.instance.update()`.
|
||||
*/
|
||||
public var songPosition(default, null):Float = 0;
|
||||
|
||||
var prevTimestamp:Float = 0;
|
||||
var prevTime:Float = 0;
|
||||
|
||||
/**
|
||||
* Beats per minute of the current song at the current time.
|
||||
*/
|
||||
public var bpm(get, never):Float;
|
||||
|
||||
function get_bpm():Float
|
||||
{
|
||||
if (bpmOverride != null) return bpmOverride;
|
||||
|
||||
if (currentTimeChange == null) return Constants.DEFAULT_BPM;
|
||||
|
||||
return currentTimeChange.bpm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beats per minute of the current song at the start time.
|
||||
*/
|
||||
public var startingBPM(get, never):Float;
|
||||
|
||||
function get_startingBPM():Float
|
||||
{
|
||||
if (bpmOverride != null) return bpmOverride;
|
||||
|
||||
var timeChange = timeChanges[0];
|
||||
if (timeChange == null) return Constants.DEFAULT_BPM;
|
||||
|
||||
return timeChange.bpm;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current value set by `forceBPM`.
|
||||
* If false, BPM is determined by time changes.
|
||||
*/
|
||||
var bpmOverride:Null<Float> = null;
|
||||
|
||||
/**
|
||||
* Duration of a measure in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public var measureLengthMs(get, never):Float;
|
||||
|
||||
function get_measureLengthMs():Float
|
||||
{
|
||||
return beatLengthMs * timeSignatureNumerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public var beatLengthMs(get, never):Float;
|
||||
|
||||
function get_beatLengthMs():Float
|
||||
{
|
||||
// Tied directly to BPM.
|
||||
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public var stepLengthMs(get, never):Float;
|
||||
|
||||
function get_stepLengthMs():Float
|
||||
{
|
||||
return beatLengthMs / timeSignatureNumerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* The numerator for the current time signature (the `3` in `3/4`).
|
||||
*/
|
||||
public var timeSignatureNumerator(get, never):Int;
|
||||
|
||||
function get_timeSignatureNumerator():Int
|
||||
{
|
||||
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM;
|
||||
|
||||
return currentTimeChange.timeSignatureNum;
|
||||
}
|
||||
|
||||
/**
|
||||
* The denominator for the current time signature (the `4` in `3/4`).
|
||||
*/
|
||||
public var timeSignatureDenominator(get, never):Int;
|
||||
|
||||
function get_timeSignatureDenominator():Int
|
||||
{
|
||||
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN;
|
||||
|
||||
return currentTimeChange.timeSignatureDen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current position in the song, in measures.
|
||||
*/
|
||||
public var currentMeasure(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in beats.
|
||||
*/
|
||||
public var currentBeat(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in steps.
|
||||
*/
|
||||
public var currentStep(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in measures and fractions of a measure.
|
||||
*/
|
||||
public var currentMeasureTime(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in beats and fractions of a measure.
|
||||
*/
|
||||
public var currentBeatTime(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in steps and fractions of a step.
|
||||
*/
|
||||
public var currentStepTime(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* An offset tied to the current chart file to compensate for a delay in the instrumental.
|
||||
*/
|
||||
public var instrumentalOffset:Float = 0;
|
||||
|
||||
/**
|
||||
* The instrumental offset, in terms of steps.
|
||||
*/
|
||||
public var instrumentalOffsetSteps(get, never):Float;
|
||||
|
||||
function get_instrumentalOffsetSteps():Float
|
||||
{
|
||||
var startingStepLengthMs:Float = ((Constants.SECS_PER_MIN / startingBPM) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
||||
|
||||
return instrumentalOffset / startingStepLengthMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* An offset tied to the file format of the audio file being played.
|
||||
*/
|
||||
public var formatOffset:Float = 0;
|
||||
|
||||
/**
|
||||
* An offset set by the user to compensate for input lag.
|
||||
* No matter if you're using a local conductor or not, this always loads
|
||||
* to/from the save file
|
||||
*/
|
||||
public var inputOffset(get, set):Int;
|
||||
|
||||
/**
|
||||
* An offset set by the user to compensate for audio/visual lag
|
||||
* No matter if you're using a local conductor or not, this always loads
|
||||
* to/from the save file
|
||||
*/
|
||||
public var audioVisualOffset(get, set):Int;
|
||||
|
||||
function get_inputOffset():Int
|
||||
{
|
||||
return Save?.instance?.options?.inputOffset ?? 0;
|
||||
}
|
||||
|
||||
function set_inputOffset(value:Int):Int
|
||||
{
|
||||
Save.instance.options.inputOffset = value;
|
||||
Save.instance.flush();
|
||||
return Save.instance.options.inputOffset;
|
||||
}
|
||||
|
||||
function get_audioVisualOffset():Int
|
||||
{
|
||||
return Save?.instance?.options?.audioVisualOffset ?? 0;
|
||||
}
|
||||
|
||||
function set_audioVisualOffset(value:Int):Int
|
||||
{
|
||||
Save.instance.options.audioVisualOffset = value;
|
||||
Save.instance.flush();
|
||||
return Save.instance.options.audioVisualOffset;
|
||||
}
|
||||
|
||||
public var combinedOffset(get, never):Float;
|
||||
|
||||
function get_combinedOffset():Float
|
||||
{
|
||||
return instrumentalOffset + audioVisualOffset + inputOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of beats in a measure. May be fractional depending on the time signature.
|
||||
*/
|
||||
public var beatsPerMeasure(get, never):Float;
|
||||
|
||||
function get_beatsPerMeasure():Float
|
||||
{
|
||||
// NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure
|
||||
return stepsPerMeasure / Constants.STEPS_PER_BEAT;
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of steps in a measure.
|
||||
* TODO: I don't think this can be fractional?
|
||||
*/
|
||||
public var stepsPerMeasure(get, never):Int;
|
||||
|
||||
function get_stepsPerMeasure():Int
|
||||
{
|
||||
// TODO: Is this always an integer?
|
||||
return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the Conductor, replacing the current instance with a fresh one.
|
||||
*/
|
||||
public static function reset():Void
|
||||
{
|
||||
set_instance(new Conductor());
|
||||
}
|
||||
|
||||
static function dispatchMeasureHit():Void
|
||||
{
|
||||
Conductor.measureHit.dispatch();
|
||||
}
|
||||
|
||||
static function dispatchBeatHit():Void
|
||||
{
|
||||
Conductor.beatHit.dispatch();
|
||||
}
|
||||
|
||||
static function dispatchStepHit():Void
|
||||
{
|
||||
Conductor.stepHit.dispatch();
|
||||
}
|
||||
|
||||
static function setupSingleton(input:Conductor):Void
|
||||
{
|
||||
input.onMeasureHit.add(dispatchMeasureHit);
|
||||
|
||||
input.onBeatHit.add(dispatchBeatHit);
|
||||
|
||||
input.onStepHit.add(dispatchStepHit);
|
||||
}
|
||||
|
||||
static function clearSingleton(input:Conductor):Void
|
||||
{
|
||||
input.onMeasureHit.remove(dispatchMeasureHit);
|
||||
|
||||
input.onBeatHit.remove(dispatchBeatHit);
|
||||
|
||||
input.onStepHit.remove(dispatchStepHit);
|
||||
}
|
||||
|
||||
static function get_instance():Conductor
|
||||
{
|
||||
if (Conductor._instance == null) set_instance(new Conductor());
|
||||
if (Conductor._instance == null) throw "Could not initialize singleton Conductor!";
|
||||
return Conductor._instance;
|
||||
}
|
||||
|
||||
static function set_instance(instance:Conductor):Conductor
|
||||
{
|
||||
// Use _instance in here to avoid recursion
|
||||
if (Conductor._instance != null) clearSingleton(Conductor._instance);
|
||||
|
||||
Conductor._instance = instance;
|
||||
|
||||
if (Conductor._instance != null) setupSingleton(Conductor._instance);
|
||||
|
||||
return Conductor._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*/
|
||||
public function new() {}
|
||||
|
||||
/**
|
||||
* Forcibly defines the current BPM of the song.
|
||||
* Useful for things like the chart editor that need to manipulate BPM in real time.
|
||||
*
|
||||
* Set to null to reset to the BPM defined by the timeChanges.
|
||||
*
|
||||
* WARNING: Avoid this for things like setting the BPM of the title screen music,
|
||||
* you should have a metadata file for it instead.
|
||||
* We should probably deprecate this in the future.
|
||||
*/
|
||||
public function forceBPM(?bpm:Float):Void
|
||||
{
|
||||
if (bpm != null)
|
||||
{
|
||||
trace('[CONDUCTOR] Forcing BPM to ${bpm}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[CONDUCTOR] Resetting BPM to default');
|
||||
}
|
||||
|
||||
this.bpmOverride = bpm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the conductor with the current song position.
|
||||
* BPM, current step, etc. will be re-calculated based on the song position.
|
||||
*
|
||||
* @param songPosition The current position in the song in milliseconds.
|
||||
* Leave blank to use the FlxG.sound.music position.
|
||||
* @param applyOffsets If it should apply the instrumentalOffset + formatOffset + audioVisualOffset
|
||||
*/
|
||||
public function update(?songPos:Float, applyOffsets:Bool = true, forceDispatch:Bool = false)
|
||||
{
|
||||
var currentTime:Float = (FlxG.sound.music != null) ? FlxG.sound.music.time : 0.0;
|
||||
var currentLength:Float = (FlxG.sound.music != null) ? FlxG.sound.music.length : 0.0;
|
||||
|
||||
if (songPos == null)
|
||||
{
|
||||
songPos = currentTime;
|
||||
}
|
||||
|
||||
// Take into account instrumental and file format song offsets.
|
||||
songPos += applyOffsets ? (combinedOffset) : 0;
|
||||
|
||||
var oldMeasure:Float = this.currentMeasure;
|
||||
var oldBeat:Float = this.currentBeat;
|
||||
var oldStep:Float = this.currentStep;
|
||||
|
||||
// If the song is playing, limit the song position to the length of the song or beginning of the song.
|
||||
if (FlxG.sound.music != null && FlxG.sound.music.playing)
|
||||
{
|
||||
this.songPosition = Math.min(currentLength, Math.max(0, songPos));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.songPosition = songPos;
|
||||
}
|
||||
|
||||
// Set the song position we are at (for purposes of calculating note positions, etc).
|
||||
|
||||
currentTimeChange = timeChanges[0];
|
||||
if (this.songPosition > 0.0)
|
||||
{
|
||||
for (i in 0...timeChanges.length)
|
||||
{
|
||||
if (this.songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i];
|
||||
|
||||
if (this.songPosition < timeChanges[i].timeStamp) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTimeChange == null && bpmOverride == null && FlxG.sound.music != null)
|
||||
{
|
||||
trace('WARNING: Conductor is broken, timeChanges is empty.');
|
||||
}
|
||||
else if (currentTimeChange != null && this.songPosition > 0.0)
|
||||
{
|
||||
// roundDecimal prevents representing 8 as 7.9999999
|
||||
this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * Constants.STEPS_PER_BEAT)
|
||||
+ (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
|
||||
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||
this.currentStep = Math.floor(currentStepTime);
|
||||
this.currentBeat = Math.floor(currentBeatTime);
|
||||
this.currentMeasure = Math.floor(currentMeasureTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assume a constant BPM equal to the forced value.
|
||||
this.currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4);
|
||||
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||
this.currentStep = Math.floor(currentStepTime);
|
||||
this.currentBeat = Math.floor(currentBeatTime);
|
||||
this.currentMeasure = Math.floor(currentMeasureTime);
|
||||
}
|
||||
|
||||
// FlxSignals are really cool.
|
||||
if (currentStep != oldStep)
|
||||
{
|
||||
this.onStepHit.dispatch();
|
||||
}
|
||||
|
||||
if (currentBeat != oldBeat)
|
||||
{
|
||||
this.onBeatHit.dispatch();
|
||||
}
|
||||
|
||||
if (currentMeasure != oldMeasure)
|
||||
{
|
||||
this.onMeasureHit.dispatch();
|
||||
}
|
||||
|
||||
// only update the timestamp if songPosition actually changed
|
||||
// which it doesn't do every frame!
|
||||
if (prevTime != this.songPosition)
|
||||
{
|
||||
// Update the timestamp for use in-between frames
|
||||
prevTime = this.songPosition;
|
||||
prevTimestamp = Std.int(Timer.stamp() * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be called in-between frames, usually for input related things
|
||||
* that can potentially get processed on exact milliseconds/timestmaps.
|
||||
* If you need song position, use `Conductor.instance.songPosition` instead
|
||||
* for use in update() related functions.
|
||||
* @param soundToCheck Which FlxSound object to check, defaults to FlxG.sound.music if no input
|
||||
* @return Float
|
||||
*/
|
||||
public function getTimeWithDiff(?soundToCheck:FlxSound):Float
|
||||
{
|
||||
if (soundToCheck == null) soundToCheck = FlxG.sound.music;
|
||||
// trace(this.songPosition);
|
||||
|
||||
@:privateAccess
|
||||
this.songPosition = soundToCheck._channel.position;
|
||||
// return this.songPosition + (Std.int(Timer.stamp() * 1000) - prevTimestamp);
|
||||
// trace("\t--> " + this.songPosition);
|
||||
return this.songPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the `SongTimeChange` data from the song metadata to this Conductor.
|
||||
* @param songTimeChanges The SongTimeChanges.
|
||||
*/
|
||||
public function mapTimeChanges(songTimeChanges:Array<SongTimeChange>):Void
|
||||
{
|
||||
timeChanges = [];
|
||||
|
||||
// Sort in place just in case it's out of order.
|
||||
SongDataUtils.sortTimeChanges(songTimeChanges);
|
||||
|
||||
for (songTimeChange in songTimeChanges)
|
||||
{
|
||||
// TODO: Maybe handle this different?
|
||||
// Do we care about BPM at negative timestamps?
|
||||
// Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`.
|
||||
if (songTimeChange.timeStamp < 0.0) songTimeChange.timeStamp = 0.0;
|
||||
|
||||
if (songTimeChange.timeStamp <= 0.0)
|
||||
{
|
||||
songTimeChange.beatTime = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Calculate the beat time of this timestamp.
|
||||
songTimeChange.beatTime = 0.0;
|
||||
|
||||
if (songTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
|
||||
{
|
||||
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
|
||||
songTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
|
||||
+ ((songTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
|
||||
4);
|
||||
}
|
||||
}
|
||||
|
||||
timeChanges.push(songTimeChange);
|
||||
}
|
||||
|
||||
if (timeChanges.length > 0)
|
||||
{
|
||||
trace('Done mapping time changes: ${timeChanges}');
|
||||
}
|
||||
|
||||
// Update currentStepTime
|
||||
this.update(this.songPosition, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a time in milliseconds, return a time in steps.
|
||||
* @param ms The time in milliseconds.
|
||||
* @return The time in steps.
|
||||
*/
|
||||
public function getTimeInSteps(ms:Float):Float
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
// Assume a constant BPM equal to the forced value.
|
||||
return Math.floor(ms / stepLengthMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
var resultStep:Float = 0;
|
||||
|
||||
var lastTimeChange:SongTimeChange = timeChanges[0];
|
||||
for (timeChange in timeChanges)
|
||||
{
|
||||
if (ms >= timeChange.timeStamp)
|
||||
{
|
||||
lastTimeChange = timeChange;
|
||||
resultStep = lastTimeChange.beatTime * Constants.STEPS_PER_BEAT;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This time change is after the requested time.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
||||
var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / lastStepLengthMs;
|
||||
resultStep += resultFractionalStep;
|
||||
|
||||
return resultStep;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a time in steps and fractional steps, return a time in milliseconds.
|
||||
* @param stepTime The time in steps.
|
||||
* @return The time in milliseconds.
|
||||
*/
|
||||
public function getStepTimeInMs(stepTime:Float):Float
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
// Assume a constant BPM equal to the forced value.
|
||||
return stepTime * stepLengthMs;
|
||||
}
|
||||
else
|
||||
{
|
||||
var resultMs:Float = 0;
|
||||
|
||||
var lastTimeChange:SongTimeChange = timeChanges[0];
|
||||
for (timeChange in timeChanges)
|
||||
{
|
||||
if (stepTime >= timeChange.beatTime * Constants.STEPS_PER_BEAT)
|
||||
{
|
||||
lastTimeChange = timeChange;
|
||||
resultMs = lastTimeChange.timeStamp;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This time change is after the requested time.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
||||
resultMs += (stepTime - lastTimeChange.beatTime * Constants.STEPS_PER_BEAT) * lastStepLengthMs;
|
||||
|
||||
return resultMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a time in beats and fractional beats, return a time in milliseconds.
|
||||
* @param beatTime The time in beats.
|
||||
* @return The time in milliseconds.
|
||||
*/
|
||||
public function getBeatTimeInMs(beatTime:Float):Float
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
// Assume a constant BPM equal to the forced value.
|
||||
return beatTime * stepLengthMs * Constants.STEPS_PER_BEAT;
|
||||
}
|
||||
else
|
||||
{
|
||||
var resultMs:Float = 0;
|
||||
|
||||
var lastTimeChange:SongTimeChange = timeChanges[0];
|
||||
for (timeChange in timeChanges)
|
||||
{
|
||||
if (beatTime >= timeChange.beatTime)
|
||||
{
|
||||
lastTimeChange = timeChange;
|
||||
resultMs = lastTimeChange.timeStamp;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This time change is after the requested time.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
||||
resultMs += (beatTime - lastTimeChange.beatTime) * lastStepLengthMs * Constants.STEPS_PER_BEAT;
|
||||
|
||||
return resultMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Conductor fields to the Flixel debugger variable display.
|
||||
* @param conductorToUse The conductor to use. Defaults to `Conductor.instance`.
|
||||
*/
|
||||
public static function watchQuick(?target:Conductor):Void
|
||||
{
|
||||
if (target == null) target = Conductor.instance;
|
||||
|
||||
FlxG.watch.addQuick('songPosition', target.songPosition);
|
||||
FlxG.watch.addQuick('bpm', target.bpm);
|
||||
FlxG.watch.addQuick('currentMeasureTime', target.currentMeasureTime);
|
||||
FlxG.watch.addQuick('currentBeatTime', target.currentBeatTime);
|
||||
FlxG.watch.addQuick('currentStepTime', target.currentStepTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package funkin;
|
||||
|
||||
/**
|
||||
* A core class which handles tracking score and combo for the current song.
|
||||
*/
|
||||
class Highscore
|
||||
{
|
||||
/**
|
||||
* Keeps track of notes hit for the current song
|
||||
* and how accurate you were with each note (bad, missed, shit, etc.)
|
||||
*/
|
||||
public static var tallies:Tallies = new Tallies();
|
||||
|
||||
/**
|
||||
* Keeps track of notes hit for the current WEEK / level
|
||||
* for use with storymode, or likely any other "playlist" esque option
|
||||
*/
|
||||
public static var talliesLevel:Tallies = new Tallies();
|
||||
|
||||
/**
|
||||
* Produces a new Tallies object which represents the sum of two existing Tallies
|
||||
* @param newTally The first tally
|
||||
* @param baseTally The second tally
|
||||
* @return The combined tally
|
||||
*/
|
||||
public static function combineTallies(newTally:Tallies, baseTally:Tallies):Tallies
|
||||
{
|
||||
var combinedTally:Tallies = new Tallies();
|
||||
combinedTally.missed = newTally.missed + baseTally.missed;
|
||||
combinedTally.shit = newTally.shit + baseTally.shit;
|
||||
combinedTally.bad = newTally.bad + baseTally.bad;
|
||||
combinedTally.good = newTally.good + baseTally.good;
|
||||
combinedTally.sick = newTally.sick + baseTally.sick;
|
||||
combinedTally.totalNotes = newTally.totalNotes + baseTally.totalNotes;
|
||||
combinedTally.totalNotesHit = newTally.totalNotesHit + baseTally.totalNotesHit;
|
||||
|
||||
// Current combo = use most recent.
|
||||
combinedTally.combo = newTally.combo;
|
||||
// Max combo = use maximum value.
|
||||
combinedTally.maxCombo = Std.int(Math.max(newTally.maxCombo, baseTally.maxCombo));
|
||||
|
||||
return combinedTally;
|
||||
}
|
||||
}
|
||||
|
||||
@:forward
|
||||
abstract Tallies(RawTallies)
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
this =
|
||||
{
|
||||
combo: 0,
|
||||
missed: 0,
|
||||
shit: 0,
|
||||
bad: 0,
|
||||
good: 0,
|
||||
sick: 0,
|
||||
totalNotes: 0,
|
||||
totalNotesHit: 0,
|
||||
maxCombo: 0,
|
||||
score: 0,
|
||||
isNewHighscore: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A structure object containing the data for highscore tallies.
|
||||
*/
|
||||
typedef RawTallies =
|
||||
{
|
||||
var combo:Int;
|
||||
|
||||
/**
|
||||
* How many notes you let scroll by.
|
||||
*/
|
||||
var missed:Int;
|
||||
|
||||
var shit:Int;
|
||||
var bad:Int;
|
||||
var good:Int;
|
||||
var sick:Int;
|
||||
var maxCombo:Int;
|
||||
|
||||
var score:Int;
|
||||
|
||||
var isNewHighscore:Bool;
|
||||
|
||||
/**
|
||||
* How many notes total that you hit. (NOT how many notes total in the song!)
|
||||
*/
|
||||
var totalNotesHit:Int;
|
||||
|
||||
/**
|
||||
* How many notes in the current chart
|
||||
*/
|
||||
var totalNotes:Int;
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
package funkin;
|
||||
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond;
|
||||
import flixel.addons.transition.TransitionData;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.FlxState;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.math.FlxRect;
|
||||
import flixel.system.debug.log.LogStyle;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.data.dialogue.conversation.ConversationRegistry;
|
||||
import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry;
|
||||
import funkin.data.dialogue.speaker.SpeakerRegistry;
|
||||
import funkin.data.freeplay.album.AlbumRegistry;
|
||||
import funkin.data.freeplay.player.PlayerRegistry;
|
||||
import funkin.data.freeplay.style.FreeplayStyleRegistry;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.stickers.StickerRegistry;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.data.story.level.LevelRegistry;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
import funkin.play.PlayStatePlaylist;
|
||||
import funkin.ui.debug.charting.ChartEditorState;
|
||||
import funkin.ui.title.TitleState;
|
||||
import funkin.ui.transition.LoadingState;
|
||||
import funkin.util.CLIUtil;
|
||||
import funkin.util.CLIUtil.CLIParams;
|
||||
import funkin.util.macro.MacroUtil;
|
||||
import funkin.util.TimerUtil;
|
||||
import funkin.util.TrackerUtil;
|
||||
import funkin.util.WindowUtil;
|
||||
import openfl.display.BitmapData;
|
||||
#if FEATURE_DISCORD_RPC
|
||||
import funkin.api.discord.DiscordClient;
|
||||
#end
|
||||
#if FEATURE_NEWGROUNDS
|
||||
import funkin.api.newgrounds.NewgroundsClient;
|
||||
#end
|
||||
|
||||
/**
|
||||
* A core class which performs initialization of the game.
|
||||
* The initialization state has several functions:
|
||||
* - Calls code to set up the game, including loading saves and parsing game data.
|
||||
* - Chooses whether to start via debug or via launching normally.
|
||||
*
|
||||
* It should not contain any sprites or rendering.
|
||||
*/
|
||||
class InitState extends FlxState
|
||||
{
|
||||
/**
|
||||
* Perform a bunch of game setup, then immediately transition to the title screen.
|
||||
*/
|
||||
public override function create():Void
|
||||
{
|
||||
// Setup a bunch of important Flixel stuff.
|
||||
setupShit();
|
||||
|
||||
// Load player options from save data.
|
||||
// Flixel has already loaded the save data, so we can just use it.
|
||||
Preferences.init();
|
||||
|
||||
// Load controls from save data.
|
||||
PlayerSettings.init();
|
||||
|
||||
startGame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a bunch of important Flixel stuff.
|
||||
*/
|
||||
function setupShit():Void
|
||||
{
|
||||
//
|
||||
// GAME SETUP
|
||||
//
|
||||
|
||||
// Setup window events (like callbacks for onWindowClose)
|
||||
// and fullscreen keybind setup
|
||||
WindowUtil.initWindowEvents();
|
||||
// Disable the thing on Windows where it tries to send a bug report to Microsoft because why do they care?
|
||||
WindowUtil.disableCrashHandler();
|
||||
|
||||
#if FEATURE_DEBUG_TRACY
|
||||
funkin.util.WindowUtil.initTracy();
|
||||
#end
|
||||
|
||||
// This ain't a pixel art game! (most of the time)
|
||||
FlxSprite.defaultAntialiasing = true;
|
||||
|
||||
// Disable default keybinds for volume (we manually control volume in MusicBeatState with custom binds)
|
||||
FlxG.sound.volumeUpKeys = [];
|
||||
FlxG.sound.volumeDownKeys = [];
|
||||
FlxG.sound.muteKeys = [];
|
||||
|
||||
// A small jumpstart to the soundtray, it usually sets itself to inactive (somewhere...)
|
||||
// but that makes our soundtray not show up on init if we have the game muted.
|
||||
// We set it to active so it at least calls it's update function once (see FlxGame.onEnterFrame(), it's called there)
|
||||
// and also see FunkinSoundTray.update() to see what we do and how we check if we are muted or not
|
||||
FlxG.game.soundTray.active = true;
|
||||
|
||||
// Set the game to a lower frame rate while it is in the background.
|
||||
FlxG.game.focusLostFramerate = 30;
|
||||
|
||||
// Makes Flixel use frame times instead of locked movements per frame for things like tweens
|
||||
FlxG.fixedTimestep = false;
|
||||
|
||||
setupFlixelDebug();
|
||||
|
||||
//
|
||||
// FLIXEL TRANSITIONS
|
||||
//
|
||||
|
||||
// Diamond Transition
|
||||
var diamond:FlxGraphic = FlxGraphic.fromClass(GraphicTransTileDiamond);
|
||||
diamond.persist = true;
|
||||
diamond.destroyOnNoUse = false;
|
||||
|
||||
// NOTE: tileData is ignored if TransitionData.type is FADE instead of TILES.
|
||||
var tileData:TransitionTileData = {asset: diamond, width: 32, height: 32};
|
||||
|
||||
FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), tileData,
|
||||
new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
|
||||
FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), tileData,
|
||||
new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
|
||||
// Don't play transition in when entering the title state.
|
||||
FlxTransitionableState.skipNextTransIn = true;
|
||||
|
||||
//
|
||||
// NEWGROUNDS API SETUP
|
||||
//
|
||||
#if FEATURE_NEWGROUNDS
|
||||
NewgroundsClient.instance.init();
|
||||
#end
|
||||
|
||||
//
|
||||
// DISCORD API SETUP
|
||||
//
|
||||
#if FEATURE_DISCORD_RPC
|
||||
DiscordClient.instance.init();
|
||||
|
||||
lime.app.Application.current.onExit.add(function(exitCode) {
|
||||
DiscordClient.instance.shutdown();
|
||||
});
|
||||
#end
|
||||
|
||||
//
|
||||
// ANDROID SETUP
|
||||
//
|
||||
#if android
|
||||
FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
|
||||
#end
|
||||
|
||||
//
|
||||
// FLIXEL PLUGINS
|
||||
//
|
||||
// Plugins provide a useful interface for globally active Flixel objects,
|
||||
// that receive update events regardless of the current state.
|
||||
// TODO: Move scripted Module behavior to a Flixel plugin.
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
funkin.util.plugins.MemoryGCPlugin.initialize();
|
||||
#end
|
||||
#if FEATURE_SCREENSHOTS
|
||||
funkin.util.plugins.ScreenshotPlugin.initialize();
|
||||
#end
|
||||
funkin.util.plugins.NewgroundsMedalPlugin.initialize();
|
||||
funkin.util.plugins.EvacuateDebugPlugin.initialize();
|
||||
funkin.util.plugins.ForceCrashPlugin.initialize();
|
||||
funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
|
||||
funkin.util.plugins.VolumePlugin.initialize();
|
||||
funkin.util.plugins.WatchPlugin.initialize();
|
||||
|
||||
//
|
||||
// GAME DATA PARSING
|
||||
//
|
||||
|
||||
// NOTE: Registries must be imported and not referenced with fully qualified names,
|
||||
// to ensure build macros work properly.
|
||||
trace('Parsing game data...');
|
||||
var perfStart:Float = TimerUtil.start();
|
||||
SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry.
|
||||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
NoteStyleRegistry.instance.loadEntries();
|
||||
PlayerRegistry.instance.loadEntries();
|
||||
ConversationRegistry.instance.loadEntries();
|
||||
DialogueBoxRegistry.instance.loadEntries();
|
||||
SpeakerRegistry.instance.loadEntries();
|
||||
FreeplayStyleRegistry.instance.loadEntries();
|
||||
AlbumRegistry.instance.loadEntries();
|
||||
StageRegistry.instance.loadEntries();
|
||||
StickerRegistry.instance.loadEntries();
|
||||
|
||||
// TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers and more prone to syntax errors.
|
||||
// Move it to use a BaseRegistry.
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
|
||||
NoteKindManager.loadScripts();
|
||||
|
||||
ModuleHandler.buildModuleCallbacks();
|
||||
ModuleHandler.loadModuleCache();
|
||||
ModuleHandler.callOnCreate();
|
||||
|
||||
funkin.input.Cursor.hide();
|
||||
|
||||
funkin.multiplayer.MultiplayerPlugin.initialize();
|
||||
|
||||
trace('Parsing game data took: ${TimerUtil.ms(perfStart)}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the game.
|
||||
*
|
||||
* By default, moves to the `TitleState`.
|
||||
* But based on compile defines, the game can start immediately on a specific song,
|
||||
* or immediately in a specific debug menu.
|
||||
*/
|
||||
function startGame():Void
|
||||
{
|
||||
#if SONG
|
||||
// -DSONG=bopeebo
|
||||
startSong(defineSong(), defineDifficulty());
|
||||
#elseif LEVEL
|
||||
// -DLEVEL=week1 -DDIFFICULTY=hard
|
||||
startLevel(defineLevel(), defineDifficulty());
|
||||
#elseif FREEPLAY
|
||||
// -DFREEPLAY
|
||||
FlxG.switchState(() -> new funkin.ui.freeplay.FreeplayState());
|
||||
#elseif DIALOGUE
|
||||
// -DDIALOGUE
|
||||
FlxG.switchState(() -> new funkin.ui.debug.dialogue.ConversationDebugState());
|
||||
#elseif ANIMATE
|
||||
// -DANIMATE
|
||||
FlxG.switchState(() -> new funkin.ui.debug.anim.FlxAnimateTest());
|
||||
#elseif WAVEFORM
|
||||
// -DWAVEFORM
|
||||
FlxG.switchState(() -> new funkin.ui.debug.WaveformTestState());
|
||||
#elseif CHARTING
|
||||
// -DCHARTING
|
||||
FlxG.switchState(() -> new funkin.ui.debug.charting.ChartEditorState());
|
||||
#elseif STAGEBUILD
|
||||
// -DSTAGEBUILD
|
||||
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
|
||||
#elseif RESULTS
|
||||
// -DRESULTS
|
||||
FlxG.switchState(() -> new funkin.play.ResultState(
|
||||
{
|
||||
storyMode: true,
|
||||
title: "Cum Song Erect by Kawai Sprite",
|
||||
songId: "cum",
|
||||
characterId: "pico",
|
||||
difficultyId: "hard",
|
||||
isNewHighscore: true,
|
||||
scoreData:
|
||||
{
|
||||
score: 1_234_567,
|
||||
tallies:
|
||||
{
|
||||
sick: 130,
|
||||
good: 60,
|
||||
bad: 69,
|
||||
shit: 69,
|
||||
missed: 69,
|
||||
combo: 69,
|
||||
maxCombo: 69,
|
||||
totalNotesHit: 140,
|
||||
totalNotes: 240
|
||||
}
|
||||
// 2400 total notes = 7% = LOSS
|
||||
// 275 total notes = 69% = NICE
|
||||
// 240 total notes = 79% = GOOD
|
||||
// 230 total notes = 82% = GREAT
|
||||
// 210 total notes = 91% = EXCELLENT
|
||||
// 190 total notes = PERFECT
|
||||
},
|
||||
}));
|
||||
#elseif ANIMDEBUG
|
||||
// -DANIMDEBUG
|
||||
FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());
|
||||
#elseif LATENCY
|
||||
// -DLATENCY
|
||||
FlxG.switchState(() -> new funkin.LatencyState());
|
||||
#else
|
||||
startGameNormally();
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the game by moving to the title state and play the game as normal.
|
||||
*/
|
||||
function startGameNormally():Void
|
||||
{
|
||||
var params:CLIParams = CLIUtil.processArgs();
|
||||
trace('Command line args: ${params}');
|
||||
|
||||
if (params.chart.shouldLoadChart)
|
||||
{
|
||||
FlxG.switchState(() -> new ChartEditorState(
|
||||
{
|
||||
fnfcTargetPath: params.chart.chartPath,
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
|
||||
FlxG.switchState(() -> new TitleState());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the game by directly loading into a specific song.
|
||||
* @param songId
|
||||
* @param difficultyId
|
||||
*/
|
||||
function startSong(songId:String, difficultyId:String = 'normal'):Void
|
||||
{
|
||||
var songData:funkin.play.song.Song = funkin.data.song.SongRegistry.instance.fetchEntry(songId);
|
||||
|
||||
if (songData == null)
|
||||
{
|
||||
startGameNormally();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Rework loading behavior so we don't have to do this.
|
||||
switch (songId)
|
||||
{
|
||||
case 'tutorial' | 'bopeebo' | 'fresh' | 'dadbattle':
|
||||
Paths.setCurrentLevel('week1');
|
||||
PlayStatePlaylist.campaignId = 'week1';
|
||||
case 'spookeez' | 'south' | 'monster':
|
||||
Paths.setCurrentLevel('week2');
|
||||
PlayStatePlaylist.campaignId = 'week2';
|
||||
case 'pico' | 'philly-nice' | 'blammed':
|
||||
Paths.setCurrentLevel('week3');
|
||||
PlayStatePlaylist.campaignId = 'week3';
|
||||
case 'high' | 'satin-panties' | 'milf':
|
||||
Paths.setCurrentLevel('week4');
|
||||
PlayStatePlaylist.campaignId = 'week4';
|
||||
case 'cocoa' | 'eggnog' | 'winter-horrorland':
|
||||
Paths.setCurrentLevel('week5');
|
||||
PlayStatePlaylist.campaignId = 'week5';
|
||||
case 'senpai' | 'roses' | 'thorns':
|
||||
Paths.setCurrentLevel('week6');
|
||||
PlayStatePlaylist.campaignId = 'week6';
|
||||
case 'ugh' | 'guns' | 'stress':
|
||||
Paths.setCurrentLevel('week7');
|
||||
PlayStatePlaylist.campaignId = 'week7';
|
||||
case 'darnell' | 'lit-up' | '2hot' | 'blazin':
|
||||
Paths.setCurrentLevel('weekend1');
|
||||
PlayStatePlaylist.campaignId = 'weekend1';
|
||||
}
|
||||
|
||||
LoadingState.loadPlayState(
|
||||
{
|
||||
targetSong: songData,
|
||||
targetDifficulty: difficultyId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the game by directly loading into a specific story mode level.
|
||||
* @param levelId
|
||||
* @param difficultyId
|
||||
*/
|
||||
function startLevel(levelId:String, difficultyId:String = 'normal'):Void
|
||||
{
|
||||
var currentLevel:funkin.ui.story.Level = funkin.data.story.level.LevelRegistry.instance.fetchEntry(levelId);
|
||||
|
||||
if (currentLevel == null)
|
||||
{
|
||||
startGameNormally();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Rework loading behavior so we don't have to do this.
|
||||
Paths.setCurrentLevel(levelId);
|
||||
PlayStatePlaylist.campaignId = levelId;
|
||||
|
||||
PlayStatePlaylist.playlistSongIds = currentLevel.getSongs();
|
||||
PlayStatePlaylist.isStoryMode = true;
|
||||
PlayStatePlaylist.campaignScore = 0;
|
||||
|
||||
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
|
||||
|
||||
var targetSong:funkin.play.song.Song = SongRegistry.instance.fetchEntry(targetSongId);
|
||||
|
||||
LoadingState.loadPlayState(
|
||||
{
|
||||
targetSong: targetSong,
|
||||
targetDifficulty: difficultyId,
|
||||
});
|
||||
}
|
||||
|
||||
function setupFlixelDebug():Void
|
||||
{
|
||||
//
|
||||
// FLIXEL DEBUG SETUP
|
||||
//
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
trace('Initializing Flixel debugger...');
|
||||
|
||||
#if !debug
|
||||
// Make errors less annoying on release builds.
|
||||
LogStyle.ERROR.openConsole = false;
|
||||
LogStyle.ERROR.errorSound = null;
|
||||
#end
|
||||
|
||||
// Make errors and warnings less annoying.
|
||||
LogStyle.WARNING.openConsole = false;
|
||||
LogStyle.WARNING.errorSound = null;
|
||||
|
||||
// Disable using ~ to open the console (we use that for the Editor menu)
|
||||
FlxG.debugger.toggleKeys = [F2];
|
||||
TrackerUtil.initTrackers();
|
||||
// Adds an additional Close Debugger button.
|
||||
// This big obnoxious white button is for MOBILE, so that you can press it
|
||||
// easily with your finger when debug bullshit pops up during testing lol!
|
||||
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
|
||||
FlxG.debugger.visible = false;
|
||||
|
||||
// Make errors and warnings less annoying.
|
||||
// Forcing this always since I have never been happy to have the debugger to pop up
|
||||
LogStyle.ERROR.openConsole = false;
|
||||
LogStyle.ERROR.errorSound = null;
|
||||
LogStyle.WARNING.openConsole = false;
|
||||
LogStyle.WARNING.errorSound = null;
|
||||
});
|
||||
|
||||
// Adds a red button to the debugger.
|
||||
// This pauses the game AND the music! This ensures the Conductor stops.
|
||||
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function() {
|
||||
if (FlxG.vcr.paused)
|
||||
{
|
||||
FlxG.vcr.resume();
|
||||
|
||||
for (snd in FlxG.sound.list)
|
||||
{
|
||||
snd.resume();
|
||||
}
|
||||
|
||||
FlxG.sound.music.resume();
|
||||
}
|
||||
else
|
||||
{
|
||||
FlxG.vcr.pause();
|
||||
|
||||
for (snd in FlxG.sound.list)
|
||||
{
|
||||
snd.pause();
|
||||
}
|
||||
|
||||
FlxG.sound.music.pause();
|
||||
}
|
||||
});
|
||||
|
||||
// Adds a blue button to the debugger.
|
||||
// This skips forward in the song.
|
||||
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() {
|
||||
FlxG.game.debugger.vcr.onStep();
|
||||
|
||||
for (snd in FlxG.sound.list)
|
||||
{
|
||||
snd.pause();
|
||||
snd.time += FlxG.elapsed * 1000;
|
||||
}
|
||||
|
||||
FlxG.sound.music.pause();
|
||||
FlxG.sound.music.time += FlxG.elapsed * 1000;
|
||||
});
|
||||
#end
|
||||
}
|
||||
|
||||
function defineSong():String
|
||||
{
|
||||
return MacroUtil.getDefine('SONG');
|
||||
}
|
||||
|
||||
function defineLevel():String
|
||||
{
|
||||
return MacroUtil.getDefine('LEVEL');
|
||||
}
|
||||
|
||||
function defineDifficulty():String
|
||||
{
|
||||
return MacroUtil.getDefine('DIFFICULTY');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package funkin;
|
||||
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import openfl.utils.AssetType;
|
||||
|
||||
/**
|
||||
* A core class which handles determining asset paths.
|
||||
*/
|
||||
class Paths
|
||||
{
|
||||
static var currentLevel:Null<String> = null;
|
||||
|
||||
public static function setCurrentLevel(name:Null<String>):Void
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
currentLevel = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLevel = name.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
public static function stripLibrary(path:String):String
|
||||
{
|
||||
var parts:Array<String> = path.split(':');
|
||||
if (parts.length < 2) return path;
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
public static function getLibrary(path:String):String
|
||||
{
|
||||
var parts:Array<String> = path.split(':');
|
||||
if (parts.length < 2) return 'preload';
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
static function getPath(file:String, type:AssetType, library:Null<String>):String
|
||||
{
|
||||
if (library != null) return getLibraryPath(file, library);
|
||||
|
||||
if (currentLevel != null)
|
||||
{
|
||||
var levelPath:String = getLibraryPathForce(file, currentLevel);
|
||||
if (Assets.exists(levelPath, type)) return levelPath;
|
||||
}
|
||||
|
||||
var levelPath:String = getLibraryPathForce(file, 'shared');
|
||||
if (Assets.exists(levelPath, type)) return levelPath;
|
||||
|
||||
return getPreloadPath(file);
|
||||
}
|
||||
|
||||
public static function getLibraryPath(file:String, library = 'preload'):String
|
||||
{
|
||||
return if (library == 'preload' || library == 'default') getPreloadPath(file); else getLibraryPathForce(file, library);
|
||||
}
|
||||
|
||||
static inline function getLibraryPathForce(file:String, library:String):String
|
||||
{
|
||||
return '$library:assets/$library/$file';
|
||||
}
|
||||
|
||||
static inline function getPreloadPath(file:String):String
|
||||
{
|
||||
return 'assets/$file';
|
||||
}
|
||||
|
||||
public static function file(file:String, type:AssetType = TEXT, ?library:String):String
|
||||
{
|
||||
return getPath(file, type, library);
|
||||
}
|
||||
|
||||
public static function animateAtlas(path:String, ?library:String):String
|
||||
{
|
||||
return getLibraryPath('images/$path', library);
|
||||
}
|
||||
|
||||
public static function txt(key:String, ?library:String):String
|
||||
{
|
||||
return getPath('data/$key.txt', TEXT, library);
|
||||
}
|
||||
|
||||
public static function frag(key:String, ?library:String):String
|
||||
{
|
||||
return getPath('shaders/$key.frag', TEXT, library);
|
||||
}
|
||||
|
||||
public static function vert(key:String, ?library:String):String
|
||||
{
|
||||
return getPath('shaders/$key.vert', TEXT, library);
|
||||
}
|
||||
|
||||
public static function xml(key:String, ?library:String):String
|
||||
{
|
||||
return getPath('data/$key.xml', TEXT, library);
|
||||
}
|
||||
|
||||
public static function json(key:String, ?library:String):String
|
||||
{
|
||||
return getPath('data/$key.json', TEXT, library);
|
||||
}
|
||||
|
||||
public static function sound(key:String, ?library:String):String
|
||||
{
|
||||
return getPath('sounds/$key.${Constants.EXT_SOUND}', SOUND, library);
|
||||
}
|
||||
|
||||
public static function soundRandom(key:String, min:Int, max:Int, ?library:String):String
|
||||
{
|
||||
return sound(key + FlxG.random.int(min, max), library);
|
||||
}
|
||||
|
||||
public static function music(key:String, ?library:String):String
|
||||
{
|
||||
return getPath('music/$key.${Constants.EXT_SOUND}', MUSIC, library);
|
||||
}
|
||||
|
||||
public static function videos(key:String, ?library:String):String
|
||||
{
|
||||
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library ?? 'videos');
|
||||
}
|
||||
|
||||
public static function voices(song:String, ?suffix:String = ''):String
|
||||
{
|
||||
if (suffix == null) suffix = ''; // no suffix, for a sorta backwards compatibility with older-ish voice files
|
||||
|
||||
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to an `Inst.mp3/ogg` song instrumental from songs:assets/songs/`song`/
|
||||
* @param song name of the song to get instrumental for
|
||||
* @param suffix any suffix to add to end of song name, used for `-erect` variants usually
|
||||
* @param withExtension if it should return with the audio file extension `.mp3` or `.ogg`.
|
||||
* @return String
|
||||
*/
|
||||
public static function inst(song:String, ?suffix:String = '', ?withExtension:Bool = true):String
|
||||
{
|
||||
var ext:String = withExtension ? '.${Constants.EXT_SOUND}' : '';
|
||||
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix$ext';
|
||||
}
|
||||
|
||||
public static function image(key:String, ?library:String):String
|
||||
{
|
||||
return getPath('images/$key.png', IMAGE, library);
|
||||
}
|
||||
|
||||
public static function font(key:String):String
|
||||
{
|
||||
return 'assets/fonts/$key';
|
||||
}
|
||||
|
||||
public static function ui(key:String, ?library:String):String
|
||||
{
|
||||
return xml('ui/$key', library);
|
||||
}
|
||||
|
||||
public static function getSparrowAtlas(key:String, ?library:String):FlxAtlasFrames
|
||||
{
|
||||
return FlxAtlasFrames.fromSparrow(image(key, library), file('images/$key.xml', library));
|
||||
}
|
||||
|
||||
public static function getPackerAtlas(key:String, ?library:String):FlxAtlasFrames
|
||||
{
|
||||
return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library));
|
||||
}
|
||||
}
|
||||
|
||||
enum abstract PathsFunction(String)
|
||||
{
|
||||
var MUSIC;
|
||||
var INST;
|
||||
var VOICES;
|
||||
var SOUND;
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package funkin;
|
||||
|
||||
import funkin.save.Save;
|
||||
import funkin.input.Controls;
|
||||
import funkin.input.PreciseInputManager;
|
||||
import flixel.input.gamepad.FlxGamepad;
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
|
||||
/**
|
||||
* A core class which represents the current player(s) and their controls and other configuration.
|
||||
*/
|
||||
class PlayerSettings
|
||||
{
|
||||
// TODO: Finish implementation of second player.
|
||||
public static var numPlayers(default, null) = 0;
|
||||
public static var numAvatars(default, null) = 0;
|
||||
public static var player1(default, null):PlayerSettings;
|
||||
public static var player2(default, null):PlayerSettings;
|
||||
|
||||
public static var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
|
||||
public static var onAvatarRemove(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
|
||||
|
||||
/**
|
||||
* The player number associated with this settings object.
|
||||
*/
|
||||
public var id(default, null):Int;
|
||||
|
||||
/**
|
||||
* The controls handler for this player.
|
||||
*/
|
||||
public var controls(default, null):Controls;
|
||||
|
||||
/**
|
||||
* Return the PlayerSettings for the given player number, or `null` if that player isn't active.
|
||||
*
|
||||
* @param id The player number this represents.
|
||||
* @return The PlayerSettings for the given player number, or `null` if that player isn't active.
|
||||
*/
|
||||
public static function get(id:Int):Null<PlayerSettings>
|
||||
{
|
||||
return switch (id)
|
||||
{
|
||||
case 1: player1;
|
||||
case 2: player2;
|
||||
default: null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the PlayerSettings singletons for each player.
|
||||
*/
|
||||
public static function init():Void
|
||||
{
|
||||
if (player1 == null)
|
||||
{
|
||||
player1 = new PlayerSettings(1);
|
||||
++numPlayers;
|
||||
}
|
||||
|
||||
FlxG.gamepads.deviceConnected.add(onGamepadAdded);
|
||||
|
||||
var numGamepads = FlxG.gamepads.numActiveGamepads;
|
||||
for (i in 0...numGamepads)
|
||||
{
|
||||
var gamepad = FlxG.gamepads.getByID(i);
|
||||
if (gamepad != null) onGamepadAdded(gamepad);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcibly destroy the PlayerSettings singletons for each player.
|
||||
*/
|
||||
public static function reset():Void
|
||||
{
|
||||
player1 = null;
|
||||
player2 = null;
|
||||
numPlayers = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when a gamepad is added.
|
||||
* @param gamepad The gamepad that was added.
|
||||
*/
|
||||
static function onGamepadAdded(gamepad:FlxGamepad):Void
|
||||
{
|
||||
// TODO: Make this detect and handle multiple players
|
||||
player1.addGamepad(gamepad);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id The player number this represents. This was refactored to START AT `1`.
|
||||
*/
|
||||
function new(id:Int)
|
||||
{
|
||||
trace('loading player settings for id: $id');
|
||||
|
||||
this.id = id;
|
||||
this.controls = new Controls('player$id', None);
|
||||
|
||||
addKeyboard();
|
||||
}
|
||||
|
||||
function addKeyboard():Void
|
||||
{
|
||||
var useDefault:Bool = true;
|
||||
if (Save.instance.hasControls(id, Keys))
|
||||
{
|
||||
var keyControlData = Save.instance.getControls(id, Keys);
|
||||
trace('Loading keyboard control scheme from user save');
|
||||
useDefault = false;
|
||||
controls.fromSaveData(keyControlData, Keys);
|
||||
}
|
||||
else
|
||||
{
|
||||
useDefault = true;
|
||||
}
|
||||
|
||||
if (useDefault)
|
||||
{
|
||||
trace('Loading default keyboard control scheme');
|
||||
controls.setKeyboardScheme(Solo);
|
||||
}
|
||||
|
||||
PreciseInputManager.instance.initializeKeys(controls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after an FlxGamepad has been detected.
|
||||
* @param gamepad The gamepad that was detected.
|
||||
*/
|
||||
function addGamepad(gamepad:FlxGamepad):Void
|
||||
{
|
||||
var useDefault = true;
|
||||
if (Save.instance.hasControls(id, Gamepad(gamepad.id)))
|
||||
{
|
||||
var padControlData = Save.instance.getControls(id, Gamepad(gamepad.id));
|
||||
trace('Loading gamepad control scheme from user save');
|
||||
useDefault = false;
|
||||
controls.addGamepadWithSaveData(gamepad.id, padControlData);
|
||||
}
|
||||
else
|
||||
{
|
||||
useDefault = true;
|
||||
}
|
||||
|
||||
if (useDefault)
|
||||
{
|
||||
trace('Loading default gamepad control scheme');
|
||||
controls.addDefaultGamepad(gamepad.id);
|
||||
}
|
||||
PreciseInputManager.instance.initializeButtons(controls, gamepad);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save this player's controls to the game's persistent save.
|
||||
*/
|
||||
public function saveControls():Void
|
||||
{
|
||||
var keyData = controls.createSaveData(Keys);
|
||||
if (keyData != null)
|
||||
{
|
||||
trace('Saving keyboard control scheme to user save');
|
||||
Save.instance.setControls(id, Keys, keyData);
|
||||
}
|
||||
|
||||
if (controls.gamepadsAdded.length > 0)
|
||||
{
|
||||
var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0]));
|
||||
if (padData != null)
|
||||
{
|
||||
trace('Saving gamepad control scheme to user save');
|
||||
Save.instance.setControls(id, Gamepad(controls.gamepadsAdded[0]), padData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
package funkin;
|
||||
|
||||
import funkin.save.Save;
|
||||
import funkin.util.WindowUtil;
|
||||
|
||||
/**
|
||||
* A core class which provides a store of user-configurable, globally relevant values.
|
||||
*/
|
||||
class Preferences
|
||||
{
|
||||
/**
|
||||
* FPS
|
||||
* @default `60`
|
||||
*/
|
||||
public static var framerate(get, set):Int;
|
||||
|
||||
static function get_framerate():Int
|
||||
{
|
||||
#if web
|
||||
return 60;
|
||||
#else
|
||||
return Save?.instance?.options?.framerate ?? 60;
|
||||
#end
|
||||
}
|
||||
|
||||
static function set_framerate(value:Int):Int
|
||||
{
|
||||
#if web
|
||||
return 60;
|
||||
#else
|
||||
var save:Save = Save.instance;
|
||||
save.options.framerate = value;
|
||||
save.flush();
|
||||
FlxG.updateFramerate = value;
|
||||
FlxG.drawFramerate = value;
|
||||
return value;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether some particularly foul language is displayed.
|
||||
* @default `true`
|
||||
*/
|
||||
public static var naughtyness(get, set):Bool;
|
||||
|
||||
static function get_naughtyness():Bool
|
||||
{
|
||||
return Save?.instance?.options?.naughtyness;
|
||||
}
|
||||
|
||||
static function set_naughtyness(value:Bool):Bool
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.naughtyness = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the strumline is at the bottom of the screen rather than the top.
|
||||
* @default `false`
|
||||
*/
|
||||
public static var downscroll(get, set):Bool;
|
||||
|
||||
static function get_downscroll():Bool
|
||||
{
|
||||
return Save?.instance?.options?.downscroll;
|
||||
}
|
||||
|
||||
static function set_downscroll(value:Bool):Bool
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.downscroll = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If disabled, flashing lights in the main menu and other areas will be less intense.
|
||||
* @default `true`
|
||||
*/
|
||||
public static var flashingLights(get, set):Bool;
|
||||
|
||||
static function get_flashingLights():Bool
|
||||
{
|
||||
return Save?.instance?.options?.flashingLights ?? true;
|
||||
}
|
||||
|
||||
static function set_flashingLights(value:Bool):Bool
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.flashingLights = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If disabled, the camera bump synchronized to the beat.
|
||||
* @default `false`
|
||||
*/
|
||||
public static var zoomCamera(get, set):Bool;
|
||||
|
||||
static function get_zoomCamera():Bool
|
||||
{
|
||||
return Save?.instance?.options?.zoomCamera;
|
||||
}
|
||||
|
||||
static function set_zoomCamera(value:Bool):Bool
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.zoomCamera = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, an FPS and memory counter will be displayed even if this is not a debug build.
|
||||
* @default `false`
|
||||
*/
|
||||
public static var debugDisplay(get, set):Bool;
|
||||
|
||||
static function get_debugDisplay():Bool
|
||||
{
|
||||
return Save?.instance?.options?.debugDisplay;
|
||||
}
|
||||
|
||||
static function set_debugDisplay(value:Bool):Bool
|
||||
{
|
||||
if (value != Save.instance.options.debugDisplay)
|
||||
{
|
||||
toggleDebugDisplay(value);
|
||||
}
|
||||
|
||||
var save = Save.instance;
|
||||
save.options.debugDisplay = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the game will automatically pause when tabbing out.
|
||||
* @default `true`
|
||||
*/
|
||||
public static var autoPause(get, set):Bool;
|
||||
|
||||
static function get_autoPause():Bool
|
||||
{
|
||||
return Save?.instance?.options?.autoPause ?? true;
|
||||
}
|
||||
|
||||
static function set_autoPause(value:Bool):Bool
|
||||
{
|
||||
if (value != Save.instance.options.autoPause) FlxG.autoPause = value;
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.autoPause = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the game will automatically launch in fullscreen on startup.
|
||||
* @default `true`
|
||||
*/
|
||||
public static var autoFullscreen(get, set):Bool;
|
||||
|
||||
static function get_autoFullscreen():Bool
|
||||
{
|
||||
return Save?.instance?.options?.autoFullscreen ?? true;
|
||||
}
|
||||
|
||||
static function set_autoFullscreen(value:Bool):Bool
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.autoFullscreen = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the game will utilize VSync (or adaptive VSync) on startup.
|
||||
* @default `OFF`
|
||||
*/
|
||||
public static var vsyncMode(get, set):lime.ui.WindowVSyncMode;
|
||||
|
||||
static function get_vsyncMode():lime.ui.WindowVSyncMode
|
||||
{
|
||||
var value = Save?.instance?.options?.vsyncMode ?? "Off";
|
||||
|
||||
return switch (value)
|
||||
{
|
||||
case "Off":
|
||||
lime.ui.WindowVSyncMode.OFF;
|
||||
case "On":
|
||||
lime.ui.WindowVSyncMode.ON;
|
||||
case "Adaptive":
|
||||
lime.ui.WindowVSyncMode.ADAPTIVE;
|
||||
default:
|
||||
lime.ui.WindowVSyncMode.OFF;
|
||||
};
|
||||
}
|
||||
|
||||
static function set_vsyncMode(value:lime.ui.WindowVSyncMode):lime.ui.WindowVSyncMode
|
||||
{
|
||||
var string;
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case lime.ui.WindowVSyncMode.OFF:
|
||||
string = "Off";
|
||||
case lime.ui.WindowVSyncMode.ON:
|
||||
string = "On";
|
||||
case lime.ui.WindowVSyncMode.ADAPTIVE:
|
||||
string = "Adaptive";
|
||||
default:
|
||||
string = "Off";
|
||||
};
|
||||
|
||||
WindowUtil.setVSyncMode(value);
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.vsyncMode = string;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
public static var unlockedFramerate(get, set):Bool;
|
||||
|
||||
static function get_unlockedFramerate():Bool
|
||||
{
|
||||
return Save?.instance?.options?.unlockedFramerate;
|
||||
}
|
||||
|
||||
static function set_unlockedFramerate(value:Bool):Bool
|
||||
{
|
||||
if (value != Save.instance.options.unlockedFramerate)
|
||||
{
|
||||
#if web
|
||||
toggleFramerateCap(value);
|
||||
#end
|
||||
}
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.unlockedFramerate = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
#if web
|
||||
// We create a haxe version of this just for readability.
|
||||
// We use these to override `window.requestAnimationFrame` in Javascript to uncap the framerate / "animation" request rate
|
||||
// Javascript is crazy since u can just do stuff like that lol
|
||||
|
||||
public static function unlockedFramerateFunction(callback, element)
|
||||
{
|
||||
var currTime = Date.now().getTime();
|
||||
var timeToCall = 0;
|
||||
var id = js.Browser.window.setTimeout(function() {
|
||||
callback(currTime + timeToCall);
|
||||
}, timeToCall);
|
||||
return id;
|
||||
}
|
||||
|
||||
// Lime already implements their own little framerate cap, so we can just use that
|
||||
// This also gets set in the init function in Main.hx, since we need to definitely override it
|
||||
public static var lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame");
|
||||
#end
|
||||
|
||||
/**
|
||||
* If >0, the game will display a semi-opaque background under the notes.
|
||||
* `0` for no background, `100` for solid black if you're freaky like that
|
||||
* @default `0`
|
||||
*/
|
||||
public static var strumlineBackgroundOpacity(get, set):Int;
|
||||
|
||||
static function get_strumlineBackgroundOpacity():Int
|
||||
{
|
||||
return (Save?.instance?.options?.strumlineBackgroundOpacity ?? 0);
|
||||
}
|
||||
|
||||
static function set_strumlineBackgroundOpacity(value:Int):Int
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.strumlineBackgroundOpacity = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the game will hide the mouse when taking a screenshot.
|
||||
* @default `true`
|
||||
*/
|
||||
public static var shouldHideMouse(get, set):Bool;
|
||||
|
||||
static function get_shouldHideMouse():Bool
|
||||
{
|
||||
return Save?.instance?.options?.screenshot?.shouldHideMouse ?? true;
|
||||
}
|
||||
|
||||
static function set_shouldHideMouse(value:Bool):Bool
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.shouldHideMouse = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the game will show a preview after taking a screenshot.
|
||||
* @default `true`
|
||||
*/
|
||||
public static var fancyPreview(get, set):Bool;
|
||||
|
||||
static function get_fancyPreview():Bool
|
||||
{
|
||||
return Save?.instance?.options?.screenshot?.fancyPreview ?? true;
|
||||
}
|
||||
|
||||
static function set_fancyPreview(value:Bool):Bool
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.fancyPreview = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If enabled, the game will show the preview only after a screenshot is saved.
|
||||
* @default `true`
|
||||
*/
|
||||
public static var previewOnSave(get, set):Bool;
|
||||
|
||||
static function get_previewOnSave():Bool
|
||||
{
|
||||
return Save?.instance?.options?.screenshot?.previewOnSave ?? true;
|
||||
}
|
||||
|
||||
static function set_previewOnSave(value:Bool):Bool
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.previewOnSave = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The game will save any screenshots taken to this format.
|
||||
* @default `PNG`
|
||||
*/
|
||||
public static var saveFormat(get, set):Any;
|
||||
|
||||
static function get_saveFormat():Any
|
||||
{
|
||||
return Save?.instance?.options?.screenshot?.saveFormat ?? 'PNG';
|
||||
}
|
||||
|
||||
static function set_saveFormat(value):Any
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.saveFormat = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The game will save JPEG screenshots with this quality percentage.
|
||||
* @default `80`
|
||||
*/
|
||||
public static var jpegQuality(get, set):Int;
|
||||
|
||||
static function get_jpegQuality():Int
|
||||
{
|
||||
return Save?.instance?.options?.screenshot?.jpegQuality ?? 80;
|
||||
}
|
||||
|
||||
static function set_jpegQuality(value:Int):Int
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.jpegQuality = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the user's preferences from the save data and apply them.
|
||||
*/
|
||||
public static function init():Void
|
||||
{
|
||||
// Apply the autoPause setting (enables automatic pausing on focus lost).
|
||||
FlxG.autoPause = Preferences.autoPause;
|
||||
// WindowUtil.setVSyncMode(Preferences.vsyncMode);
|
||||
// Apply the debugDisplay setting (enables the FPS and RAM display).
|
||||
toggleDebugDisplay(Preferences.debugDisplay);
|
||||
#if web
|
||||
toggleFramerateCap(Preferences.unlockedFramerate);
|
||||
#end
|
||||
// Apply the autoFullscreen setting (launches the game in fullscreen automatically)
|
||||
FlxG.fullscreen = Preferences.autoFullscreen;
|
||||
}
|
||||
|
||||
static function toggleFramerateCap(unlocked:Bool):Void
|
||||
{
|
||||
#if web
|
||||
var framerateFunction = unlocked ? unlockedFramerateFunction : lockedFramerateFunction;
|
||||
untyped js.Syntax.code("window.requestAnimationFrame = framerateFunction;");
|
||||
#end
|
||||
}
|
||||
|
||||
static function toggleDebugDisplay(show:Bool):Void
|
||||
{
|
||||
if (show)
|
||||
{
|
||||
// Enable the debug display.
|
||||
FlxG.stage.addChild(Main.fpsCounter);
|
||||
#if !html5
|
||||
FlxG.stage.addChild(Main.memoryCounter);
|
||||
#end
|
||||
}
|
||||
else
|
||||
{
|
||||
// Disable the debug display.
|
||||
FlxG.stage.removeChild(Main.fpsCounter);
|
||||
#if !html5
|
||||
FlxG.stage.removeChild(Main.memoryCounter);
|
||||
#end
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package funkin.api.discord;
|
||||
|
||||
#if FEATURE_DISCORD_RPC
|
||||
import hxdiscord_rpc.Discord;
|
||||
import hxdiscord_rpc.Types.DiscordButton;
|
||||
import hxdiscord_rpc.Types.DiscordEventHandlers;
|
||||
import hxdiscord_rpc.Types.DiscordRichPresence;
|
||||
import hxdiscord_rpc.Types.DiscordUser;
|
||||
import sys.thread.Thread;
|
||||
|
||||
class DiscordClient
|
||||
{
|
||||
static final CLIENT_ID:String = "816168432860790794";
|
||||
|
||||
public static var instance(get, never):DiscordClient;
|
||||
static var _instance:Null<DiscordClient> = null;
|
||||
|
||||
static function get_instance():DiscordClient
|
||||
{
|
||||
if (DiscordClient._instance == null) _instance = new DiscordClient();
|
||||
if (DiscordClient._instance == null) throw "Could not initialize singleton DiscordClient!";
|
||||
return DiscordClient._instance;
|
||||
}
|
||||
|
||||
var handlers:DiscordEventHandlers;
|
||||
|
||||
private function new()
|
||||
{
|
||||
trace('[DISCORD] Initializing event handlers...');
|
||||
|
||||
handlers = DiscordEventHandlers.create();
|
||||
|
||||
handlers.ready = cpp.Function.fromStaticFunction(onReady);
|
||||
handlers.disconnected = cpp.Function.fromStaticFunction(onDisconnected);
|
||||
handlers.errored = cpp.Function.fromStaticFunction(onError);
|
||||
}
|
||||
|
||||
public function init():Void
|
||||
{
|
||||
trace('[DISCORD] Initializing connection...');
|
||||
|
||||
// Discord.initialize(CLIENT_ID, handlers, true, null);
|
||||
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, null);
|
||||
|
||||
createDaemon();
|
||||
}
|
||||
|
||||
var daemon:Thread = null;
|
||||
|
||||
function createDaemon():Void
|
||||
{
|
||||
daemon = Thread.create(doDaemonWork);
|
||||
}
|
||||
|
||||
function doDaemonWork():Void
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
trace('[DISCORD] Performing client update...');
|
||||
|
||||
#if DISCORD_DISABLE_IO_THREAD
|
||||
Discord.updateConnection();
|
||||
#end
|
||||
|
||||
Discord.runCallbacks();
|
||||
Sys.sleep(2);
|
||||
}
|
||||
}
|
||||
|
||||
public function shutdown():Void
|
||||
{
|
||||
trace('[DISCORD] Shutting down...');
|
||||
|
||||
Discord.shutdown();
|
||||
}
|
||||
|
||||
public function setPresence(params:DiscordClientPresenceParams):Void
|
||||
{
|
||||
trace('[DISCORD] Updating presence... (${params})');
|
||||
|
||||
Discord.updatePresence(buildPresence(params));
|
||||
}
|
||||
|
||||
function buildPresence(params:DiscordClientPresenceParams):DiscordRichPresence
|
||||
{
|
||||
var presence = DiscordRichPresence.create();
|
||||
|
||||
// Presence should always be playing the game.
|
||||
presence.type = DiscordActivityType_Playing;
|
||||
|
||||
// Text when hovering over the large image. We just leave this as the game name.
|
||||
presence.largeImageText = "Friday Night Funkin'";
|
||||
|
||||
// State should be generally what the person is doing, like "In the Menus" or "Pico (Pico Mix) [Freeplay Hard]"
|
||||
presence.state = cast(params.state, Null<String>);
|
||||
// Details should be what the person is specifically doing, including stuff like timestamps (maybe something like "03:24 elapsed").
|
||||
presence.details = cast(params.details, Null<String>);
|
||||
|
||||
// The large image displaying what the user is doing.
|
||||
// This should probably be album art.
|
||||
// IMPORTANT NOTE: This can be an asset key uploaded to Discord's developer panel OR any URL you like.
|
||||
presence.largeImageKey = cast(params.largeImageKey, Null<String>) ?? "album-volume1";
|
||||
|
||||
trace('[DISCORD] largeImageKey: ${presence.largeImageKey}');
|
||||
|
||||
// TODO: Make this use the song's album art.
|
||||
// presence.largeImageKey = "icon";
|
||||
// presence.largeImageKey = "https://f4.bcbits.com/img/a0746694746_16.jpg";
|
||||
|
||||
// The small inset image for what the user is doing.
|
||||
// This can be the opponent's health icon?
|
||||
// NOTE: Like largeImageKey, this can be a URL, or an asset key.
|
||||
presence.smallImageKey = cast(params.smallImageKey, Null<String>);
|
||||
|
||||
// NOTE: In previous versions, this showed as "Elapsed", but now shows as playtime and doesn't look good
|
||||
// presence.startTimestamp = time - 10;
|
||||
// presence.endTimestamp = time + 30;
|
||||
|
||||
final button1:DiscordButton = DiscordButton.create();
|
||||
button1.label = "Play on Web";
|
||||
button1.url = Constants.URL_NEWGROUNDS;
|
||||
presence.buttons[0] = button1;
|
||||
|
||||
final button2:DiscordButton = DiscordButton.create();
|
||||
button2.label = "Download";
|
||||
button2.url = Constants.URL_ITCH;
|
||||
presence.buttons[1] = button2;
|
||||
|
||||
return presence;
|
||||
}
|
||||
|
||||
// TODO: WHAT THE FUCK get this pointer bullfuckery out of here
|
||||
private static function onReady(request:cpp.RawConstPointer<DiscordUser>):Void
|
||||
{
|
||||
trace('[DISCORD] Client has connected!');
|
||||
|
||||
final username:String = request[0].username;
|
||||
final globalName:String = request[0].username;
|
||||
final discriminator:Int = Std.parseInt(request[0].discriminator);
|
||||
|
||||
if (discriminator != 0)
|
||||
{
|
||||
trace('[DISCORD] User: ${username}#${discriminator} (${globalName})');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[DISCORD] User: @${username} (${globalName})');
|
||||
}
|
||||
}
|
||||
|
||||
private static function onDisconnected(errorCode:Int, message:cpp.ConstCharStar):Void
|
||||
{
|
||||
trace('[DISCORD] Client has disconnected! ($errorCode) "${cast (message, String)}"');
|
||||
}
|
||||
|
||||
private static function onError(errorCode:Int, message:cpp.ConstCharStar):Void
|
||||
{
|
||||
trace('[DISCORD] Client has received an error! ($errorCode) "${cast (message, String)}"');
|
||||
}
|
||||
|
||||
// public var type(get, set):DiscordActivityType;
|
||||
// public var state(get, set):String;
|
||||
// public var details(get, set):String;
|
||||
// public var startTimestamp(get, set):Int;
|
||||
// public var endTimestamp(get, set):Int;
|
||||
// public var largeImageKey(get, set):String;
|
||||
// public var largeImageText(get, set):String;
|
||||
// public var smallImageKey(get, set):String;
|
||||
// public var smallImageText(get, set):String;
|
||||
//
|
||||
//
|
||||
// public var partyId(get, set)
|
||||
// public var partySize(get, set)
|
||||
// public var partyMax(get, set)
|
||||
// public var partyPrivacy(get, set)
|
||||
//
|
||||
// public var buttons(get, set)
|
||||
//
|
||||
// public var matchSecret(get, set)
|
||||
// public var joinSecret(get, set)
|
||||
// public var spectateSecret(get, set)
|
||||
}
|
||||
|
||||
typedef DiscordClientPresenceParams =
|
||||
{
|
||||
/**
|
||||
* The first row of text below the game title.
|
||||
*/
|
||||
var state:String;
|
||||
|
||||
/**
|
||||
* The second row of text below the game title.
|
||||
* Use `null` to display no text.
|
||||
*/
|
||||
var details:Null<String>;
|
||||
|
||||
/**
|
||||
* A large, 4-row high image to the left of the content.
|
||||
*/
|
||||
var ?largeImageKey:String;
|
||||
|
||||
/**
|
||||
* A small, inset image to the bottom right of `largeImageKey`.
|
||||
*/
|
||||
var ?smallImageKey:String;
|
||||
}
|
||||
#end
|
||||
@@ -0,0 +1,106 @@
|
||||
package funkin.api.newgrounds;
|
||||
|
||||
import io.newgrounds.Call.CallOutcome;
|
||||
import io.newgrounds.NG;
|
||||
import io.newgrounds.objects.events.Outcome;
|
||||
import io.newgrounds.objects.events.Result;
|
||||
|
||||
/**
|
||||
* Use Newgrounds to perform basic telemetry. Ignore if not logged in to Newgrounds.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Events
|
||||
{
|
||||
// Only allow letters, numbers, spaces, dashes, and underscores.
|
||||
static final EVENT_NAME_REGEX:EReg = ~/[^a-zA-Z0-9 -_]/g;
|
||||
|
||||
public static function logEvent(eventName:String):Void
|
||||
{
|
||||
#if (FEATURE_NEWGROUNDS && FEATURE_NEWGROUNDS_EVENTS)
|
||||
if (NewgroundsClient.instance.isLoggedIn())
|
||||
{
|
||||
var eventHandler = NG.core.calls.event;
|
||||
|
||||
if (eventHandler != null)
|
||||
{
|
||||
var sanitizedEventName = EVENT_NAME_REGEX.replace(eventName, '');
|
||||
var outcomeHandler = onEventLogged.bind(sanitizedEventName, _);
|
||||
eventHandler.logEvent(sanitizedEventName).addOutcomeHandler(outcomeHandler).send();
|
||||
}
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
static function onEventLogged(eventName:String, outcome:CallOutcome<LogEventData>)
|
||||
{
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS(data):
|
||||
trace('[NEWGROUNDS] Logged event: ${data.eventName}');
|
||||
case FAIL(outcome):
|
||||
switch (outcome)
|
||||
{
|
||||
case HTTP(error):
|
||||
trace('[NEWGROUNDS] HTTP error while logging event: ${error}');
|
||||
case RESPONSE(error):
|
||||
trace('[NEWGROUNDS] Response error (${error.code}) while logging event: ${error.message}');
|
||||
case RESULT(error):
|
||||
switch (error.code)
|
||||
{
|
||||
case 103: // Invalid custom event name
|
||||
trace('[NEWGROUNDS] Invalid custom event name: ${eventName}');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Result error (${error.code}) while logging event: ${error.message}');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static inline function logStartGame():Void
|
||||
{
|
||||
logEvent('start-game');
|
||||
}
|
||||
|
||||
public static inline function logStartSong(songId:String, variation:String):Void
|
||||
{
|
||||
logEvent('start-song_${songId}-${variation}');
|
||||
}
|
||||
|
||||
public static inline function logFailSong(songId:String, variation:String):Void
|
||||
{
|
||||
logEvent('blueballs_${songId}-${variation}');
|
||||
}
|
||||
|
||||
public static inline function logCompleteSong(songId:String, variation:String):Void
|
||||
{
|
||||
logEvent('complete-song_${songId}-${variation}');
|
||||
}
|
||||
|
||||
public static inline function logStartLevel(levelId:String):Void
|
||||
{
|
||||
logEvent('start-level_${levelId}');
|
||||
}
|
||||
|
||||
public static inline function logCompleteLevel(levelId:String):Void
|
||||
{
|
||||
logEvent('complete-level_${levelId}');
|
||||
}
|
||||
|
||||
public static inline function logEarnRank(rankName:String):Void
|
||||
{
|
||||
logEvent('earn-rank_${rankName}');
|
||||
}
|
||||
|
||||
public static inline function logWatchCartoon():Void
|
||||
{
|
||||
logEvent('watch-cartoon');
|
||||
}
|
||||
|
||||
// Note there is already a loadReferral call for the merch link
|
||||
// and that gets logged as an event!
|
||||
|
||||
public static inline function logOpenCredits():Void
|
||||
{
|
||||
logEvent('open-credits');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
package funkin.api.newgrounds;
|
||||
|
||||
#if FEATURE_NEWGROUNDS
|
||||
import io.newgrounds.Call.CallError;
|
||||
import io.newgrounds.objects.ScoreBoard as LeaderboardData;
|
||||
import io.newgrounds.objects.events.Outcome;
|
||||
import io.newgrounds.utils.ScoreBoardList;
|
||||
|
||||
class Leaderboards
|
||||
{
|
||||
public static function listLeaderboardData():Map<Leaderboard, LeaderboardData>
|
||||
{
|
||||
var leaderboardList:Null<ScoreBoardList> = NewgroundsClient.instance.leaderboards;
|
||||
if (leaderboardList == null)
|
||||
{
|
||||
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
|
||||
return [];
|
||||
}
|
||||
else
|
||||
{
|
||||
var result:Map<Leaderboard, LeaderboardData> = [];
|
||||
|
||||
for (leaderboardId in leaderboardList.keys())
|
||||
{
|
||||
var leaderboardData = leaderboardList.get(leaderboardId);
|
||||
if (leaderboardData == null) continue;
|
||||
|
||||
// A little hacky, but it works.
|
||||
result.set(cast leaderboardId, leaderboardData);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a score to Newgrounds.
|
||||
* @param leaderboard The leaderboard to submit to.
|
||||
* @param score The score to submit.
|
||||
* @param tag An optional tag to attach to the score.
|
||||
*/
|
||||
public static function submitScore(leaderboard:Leaderboard, score:Int, ?tag:String):Void
|
||||
{
|
||||
// Silently reject submissions for unknown leaderboards.
|
||||
if (leaderboard == Leaderboard.Unknown) return;
|
||||
|
||||
if (NewgroundsClient.instance.isLoggedIn())
|
||||
{
|
||||
var leaderboardList = NewgroundsClient.instance.leaderboards;
|
||||
if (leaderboardList == null) return;
|
||||
|
||||
var leaderboardData:Null<LeaderboardData> = leaderboardList.get(leaderboard.getId());
|
||||
if (leaderboardData != null)
|
||||
{
|
||||
leaderboardData.postScore(score, function(outcome:Outcome<CallError>):Void {
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Submitted score!');
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to submit score!');
|
||||
trace(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a score for a Story Level to Newgrounds.
|
||||
*/
|
||||
public static function submitLevelScore(levelId:String, difficultyId:String, score:Int):Void
|
||||
{
|
||||
var tag = '${difficultyId}';
|
||||
Leaderboards.submitScore(Leaderboard.getLeaderboardByLevel(levelId), score, tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a score for a song to Newgrounds.
|
||||
*/
|
||||
public static function submitSongScore(songId:String, difficultyId:String, score:Int):Void
|
||||
{
|
||||
var tag = '${difficultyId}';
|
||||
Leaderboards.submitScore(Leaderboard.getLeaderboardBySong(songId, difficultyId), score, tag);
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
enum abstract Leaderboard(Int)
|
||||
{
|
||||
/**
|
||||
* Represents an undefined or invalid leaderboard.
|
||||
*/
|
||||
var Unknown = -1;
|
||||
|
||||
//
|
||||
// STORY LEVELS
|
||||
//
|
||||
var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14239 #else 9615 #end;
|
||||
var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14240 #else 9616 #end;
|
||||
var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14242 #else 9767 #end;
|
||||
var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14241 #else 9866 #end;
|
||||
var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14243 #else 9956 #end;
|
||||
var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14244 #else 9957 #end;
|
||||
var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14245 #else 14682 #end;
|
||||
var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14237 #else 14683 #end;
|
||||
|
||||
//
|
||||
// SONGS
|
||||
//
|
||||
// Tutorial
|
||||
var Tutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14249 #else 14684 #end;
|
||||
|
||||
// Week 1
|
||||
var Bopeebo = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14246 #else 9603 #end;
|
||||
var BopeeboErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14685 #end;
|
||||
var BopeeboPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14686 #end;
|
||||
var Fresh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14247 #else 9602 #end;
|
||||
var FreshErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14687 #end;
|
||||
var FreshPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14688 #end;
|
||||
var DadBattle = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 14248 #else 9605 #end;
|
||||
var DadBattleErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14689 #end;
|
||||
var DadBattlePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14690 #end;
|
||||
|
||||
// Week 2
|
||||
var Spookeez = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9604 #end;
|
||||
var SpookeezErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14691 #end;
|
||||
var SpookeezPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14692 #end;
|
||||
var South = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9606 #end;
|
||||
var SouthErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14693 #end;
|
||||
var SouthPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14694 #end;
|
||||
var Monster = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14703 #end;
|
||||
|
||||
// Week 3
|
||||
var Pico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9766 #end;
|
||||
var PicoErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14695 #end;
|
||||
var PicoPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14696 #end;
|
||||
var PhillyNice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9769 #end;
|
||||
var PhillyNiceErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14697 #end;
|
||||
var PhillyNicePicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14698 #end;
|
||||
var Blammed = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9768 #end;
|
||||
var BlammedErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14704 #end;
|
||||
var BlammedPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14705 #end;
|
||||
|
||||
// Week 4
|
||||
var SatinPanties = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9869 #end;
|
||||
var SatinPantiesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14701 #end;
|
||||
var High = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9867 #end;
|
||||
var HighErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14699 #end;
|
||||
var MILF = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9868 #end;
|
||||
|
||||
// Week 5
|
||||
var Cocoa = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14706 #end;
|
||||
var CocoaErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14707 #end;
|
||||
var CocoaPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14708 #end;
|
||||
var Eggnog = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14709 #end;
|
||||
var EggnogErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14711 #end;
|
||||
var EggnogPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14710 #end;
|
||||
var WinterHorrorland = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14712 #end;
|
||||
|
||||
// Week 6
|
||||
var Senpai = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9958 #end;
|
||||
var SenpaiErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14713 #end;
|
||||
var SenpaiPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14716 #end;
|
||||
var Roses = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9959 #end;
|
||||
var RosesErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14714 #end;
|
||||
var RosesPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14717 #end;
|
||||
var Thorns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 9960 #end;
|
||||
var ThornsErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14715 #end;
|
||||
|
||||
// Week 7
|
||||
var Ugh = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14718 #end;
|
||||
var UghErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14722 #end;
|
||||
var UghPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14721 #end;
|
||||
var Guns = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14719 #end;
|
||||
var GunsPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14723 #end;
|
||||
var Stress = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14720 #end;
|
||||
var StressPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14724 #end;
|
||||
|
||||
// Weekend 1
|
||||
var Darnell = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14725 #end;
|
||||
var DarnellErect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14727 #end;
|
||||
var DarnellBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14726 #end;
|
||||
var LitUp = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14728 #end;
|
||||
var LitUpBFMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14729 #end;
|
||||
var TwoHot = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14730 #end; // Variable names can't start with a number!
|
||||
var Blazin = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 1000000 #else 14731 #end;
|
||||
|
||||
public function getId():Int
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the leaderboard for a given level and difficulty.
|
||||
* @param levelId The ID for the story level.
|
||||
* @param difficulty The current difficulty.
|
||||
* @return The Leaderboard ID for the given level and difficulty.
|
||||
*/
|
||||
public static function getLeaderboardByLevel(levelId:String):Leaderboard
|
||||
{
|
||||
switch (levelId)
|
||||
{
|
||||
case "week1":
|
||||
return StoryWeek1;
|
||||
case "week2":
|
||||
return StoryWeek2;
|
||||
case "week3":
|
||||
return StoryWeek3;
|
||||
case "week4":
|
||||
return StoryWeek4;
|
||||
case "week5":
|
||||
return StoryWeek5;
|
||||
case "week6":
|
||||
return StoryWeek6;
|
||||
case "week7":
|
||||
return StoryWeek7;
|
||||
case "weekend1":
|
||||
return StoryWeekend1;
|
||||
default:
|
||||
return Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the leaderboard for a given level and difficulty.
|
||||
* @param levelId The ID for the story level.
|
||||
* @param difficulty The current difficulty, suffixed with the variation, like `easy-pico` or `nightmare`.
|
||||
* @return The Leaderboard ID for the given level and difficulty.
|
||||
*/
|
||||
public static function getLeaderboardBySong(songId:String, difficulty:String):Leaderboard
|
||||
{
|
||||
var variation = Constants.DEFAULT_VARIATION;
|
||||
var difficultyParts = difficulty.split('-');
|
||||
|
||||
if (difficultyParts.length >= 2)
|
||||
{
|
||||
variation = difficultyParts[difficultyParts.length - 1];
|
||||
}
|
||||
else if (Constants.DEFAULT_DIFFICULTY_LIST_ERECT.contains(difficulty))
|
||||
{
|
||||
variation = "erect";
|
||||
}
|
||||
|
||||
switch (variation)
|
||||
{
|
||||
case "pico":
|
||||
switch (songId)
|
||||
{
|
||||
case "bopeebo":
|
||||
return BopeeboPicoMix;
|
||||
case "fresh":
|
||||
return FreshPicoMix;
|
||||
case "dadbattle":
|
||||
return DadBattlePicoMix;
|
||||
case "spookeez":
|
||||
return SpookeezPicoMix;
|
||||
case "south":
|
||||
return SouthPicoMix;
|
||||
case "pico":
|
||||
return PicoPicoMix;
|
||||
case "philly-nice":
|
||||
return PhillyNicePicoMix;
|
||||
case "blammed":
|
||||
return BlammedPicoMix;
|
||||
case "cocoa":
|
||||
return CocoaPicoMix;
|
||||
case "eggnog":
|
||||
return EggnogPicoMix;
|
||||
case "senpai":
|
||||
return SenpaiPicoMix;
|
||||
case "roses":
|
||||
return RosesPicoMix;
|
||||
case "ugh":
|
||||
return UghPicoMix;
|
||||
case "guns":
|
||||
return GunsPicoMix;
|
||||
case "stress":
|
||||
return StressPicoMix;
|
||||
default:
|
||||
return Unknown;
|
||||
}
|
||||
case "bf":
|
||||
switch (songId)
|
||||
{
|
||||
case "darnell":
|
||||
return DarnellBFMix;
|
||||
case "litup":
|
||||
return LitUpBFMix;
|
||||
default:
|
||||
return Unknown;
|
||||
}
|
||||
case "erect":
|
||||
switch (songId)
|
||||
{
|
||||
case "bopeebo":
|
||||
return BopeeboErect;
|
||||
case "fresh":
|
||||
return FreshErect;
|
||||
case "dadbattle":
|
||||
return DadBattleErect;
|
||||
case "spookeez":
|
||||
return SpookeezErect;
|
||||
case "south":
|
||||
return SouthErect;
|
||||
case "pico":
|
||||
return PicoErect;
|
||||
case "philly-nice":
|
||||
return PhillyNiceErect;
|
||||
case "blammed":
|
||||
return BlammedErect;
|
||||
case "satin-panties":
|
||||
return SatinPantiesErect;
|
||||
case "high":
|
||||
return HighErect;
|
||||
case "cocoa":
|
||||
return CocoaErect;
|
||||
case "eggnog":
|
||||
return EggnogErect;
|
||||
case "senpai":
|
||||
return SenpaiErect;
|
||||
case "roses":
|
||||
return RosesErect;
|
||||
case "thorns":
|
||||
return ThornsErect;
|
||||
case "ugh":
|
||||
return UghErect;
|
||||
case "darnell":
|
||||
return DarnellErect;
|
||||
default:
|
||||
return Unknown;
|
||||
}
|
||||
case "default":
|
||||
switch (songId)
|
||||
{
|
||||
case "tutorial":
|
||||
return Tutorial;
|
||||
case "bopeebo":
|
||||
return Bopeebo;
|
||||
case "fresh":
|
||||
return Fresh;
|
||||
case "dadbattle":
|
||||
return DadBattle;
|
||||
case "spookeez":
|
||||
return Spookeez;
|
||||
case "south":
|
||||
return South;
|
||||
case "monster":
|
||||
return Monster;
|
||||
case "pico":
|
||||
return Pico;
|
||||
case "philly-nice":
|
||||
return PhillyNice;
|
||||
case "blammed":
|
||||
return Blammed;
|
||||
case "satin-panties":
|
||||
return SatinPanties;
|
||||
case "high":
|
||||
return High;
|
||||
case "milf":
|
||||
return MILF;
|
||||
case "cocoa":
|
||||
return Cocoa;
|
||||
case "eggnog":
|
||||
return Eggnog;
|
||||
case "winter-horrorland":
|
||||
return WinterHorrorland;
|
||||
case "senpai":
|
||||
return Senpai;
|
||||
case "roses":
|
||||
return Roses;
|
||||
case "thorns":
|
||||
return Thorns;
|
||||
case "ugh":
|
||||
return Ugh;
|
||||
case "guns":
|
||||
return Guns;
|
||||
case "stress":
|
||||
return Stress;
|
||||
case "darnell":
|
||||
return Darnell;
|
||||
case "litup":
|
||||
return LitUp;
|
||||
case "2hot":
|
||||
return TwoHot;
|
||||
case "blazin":
|
||||
return Blazin;
|
||||
default:
|
||||
return Unknown;
|
||||
}
|
||||
default:
|
||||
return Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package funkin.api.newgrounds;
|
||||
|
||||
#if FEATURE_NEWGROUNDS
|
||||
import io.newgrounds.objects.Medal as MedalData;
|
||||
import funkin.util.plugins.NewgroundsMedalPlugin;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import openfl.display.BitmapData;
|
||||
import io.newgrounds.utils.MedalList;
|
||||
import haxe.Json;
|
||||
|
||||
class Medals
|
||||
{
|
||||
public static var medalJSON:Array<MedalJSON> = [];
|
||||
|
||||
public static function listMedalData():Map<Medal, MedalData>
|
||||
{
|
||||
var medalList = NewgroundsClient.instance.medals;
|
||||
|
||||
if (medalList == null)
|
||||
{
|
||||
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
|
||||
return [];
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Why do I have to do this, @:nullSafety is fucked up
|
||||
var result:Map<Medal, MedalData> = [];
|
||||
|
||||
for (medalId in medalList.keys())
|
||||
{
|
||||
var medalData = medalList.get(medalId);
|
||||
if (medalData == null) continue;
|
||||
|
||||
// A little hacky, but it works.
|
||||
result.set(cast medalId, medalData);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static function award(medal:Medal):Void
|
||||
{
|
||||
if (NewgroundsClient.instance.isLoggedIn())
|
||||
{
|
||||
var medalList = NewgroundsClient.instance.medals;
|
||||
@:privateAccess
|
||||
if (medalList == null || medalList._map == null) return;
|
||||
|
||||
var medalData:Null<MedalData> = medalList.get(medal.getId());
|
||||
@:privateAccess
|
||||
if (medalData == null || medalData._data == null)
|
||||
{
|
||||
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
|
||||
return;
|
||||
}
|
||||
else if (!medalData.unlocked)
|
||||
{
|
||||
trace('[NEWGROUNDS] Awarding medal (${medal}).');
|
||||
medalData.sendUnlock();
|
||||
|
||||
// Play the medal unlock animation, but only if the user has not already unlocked it.
|
||||
#if html5
|
||||
// Web builds support parsing the bitmap data from the URL directly.
|
||||
BitmapData.loadFromFile("https:" + medalData.icon).onComplete(function(bmp:BitmapData) {
|
||||
var medalGraphic = FlxGraphic.fromBitmapData(bmp);
|
||||
medalGraphic.persist = true;
|
||||
NewgroundsMedalPlugin.play(medalData.value, medalData.name, medalGraphic);
|
||||
});
|
||||
#else
|
||||
if ((medalJSON?.length ?? 0) == 0) loadMedalJSON();
|
||||
// We have to use a medal image from the game files. We use a Base64 encoded image that NG spits out.
|
||||
// TODO: Wait, don't they give us the medal icon?
|
||||
|
||||
var localMedalData:Null<MedalJSON> = medalJSON.filter(function(jsonMedal) {
|
||||
#if FEATURE_NEWGROUNDS_TESTING_MEDALS
|
||||
return medal == jsonMedal.idTest;
|
||||
#else
|
||||
return medal == jsonMedal.id;
|
||||
#end
|
||||
})[0];
|
||||
|
||||
if (localMedalData == null) throw "You forgot to encode a Base64 image for medal: " + medal;
|
||||
|
||||
var str:String = localMedalData.icon;
|
||||
// Lime/OpenFL parses it without the included prefix stuff, so we remove it.
|
||||
str = str.replace("data:image/png;base64,", "").trim();
|
||||
var bitmapData = BitmapData.fromBase64(str, "image/png");
|
||||
var medalGraphic:Null<FlxGraphic> = null;
|
||||
if (str != null)
|
||||
{
|
||||
medalGraphic = FlxGraphic.fromBitmapData(bitmapData);
|
||||
medalGraphic.persist = true;
|
||||
}
|
||||
|
||||
NewgroundsMedalPlugin.play(medalData.value, medalData.name, medalGraphic);
|
||||
#end
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[NEWGROUNDS] User already has medal (${medal}).');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[NEWGROUNDS] Attempted to award medal (${medal}), but not logged into Newgrounds.');
|
||||
}
|
||||
}
|
||||
|
||||
public static function loadMedalJSON():Void
|
||||
{
|
||||
var jsonPath = Paths.json('medals');
|
||||
|
||||
var jsonString = Assets.getText(jsonPath);
|
||||
|
||||
var parser = new json2object.JsonParser<Array<MedalJSON>>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
trace('[NEWGROUNDS] Parsing local medal data...');
|
||||
parser.fromJson(jsonString, jsonPath);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
trace('[NEWGROUNDS] Failed to parse local medal data!');
|
||||
for (error in parser.errors)
|
||||
funkin.data.DataError.printError(error);
|
||||
medalJSON = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
medalJSON = parser.value;
|
||||
}
|
||||
}
|
||||
|
||||
public static function awardStoryLevel(id:String):Void
|
||||
{
|
||||
switch (id)
|
||||
{
|
||||
case 'tutorial':
|
||||
Medals.award(Medal.StoryTutorial);
|
||||
case 'week1':
|
||||
Medals.award(Medal.StoryWeek1);
|
||||
case 'week2':
|
||||
Medals.award(Medal.StoryWeek2);
|
||||
case 'week3':
|
||||
Medals.award(Medal.StoryWeek3);
|
||||
case 'week4':
|
||||
Medals.award(Medal.StoryWeek4);
|
||||
case 'week5':
|
||||
Medals.award(Medal.StoryWeek5);
|
||||
case 'week6':
|
||||
Medals.award(Medal.StoryWeek6);
|
||||
case 'week7':
|
||||
Medals.award(Medal.StoryWeek7);
|
||||
case 'weekend1':
|
||||
Medals.award(Medal.StoryWeekend1);
|
||||
default:
|
||||
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
/**
|
||||
* Represents NG Medal data in a JSON format.
|
||||
*/
|
||||
typedef MedalJSON =
|
||||
{
|
||||
/**
|
||||
* Medal ID to use for release builds
|
||||
*/
|
||||
var id:Int;
|
||||
|
||||
/**
|
||||
* Medal ID to use for testing builds
|
||||
*/
|
||||
var idTest:Int;
|
||||
|
||||
/**
|
||||
* The English name for the medal
|
||||
*/
|
||||
var name:String;
|
||||
|
||||
/**
|
||||
* The English name for the medal
|
||||
*/
|
||||
var icon:String;
|
||||
}
|
||||
|
||||
enum abstract Medal(Int) from Int to Int
|
||||
{
|
||||
/**
|
||||
* Represents an undefined or invalid medal.
|
||||
*/
|
||||
var Unknown = -1;
|
||||
|
||||
/**
|
||||
* I Said Funkin'!
|
||||
* Start the game for the first time.
|
||||
*/
|
||||
var StartGame = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80894 #else 60960 #end;
|
||||
|
||||
/**
|
||||
* That's How You Do It!
|
||||
* Beat Tutoria l in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryTutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80906 #else 83647 #end;
|
||||
|
||||
/**
|
||||
* More Like Daddy Queerest
|
||||
* Beat Week 1 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80899 #else 60961 #end;
|
||||
|
||||
/**
|
||||
* IT IS THE SPOOKY MONTH
|
||||
* Beat Week 2 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek2 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80900 #else 83648 #end;
|
||||
|
||||
/**
|
||||
* Pico Funny
|
||||
* Beat Week 3 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek3 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80901 #else 83649 #end;
|
||||
|
||||
/**
|
||||
* Mommy Must Murder
|
||||
* Beat Week 4 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek4 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80902 #else 83650 #end;
|
||||
|
||||
/**
|
||||
* Yule Tide Joy
|
||||
* Beat Week 5 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek5 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80903 #else 83651 #end;
|
||||
|
||||
/**
|
||||
* A Visual Novelty
|
||||
* Beat Week 6 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek6 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80904 #else 83652 #end;
|
||||
|
||||
/**
|
||||
* I <3 JohnnyUtah
|
||||
* Beat Week 7 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeek7 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80905 #else 83653 #end;
|
||||
|
||||
/**
|
||||
* Yo, Really Think So?
|
||||
* Beat Weekend 1 in Story Mode (on any difficulty).
|
||||
*/
|
||||
var StoryWeekend1 = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80907 #else 83654 #end;
|
||||
|
||||
/**
|
||||
* Stay Funky
|
||||
* Press TAB in Freeplay and unlock your first character.
|
||||
*/
|
||||
var CharSelect = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83633 #else 83655 #end;
|
||||
|
||||
/**
|
||||
* A Challenger Appears
|
||||
* Beat any Pico remix in Freeplay (on any difficulty).
|
||||
*/
|
||||
var FreeplayPicoMix = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80910 #else 83656 #end;
|
||||
|
||||
/**
|
||||
* De-Stressing
|
||||
* Beat Stress (Pico Mix) in Freeplay (on Normal difficulty or higher).
|
||||
*/
|
||||
var FreeplayStressPico = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 83629 #else 83657 #end;
|
||||
|
||||
/**
|
||||
* L
|
||||
* Earn a Loss rating on any song (on any difficulty).
|
||||
*/
|
||||
var LossRating = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80915 #else 83658 #end;
|
||||
|
||||
/**
|
||||
* Getting Freaky
|
||||
* Earn a Perfect rating on any song on Hard difficulty or higher.
|
||||
* NOTE: Should also be awarded for a Gold Perfect because otherwise that would be annoying.
|
||||
*/
|
||||
var PerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80908 #else 83659 #end;
|
||||
|
||||
/**
|
||||
* You Should Drink More Water
|
||||
* Earn a Golden Perfect rating on any song on Hard difficulty or higher.
|
||||
*/
|
||||
var GoldPerfectRatingHard = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80909 #else 83660 #end;
|
||||
|
||||
/**
|
||||
* Harder Than Hard
|
||||
* Beat any Erect remix in Freeplay on Erect or Nightmare difficulty.
|
||||
*/
|
||||
var ErectDifficulty = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80911 #else 83661 #end;
|
||||
|
||||
/**
|
||||
* The Rap God
|
||||
* Earn a Gold Perfect rating on any song on Nightmare difficulty.
|
||||
*/
|
||||
var GoldPerfectRatingNightmare = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80912 #else 83662 #end;
|
||||
|
||||
/**
|
||||
* Just like the game!
|
||||
* Get freaky on a Friday.
|
||||
* NOTE: You must beat at least one song on any difficulty.
|
||||
*/
|
||||
var FridayNight = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80913 #else 61034 #end;
|
||||
|
||||
/**
|
||||
* Nice
|
||||
* Earn a rating of EXACTLY 69% (good luck).
|
||||
*/
|
||||
var Nice = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80914 #else 83646 #end;
|
||||
|
||||
public function getId():Int
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public static function getMedalByStoryLevel(levelId:String):Medal
|
||||
{
|
||||
switch (levelId)
|
||||
{
|
||||
case "week1":
|
||||
return StoryWeek1;
|
||||
case "week2":
|
||||
return StoryWeek2;
|
||||
case "week3":
|
||||
return StoryWeek3;
|
||||
case "week4":
|
||||
return StoryWeek4;
|
||||
case "week5":
|
||||
return StoryWeek5;
|
||||
case "week6":
|
||||
return StoryWeek6;
|
||||
case "week7":
|
||||
return StoryWeek7;
|
||||
case "weekend1":
|
||||
return StoryWeekend1;
|
||||
default:
|
||||
return Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
package funkin.api.newgrounds;
|
||||
|
||||
import funkin.save.Save;
|
||||
import funkin.api.newgrounds.Medals.Medal;
|
||||
#if FEATURE_NEWGROUNDS
|
||||
import io.newgrounds.Call.CallError;
|
||||
import io.newgrounds.NG;
|
||||
import io.newgrounds.NGLite;
|
||||
import io.newgrounds.NGLite.LoginOutcome;
|
||||
import io.newgrounds.NGLite.LoginFail;
|
||||
import io.newgrounds.objects.events.Outcome;
|
||||
import io.newgrounds.utils.MedalList;
|
||||
import io.newgrounds.utils.ScoreBoardList;
|
||||
import io.newgrounds.objects.User;
|
||||
|
||||
@:nullSafety
|
||||
class NewgroundsClient
|
||||
{
|
||||
public static var instance(get, never):NewgroundsClient;
|
||||
static var _instance:Null<NewgroundsClient> = null;
|
||||
|
||||
static function get_instance():NewgroundsClient
|
||||
{
|
||||
if (NewgroundsClient._instance == null) _instance = new NewgroundsClient();
|
||||
if (NewgroundsClient._instance == null) throw "Could not initialize singleton NewgroundsClient!";
|
||||
return NewgroundsClient._instance;
|
||||
}
|
||||
|
||||
public var user(get, never):Null<User>;
|
||||
public var medals(get, never):Null<MedalList>;
|
||||
public var leaderboards(get, never):Null<ScoreBoardList>;
|
||||
|
||||
private function new()
|
||||
{
|
||||
trace('[NEWGROUNDS] Initializing client...');
|
||||
|
||||
#if FEATURE_NEWGROUNDS_DEBUG
|
||||
trace('[NEWGROUNDS] App ID: ${NewgroundsCredentials.APP_ID}');
|
||||
trace('[NEWGROUNDS] Encryption Key: ${NewgroundsCredentials.ENCRYPTION_KEY}');
|
||||
#end
|
||||
|
||||
if (!hasValidCredentials())
|
||||
{
|
||||
FlxG.log.warn("Tried to initialize Newgrounds client, but credentials are invalid!");
|
||||
return;
|
||||
}
|
||||
|
||||
var debug = #if FEATURE_NEWGROUNDS_DEBUG true #else false #end;
|
||||
NG.create(NewgroundsCredentials.APP_ID, getSessionId(), debug, onLoginResolved);
|
||||
NG.core.setupEncryption(NewgroundsCredentials.ENCRYPTION_KEY);
|
||||
}
|
||||
|
||||
public function init()
|
||||
{
|
||||
if (NG.core == null) return;
|
||||
|
||||
trace('[NEWGROUNDS] Setting up connection...');
|
||||
|
||||
#if FEATURE_NEWGROUNDS_DEBUG
|
||||
NG.core.verbose = true;
|
||||
#end
|
||||
|
||||
NG.core.onLogin.add(onLoginSuccessful);
|
||||
|
||||
if (NG.core.attemptingLogin)
|
||||
{
|
||||
// Session ID was valid and we should be logged in soon.
|
||||
trace('[NEWGROUNDS] Waiting for existing login!');
|
||||
}
|
||||
else
|
||||
{
|
||||
#if FEATURE_NEWGROUNDS_AUTOLOGIN
|
||||
// Attempt an automatic login.
|
||||
trace('[NEWGROUNDS] Attempting new login immediately!');
|
||||
this.autoLogin();
|
||||
#else
|
||||
trace('[NEWGROUNDS] Not logged in, you have to login manually!');
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to log into Newgrounds and create a session ID.
|
||||
* @param onSuccess An optional callback for when the login is successful.
|
||||
* @param onError An optional callback for when the login fails.
|
||||
*/
|
||||
public function login(?onSuccess:Void->Void, ?onError:Void->Void):Void
|
||||
{
|
||||
if (NG.core == null)
|
||||
{
|
||||
FlxG.log.warn("No Newgrounds client initialized! Are your credentials invalid?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (onSuccess != null && onError != null)
|
||||
{
|
||||
NG.core.requestLogin(onLoginResolvedWithCallbacks.bind(_, onSuccess, onError));
|
||||
}
|
||||
else
|
||||
{
|
||||
NG.core.requestLogin(onLoginResolved);
|
||||
}
|
||||
}
|
||||
|
||||
public function autoLogin(?onSuccess:Void->Void, ?onError:Void->Void):Void
|
||||
{
|
||||
if (NG.core == null)
|
||||
{
|
||||
FlxG.log.warn("No Newgrounds client initialized! Are your credentials invalid?");
|
||||
return;
|
||||
}
|
||||
|
||||
var dummyPassport:String->Void = function(_) {
|
||||
// just a dummy passport, so we don't create a popup
|
||||
// otherwise `NG.core.requestLogin()` will automatically attempt to open a tab at the beginning of the game
|
||||
// users should go to the Options Menu to login to NG
|
||||
// we cancel the request, so we can call it later
|
||||
NG.core.cancelLoginRequest();
|
||||
};
|
||||
|
||||
if (onSuccess != null && onError != null)
|
||||
{
|
||||
NG.core.requestLogin(onLoginResolvedWithCallbacks.bind(_, onSuccess, onError), dummyPassport);
|
||||
}
|
||||
else
|
||||
{
|
||||
NG.core.requestLogin(onLoginResolved, dummyPassport);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log out of Newgrounds and invalidate the current session.
|
||||
* @param onSuccess An optional callback for when the logout is successful.
|
||||
*/
|
||||
public function logout(?onSuccess:Void->Void, ?onError:Void->Void):Void
|
||||
{
|
||||
if (NG.core != null)
|
||||
{
|
||||
if (onSuccess != null && onError != null)
|
||||
{
|
||||
NG.core.logOut(onLogoutResolvedWithCallbacks.bind(_, onSuccess, onError));
|
||||
}
|
||||
else
|
||||
{
|
||||
NG.core.logOut(onLogoutResolved);
|
||||
}
|
||||
}
|
||||
|
||||
Save.instance.ngSessionId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return `true` if the user is logged in to Newgrounds.
|
||||
*/
|
||||
public function isLoggedIn():Bool
|
||||
{
|
||||
#if FEATURE_NEWGROUNDS
|
||||
return NG.core != null && NG.core.loggedIn;
|
||||
#else
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `false` if either the app ID or the encryption key is invalid.
|
||||
*/
|
||||
static function hasValidCredentials():Bool
|
||||
{
|
||||
return !(NewgroundsCredentials.APP_ID == null
|
||||
|| NewgroundsCredentials.APP_ID == ""
|
||||
|| NewgroundsCredentials.APP_ID.contains(" ")
|
||||
|| NewgroundsCredentials.ENCRYPTION_KEY == null
|
||||
|| NewgroundsCredentials.ENCRYPTION_KEY == ""
|
||||
|| NewgroundsCredentials.ENCRYPTION_KEY.contains(" "));
|
||||
}
|
||||
|
||||
function onLoginResolved(outcome:LoginOutcome):Void
|
||||
{
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
onLoginSuccessful();
|
||||
case FAIL(result):
|
||||
onLoginFailed(result);
|
||||
}
|
||||
}
|
||||
|
||||
function onLoginResolvedWithCallbacks(outcome:LoginOutcome, onSuccess:Void->Void, onError:Void->Void):Void
|
||||
{
|
||||
onLoginResolved(outcome);
|
||||
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
onSuccess();
|
||||
case FAIL(result):
|
||||
onError();
|
||||
}
|
||||
}
|
||||
|
||||
function onLogoutResolved(outcome:Outcome<CallError>):Void
|
||||
{
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
onLogoutSuccessful();
|
||||
case FAIL(result):
|
||||
onLogoutFailed(result);
|
||||
}
|
||||
}
|
||||
|
||||
function onLogoutResolvedWithCallbacks(outcome:Outcome<CallError>, onSuccess:Void->Void, onError:Void->Void):Void
|
||||
{
|
||||
onLogoutResolved(outcome);
|
||||
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
onSuccess();
|
||||
case FAIL(result):
|
||||
onError();
|
||||
}
|
||||
}
|
||||
|
||||
function onLoginSuccessful():Void
|
||||
{
|
||||
if (NG.core == null) return;
|
||||
|
||||
trace('[NEWGROUNDS] Login successful!');
|
||||
|
||||
// Persist the session ID.
|
||||
Save.instance.ngSessionId = NG.core.sessionId;
|
||||
|
||||
trace('[NEWGROUNDS] Submitting medal request...');
|
||||
NG.core.requestMedals(onFetchedMedals);
|
||||
|
||||
trace('[NEWGROUNDS] Submitting leaderboard request...');
|
||||
NG.core.scoreBoards.loadList(onFetchedLeaderboards);
|
||||
}
|
||||
|
||||
function onLoginFailed(result:LoginFail):Void
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case CANCELLED(type):
|
||||
switch (type)
|
||||
{
|
||||
case PASSPORT:
|
||||
trace('[NEWGROUNDS] Login cancelled by passport website.');
|
||||
case MANUAL:
|
||||
trace('[NEWGROUNDS] Login cancelled by application.');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Login cancelled by unknown source.');
|
||||
}
|
||||
case ERROR(error):
|
||||
switch (error)
|
||||
{
|
||||
case HTTP(error):
|
||||
trace('[NEWGROUNDS] Login failed due to HTTP error: ${error}');
|
||||
case RESPONSE(error):
|
||||
trace('[NEWGROUNDS] Login failed due to response error: ${error.message} (${error.code})');
|
||||
case RESULT(error):
|
||||
trace('[NEWGROUNDS] Login failed due to result error: ${error.message} (${error.code})');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Login failed due to unknown error: ${error}');
|
||||
}
|
||||
default:
|
||||
trace('[NEWGROUNDS] Login failed due to unknown reason.');
|
||||
}
|
||||
}
|
||||
|
||||
function onLogoutSuccessful():Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Logout successful!');
|
||||
}
|
||||
|
||||
function onLogoutFailed(result:CallError):Void
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HTTP(error):
|
||||
trace('[NEWGROUNDS] Logout failed due to HTTP error: ${error}');
|
||||
case RESPONSE(error):
|
||||
trace('[NEWGROUNDS] Logout failed due to response error: ${error.message} (${error.code})');
|
||||
case RESULT(error):
|
||||
trace('[NEWGROUNDS] Logout failed due to result error: ${error.message} (${error.code})');
|
||||
default:
|
||||
trace('[NEWGROUNDS] Logout failed due to unknown error: ${result}');
|
||||
}
|
||||
}
|
||||
|
||||
function onFetchedMedals(outcome:Outcome<CallError>):Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Fetched medals!');
|
||||
}
|
||||
|
||||
function onFetchedLeaderboards(outcome:Outcome<CallError>):Void
|
||||
{
|
||||
trace('[NEWGROUNDS] Fetched leaderboards!');
|
||||
|
||||
// trace(funkin.api.newgrounds.Leaderboards.listLeaderboardData());
|
||||
}
|
||||
|
||||
function get_user():Null<User>
|
||||
{
|
||||
if (NG.core == null || !this.isLoggedIn()) return null;
|
||||
return NG.core.user;
|
||||
}
|
||||
|
||||
function get_medals():Null<MedalList>
|
||||
{
|
||||
if (NG.core == null || !this.isLoggedIn()) return null;
|
||||
return NG.core.medals;
|
||||
}
|
||||
|
||||
function get_leaderboards():Null<ScoreBoardList>
|
||||
{
|
||||
if (NG.core == null || !this.isLoggedIn()) return null;
|
||||
return NG.core.scoreBoards;
|
||||
}
|
||||
|
||||
static function getSessionId():Null<String>
|
||||
{
|
||||
#if js
|
||||
// We can fetch the session ID from the URL.
|
||||
var result:Null<String> = NGLite.getSessionId();
|
||||
if (result != null) return result;
|
||||
#end
|
||||
|
||||
// We have to fetch the session ID from the save file.
|
||||
return Save.instance.ngSessionId;
|
||||
}
|
||||
}
|
||||
#end
|
||||
@@ -0,0 +1,55 @@
|
||||
package funkin.api.newgrounds;
|
||||
|
||||
#if FEATURE_NEWGROUNDS
|
||||
import io.newgrounds.NG;
|
||||
#end
|
||||
import funkin.api.newgrounds.NewgroundsClient;
|
||||
import funkin.util.WindowUtil;
|
||||
|
||||
/**
|
||||
* Handles referrals to links retrieved from the Newgrounds API.
|
||||
*/
|
||||
class Referral
|
||||
{
|
||||
/**
|
||||
* Opens the referral page with the given name.
|
||||
* @param referralName The name of the referral.
|
||||
* @param fallbackUrl The URL to open if the referral fails or is not available.
|
||||
*/
|
||||
static function doReferral(referralName:String, fallbackUrl:String):Void
|
||||
{
|
||||
#if FEATURE_NEWGROUNDS
|
||||
if (NewgroundsClient.instance.isLoggedIn())
|
||||
{
|
||||
NG.core?.calls.loader.loadReferral(false)
|
||||
.addComponentParameter("referral_name", referralName)
|
||||
.addResponseHandler(response -> {
|
||||
trace(response);
|
||||
if (response.success)
|
||||
{
|
||||
WindowUtil.openURL(response.result.data.url);
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowUtil.openURL(fallbackUrl);
|
||||
}
|
||||
})
|
||||
.send();
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowUtil.openURL(fallbackUrl);
|
||||
}
|
||||
#else
|
||||
WindowUtil.openURL(fallbackUrl);
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the merch referral page.
|
||||
*/
|
||||
public static function doMerchReferral():Void
|
||||
{
|
||||
doReferral("merch_link", Constants.URL_MERCH_FALLBACK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package funkin.audio;
|
||||
|
||||
import flash.media.Sound;
|
||||
import flixel.sound.FlxSound;
|
||||
import flixel.system.FlxAssets.FlxSoundAsset;
|
||||
import openfl.Assets;
|
||||
#if (openfl >= "8.0.0")
|
||||
import openfl.utils.AssetType;
|
||||
#end
|
||||
|
||||
/**
|
||||
* a FlxSound that just overrides loadEmbedded to allow for "streamed" sounds to load with better performance!
|
||||
*/
|
||||
class FlxStreamSound extends FlxSound
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
override public function loadEmbedded(EmbeddedSound:FlxSoundAsset, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound
|
||||
{
|
||||
if (EmbeddedSound == null) return this;
|
||||
|
||||
cleanup(true);
|
||||
|
||||
if ((EmbeddedSound is Sound))
|
||||
{
|
||||
_sound = EmbeddedSound;
|
||||
}
|
||||
else if ((EmbeddedSound is Class))
|
||||
{
|
||||
_sound = Type.createInstance(EmbeddedSound, []);
|
||||
}
|
||||
else if ((EmbeddedSound is String))
|
||||
{
|
||||
if (Assets.exists(EmbeddedSound, AssetType.SOUND)
|
||||
|| Assets.exists(EmbeddedSound, AssetType.MUSIC)) _sound = Assets.getMusic(EmbeddedSound);
|
||||
else
|
||||
FlxG.log.error('Could not find a Sound asset with an ID of \'$EmbeddedSound\'.');
|
||||
}
|
||||
|
||||
// NOTE: can't pull ID3 info from embedded sound currently
|
||||
return init(Looped, AutoDestroy, OnComplete);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
package funkin.audio;
|
||||
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.sound.FlxSound;
|
||||
import flixel.system.FlxAssets.FlxSoundAsset;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
import funkin.data.song.SongData.SongMusicData;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.util.tools.ICloneable;
|
||||
import funkin.util.flixel.sound.FlxPartialSound;
|
||||
import funkin.Paths.PathsFunction;
|
||||
import lime.app.Promise;
|
||||
import lime.media.AudioSource;
|
||||
import openfl.events.Event;
|
||||
import openfl.media.Sound;
|
||||
import openfl.media.SoundChannel;
|
||||
import openfl.media.SoundMixer;
|
||||
|
||||
#if (openfl >= "8.0.0")
|
||||
#end
|
||||
|
||||
/**
|
||||
* A FlxSound which adds additional functionality:
|
||||
* - Delayed playback via negative song position.
|
||||
* - Easy functions for immediate playback and recycling.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
||||
{
|
||||
static final MAX_VOLUME:Float = 1.0;
|
||||
|
||||
/**
|
||||
* An FlxSignal which is dispatched when the volume changes.
|
||||
*/
|
||||
public static var onVolumeChanged(get, never):FlxTypedSignal<Float->Void>;
|
||||
|
||||
static var _onVolumeChanged:Null<FlxTypedSignal<Float->Void>> = null;
|
||||
|
||||
static function get_onVolumeChanged():FlxTypedSignal<Float->Void>
|
||||
{
|
||||
if (_onVolumeChanged == null)
|
||||
{
|
||||
_onVolumeChanged = new FlxTypedSignal<Float->Void>();
|
||||
FlxG.sound.volumeHandler = function(volume:Float) {
|
||||
_onVolumeChanged.dispatch(volume);
|
||||
}
|
||||
}
|
||||
return _onVolumeChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Using `FunkinSound.load` will override a dead instance from here rather than creating a new one, if possible!
|
||||
*/
|
||||
static var pool(default, null):FlxTypedGroup<FunkinSound> = new FlxTypedGroup<FunkinSound>();
|
||||
|
||||
/**
|
||||
* Calculate the current time of the sound.
|
||||
* NOTE: You need to `add()` the sound to the scene for `update()` to increment the time.
|
||||
*/
|
||||
//
|
||||
public var muted(default, set):Bool = false;
|
||||
|
||||
function set_muted(value:Bool):Bool
|
||||
{
|
||||
if (value == muted) return value;
|
||||
muted = value;
|
||||
updateTransform();
|
||||
return value;
|
||||
}
|
||||
|
||||
override function set_volume(value:Float):Float
|
||||
{
|
||||
// Uncap the volume.
|
||||
_volume = FlxMath.bound(value, 0.0, MAX_VOLUME);
|
||||
updateTransform();
|
||||
return _volume;
|
||||
}
|
||||
|
||||
public var paused(get, never):Bool;
|
||||
|
||||
function get_paused():Bool
|
||||
{
|
||||
return this._paused;
|
||||
}
|
||||
|
||||
public var isPlaying(get, never):Bool;
|
||||
|
||||
function get_isPlaying():Bool
|
||||
{
|
||||
return this.playing || this._shouldPlay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waveform data for this sound.
|
||||
* This is lazily loaded, so it will be built the first time it is accessed.
|
||||
*/
|
||||
public var waveformData(get, never):WaveformData;
|
||||
|
||||
var _waveformData:Null<WaveformData> = null;
|
||||
|
||||
function get_waveformData():WaveformData
|
||||
{
|
||||
if (_waveformData == null)
|
||||
{
|
||||
_waveformData = WaveformDataParser.interpretFlxSound(this);
|
||||
if (_waveformData == null) throw 'Could not interpret waveform data!';
|
||||
}
|
||||
return _waveformData;
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, the game will forcefully add this sound's channel to the list of playing sounds.
|
||||
*/
|
||||
public var important:Bool = false;
|
||||
|
||||
/**
|
||||
* Are we in a state where the song should play but time is negative?
|
||||
*/
|
||||
var _shouldPlay:Bool = false;
|
||||
|
||||
/**
|
||||
* For debug purposes.
|
||||
*/
|
||||
var _label:String = "unknown";
|
||||
|
||||
/**
|
||||
* Whether we received a focus lost event.
|
||||
*/
|
||||
var _lostFocus:Bool = false;
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
public override function update(elapsedSec:Float)
|
||||
{
|
||||
if (!playing && !_shouldPlay) return;
|
||||
|
||||
if (_time < 0)
|
||||
{
|
||||
var elapsedMs = elapsedSec * Constants.MS_PER_SEC;
|
||||
_time += elapsedMs;
|
||||
if (_time >= 0)
|
||||
{
|
||||
super.play();
|
||||
_shouldPlay = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
super.update(elapsedSec);
|
||||
|
||||
@:privateAccess
|
||||
{
|
||||
if (important && _channel != null && !SoundMixer.__soundChannels.contains(_channel))
|
||||
{
|
||||
SoundMixer.__soundChannels.push(_channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function togglePlayback():FunkinSound
|
||||
{
|
||||
if (playing)
|
||||
{
|
||||
pause();
|
||||
}
|
||||
else
|
||||
{
|
||||
resume();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public override function play(forceRestart:Bool = false, startTime:Float = 0, ?endTime:Float):FunkinSound
|
||||
{
|
||||
if (!exists) return this;
|
||||
|
||||
if (forceRestart)
|
||||
{
|
||||
cleanup(false, true);
|
||||
}
|
||||
else if (playing)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
if (startTime < 0)
|
||||
{
|
||||
this.active = true;
|
||||
this._shouldPlay = true;
|
||||
this._time = startTime;
|
||||
this.endTime = endTime;
|
||||
return this;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_paused)
|
||||
{
|
||||
resume();
|
||||
}
|
||||
else
|
||||
{
|
||||
startSound(startTime);
|
||||
}
|
||||
|
||||
this.endTime = endTime;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public override function pause():FunkinSound
|
||||
{
|
||||
if (_shouldPlay)
|
||||
{
|
||||
// This sound will eventually play, but is still at a negative timestamp.
|
||||
// Manually set the paused flag to ensure proper focus/unfocus behavior.
|
||||
_shouldPlay = false;
|
||||
_paused = true;
|
||||
active = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
super.pause();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks to focus on the window.
|
||||
*/
|
||||
override function onFocus():Void
|
||||
{
|
||||
// Flixel can sometimes toss spurious `onFocus` events, e.g. if the Flixel debugger is toggled
|
||||
// on and off. We only want to resume the sound if we actually lost focus, and if we weren't
|
||||
// already paused before we lost focus.
|
||||
if (_lostFocus && !_alreadyPaused)
|
||||
{
|
||||
// trace('Resuming audio (${this._label}) on focus!');
|
||||
resume();
|
||||
}
|
||||
else
|
||||
{
|
||||
// trace('Not resuming audio (${this._label}) on focus!');
|
||||
}
|
||||
_lostFocus = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user tabs away from the window.
|
||||
*/
|
||||
override function onFocusLost():Void
|
||||
{
|
||||
// trace('Focus lost, pausing audio!');
|
||||
_lostFocus = true;
|
||||
_alreadyPaused = _paused;
|
||||
pause();
|
||||
}
|
||||
|
||||
public override function resume():FunkinSound
|
||||
{
|
||||
if (this._time < 0)
|
||||
{
|
||||
// Sound with negative timestamp, restart the timer.
|
||||
_shouldPlay = true;
|
||||
_paused = false;
|
||||
active = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
super.resume();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call after adjusting the volume to update the sound channel's settings.
|
||||
*/
|
||||
@:allow(flixel.sound.FlxSoundGroup)
|
||||
override function updateTransform():Void
|
||||
{
|
||||
if (_transform != null)
|
||||
{
|
||||
_transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end
|
||||
(group != null ? group.volume : 1) * _volume * _volumeAdjust;
|
||||
}
|
||||
|
||||
if (_channel != null)
|
||||
{
|
||||
_channel.soundTransform = _transform;
|
||||
}
|
||||
}
|
||||
|
||||
public function clone():FunkinSound
|
||||
{
|
||||
var sound:FunkinSound = new FunkinSound();
|
||||
|
||||
// Clone the sound by creating one with the same data buffer.
|
||||
// Reusing the `Sound` object directly causes issues with playback.
|
||||
@:privateAccess
|
||||
sound._sound = openfl.media.Sound.fromAudioBuffer(this._sound.__buffer);
|
||||
|
||||
// Call init to ensure the FlxSound is properly initialized.
|
||||
sound.init(this.looped, this.autoDestroy, this.onComplete);
|
||||
|
||||
// Oh yeah, the waveform data is the same too!
|
||||
@:privateAccess
|
||||
sound._waveformData = this._waveformData;
|
||||
|
||||
return sound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `FunkinSound` object and loads it as the current music track.
|
||||
*
|
||||
* @param key The key of the music you want to play. Music should be at `music/<key>/<key>.ogg`.
|
||||
* @param params A set of additional optional parameters.
|
||||
* Data should be at `music/<key>/<key>-metadata.json`.
|
||||
* @return Whether the music was started. `false` if music was already playing or could not be started
|
||||
*/
|
||||
public static function playMusic(key:String, params:FunkinSoundPlayMusicParams):Bool
|
||||
{
|
||||
if (!(params.overrideExisting ?? false) && (FlxG.sound.music?.exists ?? false) && FlxG.sound.music.playing) return false;
|
||||
|
||||
if (!(params.restartTrack ?? false) && FlxG.sound.music?.playing)
|
||||
{
|
||||
if (FlxG.sound.music != null && Std.isOfType(FlxG.sound.music, FunkinSound))
|
||||
{
|
||||
var existingSound:FunkinSound = cast FlxG.sound.music;
|
||||
// Stop here if we would play a matching music track.
|
||||
if (existingSound._label == Paths.music('$key/$key'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (FlxG.sound.music != null)
|
||||
{
|
||||
FlxG.sound.music.fadeTween?.cancel();
|
||||
FlxG.sound.music.stop();
|
||||
FlxG.sound.music.kill();
|
||||
}
|
||||
|
||||
if (params?.mapTimeChanges ?? true)
|
||||
{
|
||||
var songMusicData:Null<SongMusicData> = SongRegistry.instance.parseMusicData(key);
|
||||
// Will fall back and return null if the metadata doesn't exist or can't be parsed.
|
||||
if (songMusicData != null)
|
||||
{
|
||||
Conductor.instance.mapTimeChanges(songMusicData.timeChanges);
|
||||
|
||||
if (songMusicData.looped != null && params.loop == null) params.loop = songMusicData.looped;
|
||||
}
|
||||
else
|
||||
{
|
||||
FlxG.log.warn('Tried and failed to find music metadata for $key');
|
||||
}
|
||||
}
|
||||
var pathsFunction = params.pathsFunction ?? MUSIC;
|
||||
var suffix = params.suffix ?? '';
|
||||
var pathToUse = switch (pathsFunction)
|
||||
{
|
||||
case MUSIC: Paths.music('$key/$key');
|
||||
case INST: Paths.inst('$key', suffix);
|
||||
default: Paths.music('$key/$key');
|
||||
}
|
||||
|
||||
var shouldLoadPartial = params.partialParams?.loadPartial ?? false;
|
||||
|
||||
// even if we arent' trying to partial load a song, we want to error out any songs in progress,
|
||||
// so we don't get overlapping music if someone were to load a new song while a partial one is loading!
|
||||
|
||||
emptyPartialQueue();
|
||||
|
||||
if (shouldLoadPartial)
|
||||
{
|
||||
var music = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0.0, params.partialParams?.end ?? 1.0, params?.startingVolume ?? 1.0,
|
||||
params.loop ?? true, false, false, params.onComplete);
|
||||
|
||||
if (music != null)
|
||||
{
|
||||
partialQueue.push(music);
|
||||
|
||||
@:nullSafety(Off)
|
||||
music.future.onComplete(function(partialMusic:Null<FunkinSound>) {
|
||||
FlxG.sound.music = partialMusic;
|
||||
FlxG.sound.list.remove(FlxG.sound.music);
|
||||
|
||||
if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var music = FunkinSound.load(pathToUse, params?.startingVolume ?? 1.0, params.loop ?? true, false, true, params.persist ?? false, params.onComplete);
|
||||
if (music != null)
|
||||
{
|
||||
FlxG.sound.music = music;
|
||||
|
||||
// Prevent repeat update() and onFocus() calls.
|
||||
FlxG.sound.list.remove(FlxG.sound.music);
|
||||
|
||||
if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function emptyPartialQueue():Void
|
||||
{
|
||||
while (partialQueue.length > 0)
|
||||
{
|
||||
@:nullSafety(Off)
|
||||
partialQueue.pop().error("Cancel loading partial sound");
|
||||
}
|
||||
}
|
||||
|
||||
static var partialQueue:Array<Promise<Null<FunkinSound>>> = [];
|
||||
|
||||
/**
|
||||
* Creates a new `FunkinSound` object synchronously.
|
||||
*
|
||||
* @param embeddedSound The embedded sound resource you want to play. To stream, use the optional URL parameter instead.
|
||||
* @param volume How loud to play it (0 to 1).
|
||||
* @param looped Whether to loop this sound.
|
||||
* @param group The group to add this sound to.
|
||||
* @param autoDestroy Whether to destroy this sound when it finishes playing.
|
||||
* Leave this value set to `false` if you want to re-use this `FunkinSound` instance.
|
||||
* @param autoPlay Whether to play the sound immediately or wait for a `play()` call.
|
||||
* @param persist Whether to keep this `FunkinSound` between states, or destroy it.
|
||||
* @param onComplete Called when the sound finished playing.
|
||||
* @param onLoad Called when the sound finished loading. Called immediately for succesfully loaded embedded sounds.
|
||||
* @param important If `true`, the sound channel will forcefully be added onto the channel array, even if full. Use sparingly!
|
||||
* @return A `FunkinSound` object, or `null` if the sound could not be loaded.
|
||||
*/
|
||||
public static function load(embeddedSound:FlxSoundAsset, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, autoPlay:Bool = false,
|
||||
persist:Bool = false, ?onComplete:Void->Void, ?onLoad:Void->Void, important:Bool = false):Null<FunkinSound>
|
||||
{
|
||||
@:privateAccess
|
||||
if (SoundMixer.__soundChannels.length >= SoundMixer.MAX_ACTIVE_CHANNELS && !important)
|
||||
{
|
||||
FlxG.log.error('FunkinSound could not play sound, channels exhausted! Found ${SoundMixer.__soundChannels.length} active sound channels.');
|
||||
return null;
|
||||
}
|
||||
|
||||
var sound:FunkinSound = pool.recycle(construct);
|
||||
|
||||
// Load the sound.
|
||||
// Sets `exists = true` as a side effect.
|
||||
sound.loadEmbedded(embeddedSound, looped, autoDestroy, onComplete);
|
||||
|
||||
if (embeddedSound is String)
|
||||
{
|
||||
sound._label = embeddedSound;
|
||||
}
|
||||
else
|
||||
{
|
||||
sound._label = 'unknown';
|
||||
}
|
||||
|
||||
if (autoPlay) sound.play();
|
||||
sound.volume = volume;
|
||||
sound.group = FlxG.sound.defaultSoundGroup;
|
||||
sound.persist = persist;
|
||||
sound.important = important;
|
||||
|
||||
// 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();
|
||||
|
||||
return sound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will load a section of a sound file, useful for Freeplay where we don't want to load all the bytes of a song
|
||||
* @param path The path to the sound file
|
||||
* @param start The start time of the sound file
|
||||
* @param end The end time of the sound file
|
||||
* @param volume Volume to start at
|
||||
* @param looped Whether the sound file should loop
|
||||
* @param autoDestroy Whether the sound file should be destroyed after it finishes playing
|
||||
* @param autoPlay Whether the sound file should play immediately
|
||||
* @param onComplete Callback when the sound finishes playing
|
||||
* @param onLoad Callback when the sound finishes loading
|
||||
* @return A FunkinSound object
|
||||
*/
|
||||
public static function loadPartial(path:String, start:Float = 0, end:Float = 1, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false,
|
||||
autoPlay:Bool = true, ?onComplete:Void->Void, ?onLoad:Void->Void):Promise<Null<FunkinSound>>
|
||||
{
|
||||
var promise:lime.app.Promise<Null<FunkinSound>> = new lime.app.Promise<Null<FunkinSound>>();
|
||||
|
||||
// split the path and get only after first :
|
||||
// we are bypassing the openfl/lime asset library fuss on web only
|
||||
#if web
|
||||
path = Paths.stripLibrary(path);
|
||||
#end
|
||||
|
||||
var soundRequest = FlxPartialSound.partialLoadFromFile(path, start, end);
|
||||
|
||||
if (soundRequest == null)
|
||||
{
|
||||
promise.complete(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
promise.future.onError(function(e) {
|
||||
soundRequest.error("Sound loading was errored or cancelled");
|
||||
});
|
||||
|
||||
soundRequest.future.onComplete(function(partialSound) {
|
||||
var snd = FunkinSound.load(partialSound, volume, looped, autoDestroy, autoPlay, false, onComplete, onLoad);
|
||||
promise.complete(snd);
|
||||
});
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
@:nullSafety(Off)
|
||||
public override function destroy():Void
|
||||
{
|
||||
// trace('[FunkinSound] Destroying sound "${this._label}"');
|
||||
super.destroy();
|
||||
if (fadeTween != null)
|
||||
{
|
||||
fadeTween.cancel();
|
||||
fadeTween = null;
|
||||
}
|
||||
FlxTween.cancelTweensOf(this);
|
||||
this._label = 'unknown';
|
||||
}
|
||||
|
||||
@:access(openfl.media.Sound)
|
||||
@:access(openfl.media.SoundChannel)
|
||||
@:access(openfl.media.SoundMixer)
|
||||
override function startSound(startTime:Float)
|
||||
{
|
||||
if (!important)
|
||||
{
|
||||
super.startSound(startTime);
|
||||
return;
|
||||
}
|
||||
|
||||
_time = startTime;
|
||||
_paused = false;
|
||||
|
||||
if (_sound == null) return;
|
||||
|
||||
// Create a channel manually if the sound is considered important.
|
||||
var pan:Float = FlxMath.bound(SoundMixer.__soundTransform.pan + _transform.pan, -1, 1);
|
||||
var volume:Float = FlxMath.bound(SoundMixer.__soundTransform.volume * _transform.volume, 0, MAX_VOLUME);
|
||||
|
||||
var audioSource:AudioSource = new AudioSource(_sound.__buffer);
|
||||
audioSource.offset = Std.int(startTime);
|
||||
audioSource.gain = volume;
|
||||
|
||||
var position:lime.math.Vector4 = audioSource.position;
|
||||
position.x = pan;
|
||||
position.z = -1 * Math.sqrt(1 - Math.pow(pan, 2));
|
||||
audioSource.position = position;
|
||||
|
||||
_channel = new SoundChannel(_sound, audioSource, _transform);
|
||||
_channel.addEventListener(Event.SOUND_COMPLETE, stopped);
|
||||
pitch = _pitch;
|
||||
active = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound effect once, then destroy it.
|
||||
* @param key
|
||||
* @param volume
|
||||
* @return A `FunkinSound` object, or `null` if the sound could not be loaded.
|
||||
*/
|
||||
public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void, important:Bool = false):Null<FunkinSound>
|
||||
{
|
||||
var result:Null<FunkinSound> = FunkinSound.load(key, volume, false, true, true, false, onComplete, onLoad, important);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all sounds in the pool and allow them to be recycled.
|
||||
*/
|
||||
public static function stopAllAudio(musicToo:Bool = false, persistToo:Bool = false):Void
|
||||
{
|
||||
for (sound in pool)
|
||||
{
|
||||
if (sound == null) continue;
|
||||
if (!persistToo && sound.persist) continue;
|
||||
if (!musicToo && sound == FlxG.sound.music) continue;
|
||||
sound.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
static function construct():FunkinSound
|
||||
{
|
||||
var sound:FunkinSound = new FunkinSound();
|
||||
|
||||
pool.add(sound);
|
||||
FlxG.sound.list.add(sound);
|
||||
|
||||
return sound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public override function toString():String
|
||||
{
|
||||
return 'FunkinSound(${this._label})';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional parameters for `FunkinSound.playMusic()`
|
||||
*/
|
||||
typedef FunkinSoundPlayMusicParams =
|
||||
{
|
||||
/**
|
||||
* The volume you want the music to start at.
|
||||
* @default `1.0`
|
||||
*/
|
||||
var ?startingVolume:Float;
|
||||
|
||||
/**
|
||||
* The suffix of the music file to play. Usually for "-erect" tracks when loading an INST file
|
||||
* @default ``
|
||||
*/
|
||||
var ?suffix:String;
|
||||
|
||||
/**
|
||||
* Whether to override music if a different track is already playing.
|
||||
* @default `false`
|
||||
*/
|
||||
var ?overrideExisting:Bool;
|
||||
|
||||
/**
|
||||
* Whether to override music if the same track is already playing.
|
||||
* @default `false`
|
||||
*/
|
||||
var ?restartTrack:Bool;
|
||||
|
||||
/**
|
||||
* Whether the music should loop or play once.
|
||||
* @default `true`
|
||||
*/
|
||||
var ?loop:Bool;
|
||||
|
||||
/**
|
||||
* Whether to check for `SongMusicData` to update the Conductor with.
|
||||
* @default `true`
|
||||
*/
|
||||
var ?mapTimeChanges:Bool;
|
||||
|
||||
/**
|
||||
* Which Paths function to use to load a song
|
||||
* @default `MUSIC`
|
||||
*/
|
||||
var ?pathsFunction:PathsFunction;
|
||||
|
||||
var ?partialParams:PartialSoundParams;
|
||||
|
||||
/**
|
||||
* Whether the sound should be destroyed on state switches
|
||||
*/
|
||||
var ?persist:Bool;
|
||||
|
||||
var ?onComplete:Void->Void;
|
||||
var ?onLoad:Void->Void;
|
||||
}
|
||||
|
||||
typedef PartialSoundParams =
|
||||
{
|
||||
var loadPartial:Bool;
|
||||
var start:Float;
|
||||
var end:Float;
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package funkin.audio;
|
||||
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.tweens.FlxTween;
|
||||
|
||||
/**
|
||||
* A group of FunkinSounds that are all synced together.
|
||||
* Unlike FlxSoundGroup, you can also control their time and pitch.
|
||||
*/
|
||||
class SoundGroup extends FlxTypedGroup<FunkinSound>
|
||||
{
|
||||
public var time(get, set):Float;
|
||||
|
||||
public var volume(get, set):Float;
|
||||
|
||||
public var muted(get, set):Bool;
|
||||
|
||||
public var pitch(get, set):Float;
|
||||
|
||||
public var playing(get, never):Bool;
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
@:deprecated("Create sound files and call add() instead")
|
||||
public static function build(song:String, ?files:Array<String> = null):SoundGroup
|
||||
{
|
||||
var result = new SoundGroup();
|
||||
|
||||
if (files == null)
|
||||
{
|
||||
// Add an empty voice.
|
||||
result.add(new FunkinSound());
|
||||
return result;
|
||||
}
|
||||
|
||||
for (sndFile in files)
|
||||
{
|
||||
var snd:FunkinSound = FunkinSound.load(Paths.voices(song, '$sndFile'));
|
||||
result.add(snd); // adds it to main group for other shit
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the largest deviation from the desired time inside this SoundGroup.
|
||||
*
|
||||
* @param targetTime The time to check against.
|
||||
* If none is provided, it checks the time of all members against the first member of this SoundGroup.
|
||||
* @return The largest deviation from the target time found.
|
||||
*/
|
||||
public function checkSyncError(?targetTime:Float):Float
|
||||
{
|
||||
var error:Float = 0;
|
||||
|
||||
forEachAlive(function(snd) {
|
||||
if (targetTime == null) targetTime = snd.time;
|
||||
else
|
||||
{
|
||||
var diff:Float = snd.time - targetTime;
|
||||
if (Math.abs(diff) > Math.abs(error)) error = diff;
|
||||
}
|
||||
});
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sound to the group.
|
||||
*/
|
||||
public override function add(sound:FunkinSound):FunkinSound
|
||||
{
|
||||
var result:FunkinSound = super.add(sound);
|
||||
|
||||
if (result == null) return null;
|
||||
|
||||
// We have to play, then pause the sound to set the time,
|
||||
// else the sound will restart immediately when played.
|
||||
// TODO: Past me experienced that issue but present me didn't? Investigate.
|
||||
// result.play(true, 0.0);
|
||||
// result.pause();
|
||||
result.time = this.time;
|
||||
|
||||
result.onComplete = function() {
|
||||
this.onComplete();
|
||||
}
|
||||
|
||||
// Apply parameters to the new sound.
|
||||
result.pitch = this.pitch;
|
||||
result.volume = this.volume;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public dynamic function onComplete():Void {}
|
||||
|
||||
/**
|
||||
* Pause all the sounds in the group.
|
||||
*/
|
||||
public function pause()
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
sound.pause();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Play all the sounds in the group.
|
||||
*/
|
||||
public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float)
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
if (sound.length < startTime)
|
||||
{
|
||||
// trace('Queuing sound (${sound.toString()} past its length! Skipping...)');
|
||||
return;
|
||||
}
|
||||
sound.play(forceRestart, startTime, endTime);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume all the sounds in the group.
|
||||
*/
|
||||
public function resume()
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
sound.resume();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade in all the sounds in the group.
|
||||
*/
|
||||
public function fadeIn(duration:Float, ?from:Float = 0.0, ?to:Float = 1.0, ?onComplete:FlxTween->Void):Void
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
sound.fadeIn(duration, from, to, onComplete);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade out all the sounds in the group.
|
||||
*/
|
||||
public function fadeOut(duration:Float, ?to:Float = 0.0, ?onComplete:FlxTween->Void):Void
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
sound.fadeOut(duration, to, onComplete);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all the sounds in the group.
|
||||
*/
|
||||
public function stop():Void
|
||||
{
|
||||
if (members != null)
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
sound.stop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
{
|
||||
stop();
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all sounds from the group.
|
||||
*/
|
||||
public override function clear():Void
|
||||
{
|
||||
this.stop();
|
||||
|
||||
super.clear();
|
||||
}
|
||||
|
||||
function get_time():Float
|
||||
{
|
||||
if (getFirstAlive() != null)
|
||||
{
|
||||
return getFirstAlive().time;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function set_time(time:Float):Float
|
||||
{
|
||||
forEachAlive(function(snd:FunkinSound) {
|
||||
// account for different offsets per sound?
|
||||
snd.time = time;
|
||||
});
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
function get_playing():Bool
|
||||
{
|
||||
if (getFirstAlive() != null)
|
||||
{
|
||||
return getFirstAlive().playing;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function get_volume():Float
|
||||
{
|
||||
if (getFirstAlive() != null)
|
||||
{
|
||||
return getFirstAlive().volume;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// in PlayState, adjust the code so that it only mutes the player1 vocal tracks?
|
||||
function set_volume(volume:Float):Float
|
||||
{
|
||||
forEachAlive(function(snd:FunkinSound) {
|
||||
snd.volume = volume;
|
||||
});
|
||||
|
||||
return volume;
|
||||
}
|
||||
|
||||
function get_muted():Bool
|
||||
{
|
||||
if (getFirstAlive() != null) return getFirstAlive().muted;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
function set_muted(muted:Bool):Bool
|
||||
{
|
||||
forEachAlive(function(snd:FunkinSound) {
|
||||
snd.muted = muted;
|
||||
});
|
||||
|
||||
return muted;
|
||||
}
|
||||
|
||||
function get_pitch():Float
|
||||
{
|
||||
#if FLX_PITCH
|
||||
if (getFirstAlive() != null) return getFirstAlive().pitch;
|
||||
else
|
||||
#end
|
||||
return 1;
|
||||
}
|
||||
|
||||
function set_pitch(val:Float):Float
|
||||
{
|
||||
#if FLX_PITCH
|
||||
trace('Setting audio pitch to ' + val);
|
||||
forEachAlive(function(snd:FunkinSound) {
|
||||
snd.pitch = val;
|
||||
});
|
||||
#end
|
||||
return val;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package funkin.audio;
|
||||
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
|
||||
class VoicesGroup extends SoundGroup
|
||||
{
|
||||
var playerVoices:FlxTypedGroup<FunkinSound>;
|
||||
var opponentVoices:FlxTypedGroup<FunkinSound>;
|
||||
|
||||
/**
|
||||
* Control the volume of only the sounds in the player group.
|
||||
*/
|
||||
public var playerVolume(default, set):Float = 1.0;
|
||||
|
||||
/**
|
||||
* Control the volume of only the sounds in the opponent group.
|
||||
*/
|
||||
public var opponentVolume(default, set):Float = 1.0;
|
||||
|
||||
/**
|
||||
* Set the time offset for the player's vocal track.
|
||||
*/
|
||||
public var playerVoicesOffset(default, set):Float = 0.0;
|
||||
|
||||
/**
|
||||
* Set the time offset for the opponent's vocal track.
|
||||
*/
|
||||
public var opponentVoicesOffset(default, set):Float = 0.0;
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
playerVoices = new FlxTypedGroup<FunkinSound>();
|
||||
opponentVoices = new FlxTypedGroup<FunkinSound>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a voice to the player group.
|
||||
*/
|
||||
public function addPlayerVoice(sound:FunkinSound):Void
|
||||
{
|
||||
super.add(sound);
|
||||
playerVoices.add(sound);
|
||||
}
|
||||
|
||||
function set_playerVolume(volume:Float):Float
|
||||
{
|
||||
playerVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.volume = volume;
|
||||
});
|
||||
return playerVolume = volume;
|
||||
}
|
||||
|
||||
override function set_time(time:Float):Float
|
||||
{
|
||||
forEachAlive(function(snd) {
|
||||
// account for different offsets per sound?
|
||||
snd.time = time;
|
||||
});
|
||||
|
||||
playerVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.time -= playerVoicesOffset;
|
||||
});
|
||||
opponentVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.time -= opponentVoicesOffset;
|
||||
});
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
function set_playerVoicesOffset(offset:Float):Float
|
||||
{
|
||||
playerVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.time += playerVoicesOffset;
|
||||
voice.time -= offset;
|
||||
});
|
||||
return playerVoicesOffset = offset;
|
||||
}
|
||||
|
||||
function set_opponentVoicesOffset(offset:Float):Float
|
||||
{
|
||||
opponentVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.time += opponentVoicesOffset;
|
||||
voice.time -= offset;
|
||||
});
|
||||
return opponentVoicesOffset = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a voice to the opponent group.
|
||||
*/
|
||||
public function addOpponentVoice(sound:FunkinSound):Void
|
||||
{
|
||||
super.add(sound);
|
||||
opponentVoices.add(sound);
|
||||
}
|
||||
|
||||
function set_opponentVolume(volume:Float):Float
|
||||
{
|
||||
opponentVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.volume = volume;
|
||||
});
|
||||
return opponentVolume = volume;
|
||||
}
|
||||
|
||||
public function getPlayerVoice(index:Int = 0):Null<FunkinSound>
|
||||
{
|
||||
return playerVoices.members[index];
|
||||
}
|
||||
|
||||
public function getOpponentVoice(index:Int = 0):Null<FunkinSound>
|
||||
{
|
||||
return opponentVoices.members[index];
|
||||
}
|
||||
|
||||
public function getPlayerVoiceWaveform():Null<WaveformData>
|
||||
{
|
||||
if (playerVoices.members.length == 0) return null;
|
||||
|
||||
return playerVoices.members[0].waveformData;
|
||||
}
|
||||
|
||||
public function getOpponentVoiceWaveform():Null<WaveformData>
|
||||
{
|
||||
if (opponentVoices.members.length == 0) return null;
|
||||
|
||||
return opponentVoices.members[0].waveformData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The length of the player's vocal track, in milliseconds.
|
||||
*/
|
||||
public function getPlayerVoiceLength():Float
|
||||
{
|
||||
if (playerVoices.members.length == 0) return 0.0;
|
||||
|
||||
return playerVoices.members[0].length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The length of the opponent's vocal track, in milliseconds.
|
||||
*/
|
||||
public function getOpponentVoiceLength():Float
|
||||
{
|
||||
if (opponentVoices.members.length == 0) return 0.0;
|
||||
|
||||
return opponentVoices.members[0].length;
|
||||
}
|
||||
|
||||
public override function clear():Void
|
||||
{
|
||||
playerVoices.clear();
|
||||
opponentVoices.clear();
|
||||
super.clear();
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
{
|
||||
if (playerVoices != null)
|
||||
{
|
||||
playerVoices.destroy();
|
||||
playerVoices = null;
|
||||
}
|
||||
|
||||
if (opponentVoices != null)
|
||||
{
|
||||
opponentVoices.destroy();
|
||||
opponentVoices = null;
|
||||
}
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package funkin.audio.visualize;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
|
||||
|
||||
class ABot extends FlxTypedSpriteGroup<FlxSprite>
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package funkin.audio.visualize;
|
||||
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
|
||||
import flixel.sound.FlxSound;
|
||||
import funkin.vis.dsp.SpectralAnalyzer;
|
||||
|
||||
using Lambda;
|
||||
|
||||
@:nullSafety
|
||||
class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
|
||||
{
|
||||
// public var vis:VisShit;
|
||||
var analyzer:Null<SpectralAnalyzer> = null;
|
||||
|
||||
var volumes:Array<Float> = [];
|
||||
|
||||
public var snd:Null<FlxSound> = null;
|
||||
|
||||
static final BAR_COUNT:Int = 7;
|
||||
|
||||
// TODO: Make the sprites easier to soft code.
|
||||
public function new(snd:FlxSound, pixel:Bool)
|
||||
{
|
||||
super();
|
||||
|
||||
this.snd = snd;
|
||||
|
||||
var visCount = pixel ? (BAR_COUNT + 1) : (BAR_COUNT + 1);
|
||||
var visScale = pixel ? 6 : 1;
|
||||
|
||||
var visFrms:FlxAtlasFrames = Paths.getSparrowAtlas(pixel ? 'characters/abotPixel/aBotVizPixel' : 'characters/abot/aBotViz');
|
||||
|
||||
// these are the differences in X position, from left to right
|
||||
var positionX:Array<Float> = pixel ? [0, 7 * 6, 8 * 6, 9 * 6, 10 * 6, 6 * 6, 7 * 6] : [0, 59, 56, 66, 54, 52, 51];
|
||||
var positionY:Array<Float> = pixel ? [0, -2 * 6, -1 * 6, 0, 0, 1 * 6, 2 * 6] : [0, -8, -3.5, -0.4, 0.5, 4.7, 7];
|
||||
|
||||
for (index in 1...visCount)
|
||||
{
|
||||
// pushes initial value
|
||||
volumes.push(0.0);
|
||||
|
||||
// Sum the offsets up to the current index
|
||||
var sum = function(num:Float, total:Float) return total += num;
|
||||
var posX:Float = positionX.slice(0, index).fold(sum, 0);
|
||||
var posY:Float = positionY.slice(0, index).fold(sum, 0);
|
||||
|
||||
var viz:FunkinSprite = new FunkinSprite(posX, posY);
|
||||
viz.frames = visFrms;
|
||||
viz.antialiasing = pixel ? false : true;
|
||||
viz.scale.set(visScale, visScale);
|
||||
add(viz);
|
||||
|
||||
var visStr = 'viz';
|
||||
viz.animation.addByPrefix('VIZ', '$visStr${index}0', 0);
|
||||
viz.animation.play('VIZ', false, false, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public function initAnalyzer():Void
|
||||
{
|
||||
if (snd == null) return;
|
||||
|
||||
@:privateAccess
|
||||
analyzer = new SpectralAnalyzer(snd._channel.__audioSource, BAR_COUNT, 0.1, 40);
|
||||
// A-Bot tuning...
|
||||
analyzer.minDb = -65;
|
||||
analyzer.maxDb = -25;
|
||||
analyzer.maxFreq = 22000;
|
||||
// we use a very low minFreq since some songs use low low subbass like a boss
|
||||
analyzer.minFreq = 10;
|
||||
|
||||
#if desktop
|
||||
// On desktop it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5
|
||||
// So we want to manually change it!
|
||||
analyzer.fftN = 256;
|
||||
#end
|
||||
|
||||
// analyzer.maxDb = -35;
|
||||
// analyzer.fftN = 2048;
|
||||
}
|
||||
|
||||
public function dumpSound():Void
|
||||
{
|
||||
snd = null;
|
||||
analyzer = null;
|
||||
}
|
||||
|
||||
var visTimer:Float = -1;
|
||||
var visTimeMax:Float = 1 / 30;
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
static inline function min(x:Int, y:Int):Int
|
||||
{
|
||||
return x > y ? y : x;
|
||||
}
|
||||
|
||||
override function draw()
|
||||
{
|
||||
super.draw();
|
||||
drawFFT();
|
||||
}
|
||||
|
||||
/**
|
||||
* TJW funkin.vis based visualizer! updateFFT() is the old nasty shit that dont worky!
|
||||
*/
|
||||
function drawFFT():Void
|
||||
{
|
||||
var levels = (analyzer != null) ? analyzer.getLevels() : getDefaultLevels();
|
||||
|
||||
for (i in 0...min(group.members.length, levels.length))
|
||||
{
|
||||
var animFrame:Int = Math.round(levels[i].value * 6);
|
||||
|
||||
// don't display if we're at 0 volume from the level
|
||||
group.members[i].visible = animFrame > 0;
|
||||
|
||||
// decrement our animFrame, so we can get a value from 0-5 for animation frames
|
||||
animFrame -= 1;
|
||||
|
||||
animFrame = Math.floor(Math.min(5, animFrame));
|
||||
animFrame = Math.floor(Math.max(0, animFrame));
|
||||
|
||||
animFrame = Std.int(Math.abs(animFrame - 5)); // shitty dumbass flip, cuz dave got da shit backwards lol!
|
||||
|
||||
group.members[i].animation.curAnim.curFrame = animFrame;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly define the default levels to draw when the analyzer is not available.
|
||||
* @return Array<Bar>
|
||||
*/
|
||||
static function getDefaultLevels():Array<Bar>
|
||||
{
|
||||
var result:Array<Bar> = [];
|
||||
|
||||
for (i in 0...BAR_COUNT)
|
||||
{
|
||||
result.push({value: 0, peak: 0.0});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package funkin.audio.visualize;
|
||||
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.sound.FlxSound;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.audio.visualize.VisShit.CurAudioInfo;
|
||||
import funkin.graphics.rendering.MeshRender;
|
||||
import lime.utils.Int16Array;
|
||||
|
||||
class PolygonSpectogram extends MeshRender
|
||||
{
|
||||
var sampleRate:Int;
|
||||
|
||||
public var vis:VisShit;
|
||||
public var visType:VISTYPE = UPDATED;
|
||||
public var daHeight:Float = FlxG.height;
|
||||
public var realtimeVisLenght:Float = 0.2;
|
||||
public var realtimeStartOffset:Float = 0;
|
||||
|
||||
var numSamples:Int = 0;
|
||||
var setBuffer:Bool = false;
|
||||
|
||||
public var audioData:Int16Array;
|
||||
public var detail:Float = 1;
|
||||
|
||||
public var thickness:Float = 2;
|
||||
public var waveAmplitude:Int = 100;
|
||||
|
||||
public function new(?daSound:FlxSound, ?col:FlxColor = FlxColor.WHITE, ?height:Float = 720, ?detail:Float = 1)
|
||||
{
|
||||
super(0, 0, col);
|
||||
|
||||
if (daSound != null) setSound(daSound);
|
||||
|
||||
if (height != null) this.daHeight = height;
|
||||
|
||||
this.detail = detail;
|
||||
|
||||
// col not in yet
|
||||
}
|
||||
|
||||
public function setSound(daSound:FlxSound)
|
||||
{
|
||||
vis = new VisShit(daSound);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
switch (visType)
|
||||
{
|
||||
case UPDATED:
|
||||
realtimeVis();
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
var prevAudioData:Int16Array;
|
||||
|
||||
/**
|
||||
* Generates and draws a section of the audio data to a visual waveform
|
||||
* @param start start of the song in milliseconds
|
||||
* @param seconds how long to generate (also in milliseconds)
|
||||
*/
|
||||
public function generateSection(start:Float = 0, seconds:Float = 1):Void
|
||||
{
|
||||
checkAndSetBuffer();
|
||||
|
||||
if (setBuffer)
|
||||
{
|
||||
clear();
|
||||
|
||||
start = Math.max(start, 0);
|
||||
|
||||
// gets how many samples to generate
|
||||
var samplesToGen:Int = Std.int(sampleRate * seconds);
|
||||
|
||||
if (samplesToGen == 0) return;
|
||||
// gets which sample to start at
|
||||
var startSample:Int = Std.int(FlxMath.remapToRange(start, 0, vis.snd.length, 0, numSamples));
|
||||
|
||||
// Check if startSample and samplesToGen are within the bounds of the audioData array
|
||||
if (startSample < 0 || startSample >= numSamples) return;
|
||||
if (samplesToGen <= 0 || startSample + samplesToGen > numSamples) samplesToGen = numSamples - startSample;
|
||||
|
||||
var prevPoint:FlxPoint = new FlxPoint();
|
||||
|
||||
var funnyPixels:Int = Std.int(daHeight * detail); // sorta redundant but just need it for different var...
|
||||
|
||||
if (prevAudioData == audioData.subarray(startSample, startSample + samplesToGen)) return; // optimize / finish funciton here, no need to re-render
|
||||
|
||||
prevAudioData = audioData.subarray(startSample, samplesToGen);
|
||||
|
||||
for (i in 0...funnyPixels)
|
||||
{
|
||||
var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, funnyPixels, startSample, startSample + samplesToGen));
|
||||
var curAud:CurAudioInfo = VisShit.getCurAud(audioData, sampleApprox);
|
||||
|
||||
var coolPoint:FlxPoint = new FlxPoint();
|
||||
coolPoint.x = (curAud.balanced * waveAmplitude);
|
||||
coolPoint.y = (i / funnyPixels * daHeight);
|
||||
|
||||
build_quad(prevPoint.x, prevPoint.y, prevPoint.x
|
||||
+ thickness, prevPoint.y, coolPoint.x, coolPoint.y, coolPoint.x
|
||||
+ thickness, coolPoint.y
|
||||
+ thickness);
|
||||
|
||||
prevPoint.x = coolPoint.x;
|
||||
prevPoint.y = coolPoint.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var curTime:Float = 0;
|
||||
|
||||
function realtimeVis():Void
|
||||
{
|
||||
if (vis.snd != null)
|
||||
{
|
||||
if (curTime != vis.snd.time)
|
||||
{
|
||||
// trace("DOIN SHIT" + FlxG.random.int(0, 200));
|
||||
|
||||
if (vis.snd.playing) curTime = vis.snd.time;
|
||||
else
|
||||
{
|
||||
if (Math.abs(curTime - vis.snd.time) > 10) curTime = FlxMath.lerp(curTime, vis.snd.time, 0.5);
|
||||
}
|
||||
|
||||
curTime = vis.snd.time;
|
||||
|
||||
if (vis.snd.time < vis.snd.length - realtimeVisLenght) generateSection(vis.snd.time + realtimeStartOffset, realtimeVisLenght);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkAndSetBuffer()
|
||||
{
|
||||
vis.checkAndSetBuffer();
|
||||
|
||||
if (vis.setBuffer)
|
||||
{
|
||||
audioData = vis.audioData;
|
||||
sampleRate = vis.sampleRate;
|
||||
setBuffer = vis.setBuffer;
|
||||
numSamples = Std.int(audioData.length / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum VISTYPE
|
||||
{
|
||||
STATIC;
|
||||
UPDATED;
|
||||
FREQUENCIES;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package funkin.audio.visualize;
|
||||
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.sound.FlxSound;
|
||||
|
||||
class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
|
||||
{
|
||||
public var playerVis:PolygonSpectogram;
|
||||
public var opponentVis:PolygonSpectogram;
|
||||
public var instVis:PolygonSpectogram;
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
playerVis = new PolygonSpectogram();
|
||||
opponentVis = new PolygonSpectogram();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the player's visualizer to the group.
|
||||
* @param visSnd The visualizer to add.
|
||||
*/
|
||||
public function addPlayerVis(visSnd:FlxSound):Void
|
||||
{
|
||||
var vis:PolygonSpectogram = new PolygonSpectogram(visSnd);
|
||||
super.add(vis);
|
||||
playerVis = vis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the opponent's visualizer to the group.
|
||||
* @param visSnd The visualizer to add.
|
||||
*/
|
||||
public function addOpponentVis(visSnd:FlxSound):Void
|
||||
{
|
||||
var vis:PolygonSpectogram = new PolygonSpectogram(visSnd);
|
||||
super.add(vis);
|
||||
opponentVis = vis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the instrument's visualizer to the group.
|
||||
* @param visSnd The visualizer to add.
|
||||
*/
|
||||
public function addInstVis(visSnd:FlxSound):Void
|
||||
{
|
||||
var vis:PolygonSpectogram = new PolygonSpectogram(visSnd);
|
||||
super.add(vis);
|
||||
instVis = vis;
|
||||
}
|
||||
|
||||
public function clearPlayerVis():Void
|
||||
{
|
||||
if (playerVis != null)
|
||||
{
|
||||
remove(playerVis);
|
||||
playerVis.destroy();
|
||||
playerVis = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function clearOpponentVis():Void
|
||||
{
|
||||
if (opponentVis != null)
|
||||
{
|
||||
remove(opponentVis);
|
||||
opponentVis.destroy();
|
||||
opponentVis = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function clearInstVis():Void
|
||||
{
|
||||
if (instVis != null)
|
||||
{
|
||||
remove(instVis);
|
||||
instVis.destroy();
|
||||
instVis = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function clearAllVis():Void
|
||||
{
|
||||
clearPlayerVis();
|
||||
clearOpponentVis();
|
||||
clearInstVis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the add function to add a visualizer to the group.
|
||||
* @param vis The visualizer to add.
|
||||
* @return The added visualizer.
|
||||
*/
|
||||
public override function add(vis:PolygonSpectogram):PolygonSpectogram
|
||||
{
|
||||
var result:PolygonSpectogram = super.add(vis);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
{
|
||||
playerVis.destroy();
|
||||
opponentVis.destroy();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package funkin.audio.visualize;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.sound.FlxSound;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.audio.visualize.PolygonSpectogram.VISTYPE;
|
||||
import funkin.audio.visualize.VisShit.CurAudioInfo;
|
||||
import lime.utils.Int16Array;
|
||||
|
||||
using Lambda;
|
||||
using flixel.util.FlxSpriteUtil;
|
||||
|
||||
class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
|
||||
{
|
||||
var sampleRate:Int;
|
||||
|
||||
var lengthOfShit:Int = 500;
|
||||
|
||||
public var visType:VISTYPE = UPDATED;
|
||||
|
||||
public var col:Int = FlxColor.WHITE;
|
||||
public var daHeight:Float = FlxG.height;
|
||||
|
||||
public var vis:VisShit;
|
||||
|
||||
public function new(daSound:FlxSound, ?col:FlxColor = FlxColor.WHITE, ?height:Float = 720, ?amnt:Int = 500)
|
||||
{
|
||||
super();
|
||||
|
||||
vis = new VisShit(daSound);
|
||||
this.col = col;
|
||||
this.daHeight = height;
|
||||
lengthOfShit = amnt;
|
||||
|
||||
regenLineShit();
|
||||
}
|
||||
|
||||
public function regenLineShit():Void
|
||||
{
|
||||
for (i in 0...lengthOfShit)
|
||||
{
|
||||
var lineShit:FlxSprite = new FlxSprite(100, i / lengthOfShit * daHeight).makeGraphic(1, 1, col);
|
||||
lineShit.active = false;
|
||||
lineShit.ID = i;
|
||||
add(lineShit);
|
||||
}
|
||||
}
|
||||
|
||||
var setBuffer:Bool = false;
|
||||
|
||||
public var audioData:Int16Array;
|
||||
|
||||
var numSamples:Int = 0;
|
||||
|
||||
public var wavOptimiz:Int = 10;
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
switch (visType)
|
||||
{
|
||||
case UPDATED:
|
||||
updateVisulizer();
|
||||
|
||||
case FREQUENCIES:
|
||||
updateFFT();
|
||||
default:
|
||||
}
|
||||
|
||||
forEach(spr -> {
|
||||
spr.visible = spr.ID % wavOptimiz == 0;
|
||||
});
|
||||
|
||||
// if visType is static, call updateVisulizer() manually whenever you want to update it!
|
||||
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param start is the start in milliseconds?
|
||||
*/
|
||||
public function generateSection(start:Float = 0, seconds:Float = 1):Void
|
||||
{
|
||||
checkAndSetBuffer();
|
||||
|
||||
if (setBuffer)
|
||||
{
|
||||
var samplesToGen:Int = Std.int(sampleRate * seconds);
|
||||
var startingSample:Int = Std.int(FlxMath.remapToRange(start, 0, vis.snd.length, 0, numSamples));
|
||||
|
||||
var prevLine:FlxPoint = new FlxPoint();
|
||||
|
||||
for (i in 0...group.members.length)
|
||||
{
|
||||
var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, group.members.length, startingSample, startingSample + samplesToGen));
|
||||
var curAud:CurAudioInfo = VisShit.getCurAud(audioData, sampleApprox);
|
||||
|
||||
var swagheight:Int = 200;
|
||||
|
||||
group.members[i].x = prevLine.x;
|
||||
group.members[i].y = prevLine.y;
|
||||
|
||||
prevLine.x = (curAud.balanced * swagheight / 2 + swagheight / 2) + x;
|
||||
prevLine.y = (i / group.members.length * daHeight) + y;
|
||||
|
||||
var line = FlxPoint.get(prevLine.x - group.members[i].x, prevLine.y - group.members[i].y);
|
||||
|
||||
group.members[i].setGraphicSize(Std.int(Math.max(line.length, 1)), Std.int(1));
|
||||
group.members[i].angle = line.degrees;
|
||||
}
|
||||
|
||||
wavOptimiz = 1; // hard set wavOptimiz to 1 so its a pure thing
|
||||
}
|
||||
}
|
||||
|
||||
public function checkAndSetBuffer()
|
||||
{
|
||||
vis.checkAndSetBuffer();
|
||||
|
||||
if (vis.setBuffer)
|
||||
{
|
||||
audioData = vis.audioData;
|
||||
sampleRate = vis.sampleRate;
|
||||
setBuffer = vis.setBuffer;
|
||||
numSamples = Std.int(audioData.length / 2);
|
||||
}
|
||||
}
|
||||
|
||||
var doAnim:Bool = false;
|
||||
var frameCounter:Int = 0;
|
||||
|
||||
public function updateFFT()
|
||||
{
|
||||
if (vis.snd != null)
|
||||
{
|
||||
var remappedShit:Int = 0;
|
||||
|
||||
checkAndSetBuffer();
|
||||
|
||||
if (!doAnim)
|
||||
{
|
||||
frameCounter++;
|
||||
|
||||
if (frameCounter >= 0)
|
||||
{
|
||||
frameCounter = 0;
|
||||
doAnim = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (setBuffer && doAnim)
|
||||
{
|
||||
doAnim = false;
|
||||
|
||||
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples));
|
||||
else
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, numSamples));
|
||||
|
||||
var fftSamples:Array<Float> = [];
|
||||
var i = remappedShit;
|
||||
|
||||
for (sample in remappedShit...remappedShit + (Std.int((44100 * (1 / 144)))))
|
||||
{
|
||||
var curAud:CurAudioInfo = VisShit.getCurAud(audioData, i);
|
||||
i += 2;
|
||||
|
||||
fftSamples.push(curAud.balanced);
|
||||
}
|
||||
|
||||
var freqShit = vis.funnyFFT(fftSamples);
|
||||
var prevLine:FlxPoint = new FlxPoint();
|
||||
var swagheight:Int = 200;
|
||||
|
||||
for (i in 0...group.members.length)
|
||||
{
|
||||
// needs to be exponential growth / scaling
|
||||
// still need to optmize the FFT to run better, gets only samples needed?
|
||||
// not every frequency is built the same!
|
||||
// 20hz to 40z is a LOT of subtle low ends, but somethin like 20,000hz to 20,020hz, the difference is NOT the same!
|
||||
|
||||
var powedShit:Float = FlxMath.remapToRange(i, 0, group.members.length, 0, 4);
|
||||
|
||||
// a value between 10hz and 100Khz
|
||||
var hzPicker:Float = Math.pow(10, powedShit);
|
||||
|
||||
var remappedFreq:Int = Std.int(FlxMath.remapToRange(hzPicker, 0, 10000, 0, freqShit[0].length - 1));
|
||||
|
||||
group.members[i].x = prevLine.x;
|
||||
group.members[i].y = prevLine.y;
|
||||
|
||||
var freqPower:Float = 0;
|
||||
|
||||
for (pow in 0...freqShit.length)
|
||||
freqPower += freqShit[pow][remappedFreq];
|
||||
|
||||
freqPower /= freqShit.length;
|
||||
var freqIDK:Float = FlxMath.remapToRange(freqPower, 0, 0.000005, 0, 50);
|
||||
|
||||
prevLine.x = (freqIDK * swagheight / 2 + swagheight / 2) + x;
|
||||
prevLine.y = (i / group.members.length * daHeight) + y;
|
||||
|
||||
var line = FlxPoint.get(prevLine.x - group.members[i].x, prevLine.y - group.members[i].y);
|
||||
|
||||
// dont draw a line until i figure out a nicer way to view da spikes and shit idk lol!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var curTime:Float = 0;
|
||||
|
||||
public function updateVisulizer():Void
|
||||
{
|
||||
if (vis.snd != null)
|
||||
{
|
||||
var remappedShit:Int = 0;
|
||||
|
||||
checkAndSetBuffer();
|
||||
|
||||
if (setBuffer)
|
||||
{
|
||||
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples));
|
||||
else
|
||||
{
|
||||
if (curTime == Conductor.instance.songPosition)
|
||||
{
|
||||
wavOptimiz = 3;
|
||||
return; // already did shit, so finishes function early
|
||||
}
|
||||
|
||||
curTime = Conductor.instance.songPosition;
|
||||
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, numSamples));
|
||||
}
|
||||
|
||||
wavOptimiz = 8;
|
||||
|
||||
var i = remappedShit;
|
||||
var prevLine:FlxPoint = new FlxPoint();
|
||||
|
||||
var swagheight:Int = 200;
|
||||
|
||||
for (sample in remappedShit...remappedShit + lengthOfShit)
|
||||
{
|
||||
var curAud:CurAudioInfo = VisShit.getCurAud(audioData, i);
|
||||
|
||||
i += 2;
|
||||
|
||||
var remappedSample:Float = FlxMath.remapToRange(sample, remappedShit, remappedShit + lengthOfShit, 0, lengthOfShit - 1);
|
||||
|
||||
group.members[Std.int(remappedSample)].x = prevLine.x;
|
||||
group.members[Std.int(remappedSample)].y = prevLine.y;
|
||||
prevLine.x = (curAud.balanced * swagheight / 2 + swagheight / 2) + x;
|
||||
prevLine.y = (Std.int(remappedSample) / lengthOfShit * daHeight) + y;
|
||||
|
||||
var line = FlxPoint.get(prevLine.x - group.members[Std.int(remappedSample)].x, prevLine.y - group.members[Std.int(remappedSample)].y);
|
||||
|
||||
group.members[Std.int(remappedSample)].setGraphicSize(Std.int(Math.max(line.length, 1)), Std.int(1));
|
||||
group.members[Std.int(remappedSample)].angle = line.degrees;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package funkin.audio.visualize;
|
||||
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.sound.FlxSound;
|
||||
import funkin.audio.visualize.dsp.FFT;
|
||||
import lime.utils.Int16Array;
|
||||
import funkin.util.MathUtil;
|
||||
|
||||
using Lambda;
|
||||
|
||||
class VisShit
|
||||
{
|
||||
public var snd:FlxSound;
|
||||
public var setBuffer:Bool = false;
|
||||
public var audioData:Int16Array;
|
||||
public var sampleRate:Int = 44100; // default, ez?
|
||||
public var numSamples:Int = 0;
|
||||
|
||||
public function new(snd:FlxSound)
|
||||
{
|
||||
this.snd = snd;
|
||||
}
|
||||
|
||||
public function funnyFFT(samples:Array<Float>, ?skipped:Int = 1):Array<Array<Float>>
|
||||
{
|
||||
// nab multiple samples at once in while / for loops?
|
||||
|
||||
var fs:Float = 44100 / skipped; // sample rate shit?
|
||||
|
||||
final fftN = 1024;
|
||||
final halfN = Std.int(fftN / 2);
|
||||
final overlap = 0.5;
|
||||
final hop = Std.int(fftN * (1 - overlap));
|
||||
|
||||
// window function to compensate for overlapping
|
||||
final a0 = 0.5; // => Hann(ing) window
|
||||
final window = (n:Int) -> a0 - (1 - a0) * Math.cos(2 * Math.PI * n / fftN);
|
||||
|
||||
// NOTE TO SELF FOR WHEN I WAKE UP
|
||||
|
||||
// helpers, note that spectrum indexes suppose non-negative frequencies
|
||||
final binSize = fs / fftN;
|
||||
final indexToFreq = function(k:Int) {
|
||||
var powShit:Float = FlxMath.remapToRange(k, 0, halfN, 0, MathUtil.logBase(10, halfN)); // 4.3 is almost the log of 20Khz or so. Close enuf lol
|
||||
|
||||
return 1.0 * (Math.pow(10, powShit)); // we need the `1.0` to avoid overflows
|
||||
};
|
||||
|
||||
// "melodic" band-pass filter
|
||||
final minFreq = 20.70;
|
||||
final maxFreq = 4000.01;
|
||||
final melodicBandPass = function(k:Int, s:Float) {
|
||||
final freq = indexToFreq(k);
|
||||
final filter = freq > minFreq - binSize && freq < maxFreq + binSize ? 1 : 0;
|
||||
return s;
|
||||
};
|
||||
|
||||
var freqOutput:Array<Array<Float>> = [];
|
||||
|
||||
var c = 0; // index where each chunk begins
|
||||
var indexOfArray:Int = 0;
|
||||
while (c < samples.length)
|
||||
{
|
||||
// take a chunk (zero-padded if needed) and apply the window
|
||||
final chunk = [
|
||||
for (n in 0...fftN)
|
||||
(c + n < samples.length ? samples[c + n] : 0.0) * window(n)
|
||||
];
|
||||
|
||||
// compute positive spectrum with sampling correction and BP filter
|
||||
final freqs = FFT.rfft(chunk).map(z -> z.scale(1 / fftN).magnitude).mapi(melodicBandPass);
|
||||
|
||||
freqOutput.push([]);
|
||||
|
||||
// find spectral peaks and their instantaneous frequencies
|
||||
for (k => s in freqs)
|
||||
{
|
||||
final time = c / fs;
|
||||
final freq = indexToFreq(k);
|
||||
final power = s * s;
|
||||
if (FlxG.keys.justPressed.I)
|
||||
{
|
||||
trace(k);
|
||||
|
||||
haxe.Log.trace('${time};${freq};${power}', null);
|
||||
}
|
||||
if (freq < maxFreq) freqOutput[indexOfArray].push(power);
|
||||
//
|
||||
}
|
||||
|
||||
indexOfArray++;
|
||||
// move to next (overlapping) chunk
|
||||
c += hop;
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.C) trace(freqOutput.length);
|
||||
|
||||
return freqOutput;
|
||||
}
|
||||
|
||||
public static function getCurAud(aud:Int16Array, index:Int):CurAudioInfo
|
||||
{
|
||||
var left = aud[index] / 32767;
|
||||
var right = aud[index + 2] / 32767;
|
||||
var balanced = (left + right) / 2;
|
||||
|
||||
var funny:CurAudioInfo = {left: left, right: right, balanced: balanced};
|
||||
|
||||
return funny;
|
||||
}
|
||||
|
||||
public function checkAndSetBuffer()
|
||||
{
|
||||
if (snd != null && snd.playing)
|
||||
{
|
||||
if (!setBuffer)
|
||||
{
|
||||
// Math.pow3
|
||||
@:privateAccess
|
||||
var buf = snd._channel.__audioSource.buffer;
|
||||
|
||||
// @:privateAccess
|
||||
audioData = cast buf.data; // jank and hacky lol! kinda busted on HTML5 also!!
|
||||
sampleRate = buf.sampleRate;
|
||||
|
||||
trace('got audio buffer shit');
|
||||
trace(sampleRate);
|
||||
trace(buf.bitsPerSample);
|
||||
|
||||
setBuffer = true;
|
||||
numSamples = Std.int(audioData.length / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef CurAudioInfo =
|
||||
{
|
||||
var left:Float;
|
||||
var right:Float;
|
||||
var balanced:Float;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package funkin.audio.visualize.dsp;
|
||||
|
||||
/**
|
||||
Complex number representation.
|
||||
**/
|
||||
@:forward(real, imag) @:notNull @:pure
|
||||
abstract Complex({
|
||||
final real:Float;
|
||||
final imag:Float;
|
||||
})
|
||||
{
|
||||
public inline function new(real:Float, imag:Float)
|
||||
this = {real: real, imag: imag};
|
||||
|
||||
/**
|
||||
Makes a Complex number with the given Float as its real part and a zero imag part.
|
||||
**/
|
||||
@:from
|
||||
public static inline function fromReal(r:Float)
|
||||
return new Complex(r, 0);
|
||||
|
||||
/**
|
||||
Complex argument, in radians.
|
||||
**/
|
||||
public var angle(get, never):Float;
|
||||
|
||||
inline function get_angle()
|
||||
return Math.atan2(this.imag, this.real);
|
||||
|
||||
/**
|
||||
Complex module.
|
||||
**/
|
||||
public var magnitude(get, never):Float;
|
||||
|
||||
inline function get_magnitude()
|
||||
return Math.sqrt(this.real * this.real + this.imag * this.imag);
|
||||
|
||||
@:op(A + B)
|
||||
public inline function add(rhs:Complex):Complex
|
||||
return new Complex(this.real + rhs.real, this.imag + rhs.imag);
|
||||
|
||||
@:op(A - B)
|
||||
public inline function sub(rhs:Complex):Complex
|
||||
return new Complex(this.real - rhs.real, this.imag - rhs.imag);
|
||||
|
||||
@:op(A * B)
|
||||
public inline function mult(rhs:Complex):Complex
|
||||
return new Complex(this.real * rhs.real - this.imag * rhs.imag, this.real * rhs.imag + this.imag * rhs.real);
|
||||
|
||||
/**
|
||||
Returns the complex conjugate, does not modify this object.
|
||||
**/
|
||||
public inline function conj():Complex
|
||||
return new Complex(this.real, -this.imag);
|
||||
|
||||
/**
|
||||
Multiplication by a real factor, does not modify this object.
|
||||
**/
|
||||
public inline function scale(k:Float):Complex
|
||||
return new Complex(this.real * k, this.imag * k);
|
||||
|
||||
public inline function copy():Complex
|
||||
return new Complex(this.real, this.imag);
|
||||
|
||||
/**
|
||||
The imaginary unit.
|
||||
**/
|
||||
public static final im = new Complex(0, 1);
|
||||
|
||||
/**
|
||||
The complex zero.
|
||||
**/
|
||||
public static final zero = new Complex(0, 0);
|
||||
|
||||
/**
|
||||
Computes the complex exponential `e^(iw)`.
|
||||
**/
|
||||
public static inline function exp(w:Float)
|
||||
return new Complex(Math.cos(w), Math.sin(w));
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package funkin.audio.visualize.dsp;
|
||||
|
||||
using funkin.audio.visualize.dsp.OffsetArray;
|
||||
using funkin.audio.visualize.dsp.Signal;
|
||||
|
||||
// these are only used for testing, down in FFT.main()
|
||||
|
||||
/**
|
||||
Fast/Finite Fourier Transforms.
|
||||
**/
|
||||
class FFT
|
||||
{
|
||||
/**
|
||||
Computes the Discrete Fourier Transform (DFT) of a `Complex` sequence.
|
||||
|
||||
If the input has N data points (N should be a power of 2 or padding will be added)
|
||||
from a signal sampled at intervals of 1/Fs, the result will be a sequence of N
|
||||
samples from the Discrete-Time Fourier Transform (DTFT) - which is Fs-periodic -
|
||||
with a spacing of Fs/N Hz between them and a scaling factor of Fs.
|
||||
**/
|
||||
public static function fft(input:Array<Complex>):Array<Complex>
|
||||
return do_fft(input, false);
|
||||
|
||||
/**
|
||||
Like `fft`, but for a real (Float) sequence input.
|
||||
|
||||
Since the input time signal is real, its frequency representation is
|
||||
Hermitian-symmetric so we only return the positive frequencies.
|
||||
**/
|
||||
public static function rfft(input:Array<Float>):Array<Complex>
|
||||
{
|
||||
final s = fft(input.map(Complex.fromReal));
|
||||
return s.slice(0, Std.int(s.length / 2) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
Computes the Inverse DFT of a periodic input sequence.
|
||||
|
||||
If the input contains N (a power of 2) DTFT samples, each spaced Fs/N Hz
|
||||
from each other, the result will consist of N data points as sampled
|
||||
from a time signal at intervals of 1/Fs with a scaling factor of 1/Fs.
|
||||
**/
|
||||
public static function ifft(input:Array<Complex>):Array<Complex>
|
||||
return do_fft(input, true);
|
||||
|
||||
// Handles padding and scaling for forwards and inverse FFTs.
|
||||
static function do_fft(input:Array<Complex>, inverse:Bool):Array<Complex>
|
||||
{
|
||||
final n = nextPow2(input.length);
|
||||
var ts = [for (i in 0...n) if (i < input.length) input[i] else Complex.zero];
|
||||
var fs = [for (_ in 0...n) Complex.zero];
|
||||
ditfft2(ts, 0, fs, 0, n, 1, inverse);
|
||||
return inverse ? fs.map(z -> z.scale(1 / n)) : fs;
|
||||
return fs;
|
||||
}
|
||||
|
||||
// Radix-2 Decimation-In-Time variant of Cooley–Tukey's FFT, recursive.
|
||||
static function ditfft2(time:Array<Complex>, t:Int, freq:Array<Complex>, f:Int, n:Int, step:Int, inverse:Bool):Void
|
||||
{
|
||||
if (n == 1)
|
||||
{
|
||||
freq[f] = time[t].copy();
|
||||
}
|
||||
else
|
||||
{
|
||||
final halfLen = Std.int(n / 2);
|
||||
ditfft2(time, t, freq, f, halfLen, step * 2, inverse);
|
||||
ditfft2(time, t + step, freq, f + halfLen, halfLen, step * 2, inverse);
|
||||
for (k in 0...halfLen)
|
||||
{
|
||||
final twiddle = Complex.exp((inverse ? 1 : -1) * 2 * Math.PI * k / n);
|
||||
final even = freq[f + k].copy();
|
||||
final odd = freq[f + k + halfLen].copy();
|
||||
freq[f + k] = even + twiddle * odd;
|
||||
freq[f + k + halfLen] = even - twiddle * odd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Naive O(n^2) DFT, used for testing purposes.
|
||||
static function dft(ts:Array<Complex>, ?inverse:Bool):Array<Complex>
|
||||
{
|
||||
if (inverse == null) inverse = false;
|
||||
final n = ts.length;
|
||||
var fs = new Array<Complex>();
|
||||
fs.resize(n);
|
||||
for (f in 0...n)
|
||||
{
|
||||
var sum = Complex.zero;
|
||||
for (t in 0...n)
|
||||
{
|
||||
sum += ts[t] * Complex.exp((inverse ? 1 : -1) * 2 * Math.PI * f * t / n);
|
||||
}
|
||||
fs[f] = inverse ? sum.scale(1 / n) : sum;
|
||||
}
|
||||
return fs;
|
||||
}
|
||||
|
||||
/**
|
||||
Finds the power of 2 that is equal to or greater than the given natural.
|
||||
**/
|
||||
static function nextPow2(x:Int):Int
|
||||
{
|
||||
if (x < 2) return 1;
|
||||
else if ((x & (x - 1)) == 0) return x;
|
||||
var pow = 2;
|
||||
x--;
|
||||
while ((x >>= 1) != 0)
|
||||
pow <<= 1;
|
||||
return pow;
|
||||
}
|
||||
|
||||
// testing, but also acts like an example
|
||||
static function main()
|
||||
{
|
||||
// sampling and buffer parameters
|
||||
final Fs = 44100.0;
|
||||
final N = 512;
|
||||
final halfN = Std.int(N / 2);
|
||||
|
||||
// build a time signal as a sum of sinusoids
|
||||
final freqs = [5919.911];
|
||||
final ts = [for (n in 0...N) freqs.map(f -> Math.sin(2 * Math.PI * f * n / Fs)).sum()];
|
||||
|
||||
// get positive spectrum and use its symmetry to reconstruct negative domain
|
||||
final fs_pos = rfft(ts);
|
||||
final fs_fft = new OffsetArray([for (k in -(halfN - 1)...0) fs_pos[-k].conj()].concat(fs_pos), -(halfN - 1));
|
||||
|
||||
// double-check with naive DFT
|
||||
final fs_dft = new OffsetArray(dft(ts.map(Complex.fromReal)).circShift(halfN - 1), -(halfN - 1));
|
||||
final fs_err = [for (k in -(halfN - 1)...halfN) fs_fft[k] - fs_dft[k]];
|
||||
final max_fs_err = fs_err.map(z -> z.magnitude).max();
|
||||
if (max_fs_err > 1e-6) haxe.Log.trace('FT Error: ${max_fs_err}', null);
|
||||
// else for (k => s in fs_fft) haxe.Log.trace('${k * Fs / N};${s.scale(1 / Fs).magnitude}', null);
|
||||
|
||||
// find spectral peaks to detect signal frequencies
|
||||
final freqis = fs_fft.array.map(z -> z.magnitude)
|
||||
.findPeaks()
|
||||
.map(k -> (k - (halfN - 1)) * Fs / N)
|
||||
.filter(f -> f >= 0);
|
||||
if (freqis.length != freqs.length)
|
||||
{
|
||||
trace('Found frequencies: ${freqis}');
|
||||
}
|
||||
else
|
||||
{
|
||||
final freqs_err = [for (i in 0...freqs.length) freqis[i] - freqs[i]];
|
||||
final max_freqs_err = freqs_err.map(Math.abs).max();
|
||||
if (max_freqs_err > Fs / N) trace('Frequency Errors: ${freqs_err}');
|
||||
}
|
||||
|
||||
// recover time signal from the frequency domain
|
||||
final ts_ifft = ifft(fs_fft.array.circShift(-(halfN - 1)).map(z -> z.scale(1 / Fs)));
|
||||
final ts_err = [for (n in 0...N) ts_ifft[n].scale(Fs).real - ts[n]];
|
||||
final max_ts_err = ts_err.map(Math.abs).max();
|
||||
if (max_ts_err > 1e-6) haxe.Log.trace('IFT Error: ${max_ts_err}', null);
|
||||
// else for (n in 0...ts_ifft.length) haxe.Log.trace('${n / Fs};${ts_ifft[n].scale(Fs).real}', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package funkin.audio.visualize.dsp;
|
||||
|
||||
/**
|
||||
A view into an Array with an indexing offset.
|
||||
|
||||
Usages include 1-indexed sequences or zero-centered buffers with negative indexing.
|
||||
**/
|
||||
@:forward(array, offset)
|
||||
abstract OffsetArray<T>({
|
||||
final array:Array<T>;
|
||||
final offset:Int;
|
||||
})
|
||||
{
|
||||
public inline function new(array:Array<T>, offset:Int)
|
||||
this = {array: array, offset: offset};
|
||||
|
||||
public var length(get, never):Int;
|
||||
|
||||
inline function get_length()
|
||||
return this.array.length;
|
||||
|
||||
@:arrayAccess
|
||||
public inline function get(index:Int):T
|
||||
return this.array[index - this.offset];
|
||||
|
||||
@:arrayAccess
|
||||
public inline function set(index:Int, value:T):Void
|
||||
this.array[index - this.offset] = value;
|
||||
|
||||
/**
|
||||
Iterates through items in their original order while providing the altered indexes as keys.
|
||||
**/
|
||||
public inline function keyValueIterator():KeyValueIterator<Int, T>
|
||||
return new OffsetArrayIterator(this.array, this.offset);
|
||||
|
||||
@:from
|
||||
static inline function fromArray<T>(array:Array<T>)
|
||||
return new OffsetArray(array, 0);
|
||||
|
||||
@:to
|
||||
inline function toArray()
|
||||
return this.array;
|
||||
|
||||
/**
|
||||
Makes a shifted version of the given `array`, where elements are in the
|
||||
same order but shifted by `n` positions (to the right if positive and to
|
||||
the left if negative) in **circular** fashion (no elements discarded).
|
||||
**/
|
||||
public static function circShift<T>(array:Array<T>, n:Int):Array<T>
|
||||
{
|
||||
if (n < 0) return circShift(array, array.length + n);
|
||||
|
||||
var shifted = new Array<T>();
|
||||
|
||||
n = n % array.length;
|
||||
for (i in array.length - n...array.length)
|
||||
shifted.push(array[i]);
|
||||
for (i in 0...array.length - n)
|
||||
shifted.push(array[i]);
|
||||
|
||||
return shifted;
|
||||
}
|
||||
}
|
||||
|
||||
private class OffsetArrayIterator<T>
|
||||
{
|
||||
final array:Array<T>;
|
||||
final offset:Int;
|
||||
var enumeration:Int;
|
||||
|
||||
public inline function new(array:Array<T>, offset:Int)
|
||||
{
|
||||
this.array = array;
|
||||
this.offset = offset;
|
||||
this.enumeration = 0;
|
||||
}
|
||||
|
||||
public inline function next():{key:Int, value:T}
|
||||
{
|
||||
final i = this.enumeration++;
|
||||
return {key: i + this.offset, value: this.array[i]};
|
||||
}
|
||||
|
||||
public inline function hasNext():Bool
|
||||
return this.enumeration < this.array.length;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package funkin.audio.visualize.dsp;
|
||||
|
||||
using Lambda;
|
||||
|
||||
/**
|
||||
Signal processing miscellaneous utilities.
|
||||
**/
|
||||
class Signal
|
||||
{
|
||||
/**
|
||||
Returns a smoothed version of the input array using a moving average.
|
||||
**/
|
||||
public static function smooth(y:Array<Float>, n:Int):Array<Float>
|
||||
{
|
||||
if (n <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if (n == 1)
|
||||
{
|
||||
return y.copy();
|
||||
}
|
||||
else
|
||||
{
|
||||
var smoothed = new Array<Float>();
|
||||
smoothed.resize(y.length);
|
||||
for (i in 0...y.length)
|
||||
{
|
||||
var m = i + 1 < n ? i : n - 1;
|
||||
smoothed[i] = sum(y.slice(i - m, i + 1));
|
||||
}
|
||||
return smoothed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Finds indexes of peaks in the order they appear in the input sequence.
|
||||
|
||||
@param threshold Minimal peak height wrt. its neighbours, defaults to 0.
|
||||
@param minHeight Minimal peak height wrt. the whole input, defaults to global minimum.
|
||||
**/
|
||||
public static function findPeaks(y:Array<Float>, ?threshold:Float, ?minHeight:Float):Array<Int>
|
||||
{
|
||||
threshold = threshold == null ? 0.0 : Math.abs(threshold);
|
||||
minHeight = minHeight == null ? Signal.min(y) : minHeight;
|
||||
|
||||
var peaks = new Array<Int>();
|
||||
|
||||
final dy = [for (i in 1...y.length) y[i] - y[i - 1]];
|
||||
for (i in 1...dy.length)
|
||||
{
|
||||
// peak: function growth positive to its left and negative to its right
|
||||
if (dy[i - 1] > threshold && dy[i] < -threshold && y[i] > minHeight)
|
||||
{
|
||||
peaks.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
return peaks;
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the sum of all the elements of a given array.
|
||||
|
||||
This function tries to minimize floating-point precision errors.
|
||||
**/
|
||||
public static function sum(array:Array<Float>):Float
|
||||
{
|
||||
// Neumaier's "improved Kahan-Babuska algorithm":
|
||||
|
||||
var sum = 0.0;
|
||||
var c = 0.0; // running compensation for lost precision
|
||||
|
||||
for (v in array)
|
||||
{
|
||||
var t = sum + v;
|
||||
c += Math.abs(sum) >= Math.abs(v) ? (sum - t) + v // sum is bigger => low-order digits of v are lost
|
||||
: (v - t) + sum; // v is bigger => low-order digits of sum are lost
|
||||
sum = t;
|
||||
}
|
||||
|
||||
return sum + c; // correction only applied at the very end
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the average value of an array.
|
||||
**/
|
||||
public static function mean(y:Array<Float>):Float
|
||||
return sum(y) / y.length;
|
||||
|
||||
/**
|
||||
Returns the global maximum.
|
||||
**/
|
||||
public static function max(y:Array<Float>):Float
|
||||
return y.fold(Math.max, y[0]);
|
||||
|
||||
/**
|
||||
Returns the global maximum's index.
|
||||
**/
|
||||
public static function maxi(y:Array<Float>):Int
|
||||
return y.foldi((yi, m, i) -> yi > y[m] ? i : m, 0);
|
||||
|
||||
/**
|
||||
Returns the global minimum.
|
||||
**/
|
||||
public static function min(y:Array<Float>):Float
|
||||
return y.fold(Math.min, y[0]);
|
||||
|
||||
/**
|
||||
Returns the global minimum's index.
|
||||
**/
|
||||
public static function mini(y:Array<Float>):Int
|
||||
return y.foldi((yi, m, i) -> yi < y[m] ? i : m, 0);
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
package funkin.audio.waveform;
|
||||
|
||||
@:nullSafety
|
||||
class WaveformData
|
||||
{
|
||||
static final DEFAULT_VERSION:Int = 2;
|
||||
|
||||
/**
|
||||
* The version of the waveform data format.
|
||||
* @default `2` (-1 if not specified/invalid)
|
||||
*/
|
||||
public var version(default, null):Int = -1;
|
||||
|
||||
/**
|
||||
* The number of channels in the waveform.
|
||||
*/
|
||||
public var channels(default, null):Int = 1;
|
||||
|
||||
@:alias('sample_rate')
|
||||
public var sampleRate(default, null):Int = 44100;
|
||||
|
||||
/**
|
||||
* Number of input audio samples per output waveform data point.
|
||||
* At base zoom level this is number of samples per pixel.
|
||||
* Lower values can more accurately represent the waveform when zoomed in, but take more data.
|
||||
*/
|
||||
@:alias('samples_per_pixel')
|
||||
public var samplesPerPoint(default, null):Int = 256;
|
||||
|
||||
/**
|
||||
* Number of bits to use for each sample value. Valid values are `8` and `16`.
|
||||
*/
|
||||
public var bits(default, null):Int = 16;
|
||||
|
||||
/**
|
||||
* The length of the data array, in points.
|
||||
*/
|
||||
public var length(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Array of Int16 values representing the waveform.
|
||||
* TODO: Use an `openfl.Vector` for performance.
|
||||
*/
|
||||
public var data(default, null):Array<Int> = [];
|
||||
|
||||
@:jignored
|
||||
var channelData:Null<Array<WaveformDataChannel>> = null;
|
||||
|
||||
public function new(?version:Int, channels:Int, sampleRate:Int, samplesPerPoint:Int, bits:Int, length:Int, data:Array<Int>)
|
||||
{
|
||||
this.version = version ?? DEFAULT_VERSION;
|
||||
this.channels = channels;
|
||||
this.sampleRate = sampleRate;
|
||||
this.samplesPerPoint = samplesPerPoint;
|
||||
this.bits = bits;
|
||||
this.length = length;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
function buildChannelData():Array<WaveformDataChannel>
|
||||
{
|
||||
channelData = [];
|
||||
for (i in 0...channels)
|
||||
{
|
||||
channelData.push(new WaveformDataChannel(this, i));
|
||||
}
|
||||
return channelData;
|
||||
}
|
||||
|
||||
public function channel(index:Int)
|
||||
{
|
||||
return (channelData == null) ? buildChannelData()[index] : channelData[index];
|
||||
}
|
||||
|
||||
public function get(index:Int):Int
|
||||
{
|
||||
return data[index] ?? 0;
|
||||
}
|
||||
|
||||
public function set(index:Int, value:Int)
|
||||
{
|
||||
data[index] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum possible value for a waveform data point.
|
||||
* The minimum possible value is (-1 * maxSampleValue)
|
||||
*/
|
||||
public function maxSampleValue():Int
|
||||
{
|
||||
if (_maxSampleValue != 0) return _maxSampleValue;
|
||||
return _maxSampleValue = Std.int(Math.pow(2, bits));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the value because `Math.pow` is expensive and the value gets used a lot.
|
||||
*/
|
||||
@:jignored
|
||||
var _maxSampleValue:Int = 0;
|
||||
|
||||
/**
|
||||
* @return The length of the waveform in samples.
|
||||
*/
|
||||
public function lenSamples():Int
|
||||
{
|
||||
return length * samplesPerPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The length of the waveform in seconds.
|
||||
*/
|
||||
public function lenSeconds():Float
|
||||
{
|
||||
return inline lenSamples() / sampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the time in seconds, return the waveform data point index.
|
||||
*/
|
||||
public function secondsToIndex(seconds:Float):Int
|
||||
{
|
||||
return Std.int(seconds * inline pointsPerSecond());
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a waveform data point index, return the time in seconds.
|
||||
*/
|
||||
public function indexToSeconds(index:Int):Float
|
||||
{
|
||||
return index / inline pointsPerSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of data points this waveform data provides per second of audio.
|
||||
*/
|
||||
public inline function pointsPerSecond():Float
|
||||
{
|
||||
return sampleRate / samplesPerPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the percentage progress through the waveform, return the waveform data point index.
|
||||
*/
|
||||
public function percentToIndex(percent:Float):Int
|
||||
{
|
||||
return Std.int(percent * length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a waveform data point index, return the percentage progress through the waveform.
|
||||
*/
|
||||
public function indexToPercent(index:Int):Float
|
||||
{
|
||||
return index / length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resample the waveform data to create a new WaveformData object matching the desired `samplesPerPoint` value.
|
||||
* This is useful for zooming in/out of the waveform in a performant manner.
|
||||
*
|
||||
* @param newSamplesPerPoint The new value for `samplesPerPoint`.
|
||||
*/
|
||||
public function resample(newSamplesPerPoint:Int):WaveformData
|
||||
{
|
||||
var result = this.clone();
|
||||
|
||||
var ratio = newSamplesPerPoint / samplesPerPoint;
|
||||
if (ratio == 1) return result;
|
||||
if (ratio < 1) trace('[WARNING] Downsampling will result in a low precision.');
|
||||
|
||||
var inputSampleCount = this.lenSamples();
|
||||
var outputSampleCount = Std.int(inputSampleCount * ratio);
|
||||
|
||||
var inputPointCount = this.length;
|
||||
var outputPointCount = Std.int(inputPointCount / ratio);
|
||||
var outputChannelCount = this.channels;
|
||||
|
||||
// TODO: Actually figure out the dumbass logic for this.
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new WaveformData whose data represents the two waveforms overlayed.
|
||||
*/
|
||||
public function merge(that:WaveformData):WaveformData
|
||||
{
|
||||
if (that == null) return this.clone();
|
||||
|
||||
var result = this.clone([]);
|
||||
|
||||
for (channelIndex in 0...this.channels)
|
||||
{
|
||||
var thisChannel = this.channel(channelIndex);
|
||||
var thatChannel = that.channel(channelIndex);
|
||||
var resultChannel = result.channel(channelIndex);
|
||||
|
||||
for (index in 0...this.length)
|
||||
{
|
||||
var thisMinSample = thisChannel.minSample(index);
|
||||
var thatMinSample = thatChannel.minSample(index);
|
||||
|
||||
var thisMaxSample = thisChannel.maxSample(index);
|
||||
var thatMaxSample = thatChannel.maxSample(index);
|
||||
|
||||
resultChannel.setMinSample(index, Std.int(Math.min(thisMinSample, thatMinSample)));
|
||||
resultChannel.setMaxSample(index, Std.int(Math.max(thisMaxSample, thatMaxSample)));
|
||||
}
|
||||
}
|
||||
|
||||
@:privateAccess
|
||||
result.length = this.length;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new WaveformData whose parameters match the current object.
|
||||
*/
|
||||
public function clone(?newData:Array<Int> = null):WaveformData
|
||||
{
|
||||
if (newData == null)
|
||||
{
|
||||
newData = this.data.clone();
|
||||
}
|
||||
|
||||
var clone = new WaveformData(this.version, this.channels, this.sampleRate, this.samplesPerPoint, this.bits, newData.length, newData);
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
@:nullSafety
|
||||
class WaveformDataChannel
|
||||
{
|
||||
var parent:WaveformData;
|
||||
var channelId:Int;
|
||||
|
||||
public function new(parent:WaveformData, channelId:Int)
|
||||
{
|
||||
this.parent = parent;
|
||||
this.channelId = channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a given minimum point at an index.
|
||||
*/
|
||||
public function minSample(i:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2;
|
||||
return inline parent.get(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function minSampleMapped(i:Int)
|
||||
{
|
||||
return inline minSample(i) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum value within the range of samples.
|
||||
* NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead.
|
||||
*/
|
||||
public function minSampleRange(start:Int, end:Int)
|
||||
{
|
||||
var min = inline parent.maxSampleValue();
|
||||
for (i in start...end)
|
||||
{
|
||||
var sample = inline minSample(i);
|
||||
if (sample < min) min = sample;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum value within the range of samples, mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function minSampleRangeMapped(start:Int, end:Int)
|
||||
{
|
||||
return inline minSampleRange(start, end) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a given maximum point at an index.
|
||||
*/
|
||||
public function maxSample(i:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2 + 1;
|
||||
return inline parent.get(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function maxSampleMapped(i:Int)
|
||||
{
|
||||
return inline maxSample(i) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum value within the range of samples.
|
||||
* NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead.
|
||||
*/
|
||||
public function maxSampleRange(start:Int, end:Int)
|
||||
{
|
||||
var max = -(inline parent.maxSampleValue());
|
||||
for (i in start...end)
|
||||
{
|
||||
var sample = inline maxSample(i);
|
||||
if (sample > max) max = sample;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum value within the range of samples, mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function maxSampleRangeMapped(start:Int, end:Int)
|
||||
{
|
||||
return inline maxSampleRange(start, end) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
public function setMinSample(i:Int, value:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2;
|
||||
inline parent.set(offset, value);
|
||||
}
|
||||
|
||||
public function setMaxSample(i:Int, value:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2 + 1;
|
||||
inline parent.set(offset, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package funkin.audio.waveform;
|
||||
|
||||
import funkin.util.TimerUtil;
|
||||
|
||||
class WaveformDataParser
|
||||
{
|
||||
static final INT16_MAX:Int = 32767;
|
||||
static final INT16_MIN:Int = -32768;
|
||||
|
||||
static final INT8_MAX:Int = 127;
|
||||
static final INT8_MIN:Int = -128;
|
||||
|
||||
public static function interpretFlxSound(sound:flixel.sound.FlxSound):Null<WaveformData>
|
||||
{
|
||||
if (sound == null) return null;
|
||||
|
||||
// Method 1. This only works if the sound has been played before.
|
||||
@:privateAccess
|
||||
var soundBuffer:Null<lime.media.AudioBuffer> = sound?._channel?.__audioSource?.buffer;
|
||||
|
||||
if (soundBuffer == null)
|
||||
{
|
||||
// Method 2. This works if the sound has not been played before.
|
||||
@:privateAccess
|
||||
soundBuffer = sound?._sound?.__buffer;
|
||||
|
||||
if (soundBuffer == null)
|
||||
{
|
||||
trace('[WAVEFORM] Failed to interpret FlxSound: ${sound}');
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// trace('[WAVEFORM] Method 2 worked.');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// trace('[WAVEFORM] Method 1 worked.');
|
||||
}
|
||||
|
||||
return interpretAudioBuffer(soundBuffer);
|
||||
}
|
||||
|
||||
public static function interpretAudioBuffer(soundBuffer:lime.media.AudioBuffer):Null<WaveformData>
|
||||
{
|
||||
var sampleRate = soundBuffer.sampleRate;
|
||||
var channels = soundBuffer.channels;
|
||||
var bitsPerSample = soundBuffer.bitsPerSample;
|
||||
var samplesPerPoint:Int = 256; // I don't think we need to configure this.
|
||||
var pointsPerSecond:Float = sampleRate / samplesPerPoint; // 172 samples per second for most songs is plenty precise while still being performant..
|
||||
|
||||
// TODO: Make this work better on HTML5.
|
||||
var soundData:lime.utils.Int16Array = cast soundBuffer.data;
|
||||
|
||||
var soundDataRawLength:Int = soundData.length;
|
||||
var soundDataSampleCount:Int = Std.int(Math.ceil(soundDataRawLength / channels / (bitsPerSample == 16 ? 2 : 1)));
|
||||
var outputPointCount:Int = Std.int(Math.ceil(soundDataSampleCount / samplesPerPoint));
|
||||
|
||||
// trace('Interpreting audio buffer:');
|
||||
// trace(' sampleRate: ${sampleRate}');
|
||||
// trace(' channels: ${channels}');
|
||||
// trace(' bitsPerSample: ${bitsPerSample}');
|
||||
// trace(' samplesPerPoint: ${samplesPerPoint}');
|
||||
// trace(' pointsPerSecond: ${pointsPerSecond}');
|
||||
// trace(' soundDataRawLength: ${soundDataRawLength}');
|
||||
// trace(' soundDataSampleCount: ${soundDataSampleCount}');
|
||||
// trace(' soundDataRawLength/4: ${soundDataRawLength / 4}');
|
||||
// trace(' outputPointCount: ${outputPointCount}');
|
||||
|
||||
var minSampleValue:Int = bitsPerSample == 16 ? INT16_MIN : INT8_MIN;
|
||||
var maxSampleValue:Int = bitsPerSample == 16 ? INT16_MAX : INT8_MAX;
|
||||
|
||||
var outputData:Array<Int> = [];
|
||||
|
||||
var perfStart:Float = TimerUtil.start();
|
||||
|
||||
for (pointIndex in 0...outputPointCount)
|
||||
{
|
||||
// minChannel1, maxChannel1, minChannel2, maxChannel2, ...
|
||||
var values:Array<Int> = [];
|
||||
|
||||
for (i in 0...channels)
|
||||
{
|
||||
values.push(bitsPerSample == 16 ? INT16_MAX : INT8_MAX);
|
||||
values.push(bitsPerSample == 16 ? INT16_MIN : INT8_MIN);
|
||||
}
|
||||
|
||||
var rangeStart = pointIndex * samplesPerPoint;
|
||||
var rangeEnd = rangeStart + samplesPerPoint;
|
||||
if (rangeEnd > soundDataSampleCount) rangeEnd = soundDataSampleCount;
|
||||
|
||||
for (sampleIndex in rangeStart...rangeEnd)
|
||||
{
|
||||
for (channelIndex in 0...channels)
|
||||
{
|
||||
var sampleIndex:Int = sampleIndex * channels + channelIndex;
|
||||
var sampleValue = soundData[sampleIndex];
|
||||
|
||||
if (sampleValue < values[channelIndex * 2]) values[(channelIndex * 2)] = sampleValue;
|
||||
if (sampleValue > values[channelIndex * 2 + 1]) values[(channelIndex * 2) + 1] = sampleValue;
|
||||
}
|
||||
}
|
||||
|
||||
// We now have the min and max values for the range.
|
||||
for (value in values)
|
||||
outputData.push(value);
|
||||
}
|
||||
|
||||
var outputDataLength:Int = Std.int(outputData.length / channels / 2);
|
||||
var result = new WaveformData(null, channels, sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData);
|
||||
|
||||
trace('[WAVEFORM] Interpreted audio buffer in ${TimerUtil.seconds(perfStart)}.');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static function parseWaveformData(path:String):Null<WaveformData>
|
||||
{
|
||||
var rawJson:String = openfl.Assets.getText(path).trim();
|
||||
return parseWaveformDataString(rawJson, path);
|
||||
}
|
||||
|
||||
public static function parseWaveformDataString(contents:String, ?fileName:String):Null<WaveformData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<WaveformData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
trace('[WAVEFORM] Parsing waveform data: ${contents}');
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
static function printErrors(errors:Array<json2object.Error>, id:String = ''):Void
|
||||
{
|
||||
trace('[WAVEFORM] Failed to parse waveform data: ${id}');
|
||||
|
||||
for (error in errors)
|
||||
funkin.data.DataError.printError(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
package funkin.audio.waveform;
|
||||
|
||||
import funkin.graphics.rendering.MeshRender;
|
||||
import flixel.util.FlxColor;
|
||||
|
||||
class WaveformSprite extends MeshRender
|
||||
{
|
||||
static final DEFAULT_COLOR:FlxColor = FlxColor.WHITE;
|
||||
static final DEFAULT_DURATION:Float = 5.0;
|
||||
static final DEFAULT_ORIENTATION:WaveformOrientation = HORIZONTAL;
|
||||
static final DEFAULT_X:Float = 0.0;
|
||||
static final DEFAULT_Y:Float = 0.0;
|
||||
static final DEFAULT_WIDTH:Float = 100.0;
|
||||
static final DEFAULT_HEIGHT:Float = 100.0;
|
||||
|
||||
/**
|
||||
* Set this to true to tell the waveform to rebuild itself.
|
||||
* Do this any time the data or drawable area of the waveform changes.
|
||||
* This often (but not always) needs to be done every frame.
|
||||
*/
|
||||
var isWaveformDirty:Bool = true;
|
||||
|
||||
/**
|
||||
* If true, force the waveform to redraw every frame.
|
||||
* Useful if the waveform's clipRect is constantly changing.
|
||||
*/
|
||||
public var forceUpdate:Bool = false;
|
||||
|
||||
public var waveformData(default, set):Null<WaveformData>;
|
||||
|
||||
function set_waveformData(value:Null<WaveformData>):Null<WaveformData>
|
||||
{
|
||||
if (waveformData == value) return value;
|
||||
|
||||
waveformData = value;
|
||||
isWaveformDirty = true;
|
||||
return waveformData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The color to render the waveform with.
|
||||
*/
|
||||
public var waveformColor(default, set):FlxColor;
|
||||
|
||||
function set_waveformColor(value:FlxColor):FlxColor
|
||||
{
|
||||
if (waveformColor == value) return value;
|
||||
|
||||
waveformColor = value;
|
||||
// We don't need to dirty the waveform geometry, just rebuild the texture.
|
||||
rebuildGraphic();
|
||||
return waveformColor;
|
||||
}
|
||||
|
||||
public var orientation(default, set):WaveformOrientation;
|
||||
|
||||
function set_orientation(value:WaveformOrientation):WaveformOrientation
|
||||
{
|
||||
if (orientation == value) return value;
|
||||
|
||||
orientation = value;
|
||||
isWaveformDirty = true;
|
||||
return orientation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time, in seconds, at which the waveform starts.
|
||||
*/
|
||||
public var time(default, set):Float;
|
||||
|
||||
function set_time(value:Float)
|
||||
{
|
||||
if (time == value) return value;
|
||||
|
||||
time = value;
|
||||
isWaveformDirty = true;
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* The duration, in seconds, that the waveform represents.
|
||||
* The section of waveform from `time` to `time + duration` and `width` are used to determine how many samples each pixel represents.
|
||||
*/
|
||||
public var duration(default, set):Float;
|
||||
|
||||
function set_duration(value:Float)
|
||||
{
|
||||
if (duration == value) return value;
|
||||
|
||||
duration = value;
|
||||
isWaveformDirty = true;
|
||||
return duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the physical size of the waveform with `this.height = value`.
|
||||
*/
|
||||
override function set_height(value:Float):Float
|
||||
{
|
||||
if (height == value) return super.set_height(value);
|
||||
|
||||
isWaveformDirty = true;
|
||||
return super.set_height(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the physical size of the waveform with `this.width = value`.
|
||||
*/
|
||||
override function set_width(value:Float):Float
|
||||
{
|
||||
if (width == value) return super.set_width(value);
|
||||
|
||||
isWaveformDirty = true;
|
||||
return super.set_width(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum size, in pixels, that a waveform will display with.
|
||||
* Useful for preventing the waveform from becoming too small to see.
|
||||
*
|
||||
* NOTE: This is technically doubled since it's applied above and below the center of the waveform.
|
||||
*/
|
||||
public var minWaveformSize:Int = 1;
|
||||
|
||||
/**
|
||||
* A multiplier on the size of the waveform.
|
||||
* Still capped at the width and height set for the sprite.
|
||||
*/
|
||||
public var amplitude:Float = 1.0;
|
||||
|
||||
public function new(?waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
{
|
||||
super(DEFAULT_X, DEFAULT_Y, DEFAULT_COLOR);
|
||||
this.waveformColor = color ?? DEFAULT_COLOR;
|
||||
this.width = DEFAULT_WIDTH;
|
||||
this.height = DEFAULT_HEIGHT;
|
||||
|
||||
this.waveformData = waveformData;
|
||||
this.orientation = orientation ?? DEFAULT_ORIENTATION;
|
||||
this.time = 0.0;
|
||||
this.duration = duration ?? DEFAULT_DURATION;
|
||||
|
||||
this.forceUpdate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually tell the waveform to rebuild itself, even if none of its properties have changed.
|
||||
*/
|
||||
public function markDirty():Void
|
||||
{
|
||||
isWaveformDirty = true;
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
if (forceUpdate || isWaveformDirty)
|
||||
{
|
||||
// Recalculate the waveform vertices.
|
||||
drawWaveform();
|
||||
isWaveformDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildGraphic():Void
|
||||
{
|
||||
// The waveform is rendered using a single colored pixel as a texture.
|
||||
// If you want something more elaborate, make sure to modify `build_vertex` below to use the UVs you want.
|
||||
makeGraphic(1, 1, this.waveformColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param offsetX Horizontal offset to draw the waveform at, in samples.
|
||||
*/
|
||||
function drawWaveform():Void
|
||||
{
|
||||
// For each sample in the waveform...
|
||||
// Add a MAX vertex and a MIN vertex.
|
||||
// If previous MAX/MIN is empty, store.
|
||||
// If previous MAX/MIN is not empty, draw a quad using current and previous MAX/MIN. Then store current MAX/MIN.
|
||||
// Continue until end of waveform.
|
||||
|
||||
this.clear();
|
||||
|
||||
if (waveformData == null) return;
|
||||
|
||||
// Center point of the waveform. When horizontal this is half the height, when vertical this is half the width.
|
||||
var waveformCenterPos:Int = orientation == HORIZONTAL ? Std.int(this.height / 2) : Std.int(this.width / 2);
|
||||
|
||||
var oneSecondInIndices:Int = waveformData.secondsToIndex(1);
|
||||
|
||||
var startTime:Float = time;
|
||||
var endTime:Float = time + duration;
|
||||
|
||||
var startIndex:Int = waveformData.secondsToIndex(startTime);
|
||||
var endIndex:Int = waveformData.secondsToIndex(endTime);
|
||||
|
||||
var pixelsPerIndex:Float = (orientation == HORIZONTAL ? this.width : this.height) / (endIndex - startIndex);
|
||||
var indexesPerPixel:Float = 1 / pixelsPerIndex;
|
||||
|
||||
var topLeftVertexIndex:Int = -1;
|
||||
var topRightVertexIndex:Int = -1;
|
||||
var bottomLeftVertexIndex:Int = -1;
|
||||
var bottomRightVertexIndex:Int = -1;
|
||||
|
||||
if (clipRect != null)
|
||||
{
|
||||
topLeftVertexIndex = this.build_vertex(clipRect.x, clipRect.y);
|
||||
topRightVertexIndex = this.build_vertex(clipRect.x + clipRect.width, clipRect.y);
|
||||
bottomLeftVertexIndex = this.build_vertex(clipRect.x, clipRect.y + clipRect.height);
|
||||
bottomRightVertexIndex = this.build_vertex(clipRect.x + clipRect.width, clipRect.y + clipRect.height);
|
||||
}
|
||||
|
||||
if (pixelsPerIndex >= 1.0)
|
||||
{
|
||||
// Each index is at least one pixel wide, so we render each index.
|
||||
var prevVertexTopIndex:Int = -1;
|
||||
var prevVertexBottomIndex:Int = -1;
|
||||
for (i in startIndex...endIndex)
|
||||
{
|
||||
var pixelPos:Int = Std.int((i - startIndex) * pixelsPerIndex);
|
||||
|
||||
var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y);
|
||||
|
||||
if (isBeforeClipRect) continue;
|
||||
|
||||
var isAfterClipRect:Bool = (clipRect != null)
|
||||
&& ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height));
|
||||
|
||||
if (isAfterClipRect)
|
||||
{
|
||||
break;
|
||||
};
|
||||
|
||||
var sampleMax:Float = Math.min(waveformData.channel(0).maxSampleMapped(i) * amplitude, 1.0);
|
||||
var sampleMin:Float = Math.max(waveformData.channel(0).minSampleMapped(i) * amplitude, -1.0);
|
||||
var sampleMaxSize:Float = sampleMax * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMaxSize < minWaveformSize) sampleMaxSize = minWaveformSize;
|
||||
var sampleMinSize:Float = sampleMin * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMinSize > -minWaveformSize) sampleMinSize = -minWaveformSize;
|
||||
var vertexTopY:Int = Std.int(waveformCenterPos - sampleMaxSize);
|
||||
var vertexBottomY:Int = Std.int(waveformCenterPos - sampleMinSize);
|
||||
|
||||
if (vertexBottomY - vertexTopY < minWaveformSize) vertexTopY = vertexBottomY - minWaveformSize;
|
||||
|
||||
var vertexTopIndex:Int = -1;
|
||||
var vertexBottomIndex:Int = -1;
|
||||
|
||||
if (clipRect != null)
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(pixelPos, vertexTopY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(pixelPos, vertexBottomY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(vertexTopY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(vertexBottomY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(pixelPos, vertexTopY);
|
||||
vertexBottomIndex = this.build_vertex(pixelPos, vertexBottomY);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(vertexTopY, pixelPos);
|
||||
vertexBottomIndex = this.build_vertex(vertexBottomY, pixelPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't render if we don't have a previous different set of vertices to create a quad from.
|
||||
if (prevVertexTopIndex != -1
|
||||
&& prevVertexBottomIndex != -1
|
||||
&& prevVertexTopIndex != vertexTopIndex
|
||||
&& prevVertexBottomIndex != vertexBottomIndex)
|
||||
{
|
||||
switch (orientation) // the line of code that makes you gay
|
||||
{
|
||||
case HORIZONTAL:
|
||||
this.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex);
|
||||
case VERTICAL:
|
||||
this.add_quad(prevVertexBottomIndex, prevVertexTopIndex, vertexTopIndex, vertexBottomIndex);
|
||||
}
|
||||
}
|
||||
|
||||
prevVertexTopIndex = vertexTopIndex;
|
||||
prevVertexBottomIndex = vertexBottomIndex;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Indexes are less than one pixel wide, so for each pixel we render the maximum of the samples that fall within it.
|
||||
var prevVertexTopIndex:Int = -1;
|
||||
var prevVertexBottomIndex:Int = -1;
|
||||
var waveformLengthPixels:Int = orientation == HORIZONTAL ? Std.int(this.width) : Std.int(this.height);
|
||||
for (i in 0...waveformLengthPixels)
|
||||
{
|
||||
var pixelPos:Int = i;
|
||||
|
||||
var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y);
|
||||
|
||||
if (isBeforeClipRect) continue;
|
||||
|
||||
var isAfterClipRect:Bool = (clipRect != null)
|
||||
&& ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height));
|
||||
|
||||
if (isAfterClipRect)
|
||||
{
|
||||
break;
|
||||
};
|
||||
|
||||
// Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming.
|
||||
var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex);
|
||||
var rangeEnd:Int = Std.int((i + 1) * indexesPerPixel + startIndex);
|
||||
|
||||
var sampleMax:Float = Math.min(waveformData.channel(0).maxSampleRangeMapped(rangeStart, rangeEnd) * amplitude, 1.0);
|
||||
var sampleMin:Float = Math.max(waveformData.channel(0).minSampleRangeMapped(rangeStart, rangeEnd) * amplitude, -1.0);
|
||||
var sampleMaxSize:Float = sampleMax * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMaxSize < minWaveformSize) sampleMaxSize = minWaveformSize;
|
||||
var sampleMinSize:Float = sampleMin * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMinSize > -minWaveformSize) sampleMinSize = -minWaveformSize;
|
||||
var vertexTopY:Int = Std.int(waveformCenterPos - sampleMaxSize);
|
||||
var vertexBottomY:Int = Std.int(waveformCenterPos - sampleMinSize);
|
||||
|
||||
var vertexTopIndex:Int = -1;
|
||||
var vertexBottomIndex:Int = -1;
|
||||
|
||||
if (clipRect != null)
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(pixelPos, vertexTopY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(pixelPos, vertexBottomY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(vertexTopY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(vertexBottomY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(pixelPos, vertexTopY);
|
||||
vertexBottomIndex = this.build_vertex(pixelPos, vertexBottomY);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(vertexTopY, pixelPos);
|
||||
vertexBottomIndex = this.build_vertex(vertexBottomY, pixelPos);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1)
|
||||
{
|
||||
switch (orientation)
|
||||
{
|
||||
case HORIZONTAL:
|
||||
this.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex);
|
||||
case VERTICAL:
|
||||
this.add_quad(prevVertexBottomIndex, prevVertexTopIndex, vertexTopIndex, vertexBottomIndex);
|
||||
}
|
||||
}
|
||||
prevVertexTopIndex = vertexTopIndex;
|
||||
prevVertexBottomIndex = vertexBottomIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildClippedVertex(x:Int, y:Int, topLeftVertexIndex:Int, topRightVertexIndex:Int, bottomLeftVertexIndex:Int, bottomRightVertexIndex:Int):Int
|
||||
{
|
||||
var shouldClipXLeft = x < clipRect.x;
|
||||
var shouldClipXRight = x > (clipRect.x + clipRect.width);
|
||||
var shouldClipYTop = y < clipRect.y;
|
||||
var shouldClipYBottom = y > (clipRect.y + clipRect.height);
|
||||
|
||||
// If the vertex is fully outside the clipRect, use a pre-existing vertex.
|
||||
// Else, if the vertex is outside the clipRect on one axis, create a new vertex constrained on that axis.
|
||||
// Else, create a whole new vertex.
|
||||
if (shouldClipXLeft && shouldClipYTop)
|
||||
{
|
||||
return topLeftVertexIndex;
|
||||
}
|
||||
else if (shouldClipXRight && shouldClipYTop)
|
||||
{
|
||||
return topRightVertexIndex;
|
||||
}
|
||||
else if (shouldClipXLeft && shouldClipYBottom)
|
||||
{
|
||||
return bottomLeftVertexIndex;
|
||||
}
|
||||
else if (shouldClipXRight && shouldClipYBottom)
|
||||
{
|
||||
return bottomRightVertexIndex;
|
||||
}
|
||||
else if (shouldClipXLeft)
|
||||
{
|
||||
return this.build_vertex(clipRect.x, y);
|
||||
}
|
||||
else if (shouldClipXRight)
|
||||
{
|
||||
return this.build_vertex(clipRect.x + clipRect.width, y);
|
||||
}
|
||||
else if (shouldClipYTop)
|
||||
{
|
||||
return this.build_vertex(x, clipRect.y);
|
||||
}
|
||||
else if (shouldClipYBottom)
|
||||
{
|
||||
return this.build_vertex(x, clipRect.y + clipRect.height);
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.build_vertex(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
{
|
||||
return new WaveformSprite(data, orientation, color, duration);
|
||||
}
|
||||
|
||||
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
{
|
||||
// TODO: Build waveform data from FunkinSound.
|
||||
var data = null;
|
||||
|
||||
return buildFromWaveformData(data, orientation, color, duration);
|
||||
}
|
||||
}
|
||||
|
||||
enum WaveformOrientation
|
||||
{
|
||||
HORIZONTAL;
|
||||
VERTICAL;
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package funkin.data;
|
||||
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.util.VersionUtil;
|
||||
import haxe.Constraints.Constructible;
|
||||
|
||||
/**
|
||||
* The entry's constructor function must take a single argument, the entry's ID.
|
||||
*/
|
||||
typedef EntryConstructorFunction = String->Void;
|
||||
|
||||
/**
|
||||
* A base type for a Registry, which is an object which handles loading scriptable objects.
|
||||
*
|
||||
* @param T The type to construct. Must implement `IRegistryEntry`.
|
||||
* @param J The type of the JSON data used when constructing.
|
||||
*/
|
||||
@:generic
|
||||
@:autoBuild(funkin.util.macro.DataRegistryMacro.buildRegistry())
|
||||
abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J>
|
||||
{
|
||||
/**
|
||||
* The ID of the registry. Used when logging.
|
||||
*/
|
||||
public final registryId:String;
|
||||
|
||||
final dataFilePath:String;
|
||||
|
||||
/**
|
||||
* A map of entry IDs to entries.
|
||||
*/
|
||||
final entries:Map<String, T>;
|
||||
|
||||
/**
|
||||
* A map of entry IDs to scripted class names.
|
||||
*/
|
||||
final scriptedEntryIds:Map<String, String>;
|
||||
|
||||
/**
|
||||
* The version rule to use when loading entries.
|
||||
* If the entry's version does not match this rule, migration is needed.
|
||||
*/
|
||||
final versionRule:thx.semver.VersionRule;
|
||||
|
||||
// public abstract static final instance:BaseRegistry<T, J> = new BaseRegistry<>();
|
||||
|
||||
/**
|
||||
* @param registryId A readable ID for this registry, used when logging.
|
||||
* @param dataFilePath The path (relative to `assets/data`) to search for JSON files.
|
||||
*/
|
||||
public function new(registryId:String, dataFilePath:String, ?versionRule:thx.semver.VersionRule)
|
||||
{
|
||||
this.registryId = registryId;
|
||||
this.dataFilePath = dataFilePath;
|
||||
this.versionRule = versionRule == null ? '1.0.x' : versionRule;
|
||||
|
||||
this.entries = new Map<String, T>();
|
||||
this.scriptedEntryIds = [];
|
||||
|
||||
// Lazy initialization of singletons should let this get called,
|
||||
// but we have this check just in case.
|
||||
if (FlxG.game != null)
|
||||
{
|
||||
FlxG.console.registerObject('registry$registryId', this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Create a `loadEntriesAsync(onProgress, onComplete)` function.
|
||||
*/
|
||||
public function loadEntries():Void
|
||||
{
|
||||
clearEntries();
|
||||
|
||||
//
|
||||
// SCRIPTED ENTRIES
|
||||
//
|
||||
var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
|
||||
log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
|
||||
|
||||
for (entryCls in scriptedEntryClassNames)
|
||||
{
|
||||
var entry:Null<T> = null;
|
||||
try
|
||||
{
|
||||
entry = createScriptedEntry(entryCls);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
log('Failed to create scripted entry (${entryCls})');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
log('Successfully created scripted entry (${entryCls} = ${entry.id})');
|
||||
entries.set(entry.id, entry);
|
||||
scriptedEntryIds.set(entry.id, entryCls);
|
||||
}
|
||||
else
|
||||
{
|
||||
log('Failed to create scripted entry (${entryCls})');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UNSCRIPTED ENTRIES
|
||||
//
|
||||
var entryIdList:Array<String> = DataAssets.listDataFilesInPath('${dataFilePath}/');
|
||||
var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
|
||||
return !entries.exists(entryId);
|
||||
});
|
||||
log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
|
||||
for (entryId in unscriptedEntryIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entry:T = createEntry(entryId);
|
||||
if (entry != null)
|
||||
{
|
||||
trace(' Loaded entry data: ${entry}');
|
||||
entries.set(entry.id, entry);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// Print the error.
|
||||
trace(' Failed to load entry data: ${entryId}');
|
||||
trace(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of all entry IDs in this registry.
|
||||
* @return The list of entry IDs.
|
||||
*/
|
||||
public function listEntryIds():Array<String>
|
||||
{
|
||||
return entries.keys().array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of entries in this registry.
|
||||
* @return The number of entries.
|
||||
*/
|
||||
public function countEntries():Int
|
||||
{
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the entry ID is known to have an attached script.
|
||||
* @param id The ID of the entry.
|
||||
* @return `true` if the entry has an attached script, `false` otherwise.
|
||||
*/
|
||||
public function isScriptedEntry(id:String):Bool
|
||||
{
|
||||
return scriptedEntryIds.exists(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the class name of the scripted entry with the given ID, if it exists.
|
||||
* @param id The ID of the entry.
|
||||
* @return The class name, or `null` if it does not exist.
|
||||
*/
|
||||
public function getScriptedEntryClassName(id:String):String
|
||||
{
|
||||
return scriptedEntryIds.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the registry has successfully parsed an entry with the given ID.
|
||||
* @param id The ID of the entry.
|
||||
* @return `true` if the entry exists, `false` otherwise.
|
||||
*/
|
||||
public function hasEntry(id:String):Bool
|
||||
{
|
||||
return entries.exists(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an entry by its ID.
|
||||
* @param id The ID of the entry to fetch.
|
||||
* @return The entry, or `null` if it does not exist.
|
||||
*/
|
||||
public function fetchEntry(id:String):Null<T>
|
||||
{
|
||||
return entries.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all entries included in the base game.
|
||||
* The actual function exists and is auto-generated on each registry at build time.
|
||||
* @return Array<String>
|
||||
*/
|
||||
// public function listBaseGameEntryIds():Array<String> {}
|
||||
|
||||
/**
|
||||
* A list of all entries that are not included in the base game.
|
||||
* @return Array<String>
|
||||
*/
|
||||
// public function listModdedEntryIds():Array<String> {}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Registry(' + registryId + ', ${countEntries()} entries)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the data for an entry and parse its Semantic Version.
|
||||
* @param id The ID of the entry.
|
||||
* @return The entry's version, or `null` if it does not exist or is invalid.
|
||||
*/
|
||||
public function fetchEntryVersion(id:String):Null<thx.semver.Version>
|
||||
{
|
||||
var entryStr:String = loadEntryFile(id).contents;
|
||||
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
|
||||
return entryVersion;
|
||||
}
|
||||
|
||||
function log(message:String):Void
|
||||
{
|
||||
trace('[' + registryId + '] ' + message);
|
||||
}
|
||||
|
||||
function loadEntryFile(id:String):JsonFile
|
||||
{
|
||||
var entryFilePath:String = Paths.json('${dataFilePath}/${id}');
|
||||
var rawJson:String = openfl.Assets.getText(entryFilePath).trim();
|
||||
return {
|
||||
fileName: entryFilePath,
|
||||
contents: rawJson
|
||||
};
|
||||
}
|
||||
|
||||
function clearEntries():Void
|
||||
{
|
||||
for (entry in entries)
|
||||
{
|
||||
entry.destroy();
|
||||
}
|
||||
|
||||
entries.clear();
|
||||
}
|
||||
|
||||
//
|
||||
// FUNCTIONS TO IMPLEMENT
|
||||
//
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||
*
|
||||
* NOTE: Must be implemented on the implementation class.
|
||||
* @param id The ID of the entry.
|
||||
* @return The created entry.
|
||||
*/
|
||||
public abstract function parseEntryData(id:String):Null<J>;
|
||||
|
||||
/**
|
||||
* Parse and validate the JSON data and produce the corresponding data object.
|
||||
*
|
||||
* NOTE: Must be implemented on the implementation class.
|
||||
* @param contents The JSON as a string.
|
||||
* @param fileName An optional file name for error reporting.
|
||||
* @return The created entry.
|
||||
*/
|
||||
public abstract function parseEntryDataRaw(contents:String, ?fileName:String):Null<J>;
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object,
|
||||
* accounting for old versions of the data.
|
||||
*
|
||||
* NOTE: Extend this function to handle migration.
|
||||
* @param id The ID of the entry.
|
||||
* @param version The entry's version (use `fetchEntryVersion(id)`).
|
||||
* @return The created entry.
|
||||
*/
|
||||
public function parseEntryDataWithMigration(id:String, version:Null<thx.semver.Version>):Null<J>
|
||||
{
|
||||
if (version == null)
|
||||
{
|
||||
throw '[${registryId}] Entry ${id} could not be JSON-parsed or does not have a parseable version.';
|
||||
}
|
||||
|
||||
// If a version rule is not specified, do not check against it.
|
||||
if (versionRule == null || VersionUtil.validateVersion(version, versionRule))
|
||||
{
|
||||
return parseEntryData(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw '[${registryId}] Entry ${id} does not support migration to version ${versionRule}.';
|
||||
}
|
||||
|
||||
/*
|
||||
* An example of what you should override this with:
|
||||
*
|
||||
* ```haxe
|
||||
* if (VersionUtil.validateVersion(version, "0.1.x")) {
|
||||
* return parseEntryData_v0_1_x(id);
|
||||
* } else {
|
||||
* super.parseEntryDataWithMigration(id, version);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of scripted class names to load.
|
||||
* @return An array of scripted class names.
|
||||
*/
|
||||
abstract function getScriptedClassNames():Array<String>;
|
||||
|
||||
/**
|
||||
* Create an entry from the given ID.
|
||||
* @param id
|
||||
*/
|
||||
function createEntry(id:String):Null<T>
|
||||
{
|
||||
// We enforce that T is Constructible to ensure this is valid.
|
||||
return new T(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a entry, attached to a scripted class, from the given class name.
|
||||
* @param clsName
|
||||
*/
|
||||
abstract function createScriptedEntry(clsName:String):Null<T>;
|
||||
|
||||
function printErrors(errors:Array<json2object.Error>, id:String = ''):Void
|
||||
{
|
||||
trace('[${registryId}] Failed to parse entry data: ${id}');
|
||||
|
||||
for (error in errors)
|
||||
{
|
||||
DataError.printError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package funkin.data;
|
||||
|
||||
import json2object.Position;
|
||||
import json2object.Position.Line;
|
||||
import json2object.Error;
|
||||
|
||||
class DataError
|
||||
{
|
||||
public static function printError(error:Error):Void
|
||||
{
|
||||
switch (error)
|
||||
{
|
||||
case IncorrectType(vari, expected, pos):
|
||||
trace(' Expected field "$vari" to be of type "$expected".');
|
||||
printPos(pos);
|
||||
case IncorrectEnumValue(value, expected, pos):
|
||||
trace(' Invalid enum value (expected "$expected", got "$value")');
|
||||
printPos(pos);
|
||||
case InvalidEnumConstructor(value, expected, pos):
|
||||
trace(' Invalid enum constructor (epxected "$expected", got "$value")');
|
||||
printPos(pos);
|
||||
case UninitializedVariable(vari, pos):
|
||||
trace(' Uninitialized variable "$vari"');
|
||||
printPos(pos);
|
||||
case UnknownVariable(vari, pos):
|
||||
trace(' Unknown variable "$vari"');
|
||||
printPos(pos);
|
||||
case ParserError(message, pos):
|
||||
trace(' Parsing error: ${message}');
|
||||
printPos(pos);
|
||||
case CustomFunctionException(e, pos):
|
||||
if (Std.isOfType(e, String))
|
||||
{
|
||||
trace(' ${e}');
|
||||
}
|
||||
else
|
||||
{
|
||||
printUnknownError(e);
|
||||
}
|
||||
printPos(pos);
|
||||
default:
|
||||
printUnknownError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public static function printUnknownError(e:Dynamic):Void
|
||||
{
|
||||
switch (Type.typeof(e))
|
||||
{
|
||||
case TClass(c):
|
||||
trace(' [${Type.getClassName(c)}] ${e.toString()}');
|
||||
case TEnum(c):
|
||||
trace(' [${Type.getEnumName(c)}] ${e.toString()}');
|
||||
default:
|
||||
trace(' [${Type.typeof(e)}] ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Figure out the nicest way to print this.
|
||||
* Maybe look up how other JSON parsers format their errors?
|
||||
* @see https://github.com/elnabo/json2object/blob/master/src/json2object/Position.hx
|
||||
*/
|
||||
static function printPos(pos:Position):Void
|
||||
{
|
||||
if (pos.lines[0].number == pos.lines[pos.lines.length - 1].number)
|
||||
{
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package funkin.data;
|
||||
|
||||
import funkin.data.song.importer.FNFLegacyData.LegacyNote;
|
||||
import funkin.data.song.importer.FNFLegacyData.LegacyNoteData;
|
||||
import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection;
|
||||
import funkin.data.song.importer.FNFLegacyData.LegacyScrollSpeeds;
|
||||
import haxe.ds.Either;
|
||||
import hxjsonast.Json;
|
||||
import hxjsonast.Json.JObjectField;
|
||||
import hxjsonast.Tools;
|
||||
import thx.semver.Version;
|
||||
import thx.semver.VersionRule;
|
||||
|
||||
/**
|
||||
* `json2object` has an annotation `@:jcustomparse` which allows for mutation of parsed values.
|
||||
*
|
||||
* It also allows for validation, since throwing an error in this function will cause the issue to be properly caught.
|
||||
* Parsing will fail and `parser.errors` will contain the thrown exception.
|
||||
*
|
||||
* Functions must be of the signature `(hxjsonast.Json, String) -> T`, where the String is the property name and `T` is the type of the property.
|
||||
*/
|
||||
class DataParse
|
||||
{
|
||||
/**
|
||||
* `@:jcustomparse(funkin.data.DataParse.stringNotEmpty)`
|
||||
* @param json Contains the `pos` and `value` of the property.
|
||||
* @param name The name of the property.
|
||||
* @throws Error If the property is not a string or is empty.
|
||||
* @return The string value.
|
||||
*/
|
||||
public static function stringNotEmpty(json:Json, name:String):String
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JString(s):
|
||||
if (s == "") throw 'Expected property $name to be non-empty.';
|
||||
return s;
|
||||
default:
|
||||
throw 'Expected property $name to be a string, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `@:jcustomparse(funkin.data.DataParse.semverVersion)`
|
||||
* @param json Contains the `pos` and `value` of the property.
|
||||
* @param name The name of the property.
|
||||
* @return The value of the property as a `thx.semver.Version`.
|
||||
*/
|
||||
public static function semverVersion(json:Json, name:String):Version
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JString(s):
|
||||
if (s == "") throw 'Expected version property $name to be non-empty.';
|
||||
return s;
|
||||
default:
|
||||
throw 'Expected version property $name to be a string, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `@:jcustomparse(funkin.data.DataParse.semverVersionRule)`
|
||||
* @param json Contains the `pos` and `value` of the property.
|
||||
* @param name The name of the property.
|
||||
* @return The value of the property as a `thx.semver.VersionRule`.
|
||||
*/
|
||||
public static function semverVersionRule(json:Json, name:String):VersionRule
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JString(s):
|
||||
if (s == "") throw 'Expected version rule property $name to be non-empty.';
|
||||
return s;
|
||||
default:
|
||||
throw 'Expected version rule property $name to be a string, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser which outputs a Dynamic value, either a object or something else.
|
||||
* @param json
|
||||
* @param name
|
||||
* @return The value of the property.
|
||||
*/
|
||||
public static function dynamicValue(json:Json, name:String):Dynamic
|
||||
{
|
||||
return Tools.getValue(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser which outputs a `Either<Array<LegacyNoteSection>, LegacyNoteData>`.
|
||||
* Used by the FNF legacy JSON importer.
|
||||
*/
|
||||
public static function eitherLegacyNoteData(json:Json, name:String):Either<Array<LegacyNoteSection>, LegacyNoteData>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JArray(values):
|
||||
return Either.Left(legacyNoteSectionArray(json, name));
|
||||
case JObject(fields):
|
||||
return Either.Right(cast Tools.getValue(json));
|
||||
default:
|
||||
throw 'Expected property $name to be note data, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser which outputs a `Either<Float, Array<Float>>`.
|
||||
*/
|
||||
public static function eitherFloatOrFloats(json:Json, name:String):Null<Either<Float, Array<Float>>>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JNumber(f):
|
||||
return Either.Left(Std.parseFloat(f));
|
||||
case JArray(fields):
|
||||
return Either.Right(fields.map((field) -> cast Tools.getValue(field)));
|
||||
default:
|
||||
throw 'Expected property $name to be one or multiple floats, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
public static function backdropData(json:Json, name:String):funkin.data.dialogue.conversation.ConversationData.BackdropData
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
var result:Dynamic = {};
|
||||
var backdropType:String = '';
|
||||
|
||||
for (field in fields)
|
||||
{
|
||||
switch (field.name)
|
||||
{
|
||||
case 'type':
|
||||
backdropType = Tools.getValue(field.value);
|
||||
}
|
||||
Reflect.setField(result, field.name, Tools.getValue(field.value));
|
||||
}
|
||||
|
||||
switch (backdropType)
|
||||
{
|
||||
case 'solid':
|
||||
return SOLID(result);
|
||||
default:
|
||||
throw 'Expected Backdrop property $name to be specify a valid "type", but it was "${backdropType}".';
|
||||
}
|
||||
|
||||
return null;
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
public static function outroData(json:Json, name:String):Null<funkin.data.dialogue.conversation.ConversationData.OutroData>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
var result:Dynamic = {};
|
||||
var outroType:String = '';
|
||||
|
||||
for (field in fields)
|
||||
{
|
||||
switch (field.name)
|
||||
{
|
||||
case 'type':
|
||||
outroType = Tools.getValue(field.value);
|
||||
}
|
||||
Reflect.setField(result, field.name, Tools.getValue(field.value));
|
||||
}
|
||||
|
||||
switch (outroType)
|
||||
{
|
||||
case 'none':
|
||||
return NONE(result);
|
||||
case 'fade':
|
||||
return FADE(result);
|
||||
default:
|
||||
throw 'Expected Outro property $name to be specify a valid "type", but it was "${outroType}".';
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser which outputs a `Either<Float, LegacyScrollSpeeds>`.
|
||||
* Used by the FNF legacy JSON importer.
|
||||
*/
|
||||
public static function eitherLegacyScrollSpeeds(json:Json, name:String):Either<Float, LegacyScrollSpeeds>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JNumber(f):
|
||||
return Either.Left(Std.parseFloat(f));
|
||||
case JObject(fields):
|
||||
return Either.Right(cast Tools.getValue(json));
|
||||
default:
|
||||
throw 'Expected property $name to be scroll speeds, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of JSON fields `[{key, value}, {key, value}]` to a Dynamic object `{key:value, key:value}`.
|
||||
* @param fields
|
||||
* @return Dynamic
|
||||
*/
|
||||
static function jsonFieldsToDynamicObject(fields:Array<JObjectField>):Dynamic
|
||||
{
|
||||
var result:Dynamic = {};
|
||||
for (field in fields)
|
||||
{
|
||||
Reflect.setField(result, field.name, Tools.getValue(field.value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of JSON elements `[Json, Json, Json]` to a Dynamic array `[String, Object, Int, Array]`
|
||||
* @param jsons
|
||||
* @return Array<Dynamic>
|
||||
*/
|
||||
static function jsonArrayToDynamicArray(jsons:Array<Json>):Array<Null<Dynamic>>
|
||||
{
|
||||
return [for (json in jsons) Tools.getValue(json)];
|
||||
}
|
||||
|
||||
static function legacyNoteSectionArray(json:Json, name:String):Array<LegacyNoteSection>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JArray(values):
|
||||
return [for (value in values) legacyNoteSection(value, name)];
|
||||
default:
|
||||
throw 'Expected property to be an array, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
static function legacyNoteSection(json:Json, name:String):LegacyNoteSection
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
var result:LegacyNoteSection =
|
||||
{
|
||||
mustHitSection: false,
|
||||
sectionNotes: [],
|
||||
};
|
||||
for (field in fields)
|
||||
{
|
||||
switch (field.name)
|
||||
{
|
||||
case 'sectionNotes':
|
||||
result.sectionNotes = legacyNotes(field.value, field.name);
|
||||
|
||||
case 'mustHitSection':
|
||||
result.mustHitSection = Tools.getValue(field.value);
|
||||
case 'typeOfSection':
|
||||
result.typeOfSection = Tools.getValue(field.value);
|
||||
case 'lengthInSteps':
|
||||
result.lengthInSteps = Tools.getValue(field.value);
|
||||
case 'changeBPM':
|
||||
result.changeBPM = Tools.getValue(field.value);
|
||||
case 'bpm':
|
||||
result.bpm = Tools.getValue(field.value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
public static function legacyNoteData(json:Json, name:String):LegacyNoteData
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
var result = {};
|
||||
for (field in fields)
|
||||
{
|
||||
Reflect.setField(result, field.name, legacyNoteSectionArray(field.value, field.name));
|
||||
}
|
||||
return result;
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
public static function legacyNotes(json:Json, name:String):Array<LegacyNote>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JArray(values):
|
||||
return [for (value in values) legacyNote(value, name)];
|
||||
default:
|
||||
throw 'Expected property $name to be an array of notes, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
public static function legacyNote(json:Json, name:String):LegacyNote
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JArray(values):
|
||||
var time:Null<Float> = values[0] == null ? null : Tools.getValue(values[0]);
|
||||
var data:Null<Int> = values[1] == null ? null : Tools.getValue(values[1]);
|
||||
var length:Null<Float> = values[2] == null ? null : Tools.getValue(values[2]);
|
||||
var alt:Null<Bool> = values[3] == null ? null : Tools.getValue(values[3]);
|
||||
|
||||
return new LegacyNote(time, data, length, alt);
|
||||
// return null;
|
||||
default:
|
||||
throw 'Expected property $name to be a note, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package funkin.data;
|
||||
|
||||
import funkin.util.SerializerUtil;
|
||||
import thx.semver.Version;
|
||||
import thx.semver.VersionRule;
|
||||
import haxe.ds.Either;
|
||||
|
||||
/**
|
||||
* `json2object` has an annotation `@:jcustomwrite` which allows for custom serialization of values to be written to JSON.
|
||||
*
|
||||
* Functions must be of the signature `(T) -> String`, where `T` is the type of the property.
|
||||
*
|
||||
* NOTE: Result must include quotation marks if the value is a string! json2object will not add them for you!
|
||||
*/
|
||||
class DataWrite
|
||||
{
|
||||
/**
|
||||
* `@:jcustomwrite(funkin.data.DataWrite.dynamicValue)`
|
||||
* @param value
|
||||
* @return String
|
||||
*/
|
||||
public static function dynamicValue(value:Dynamic):String
|
||||
{
|
||||
// Is this cheating? Yes. Do I care? No.
|
||||
return SerializerUtil.toJSON(value);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* `@:jcustomwrite(funkin.data.DataWrite.semverVersion)`
|
||||
*/
|
||||
public static function semverVersion(value:Version):String
|
||||
{
|
||||
return '"${value.toString()}"';
|
||||
}
|
||||
|
||||
/**
|
||||
* `@:jcustomwrite(funkin.data.DataWrite.semverVersionRule)`
|
||||
*/
|
||||
public static function semverVersionRule(value:VersionRule):String
|
||||
{
|
||||
return '"${value.toString()}"';
|
||||
}
|
||||
|
||||
/**
|
||||
* `@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)`
|
||||
*/
|
||||
public static function eitherFloatOrFloats(value:Null<Either<Float, Array<Float>>>):String
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case null:
|
||||
return '${1.0}';
|
||||
case Left(inner):
|
||||
return '$inner';
|
||||
case Right(inner):
|
||||
return dynamicValue(inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package funkin.data;
|
||||
|
||||
/**
|
||||
* An interface which automatically implements essential fields for a class extending `BaseRegistry`.
|
||||
*
|
||||
* @see `funkin.data.BaseRegistry`
|
||||
*/
|
||||
@:autoBuild(funkin.util.macro.RegistryMacro.buildRegistry())
|
||||
interface DefaultRegistryImpl {}
|
||||
@@ -0,0 +1,21 @@
|
||||
package funkin.data;
|
||||
|
||||
/**
|
||||
* An interface defining the necessary functions for a registry entry.
|
||||
* A `String->Void` constructor is also mandatory, but enforced elsewhere.
|
||||
* @param T The JSON data type of the registry entry.
|
||||
*/
|
||||
@:autoBuild(funkin.util.macro.RegistryMacro.buildEntry())
|
||||
interface IRegistryEntry<T>
|
||||
{
|
||||
public final id:String;
|
||||
|
||||
// public function new(id:String):Void;
|
||||
public function destroy():Void;
|
||||
public function toString():String;
|
||||
|
||||
// Can't make an interface field private I guess.
|
||||
public final _data:T;
|
||||
// Can't make a static field required by an interface I guess.
|
||||
// private static function _fetchData(id:String):Null<T>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package funkin.data;
|
||||
|
||||
/**
|
||||
* A pair of a file name and its contents.
|
||||
*/
|
||||
typedef JsonFile =
|
||||
{
|
||||
fileName:String,
|
||||
contents:String
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
# funkin.data
|
||||
|
||||
Data structures are parsed using `json2object`, which uses macros to generate parser classes based on anonymous structures OR classes.
|
||||
|
||||
Parsing errors will be returned in `parser.errors`. See `json2object.Error` for an enumeration of possible parsing errors. If an error occurred, `parser.value` will be null.
|
||||
|
||||
The properties of these anonymous structures can have their behavior changed with annotations:
|
||||
|
||||
- `@:optional`: The value is optional and will not throw a parsing error if it is not present in the JSON data.
|
||||
- `@:default("test")`: If the value is optional, this value will be used instead of `null`. Replace `"test"` with a value of the property's type.
|
||||
- `@:default(auto)`: If the value is an anonymous structure with `json2object` annotations, each field will be initialized to its default value.
|
||||
- `@:jignored`: This value will be ignored by the parser. Their presence will not be checked in the JSON data and their values will not be parsed.
|
||||
- `@:alias`: Choose the name the value will use in the JSON data to be separate from the property name. Useful if the desired name is a reserved word like `public`.
|
||||
- `@:jcustomparse`: Provide a custom function for parsing from a JSON string into a value.
|
||||
- Functions must be of the signature `(hxjsonast.Json, String) -> T`, where the String is the property name and `T` is the type of the property.
|
||||
- `hxjsonast.Json` contains a `pos` and a `value`, with `value` being an enum: https://nadako.github.io/hxjsonast/hxjsonast/JsonValue.html
|
||||
- Errors thrown in this function will cause a parsing error (`CustomFunctionException`) along with a position!
|
||||
- Make sure to provide the FULLY QUALIFIED path to the custom function.
|
||||
- `@:jcustomwrite`: Provide a custom function for serializing the property into a string for storage as JSON.
|
||||
- Functions must be of the signature `(T) -> String`, where `T` is the type of the property.
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package funkin.data.animation;
|
||||
|
||||
class AnimationDataUtil
|
||||
{
|
||||
public static function toNamed(data:UnnamedAnimationData, ?name:String = ""):AnimationData
|
||||
{
|
||||
return {
|
||||
name: name,
|
||||
prefix: data.prefix,
|
||||
assetPath: data.assetPath,
|
||||
offsets: data.offsets,
|
||||
looped: data.looped,
|
||||
flipX: data.flipX,
|
||||
flipY: data.flipY,
|
||||
frameRate: data.frameRate,
|
||||
frameIndices: data.frameIndices
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data
|
||||
* @param name (adds index to name)
|
||||
* @return Array<AnimationData>
|
||||
*/
|
||||
public static function toNamedArray(data:Array<UnnamedAnimationData>, ?name:String = ""):Array<AnimationData>
|
||||
{
|
||||
return data.mapi(function(animItem, ind) return toNamed(animItem, '$name$ind'));
|
||||
}
|
||||
|
||||
public static function toUnnamed(data:AnimationData):UnnamedAnimationData
|
||||
{
|
||||
return {
|
||||
prefix: data.prefix,
|
||||
assetPath: data.assetPath,
|
||||
offsets: data.offsets,
|
||||
looped: data.looped,
|
||||
flipX: data.flipX,
|
||||
flipY: data.flipY,
|
||||
frameRate: data.frameRate,
|
||||
frameIndices: data.frameIndices
|
||||
};
|
||||
}
|
||||
|
||||
public static function toUnnamedArray(data:Array<AnimationData>):Array<UnnamedAnimationData>
|
||||
{
|
||||
return data.map(toUnnamed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data structure representing an animation in a spritesheet.
|
||||
* This is a generic data structure used by characters, stage props, and more!
|
||||
* BE CAREFUL when changing it.
|
||||
*/
|
||||
typedef AnimationData =
|
||||
{
|
||||
> UnnamedAnimationData,
|
||||
|
||||
/**
|
||||
* The name for the animation.
|
||||
* This should match the animation name queried by the game;
|
||||
* for example, characters need animations with names `idle`, `singDOWN`, `singUPmiss`, etc.
|
||||
*/
|
||||
var name:String;
|
||||
}
|
||||
|
||||
/**
|
||||
* A data structure representing an animation in a spritesheet.
|
||||
* This animation doesn't specify a name, that's presumably specified by the parent data structure.
|
||||
*/
|
||||
typedef UnnamedAnimationData =
|
||||
{
|
||||
/**
|
||||
* The prefix for the frames of the animation as defined by the XML file.
|
||||
* This will may or may not differ from the `name` of the animation,
|
||||
* depending on how your animator organized their FLA or whatever.
|
||||
*
|
||||
* NOTE: For Sparrow animations, this is not optional, but for Packer animations it is.
|
||||
*/
|
||||
@:optional
|
||||
var prefix:String;
|
||||
|
||||
/**
|
||||
* Optionally specify an asset path to use for this specific animation.
|
||||
* ONLY for use by MultiSparrow characters.
|
||||
* @default The assetPath of the parent sprite
|
||||
*/
|
||||
@:optional
|
||||
var assetPath:Null<String>;
|
||||
|
||||
/**
|
||||
* Offset the character's position by this amount when playing this animation.
|
||||
* @default [0, 0]
|
||||
*/
|
||||
@:default([0, 0])
|
||||
@:optional
|
||||
var offsets:Null<Array<Float>>;
|
||||
|
||||
/**
|
||||
* Whether the animation should loop when it finishes.
|
||||
* @default false
|
||||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var looped:Bool;
|
||||
|
||||
/**
|
||||
* Whether the animation's sprites should be flipped horizontally.
|
||||
* @default false
|
||||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var flipX:Null<Bool>;
|
||||
|
||||
/**
|
||||
* Whether the animation's sprites should be flipped vertically.
|
||||
* @default false
|
||||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var flipY:Null<Bool>;
|
||||
|
||||
/**
|
||||
* The frame rate of the animation.
|
||||
* @default 24
|
||||
*/
|
||||
@:default(24)
|
||||
@:optional
|
||||
var frameRate:Null<Int>;
|
||||
|
||||
/**
|
||||
* If you want this animation to use only certain frames of an animation with a given prefix,
|
||||
* select them here.
|
||||
* @example [0, 1, 2, 3] (use only the first four frames)
|
||||
* @default [] (all frames)
|
||||
*/
|
||||
@:default([])
|
||||
@:optional
|
||||
var frameIndices:Null<Array<Int>>;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user