1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-09-04 04:38:09 +00:00

Compare commits

..

No commits in common. "a0bf5dc1a5639869c39c18c419c699daa00c5ad3" and "a1cf8d218d5aec536fd6d3896e4a807738c49542" have entirely different histories.

351 changed files with 7302 additions and 20695 deletions

View file

@ -20,13 +20,11 @@ body:
label: Platform
description: Which platform are you playing on?
options:
- Android
- iOS/iPadOS
- Newgrounds (Web/HTML5)
- Itch.io (Web/HTML5)
- Windows (Downloadable Build)
- MacOS (Downloadable Build)
- Linux (Downloadable Build)
- Itch.io (Downloadable Build) - Windows
- Itch.io (Downloadable Build) - MacOS
- Itch.io (Downloadable Build) - Linux
- Compiled from GitHub Source Code
validations:
required: true
@ -43,23 +41,11 @@ body:
- Safari
- Other (Specify in Description field)
- type: input
attributes:
label: Mobile Device Model
description: (Mobile users only) What mobile device are you playing on?
placeholder: ex. iPhone 16, Galaxy S25, iPad 11th Gen
- type: input
attributes:
label: Mobile OS Version
description: (Mobile users only) What version is your Operating System?
placeholder: ex. iOS 18.5, Android 15, iPadOS 18.5
- 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.7.4
placeholder: ex. 0.6.4
validations:
required: true

View file

@ -6,7 +6,7 @@ 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
@ -21,25 +21,35 @@ body:
label: Platform
description: Which platform are you playing on?
options:
- Android
- iOS/iPadOS
- Newgrounds (Web/HTML5)
- Itch.io (Web/HTML5)
- Windows (Downloadable Build)
- MacOS (Downloadable Build)
- Linux (Downloadable Build)
- 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.7.4
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)."

View file

@ -6,7 +6,7 @@ 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
@ -22,8 +22,6 @@ body:
label: Platform
description: Which platform are you compiling for?
options:
- Android
- iOS/iPadOS
- Web/HTML5
- Desktop (Windows)
- Desktop (Mac)
@ -35,11 +33,11 @@ body:
- 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.7.4
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."

View file

@ -6,7 +6,7 @@ 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
@ -21,13 +21,11 @@ body:
label: Platform
description: Which platform are you playing on?
options:
- Android
- iOS/iPadOS
- Newgrounds (Web/HTML5)
- Itch.io (Web/HTML5)
- Windows (Downloadable Build)
- MacOS (Downloadable Build)
- Linux (Downloadable Build)
- Itch.io (Downloadable Build) - Windows
- Itch.io (Downloadable Build) - MacOS
- Itch.io (Downloadable Build) - Linux
- Compiled from GitHub Source Code
validations:
required: true
@ -44,23 +42,11 @@ body:
- Safari
- Other (Specify in Description field)
- type: input
attributes:
label: Mobile Device Model
description: (Mobile users only) What mobile device are you playing on?
placeholder: ex. iPhone 16, Galaxy S25, iPad 11th Gen
- type: input
attributes:
label: Mobile OS Version
description: (Mobile users only) What version is your Operating System?
placeholder: ex. iOS 18.5, Android 15, iPadOS 18.5
- 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.7.4
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
@ -75,7 +61,7 @@ body:
- type: textarea
attributes:
label: Description
description: Include screenshots or videos of the crash happening. Provide as much detail as you can.
description: Include screenshots or videos of the crash happening. Provide as much detail as you can.
placeholder: Describe the crash here...
validations:
required: true
@ -91,7 +77,7 @@ body:
- type: textarea
attributes:
label: Crash logs
description: These can be found in the logs folder where Funkin.exe is, or in your mobile device's file explorer.
description: These can be found in the logs folder where Funkin.exe is.
placeholder: Upload your logs here...
validations:
required: true

View file

@ -1,6 +1,3 @@
# Builds the game on all platforms, to ensure it compiles on all target platforms.
# This helps to ensure workers focus on the master branch.
name: Build and Upload nightly game builds
on:
@ -24,7 +21,7 @@ jobs:
trigger-build: ${{ steps.should-trigger.outputs.result }}
steps:
- name: Checkout repo
uses: funkincrew/ci-checkout@v7.3.3
uses: funkincrew/ci-checkout@v6
with:
submodules: false
- uses: dorny/paths-filter@v3
@ -96,7 +93,7 @@ jobs:
packages: write
steps:
- name: Checkout repo
uses: funkincrew/ci-checkout@v7.3.3
uses: funkincrew/ci-checkout@v6
with:
submodules: false
- name: Log into GitHub Container Registry
@ -128,9 +125,6 @@ jobs:
runs-on: windows
- target: macos
runs-on: macos
# TODO: Install XCode to build iOS
# - target: ios
# runs-on: macos
runs-on:
- ${{ matrix.runs-on }}
defaults:
@ -150,14 +144,11 @@ jobs:
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v7.3.3
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
persist-credentials: false
submodule-aliases: |
https://github.com/FunkinCrew/Funkin.assets > https://github.com/FunkinCrew/Funkin-assets-secret
https://github.com/FunkinCrew/Funkin.art > https://github.com/FunkinCrew/Funkin-art-secret
- name: Setup build environment
uses: ./.github/actions/setup-haxe
with:
@ -199,7 +190,6 @@ jobs:
include:
- target: linux
- target: html5
# - target: android
defaults:
run:
shell: bash
@ -214,14 +204,11 @@ jobs:
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: Checkout repo
uses: funkincrew/ci-checkout@v7.3.3
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
persist-credentials: false
submodule-aliases: |
https://github.com/FunkinCrew/Funkin.assets > https://github.com/FunkinCrew/Funkin-assets-secret
https://github.com/FunkinCrew/Funkin.art > https://github.com/FunkinCrew/Funkin-art-secret
- name: Config haxelib
run: |
haxelib --never newrepo

View file

@ -1,6 +1,3 @@
# When a pull request is merged, cancel all queued workflows for that branch
# This helps to ensure workers focus on the master branch.
name: Cancel queued workflows on PR merge
on:
@ -26,7 +23,7 @@ jobs:
let branch_workflows = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: "build-game.yml",
workflow_id: "build-shit.yml",
status: "queued",
branch: "${{ github.event.pull_request.head.ref }}"
});

View file

@ -1,6 +1,5 @@
# Perform actions when labels are applied to issues, discussions, or pull requests
# See .github/label-actions.yml
name: 'Label Actions'
on:

View file

@ -1,13 +1,9 @@
# Applies the following labels to pull requests when created:
# - status: pending triage
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:

View file

@ -1,19 +1,7 @@
# Applies the following labels to pull requests whenver they are created or modified:
# - pr: documentation
# - pr: haxe
# - pr: github
# - size: tiny
# - size: small
# - size: medium
# - size: large
# - size: huge
# see .github/labeler.yml and .github/changed-lines-count-labeler.yml
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
# Apply labels to pull requests based on which files were edited
labeler:

9
.gitignore vendored
View file

@ -3,21 +3,14 @@
.vs/
APIStuff.hx
dump/
temp/
docs/temp/
export/
astc-textures/
project/
RECOVER_*.fla
shitAudio/
.build_time
.swp
NewgroundsCredentials.hx
# Mobile signing stuff
.apple/
.env
key.keystore
# Exclude JS stuff
node_modules/
package.json

View file

@ -1,66 +0,0 @@
{
"type": "object",
"properties": {
"dependencies": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"title": "Name",
"type": "string",
"description": "The name of the Haxe library"
},
"type": {
"title": "Type",
"type": "string",
"description": "one of haxelib, git, hg, or dev",
"enum": ["haxelib", "git", "hg", "dev"]
},
"ref": {
"type": "string",
"description": "the git/hg ref (branch, commit, tag, etc.)"
},
"url": {
"title": "URL",
"type": "string",
"description": "the git/hg URL"
},
"version": {
"title": "Version",
"type": "string",
"description": "the haxelib library version. Must be in SemVer format (MAJOR.MINOR.PATCH)",
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
}
},
"required": ["name", "type"],
"dependentSchemas": {
"type": {
"if": {
"properties": {
"type": {
"const": "git"
}
}
},
"then": {
"required": ["url"]
},
"else": {
"if": {
"properties": {
"type": {
"const": "haxelib"
}
}
},
"then": {
"required": ["version"]
}
}
}
}
}
}
}
}

25
.vscode/settings.json vendored
View file

@ -90,8 +90,6 @@
"haxecheckstyle.configurationFile": "checkstyle.json",
"haxecheckstyle.codeSimilarityBufferSize": 100,
"lime.projectFile": "project.hxp",
"lime.targetConfigurations": [
{
"label": "Windows / Debug (Discord)",
@ -152,16 +150,6 @@
"target": "windows",
"args": ["-debug", "-DRESULTS"]
},
{
"label": "Windows / Debug (Straight to Stage Editor)",
"target": "windows",
"args": ["-debug", "-DSTAGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Debug (Straight to Stage Builder)",
"target": "windows",
"args": ["-debug", "-DSTAGEBUILD", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",
@ -170,7 +158,7 @@
{
"label": "Windows / Debug (Debug hxvlc)",
"target": "windows",
"args": ["-debug", "-DHXVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
"args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
@ -213,10 +201,6 @@
"label": "Debug",
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Debug (Unlock Everything)",
"args": ["-debug", "-DUNLOCK_EVERYTHING"]
},
{
"label": "Debug (Tracy)",
"args": ["-debug", "-DFEATURE_DEBUG_TRACY", "-DFEATURE_DEBUG_FUNCTIONS"]
@ -241,12 +225,5 @@
"coverage.xml",
"jacoco.xml",
"coverage.cobertura.xml"
],
"vscord.app.privacyMode.enable": true,
"json.schemas": [
{
"fileMatch": ["/hmm.json"],
"url": "./.vscode/schema/hmm.json"
}
]
}

View file

@ -4,303 +4,6 @@ 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.7.3] - 2025-07-21
### Fixed
- Fixed stuttering throughout the game caused by the Polymod upgrade. (Thanks NotHyper-474!)
- [MOBILE] Fixed buttons in the Main Menu not working.
- [iOS] The Upgrade button no longer appears if you have already purchased it (actually this time).
- Fixed the countdown overlapping itself when restarting the song. (Thanks NotHyper-474!)
- Optimized the Week 6 Erect stage.
- Fixed an oversight when clearing the cache. (Thanks cherrythecool!)
- The Input Offset Test menu text now displays in the correct position.
- Fixed script errors appearing in the Week 3 Erect stage.
- Fixed adding variations in the Chart Editor erasing difficulties. (Thanks NotHyper-474!)
## New Contributors for 0.7.3
* @cherrythecool made their first contribution in [#5458](https://github.com/FunkinCrew/Funkin/pull/5458)
## [0.7.2] - 2025-07-18
### Added
- [ANDROID] Added a button in the Options menu to access the mods folder.
- [MOBILE] Added a preference to adjust the intensity of haptic feedback, ranging from 0.1 to 5.
- [MOBILE] Added an easter egg when tapping the player's healthbar icon.
### Changed
- Changed default OpenAL configuration settings to improve audio quality. (Thanks Smokey555, cyn0x8, and CCobaltDev!)
- The difference may be more or less noticeable on different devices and hardware.
- Applies to Desktop and Android, but not iOS yet.
- Made several improvements to Polymod and HScript. These changes might break some mods, so please update them accordingly!
- Fixed an issue where scripted classes can define two or more fields with the same name.
- Fixed an issue causing some syntax errors (such as missing commas) to be ignored by the parser.
- Scripted classes can now create static fields and functions.
- Scripted classes can now create variables with the `final` keyword.
- Scripted classes can now access variables from another scripted class with `class.someVariable`, instead of `class.scriptGet("someVariable")`
- This applies to functions too: `class.someFunction()`
- Scripted classes that don't extend another class can now be created!
- This only works if you access the class in a static context. Creating an instance of said class doesn't work just yet!
- Added support for properties (`get_` and `set_` functions) (Thanks KoloInDaCrib!)
- Added support for abstracts in a static context. (Thanks lemz1!)
- You can now use classes like `FlxColor` properly!
- Added support for creating and using enums. (Thanks lemz1!)
- You can import them in another script as usual.
- Added support for renaming imported classes using the `as` keyword. (Thanks KoloInDaCrib!)
- Fixed `try`/`catch` blocks not working properly. (Thanks NotHyper-474!)
- Fixed null-safe field access not working properly for functions (ex. `class?.someFunction()). (Thanks KoloInDaCrib!)
### Fixed
- [MOBILE] Weekend 1 Story Mode no longer crashes before loading into Blazin'.
- [MOBILE] Beating 2hot from Freeplay no longer crashes in the Results screen.
- [MOBILE] Retrying and pressing the Back button at the same time no longer crashes the game.
- [MOBILE] Pressing the Options and Back buttons at the same time no longer softlocks the game.
- [HTML5] Pausing while the train passes by on the Week 3 Erect stage no longer crashes.
- [DESKTOP] Getting a Bad/Shit rating on Blazin' no longer breaks animations.
- The scroll sound no longer plays once after entering Freeplay.
- The Freeplay song preview and album cover now update properly when switching variations.
- The Freeplay clear percent counter now consistently displays the correct value on unranked songs.
- The Freeplay difficulty star flames no longer become offset from the stars.
- The Freeplay difficulty star flames no longer appear during a new rank animation.
- The Freeplay menu now correctly assigns the `currentCharacterId`. (Thanks TechnikTil!)
- Boyfriend's Perfect (Gold) Results animation now loops properly.
- [DESKTOP] The Input Offsets menu no longer activates the debug cursor.
- The Input Offsets Test menu no longer generates stacked notes.
- The Input Offsets Test menu drums no longer desync from the rest of the track.
- The Input Offsets Test menu no longer breaks when a keyboard or controller is connected.
- [MOBILE] Sustain trails now display properly with upscroll enabled.
- [MOBILE] Added a Back button to the keyboard/gamepad Controls menu.
- [iOS] Fixed app name spacing on the Home Screen.
- [iOS] Adjusted the preloader to accommodate for different screen sizes.
- [iOS] The Upgrade button no longer appears if you have already purchased it.
- [ANDROID] Fixed some issues with scrolling.
- [ANDROID] Toasts with blank messages no longer appear.
- Fixed a critical security vulnerability that could be exploited in mods.
- A few more bugfixes and optimizations here and there.
## New Contributors for 0.7.2
* @Smokey555 made their first contribution in [#3318](https://github.com/FunkinCrew/Funkin/pull/3318)
* @CCobaltDev made their first contribution in [#3318](https://github.com/FunkinCrew/Funkin/pull/3318)
## [0.7.1] - 2025-07-15
### Fixed
- Properly implemented ad playback on iOS devices.
## [0.7.0] - 2025-07-15
### Added
- Friday Night Funkin' now has OFFICIAL mobile versions for Android and iOS, available on the Google Play Store and Apple App Store!
- This version contains 100% of the songs from the desktop version of the game.
- [MOBILE] New touch input compatibility for all menus.
- [MOBILE] Added banner and interstitial advertisements to the game. You can upgrade to the full version through an in-app purchase to permanently disable advertisements.
- [MOBILE] Graphics are compressed using the ASTC algorithm, decreasing memory usage in exchange for a slightly larger file size.
- [MOBILE] Added haptic feedback to several areas of the game.
- Added a visual indicator that shows available difficulties for the currently selected song in Freeplay.
- Overhauled the input offsets system, including:
- One unified "offset" value.
- An "Offset Calibration" screen where the game determines your ideal offset.
- A "Test" screen where you can play a short note pattern to try out your offset.
- A brand new offset testing theme: Syncobation by Kawai Sprite!
- The ability to change your offsets in the Pause Menu, mid-song!
- The Input Offsets menu isn't yet available on HTML5, but offsets are still configurable through the Pause Menu.
- Added null-safety to a bunch of classes in the source code.
- Added the Changelog back to the game files, written by Hundrec and AbnormalPoof!
- Added a few sandboxed classes to give mods limited access to the Discord and Newgrounds APIs. ([50d9584](https://github.com/FunkinCrew/Funkin/commit/50d9584a388bd891aa2f8b68a5cde894a6e1ede6)) - by @KoloInDaCrib in [#5040](https://github.com/FunkinCrew/Funkin/pull/5040)
- Added script support for Freeplay Backing Cards. ([0001017](https://github.com/FunkinCrew/Funkin/commit/0001017c003be653236c6cc56487c7d0ee33633e)) - by @KoloInDaCrib in [#5233](https://github.com/FunkinCrew/Funkin/pull/5233)
- Sparrow results screen animations can now be scriptable. ([7bb2336](https://github.com/FunkinCrew/Funkin/commit/7bb23369727ca4955aa1fbe25e5798809e8169bd)) - by @KoloInDaCrib in [#5168](https://github.com/FunkinCrew/Funkin/pull/5168)
- Added a blank `Object` class for scripts to extend, and made `FlxObject` and `FlxBasic` scriptable. ([eb6becc](https://github.com/FunkinCrew/Funkin/commit/eb6becc03fff76117ee3fcbeb32fe254236ca232)) - by @cyn0x8 in [#3119](https://github.com/FunkinCrew/Funkin/pull/3119)
- Added default gamepad controls for two recently added Freeplay controls. ([a0d3f8e](https://github.com/FunkinCrew/Funkin/commit/a0d3f8ec553e06b625b463c7989658edbebbbdf5)) - by @MrMadera in [#4559](https://github.com/FunkinCrew/Funkin/pull/4559)
- Added the ability to press the Chart Editor keybind in Freeplay with a song capsule selected. ([2221594](https://github.com/FunkinCrew/Funkin/commit/2221594883afa7cd0e518fca7ea975d05626692a)) - by @Lasercar in [#4114](https://github.com/FunkinCrew/Funkin/pull/4114)
- The Chart Editor now highlights and deletes stacked notes using a customizable threshold. ([8cae34e](https://github.com/FunkinCrew/Funkin/commit/8cae34eed711bff70e5348ffc6178a0fd69b5846)) - by @NotHyper-474 in [#3574](https://github.com/FunkinCrew/Funkin/pull/3574)
- Added a variation indicator next to the Chart Editor playbar difficulty. ([ccd0148](https://github.com/FunkinCrew/Funkin/commit/ccd0148e9b46d512a22b4958d3f289cfc7854965)) - by @KoloInDaCrib in [#5236](https://github.com/FunkinCrew/Funkin/pull/5236)
- Added more tween types to certain Chart Editor events. ([5177e12](https://github.com/FunkinCrew/Funkin/commit/5177e1275eb2fb2b016224c139e84debb421b895)) - by @Lasercar in [#4249](https://github.com/FunkinCrew/Funkin/pull/4249)
- Pressing Ctrl + N now creates a new stage in the Stage Editor. ([576f8e5](https://github.com/FunkinCrew/Funkin/commit/576f8e54ff8ca8e205241fafa33d0256b62d11d5)) - by @Lasercar in [#5175](https://github.com/FunkinCrew/Funkin/pull/5175)
- Added "Flip character horizontally" to the list of shortcuts in the Animation Editor. ([c464cae](https://github.com/FunkinCrew/funkin.assets/commit/c464caec921dcefef7b0b74b2abf95e76ce64491)) - by @AbnormalPoof in [funkin.assets#60](https://github.com/FunkinCrew/funkin.assets/pull/60)
- Added Perfect (Gold) to the list of available ranks in Results Debug menu. ([c5308cc](https://github.com/FunkinCrew/Funkin/commit/c5308ccbb9d2b98c62fa4974b8ad7ac1e1ec7d19)) - by @AbnormalPoof in [#4642](https://github.com/FunkinCrew/Funkin/pull/4642)
- [MOBILE] Implemented Kevin and Michael.
### Changed
- The mod API version now supports v0.7.0, along with v0.6.3. Be sure to check that your mods still work!
- Updated the app icon for Desktop platforms.
- [MOBILE] Modified several parts of the game to look better on phone screens with wider aspect ratios, up to 20:9.
- [DESKTOP] The game now tries to match the window's aspect ratio when changing states, extending as wide as 20:9.
- [DESKTOP] Included Mobile stage expansions on Desktop as well. Now you'll have more room for camera events!
- Playable Pico and Weekend 1 songs are now always unlocked in Freeplay, even on new saves.
- The Freeplay difficulty graphic now scrolls smoothly when changing difficulties.
- The "Pause on Unfocus" preference now opens the Pause Menu when unfocusing during a song.
- Scripts can now make hold note trails semi-transparent.
- Completely reformatted every script file within the game's assets for better readability.
- Completely reformatted and optimized every single chart file in the game.
- Recharted pico-speaker's chart in Stress
- Tweaked charts for the following songs:
- Bopeebo [all difficulties] - Removed an extra hey animation event
- Bopeebo (Pico Mix) [Hard] - Added a missing note in Section 24
- Fresh Erect [Nightmare] - Added a missing grace note for BF in Section 24
- South Erect [Nightmare] - Added missing grace notes for BF in Sections 13, 17, and 53
- Philly Nice [Hard] - Added missing grace notes for Pico in Sections 30 and 62
- Philly Nice [all difficulties] - Added hey animations throughout the song
- Philly Nice Erect [Erect] - Added a grace note for BF in Section 33, removed a stacked note for opponent in Section 12
- Philly Nice (Pico Mix) [Normal] - Adjusted a left note by 1/96 in Section 60
- Blammed (Pico Mix) [Hard] - Added a missing jack in Section 46
- Satin Panties [Hard] - Added grace notes in Sections 7-10
- Satin Panties [Normal/Hard] - Made Mom sing a sustain rather than two notes in Section 30
- High Erect [Erect/Nightmare] - Added a missing note in Section 16
- Cocoa [Easy] - Added some notes to reduce sparseness, fixed Mom singing Dad's notes
- Cocoa Erect [Erect/Nightmare] - Reimplemented BF's censored notes for Nightmare, adjusted one note by 1/48 in Section 63
- Eggnog Erect [Erect/Nightmare] Added two grace notes in Sections 10 and 14 and a missing note for Dad in Section 44
- Eggnog (Pico Mix) [Hard] - Added a missing grace note for Pico that was present on Normal in Section 20
- Roses [Normal/Hard] - Made Senpai sing a sustain rather than two notes (sneaky)
- Roses Erect [Erect/Nightmare] - Mirrored the changes from normal Roses
- Guns [all difficulties] - Added a missing note in Sections 28 and 32 and adjusted a hold note's length in Section 73
- Stress [Hard] - Split whole notes in halves in Sections 57-60
- Darnell [Hard] - Added one missing note for Pico in Section 35
- Darnell [all difficulties] - Adjusted camera event timings for consistency
- Darnell (BF Mix) [all difficulties] - Removed 3 extra notes and fixed Darnell's pattern being offset
- Lit Up [all difficulties] - Added 4 sustains for Darnell throughout the song
- Lit Up (BF Mix) [all difficulties] - Added 4 sustains for Darnell throughout the song
- 2hot [Easy/Hard] - Fixed remaining offset rhythms (for real this time)
- Notes now scroll more smoothly by rendering based on delta timing. ([6ad9ffc](https://github.com/FunkinCrew/Funkin/commit/6ad9ffc7f9d66bbaf6ba343663de4c7268f4be3b)) - by @KutikiPlayz in [#3544](https://github.com/FunkinCrew/Funkin/pull/3544)
- The Freeplay character select hint now always displays if you have more than one character unlocked. ([7ccf75c](https://github.com/FunkinCrew/Funkin/commit/7ccf75cd869ba4b6f18a5adc01e65e52ae7bb809)) - by @Hundrec in [#5023](https://github.com/FunkinCrew/Funkin/pull/5023)
- Favorite songs in Freeplay are now sorted by Week order instead of alphabetically. ([da0964a](https://github.com/FunkinCrew/Funkin/commit/da0964a7b7bbe4ece1bdbd19233eb6dba0de3ac5)) - by @Hundrec in [#3609](https://github.com/FunkinCrew/Funkin/pull/3609)
- Shifted Mommy Mearest's pixel icon to the left in Freeplay. ([d861eba](https://github.com/FunkinCrew/funkin.assets/commit/d861ebac027dd07d0254c79d7c89b59ee04b38f1)) - by @KoloInDaCrib in [funkin.assets#197](https://github.com/FunkinCrew/funkin.assets/pull/197)
- The Character Select screen now opens on the currently selected character. ([4819a74](https://github.com/FunkinCrew/Funkin/commit/4819a74c2959cc9b32dfe2cb76c3a4c00e7c7f9a)) - by @Lasercar in [#4072](https://github.com/FunkinCrew/Funkin/pull/4072)
- Visualizers now zero out when the game audio is muted. ([6dcec59](https://github.com/FunkinCrew/Funkin/commit/6dcec592f467a0daeb8ff1e0ce122916e36ca869)) - by @Lasercar in [#5266](https://github.com/FunkinCrew/Funkin/pull/5266)
- The Options Menu can now scroll to display more menu items. ([70f0a54](https://github.com/FunkinCrew/Funkin/commit/70f0a54191597bd72a6d30d4d12ef5ece6ba078c)) - by @AbnormalPoof in [#4706](https://github.com/FunkinCrew/Funkin/pull/4706)
- Raised the FPS cap preference from 300 to 500. ([be73134](https://github.com/FunkinCrew/Funkin/commit/be7313453f70983fff55e69a8b52d741f0cc53b4)) - by @Hundrec in [#5044](https://github.com/FunkinCrew/Funkin/pull/5044)
- The Credits menu now uses less memory, especially with many entries. ([1b68c3a](https://github.com/FunkinCrew/Funkin/commit/1b68c3a8d6f66905a9a508a1cb692fe3beb7b4a2)) - by @lemz1 in [#2655](https://github.com/FunkinCrew/Funkin/pull/2655)
- Added a timer sequence class to queue up multiple timers in scripts with ease. ([9e182f7](https://github.com/FunkinCrew/Funkin/commit/9e182f70d2bcc92eb68d730d74af143c45f7dcf8)) - by @cyn0x8 in [#2391](https://github.com/FunkinCrew/Funkin/pull/2391)
- Replaced smoothLerp and coolLerp with smoothLerpPrecision to fix a few lerp-related bugs. ([94eae11](https://github.com/FunkinCrew/Funkin/commit/94eae116c7a5e6039683d6391208b169378b5ff1)) - by @cyn0x8 in [#3617](https://github.com/FunkinCrew/Funkin/pull/3617)
- Fixed empty text strings softlocking the dialogue box. ([88d0e8c](https://github.com/FunkinCrew/Funkin/commit/88d0e8c3b0529654fb7eee8aebb099f7fb346f66)) - by @xenkap in [#4671](https://github.com/FunkinCrew/Funkin/pull/4671)
- Adjusted the size of the Beat/Step display in the Chart Editor. ([905181c](https://github.com/FunkinCrew/Funkin/commit/905181c9af29bb11280bda33ef9343069678a762)) - by @NotHyper-474 in [#4994](https://github.com/FunkinCrew/Funkin/pull/4994)
- The Chart Editor will now only fall back to the first available difficulty if the selected difficulty cannot be found. ([1c25713](https://github.com/FunkinCrew/Funkin/commit/1c257134648ebd89acf6c9d07f5a0c088fb915c6)) - by @Lasercar in [#4949](https://github.com/FunkinCrew/Funkin/pull/4949)
- The undo/redo history is now cleared when loading another song in the Chart Editor. ([426a9c0](https://github.com/FunkinCrew/Funkin/commit/426a9c0c108ac65a042295194679d46444ec1ea5)) - by @Lasercar in [#4308](https://github.com/FunkinCrew/Funkin/pull/4308)
- Blacklisted more classes for security reasons. ([cadfa3b](https://github.com/FunkinCrew/Funkin/commit/cadfa3b7ceae2ecabe2d544ddc4c9f453b0dfd56)) - by @NotHyper-474 in [#5185](https://github.com/FunkinCrew/Funkin/pull/5185)
### Fixed
- Fixed a ton of performance issues to help the game run better on mobile devices.
- Exiting the Freeplay Menu no longer freezes the game for a really long time (thanks NotHyper-474!)
- Notestyle graphics are now preloaded before the song starts, fixing the stutter at the beginning of the song.
- Hitting many hold note trails in one song no longer leads to a lag spike.
- The first lightning strike in Week 2 Erect no longer creates a lag spike.
- [DESKTOP] The conductor and music no longer gradually drift out of sync to eventually trigger a resync.
- Pixel notestyle strumlines are now properly positioned when Downscroll is enabled.
- Added the missing graffiti to the wall in the Weekend 1 Blazin' stage.
- Accept keybinds now properly scroll faster through the Credits.
- Typing in most text fields in debug editors no longer triggers keyboard shortcuts.
- The Chart Editor playback speed feature now works properly.
- The Chart Editor metronome and hitsounds now play at exactly the right time.
- The Chart Editor notification box no longer covers playbar info.
- Selecting a Recent File too quickly in the Stage Editor no longer crashes the game.
- Blacklisted a few classes for security.
- Opening the logs or backups folder before it's created no longer crashes the game. ([d3490f8](https://github.com/FunkinCrew/Funkin/commit/d3490f8c9929eefb9879ad65ce43038c193642d6)) - by @NotHyper-474 in [#4940](https://github.com/FunkinCrew/Funkin/pull/4940)
- Fixed a crash when mashing D or I during startup. ([b52c73f](https://github.com/FunkinCrew/Funkin/commit/b52c73f2b0fa32fbc349804621cf947dff2d364e)) - by @CrusherNotDrip in [#5160](https://github.com/FunkinCrew/Funkin/pull/5160)
- Hot-reloading with F5 during gameplay no longer crashes the game. ([d2acb5d](https://github.com/FunkinCrew/Funkin/commit/d2acb5d167afd42299d7200ab8c67972044a09c6)) - by @AbnormalPoof in [#5065](https://github.com/FunkinCrew/Funkin/pull/5065)
- Hot-reloading with F5 in the Input Offsets menu no longer crashes the game. ([58257f6](https://github.com/FunkinCrew/Funkin/commit/58257f6ac187925f3b23d3f1eef2c812ae569a6b)) - by @NotHyper-474 in [#5085](https://github.com/FunkinCrew/Funkin/pull/5085)
- Songs no longer skip forward at the beginning with high offsets. ([1f75a64](https://github.com/FunkinCrew/Funkin/commit/1f75a641e0c80d15f1af10bae7ee71a6ffecf219)) - by @xenkap in [#3732](https://github.com/FunkinCrew/Funkin/pull/3732)
- The song countdown no longer stacks when restarting or continues behind the Pause Menu.
([63eca96](https://github.com/FunkinCrew/Funkin/commit/63eca96c98a87e7155df8b2a1735f269ea83e1b5)) - by @KoloInDaCrib and @NotHyper-474 in [#4875](https://github.com/FunkinCrew/Funkin/pull/4875)
- Fixed incorrect highlighting and squashed text on Freeplay song capsules. ([0c62428](https://github.com/FunkinCrew/Funkin/commit/0c62428fc883c0fa6d09cd403efb287ff3af8c53)) - by @VioletSnowLeopard in [#5036](https://github.com/FunkinCrew/Funkin/pull/5036)
- The Freeplay song preview no longer plays twice after returning from Character
Select. ([3d3e2bd](https://github.com/FunkinCrew/Funkin/commit/3d3e2bd3786b858143d214caf55be2ee3e9483fc)) - by @Lasercar in [#5248](https://github.com/FunkinCrew/Funkin/pull/5248)
- Freeplay song ranks no longer disappear after changing variations. ([7cc9464](https://github.com/FunkinCrew/Funkin/commit/7cc9464573d07996e4bd0d557f82847809d3a786)) - by @VioletSnowLeopard in [#4583](https://github.com/FunkinCrew/Funkin/pull/4583)
- Freeplay song capsules now cycle through long names consistently. ([e193f73](https://github.com/FunkinCrew/Funkin/commit/e193f7392a83a04e4aac85fba4f441a78f7b6668)) - by @VioletSnowLeopard in [#4677](https://github.com/FunkinCrew/Funkin/pull/4677)
- Fixed a few visual issues with Freeplay's rank slam animation. ([ab817bb](https://github.com/FunkinCrew/Funkin/commit/ab817bb1eab74ae71c1b1fd74d7512a11e6d4339)) - by @Lasercar in [#4986](https://github.com/FunkinCrew/Funkin/pull/4986)
- Fixed visual errors in Freeplay after exiting Character Select. ([56a18e1](https://github.com/FunkinCrew/Funkin/commit/56a18e1cf6a15971feebcc828be8818037948cef)) - by @KoloInDaCrib in [#5245](https://github.com/FunkinCrew/Funkin/pull/5245)
- Freeplay styles are now reloaded when hot-reloading with F5. ([f54e140](https://github.com/FunkinCrew/Funkin/commit/f54e140b65e36fcf810c52ce464799dbc0c73c6d)) - by @Keoiki in [#5286](https://github.com/FunkinCrew/Funkin/pull/5286)
- Adjusted offsets for Freeplay DJ Pico's fistPump animation. ([382e286](https://github.com/FunkinCrew/funkin.assets/commit/382e286a2478939d3d6aca1c7c90719f33815014)) - by @AbnormalPoof in [funkin.assets#91](https://github.com/FunkinCrew/funkin.assets/pull/91)
- The "New Highscore" text no longer appears more than once in the Results screen. ([4e31003](https://github.com/FunkinCrew/Funkin/commit/4e31003a0f60cd394edc2bd4586f9f385b3d07bc)) - by @Lasercar in [#4319](https://github.com/FunkinCrew/Funkin/pull/4319)
- The Results Debug menu now shows the correct rank after recent scoring changes. ([11d9998](https://github.com/FunkinCrew/Funkin/commit/11d9998e5c73a3839ea39e06b7f217cbaab69c6d)) - by @NotHyper-474 in [#4905](https://github.com/FunkinCrew/Funkin/pull/4905)
- The Input Offsets menu now exits to the Options menu instead of the Main menu. ([5361df2](https://github.com/FunkinCrew/Funkin/commit/5361df254470e68d7571b4534cf456f08d5ffd60)) - by @JackXson-Real in [#5076](https://github.com/FunkinCrew/Funkin/pull/5076)
- The Debug Menu can no longer be opened after selecting an item in the Main Menu.
([5695bc2](https://github.com/FunkinCrew/Funkin/commit/5695bc20e721f0afd5b97a36167f13e550c12b16)) - by @Lasercar in [#4211](https://github.com/FunkinCrew/Funkin/pull/4211)
- The debug cursor is now always hidden when the game starts. ([6222c38](https://github.com/FunkinCrew/Funkin/commit/6222c389e301fa2bb4939697376c4e51e29a9977)) - by @Hundrec in [#4520](https://github.com/FunkinCrew/Funkin/pull/4520)
- The Story Mode Weekend 1 level title no longer clips into other level titles below it. ([19d1a8c](https://github.com/FunkinCrew/Funkin/commit/19d1a8c59380009f0d0814c94fd0b4eccb0c80cd)) - by @KoloInDaCrib in [#4348](https://github.com/FunkinCrew/Funkin/pull/4348)
- Hold note covers now display properly if a hold note was previously dropped. ([96d1324](https://github.com/FunkinCrew/Funkin/commit/96d1324af140858cb93edd07dc36a969c8ae84c0)) - by @T5mpler in [#5275](https://github.com/FunkinCrew/Funkin/pull/5275)
- Girlfriend's and Nene's combo drop animations now play consistently. ([34d5ed1](https://github.com/FunkinCrew/Funkin/commit/34d5ed11695cef7348c13505f13fb1da38b7988c)) - by @VioletSnowLeopard in [#4968](https://github.com/FunkinCrew/Funkin/pull/4968)
- Girlfriend (Tankman Stickup) now plays her combo drop animation. ([e329601](https://github.com/FunkinCrew/funkin.assets/commit/e329601834f270910ce80ea5539e4487b9895a8a)) - by @qt2k4 in [funkin.assets#149](https://github.com/FunkinCrew/funkin.assets/pull/149)
- Darnell's idle animation now loops consistently. ([df64586](https://github.com/FunkinCrew/funkin.assets/commit/df64586771ea41c544913e049fd0f32bdc655417)) - by @qt2k4 in [funkin.assets#159](https://github.com/FunkinCrew/funkin.assets/pull/159)
- Darnell's kneeCan animation now plays properly in 2hot. ([a2e9931](https://github.com/FunkinCrew/funkin.assets/commit/a2e993167aaa2ff6bff8806940f912e444608645)) - by @biomseed in [funkin.assets#78](https://github.com/FunkinCrew/funkin.assets/pull/78)
- Otis and Pico (Speaker) no longer spaz out when playtesting Stress. ([3f6d75f](https://github.com/FunkinCrew/funkin.assets/commit/3f6d75f3b6f6c8deb660323e3fb9bf1974c06520)) - by @Lasercar in [funkin.assets#124](https://github.com/FunkinCrew/funkin.assets/pull/124)
- The gasp sound now only plays once in the Week 3 Pico Mix doppelganger cutscene. ([ab4598b](https://github.com/FunkinCrew/funkin.assets/commit/ab4598baf3c6790d30cfb727d04c8b57fa18dd0d)) - by @KoloInDaCrib in [funkin.assets#126](https://github.com/FunkinCrew/funkin.assets/pull/126)
- Fixed a few issues with the train in Week 3. ([8db2426](https://github.com/FunkinCrew/funkin.assets/commit/8db2426991caebde44d19c81b198e8e2ad86f700)) - by @ShadzXD in [funkin.assets#180](https://github.com/FunkinCrew/funkin.assets/pull/180)
- The cars in Week 4 and Weekend 1 no longer get stuck when the song is restarted. ([9c511e3](https://github.com/FunkinCrew/funkin.assets/commit/9c511e371fd08a43ddf834766a6d35667bdef4f7)) - by @MetaBreeze in [funkin.assets#186](https://github.com/FunkinCrew/funkin.assets/pull/186)
- A-Bot's visualizer no longer jumps to a random volume when the song ends. ([51cc118](https://github.com/FunkinCrew/funkin.assets/commit/51cc1186bc77a2ee45a47fdbde2d75d9ec69de3a)) - by @VioletSnowLeopard in [funkin.assets#183](https://github.com/FunkinCrew/funkin.assets/pull/183)
- Pico's burpShit animation now re-enables volume for player vocals. ([cefda0e](https://github.com/FunkinCrew/funkin.assets/commit/cefda0e52fabbbe04afe60b7aed560267e2cb01e)) - by @Hundrec in [funkin.assets#71](https://github.com/FunkinCrew/funkin.assets/pull/71)
- Removed vocals from Monster's instrumental on web builds. ([1c9473f](https://github.com/FunkinCrew/funkin.assets/commit/1c9473f3dfdfb97d97f6c8457001055322abf5ab)) - by @JVNpixels in [funkin.assets#182](https://github.com/FunkinCrew/funkin.assets/pull/182)
- Fixed the retry sound not playing after a Tankman death quote finishes. ([e7c4b1b](https://github.com/FunkinCrew/Funkin/commit/e7c4b1ba38ba0739cfe347f6c4763f9811fb95b0)) - by @VioletSnowLeopard in [#4726](https://github.com/FunkinCrew/Funkin/pull/4726)
- Darnell (BF Mix)'s alternate instrumental is now properly accessible. ([5abdabf](https://github.com/FunkinCrew/funkin.assets/commit/5abdabf69b39ca4eebd36ae6bbd77fab736d0b86)) - by @Hundrec in [funkin.assets#168](https://github.com/FunkinCrew/funkin.assets/pull/168)
- Fixed Newgrounds score submissions for Lit Up and Lit Up (BF Mix). ([183cec6](https://github.com/FunkinCrew/Funkin/commit/183cec62dc1fd3c7f3f634dd3e2400e6ee77b476)) - by @Raltyro in [#4577](https://github.com/FunkinCrew/Funkin/pull/4577)
- Inputs are now disabled before Senpai's dialogue appears. ([c43d906](https://github.com/FunkinCrew/funkin.assets/commit/c43d906d19d91d71b9096e65d5e0d3543af8cd31)) - by @anysad in [funkin.assets#165](https://github.com/FunkinCrew/funkin.assets/pull/165)
- An easter egg now restarts the song using the correct instrumental. ([e657bc9](https://github.com/FunkinCrew/Funkin/commit/e657bc900bc62cc220276dc171dd47f0a176ac66)) - by @KoloInDaCrib in [#4956](https://github.com/FunkinCrew/Funkin/pull/4956)
- Encountering an easter egg during a Chart Editor playtest no longer crashes the game. ([b53b5bd](https://github.com/FunkinCrew/funkin.assets/commit/b53b5bdaecf975555538725a4cdfe71d38565b08)) - by @NotHyper-474 in [funkin.assets#133](https://github.com/FunkinCrew/funkin.assets/pull/133)
- Nonexistent characters no longer crash the Chart Editor. ([3bbb4b0](https://github.com/FunkinCrew/Funkin/commit/3bbb4b06c8c1a1885a18d4354fcfa4363a0c6c75)) - by @Lasercar in [#5008](https://github.com/FunkinCrew/Funkin/pull/5008)
- Holding Ctrl and clicking on a hold note trail no longer crashes the Chart Editor. ([dc56cca](https://github.com/FunkinCrew/Funkin/commit/dc56ccada50e671996caf0557de1977b7ff8d236)) - by @Lasercar in [#4203](https://github.com/FunkinCrew/Funkin/pull/4203)
- Tweens and timers are now canceled when returning to the Chart Editor. ([7e76cf6](https://github.com/FunkinCrew/Funkin/commit/7e76cf66340c00ef6ec84358e6304d62815173b6)) - by @KoloInDaCrib in [#5278](https://github.com/FunkinCrew/Funkin/pull/5278)
- Reduced the severity of a memory leak in the Chart Editor. ([cce8c18](https://github.com/FunkinCrew/Funkin/commit/cce8c18822e083910200597f5db4d87b6e3b521f)) - by @NotHyper-474 in [#5247](https://github.com/FunkinCrew/Funkin/pull/5247)
- Pressing the Chart Editor keybind during a song now opens to the variation and difficulty you were playing. ([e3fca16](https://github.com/FunkinCrew/Funkin/commit/e3fca167938642bea85398fe57347c10439c1892)) - by @Lasercar in [#4116](https://github.com/FunkinCrew/Funkin/pull/4116)
- The Chart Editor now properly saves audio levels when exiting. ([f78ab4d](https://github.com/FunkinCrew/Funkin/commit/f78ab4da1db4f9527a2e1715d5cfb37670e11a74)) - by @Lasercar in [#4149](https://github.com/FunkinCrew/Funkin/pull/4149)
- The Chart Editor "Load Metadata File" and "Load Chart File" buttons now function properly. ([9df5395](https://github.com/FunkinCrew/Funkin/commit/9df5395ff888cb6740e21204c8e19116e0472db4)) - by @Lasercar in [#4278](https://github.com/FunkinCrew/Funkin/pull/4278)
- FNF Legacy files can now be opened in the Chart Editor on MacOS. ([d98628c](https://github.com/FunkinCrew/Funkin/commit/d98628ca0f9f60357715bd7f95fc686a83201209)) - by @AbnormalPoof in [#4580](https://github.com/FunkinCrew/Funkin/pull/4580)
- The Chart Editor now consistently displays the correct waveform for vocal tracks. ([c0e0523](https://github.com/FunkinCrew/Funkin/commit/c0e0523651e8aaaae2a0eed6d5fef6c5ef1b7315)) - by @NotHyper-474 in [#5231](https://github.com/FunkinCrew/Funkin/pull/5231)
- Fixed selection boxes duplicating in the Chart Editor. ([65ed583](https://github.com/FunkinCrew/Funkin/commit/65ed58350b798bca0044603510540cfe81b48611)) - by @NotHyper-474 in [#5073](https://github.com/FunkinCrew/Funkin/pull/5073)
- Fixed the Chart Editor timer occasionally displaying incorrect millisecond values. ([26dc895](https://github.com/FunkinCrew/Funkin/commit/26dc895a27e0d7e49469251cd68b83be66384e15)) - by @Hundrec in [#4257](https://github.com/FunkinCrew/Funkin/pull/4257)
- The Chart Editor playhead can no longer be scrolled to before the beginning of the song. ([7c7dc11](https://github.com/FunkinCrew/Funkin/commit/7c7dc11f18644882444df97ac927e11adaa4ce50)) - by @Hundrec in [#5024](https://github.com/FunkinCrew/Funkin/pull/5024)
- The Chart Editor playbar no longer extends past the right of the grid. ([c7abb19](https://github.com/FunkinCrew/Funkin/commit/c7abb196989476cfa4db6354cc6fac5bacb2e56a)) - by @anysad in [#5090](https://github.com/FunkinCrew/Funkin/pull/5090)
- Dragging a hold note in the Chart Editor now drags its trail along with its head. ([d3d8aaa](https://github.com/FunkinCrew/Funkin/commit/d3d8aaae7bfbfa8975a9573807b9a3ca68a1ff55)) - by @KoloInDaCrib in [#4127](https://github.com/FunkinCrew/Funkin/pull/4127)
- Hold note trails will no longer disappear when dragged too far in the Chart Editor. ([37dc66b](https://github.com/FunkinCrew/Funkin/commit/37dc66bc189bc1941cf60587e1c068770aeec872)) - by @NotHyper-474 in
[#5261](https://github.com/FunkinCrew/Funkin/pull/5261)
- Undoing and redoing hold note length changes now visually updates the trail in the Chart Editor. ([06a440f](https://github.com/FunkinCrew/Funkin/commit/06a440f21c285666ce2d2bdf17a91ee82ab01061)) - by @NotHyper-474 in [#5265](https://github.com/FunkinCrew/Funkin/pull/5265)
- The Chart Editor hold note context menu now displays the correct options. ([4801316](https://github.com/FunkinCrew/Funkin/commit/48013168ef09ddc09549268a2e5309520d3fcc18)) - by @Lasercar in [#4231](https://github.com/FunkinCrew/Funkin/pull/4231)
- The buttons in the Chart Editor context menu for selections now do the right thing. ([62d24fc](https://github.com/FunkinCrew/Funkin/commit/62d24fcf4cbe7be328a995bad04f3eeb265262c5)) - by @Lasercar in [#4233](https://github.com/FunkinCrew/Funkin/pull/4233)
- The charter field in the song metadata now properly displays the charter in the Chart Editor. ([894d8cb](https://github.com/FunkinCrew/Funkin/commit/894d8cb4637fd0a64359a5edc805a23403f3045c)) - by @Lasercar in [#4879](https://github.com/FunkinCrew/Funkin/pull/4879)
- Chart Editor difficulties are now sorted in a consistent order. ([7aa77a1](https://github.com/FunkinCrew/Funkin/commit/7aa77a11cf7508fc05758045a13b2220aca96dd5)) - by @Lasercar in [#4528](https://github.com/FunkinCrew/Funkin/pull/4528)
- Chart Editor and Stage Editor windows now consistently show a close button. ([b23b7b8](https://github.com/FunkinCrew/funkin.assets/commit/b23b7b81d843c8fa2334fbdbe1ef979633956bb9)) - by @Lasercar in [funkin.assets#121](https://github.com/FunkinCrew/funkin.assets/pull/121)
- The Chart Editor playbar's font size no longer becomes too small. ([f9c1f7a](https://github.com/FunkinCrew/Funkin/commit/f9c1f7a5f7d3906155c2004ad2ec1d02556b9730)) - by @KoloInDaCrib in [#5253](https://github.com/FunkinCrew/Funkin/pull/5253)
- The Chart Editor copy notification no longer chases the mouse cursor. ([0ea42e1](https://github.com/FunkinCrew/Funkin/commit/0ea42e18e93b0b57f1ae5499d70c4681f191dacb)) - by @KoloInDaCrib in [#4029](https://github.com/FunkinCrew/Funkin/pull/4029)
- Changed "Tankman Battlefield (Erect)" to "Tankman Battlefield [Erect]" in the Chart Editor for consistency. ([52852a0](https://github.com/FunkinCrew/funkin.assets/commit/52852a02f60df81c281bfd0e49fd8fa09e118409)) - by @JVNpixels in [funkin.assets#155](https://github.com/FunkinCrew/funkin.assets/pull/155)
- Fixed Pico (Pixel) having the incorrect name in the Chart Editor. ([3ff0e9c](https://github.com/FunkinCrew/funkin.assets/commit/3ff0e9ca4f8cdc5734c335f2d7d72fa310686d46)) - by @ExtraCode75 in [funkin.assets#158](https://github.com/FunkinCrew/funkin.assets/pull/158)
- Fixed various issues and added missing functionalities to the Stage Editor. ([a776ce1](https://github.com/FunkinCrew/Funkin/commit/a776ce1a81a539f75f8bd2220a9768c03d347058)) - by @KoloInDaCrib in [#3974](https://github.com/FunkinCrew/Funkin/pull/3974)
- Stage Editor windows are now able to be closed. ([65461d8](https://github.com/FunkinCrew/Funkin/commit/65461d839b3764659aab33f1a35edf64ee514952)) - by @Lasercar in [#5238](https://github.com/FunkinCrew/Funkin/pull/5238)
- Fixed duplicate exit prompts appearing in the Stage Editor. ([136a5df](https://github.com/FunkinCrew/Funkin/commit/136a5dfad430b461a909be3a7e89f46c8be1d3b3)) - by @Lasercar in [#5239](https://github.com/FunkinCrew/Funkin/pull/5239)
- The help guide in the Stage Editor can no longer be opened multiple times. ([564d679](https://github.com/FunkinCrew/Funkin/commit/564d679f969c500834426ce3cf50507091d69697)) - by @Lasercar in [#4128](https://github.com/FunkinCrew/Funkin/pull/4128)
- Removed a spammy console trace from Spooky Kids (Dark). ([5935a61](https://github.com/FunkinCrew/funkin.assets/commit/5935a61dd71f51e6264dcd039f1164cd1ff295ef)) - by @NotHyper-474 in [funkin.assets#187](https://github.com/FunkinCrew/funkin.assets/pull/187)
- Removed spammy console traces from `DiscordClient`. ([e89f9f5](https://github.com/FunkinCrew/Funkin/commit/e89f9f50dc6085e3550736459ce9e4fd02c1fc5b)) - by @AbnormalPoof in [#4207](https://github.com/FunkinCrew/Funkin/pull/4207)
- Removed a spammy console trace from some Chart Editor events. ([b883ad3](https://github.com/FunkinCrew/Funkin/commit/b883ad3d50b990e605d7b79d042292c7371db5f0)) - by @anysad in [#5097](https://github.com/FunkinCrew/Funkin/pull/5097)
- ANSI colors now display in the console on more computers. ([3747b94](https://github.com/FunkinCrew/Funkin/commit/3747b942461c3644fe31f25d2ce847fd74d1b1e0)) - by @AbnormalPoof in [#4676](https://github.com/FunkinCrew/Funkin/pull/4676)
- Properly blacklist a certain class from scripts. ([3dc7699](https://github.com/FunkinCrew/Funkin/commit/3dc7699aac737998b637ff9a9f16986a434424be)) - by @charlesisfeline in [#4773](https://github.com/FunkinCrew/Funkin/pull/4773)
### Removed
- Removed the VSync preference from web builds, where it's non-functional. ([0b7a94b](https://github.com/FunkinCrew/Funkin/commit/0b7a94b1cc5c5d0dbac5a4c3d595349c4e6eb6e4)) - by @NotHyper-474 in [#5062](https://github.com/FunkinCrew/Funkin/pull/5062)
- Removed a few non-functional screenshot preferences. ([93e4f79](https://github.com/FunkinCrew/Funkin/commit/93e4f799f4f0ea435b3d77ca8ef2ab6beeb0a955)) - by @Lasercar in [#4895](https://github.com/FunkinCrew/Funkin/pull/4895)
## New Contributors for 0.7.0
* @KutikiPlayz made their first contribution in [#3544](https://github.com/FunkinCrew/Funkin/pull/3544)
* @xenkap made their first contribution in [#3732](https://github.com/FunkinCrew/Funkin/pull/3732)
* @Raltyro made their first contribution in [#4577](https://github.com/FunkinCrew/Funkin/pull/4577)
* @charlesisfeline made their first contribution in [#4773](https://github.com/FunkinCrew/Funkin/pull/4773)
* @T5mpler made their first contribution in [#5275](https://github.com/FunkinCrew/Funkin/pull/5275)
* @biomseed made their first contribution in [funkin.assets#78](https://github.com/FunkinCrew/funkin.assets/pull/78)
* @qt2k4 made their first contribution in [funkin.assets#149](https://github.com/FunkinCrew/funkin.assets/pull/149)
* @ExtraCode75 made their first contribution in [funkin.assets#158](https://github.com/FunkinCrew/funkin.assets/pull/158)
* @MetaBreeze made their first contribution in [funkin.assets#186](https://github.com/FunkinCrew/funkin.assets/pull/186)
## [0.6.4] - 2025-05-02
@ -432,7 +135,7 @@ Select. ([3d3e2bd](https://github.com/FunkinCrew/Funkin/commit/3d3e2bd3786b85814
* @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 [funkin.assets#62](https://github.com/FunkinCrew/funkin.assets/pull/62)
* @ShadzXD made their first contribution in [#62](https://github.com/FunkinCrew/Funkin/pull/4729)
@ -1042,7 +745,7 @@ The Weekend 1 update!
- 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 Ludem Dare game held together with sticks and glue you played three years ago.
- 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.

View file

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<simulatedScreenMetrics key="simulatedMetrics" type="freeform" size="375x667"/>
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0" green="0" blue="0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

103
README.md
View file

@ -1,55 +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)
- [Download Android builds from Google Play!](https://play.google.com/store/apps/details?id=me.funkin.fnf)
- [Download iOS builds from the App Store!](https://apps.apple.com/app/id6740428530)
# 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
- [ZackDroid](https://x.com/ZackDroidCoder) - Lead Mobile Programmer
- [MAJigsaw77](https://github.com/MAJigsaw77) - Mobile Programmer
- [Karim-Akra](https://x.com/KarimAkra_0) - Mobile Programmer
- [Sector_5](https://github.com/sector-a) - Mobile Programmer
- [Luckydog7](https://github.com/luckydog7) - Mobile Programmer
- 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
# 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

2
art

@ -1 +1 @@
Subproject commit 490e97f4c6e673a52ee4f9af98325b1aa2d0c3fe
Subproject commit 8402339e2e63ae99c441941a46d54bf3f0c0d5fa

2
assets

@ -1 +1 @@
Subproject commit 14808c4de5be43b6bc1e60d0285eba628ea6539d
Subproject commit c108a7ff0d11bf328e7b232160b8f68c71e21bca

View file

@ -70,10 +70,9 @@
},
{
"props": {
"severity": "IGNORE",
"policy": "aligned",
"allowSingleline": true
"allowSingleline": true,
"severity": "INFO"
},
"type": "ConditionalCompilation"
},

View file

@ -87,3 +87,4 @@ filter_commits = false
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

View file

@ -1,21 +0,0 @@
assets/preload/images/cursor/*
assets/preload/images/freeplay/*
assets/preload/images/icons/*
assets/preload/images/titleEnter.png
assets/preload/images/titleEnter_mobile.png
assets/preload/images/soundtray/*
assets/preload/images/stageBuild/*
assets/preload/images/ui/popup/pixel/
assets/shared/images/characters/abotPixel/
assets/shared/images/characters/bfPixel.png
assets/shared/images/characters/bfPixelsDEAD.png
assets/shared/images/characters/gfPixel.png
assets/shared/images/characters/nenePixel/
assets/shared/images/characters/picoPixel/
assets/shared/images/characters/senpai.png
assets/shared/images/characters/spirit.png
assets/shared/images/resultScreen/*
assets/shared/images/ui/chart-editor/*
assets/shared/images/ui/countdown/pixel/
assets/week1/
assets/week6/*

View file

@ -40,8 +40,8 @@ There are several useful build flags you can add to a build to affect how it wor
- 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_SCREENSHOTS` or `-DNO_FEATURE_SCREENSHOTS` to forcibly enable or disable the screenshots feature.
- `-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.

View file

@ -1,98 +0,0 @@
# Compiling Friday Night Funkin' for Mobile Devices
Before starting, **make sure your game builds on desktop.**
Check [COMPILING.md](./COMPILING.md) if you havent done that yet.
## Android
0. **Create a new folder** this will store Android tools (remember where you put it!).
1. **Open a terminal as Administrator.**
2. Run this in the terminal (replace the path with your actual folder):
```bash
setx ANDROID_HOME "C:\path\to\your\folder" /M
```
3. Download [Android Studio Command-line Tools](https://developer.android.com/studio#command-line-tools-only).
4. Extract the ZIP into your folder from step 1.
5. (Optional) Close and reopen the terminal if needed.
6. Run:
```bash
sdkmanager --install "build-tools;35.0.0" "ndk;29.0.13113456" "platforms;android-29" "platforms;android-35"
```
- The latest NDK is not compatible with Lime you have to use the old one.
7. Download and install [JDK 17 (MSI)](https://adoptium.net/temurin/releases/?version=17&os=windows).
8. Run:
```bash
lime setup android
```
Use these when asked:
- **Android SDK:** `C:\path\to\your\folder`
- **Android NDK:** `C:\path\to\your\folder\ndk\29.0.13113456`
- **JDK:** `C:\Program Files\Java\jdk-17`
9. Now build your game:
```bash
lime test android
```
### macOS
0. **Create a new folder** this will store Android tools (remember where you put it!).
1. Open **Terminal** (Command ⌘ + Space → type “terminal” → Enter).
2. In Terminal:
```bash
cd /path/to/your/folder
export ANDROID_HOME=/path/to/your/folder
export PATH=$PATH:$ANDROID_HOME/cmdline-tools:$ANDROID_HOME/cmdline-tools/bin:$ANDROID_HOME/platform-tools
```
3. Download [Android Studio Command-line Tools](https://developer.android.com/studio#command-line-tools-only).
4. Extract the ZIP into your folder from step 1.
5. (Optional) Restart Terminal if needed.
6. Run:
```bash
sdkmanager --install "build-tools;35.0.0" "ndk;29.0.13113456" "platforms;android-29" "platforms;android-35"
```
7. Download and install [JDK 17 for macOS](https://adoptium.net/temurin/releases/?os=mac&version=17).
8. Run:
```bash
lime setup android
```
Use these when asked:
- **Android SDK:** `/path/to/your/folder`
- **Android NDK:** `/path/to/your/folder/ndk/28.0.13004108`
- **JDK:** `/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home`
_(If not asked for JDK, dont worry — just skip it.)_
9. Build your game:
```bash
lime test android
```
## iOS
Note that you can only build the game for iOS on a computer running MacOS.
0. Build the game for desktop to make sure everything works. Check [COMPILING.md](./COMPILING.md).
1. Get Xcode from the app store on your MacOS Machine.
2. Download the iPhone SDK (First thing that pops up in Xcode)
3. Open up a terminal tab and run `lime test ios -xcode`
4. You will need to sign your own copy in order to run the game with a real iOS device! That requires an Apple Developer account, sorry!
- To run with an iOS simulator instead of `-xcode` use `-simulator`
### iOS Troubleshooting
- **A required plugin failed to load. Please ensure system content is up-to-date — try running 'xcodebuild -runFirstLaunch'.**
Make sure you have the iOS SDK isntalled, see Step 2.
- **error: No Accounts: Add a new account in Accounts settings. (in target 'Funkin' from project 'Funkin')**
Open XCode, press CMD+, to open Settings, select Accounts, add an Apple ID.
- error: No Account for Team "Z7G7AVNGSH". Add a new account in Accounts settings or verify that your accounts have valid credentials.
Open `project.hxp` and change `IOS_TEAM_ID` to your personal team's ID.
- error: Failed Registering Bundle Identifier: The app identifier "me.funkin.fnf" cannot be registered to your development team because it is not available.
The Funkin' Crew are the only ones that can build an iOS app with the identifier `me.funkin.fnf`. Open `project.hxp` and change `PACKAGE_NAME` to a unique value.
- error: No profiles for 'me.funkin.fnf' were found: Xcode couldn't find any iOS App Development provisioning profiles matching 'me.funkin.fnf'

View file

@ -60,7 +60,7 @@ This section provides guidelines to follow when [opening an issue](https://githu
## Requirements
Make sure you're playing:
- the latest version of the game (currently v0.7.4)
- 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)

View file

@ -1,32 +0,0 @@
# HTML5/Web
1. Nope
# Windows
1. Start the game at least once. This will create a `mods` folder if it doesn't already exist, alongside the executable.
2. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
3. Restart the game. The game should detect the mod and start with it.
# Linux
1. Start the game at least once. This will create a `mods` folder if it doesn't already exist, alongside the executable.
2. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
3. Restart the game. The game should detect the mod and start with it.
# MacOS
1. Start the game at least once. This will create a `mods` folder if it doesn't already exist in the game's system files.
2. Right click `Funkin.app` and select `Show Package Contents`.
3. Navigate to `Contents/Resources/mods`.
4. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
5. Restart the game. The game should detect the mod and start with it.
# Android
1. Start the game at least once. This will create a `mods` folder deep in your system files.
2. Get an Android file browser that lets you view the app data files, or use Android Studio and open up the Device Explorer.
3. Locate the `/sdcard/Android/obb/me.funkin.fnf/mods` folder.
4. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
5. Restart the game (you may have to [force close](https://support.google.com/android/answer/9079646?hl=en) the app first). The game should detect the mod and start with it.
# iOS
1. Start the game at least once. This will create a `mods` folder in your system files.
2. Open the Files app, and navigate to `On My iPhone` -> `Friday Night Funkin` -> `mods`.
3. Extract the mod you downloaded from its ZIP file, and place the mod folder into the game's `mods` folder.
4. Restart the game (you may have to [force close](https://support.apple.com/en-us/109359) the app first). The game should detect the mod and start with it.

View file

@ -1,66 +0,0 @@
@echo off
set ZIP_FILE="./temp/_temp_jdk.zip"
set OUTPUT_DIR="./temp/"
set SIX_LINK="https://drive.usercontent.google.com/download?id=1GqFpIk_bkxFb0tNN3x9LxnN-Zh_oDUX5&export=download&authuser=0&confirm=t&uuid=43108c0a-bd53-4465-86f3-80aaceaa7a38&at=APZUnTVNS_BV9cNyC_iicDInosmz%3A1718921284514"
set EIGHT_LINK="https://drive.usercontent.google.com/download?id=1X8jjtYYos8aDfZKwehGS9B3zFQa-sCb-&export=download&authuser=0&confirm=t&uuid=07b24a6c-5352-4ba5-9fb8-cff151a6d91e&at=APZUnTUfw26NBAl0nCMn6HBKgHwK%3A1718922303598"
echo MAKING TEMP
mkdir %OUTPUT_DIR%
echo MADE TEMP
echo INSTALLING ANDROID BUILD TOOLS
call .\asclt\bin\sdkmanager "build-tools;32.0.0" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
call .\asclt\bin\sdkmanager "build-tools;32.1.0-rc1" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo INSTALLED ANDROID BUILD TOOLS
echo INSTALLING ANDROID SDK
REM First install the sdks
call .\asclt\bin\sdkmanager "platforms;android-29" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo ANDROID SDK INSTALLED
echo INSTALLING ANDROID NDK
REM then the ndks
call ./asclt/bin/sdkmanager "ndk;21.4.7075529" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo ANDROID NDK INSTALLED
echo DOWNLOADING JDK
call curl -o %ZIP_FILE% %SIX_LINK%
echo DOWNLOADED JDK
echo UNZIPPING JDK
call powershell -Command "Expand-Archive -Path '%ZIP_FILE%' -DestinationPath '%OUTPUT_DIR%' -Force"
echo UNZIPPED JDK
echo MAKING JDK PATH
mkdir "%LOCALAPPDATA%/Android/jdk"
echo MADE JDK PATH
echo MOVING JDK TO PROPER PATH
call move "%OUTPUT_DIR%/jdk-17.0.11+9" "%LOCALAPPDATA%/Android/jdk/"
echo MOVED JDK
echo LIME SETTING UP
haxelib run lime config ANDROID_SDK %LOCALAPPDATA%\Android\Sdk
haxelib run lime config ANDROID_NDK_ROOT %LOCALAPPDATA%\Android\Sdk\ndk\21.4.7075529
haxelib run lime config JAVA_HOME %LOCALAPPDATA%\Android\Sdk\jdk\jdk-17.0.11+9
haxelib run lime config ANDROID_SETUP true
echo DONE
pause

View file

@ -1,66 +0,0 @@
@echo off
set ZIP_FILE="./temp/_temp_jdk.zip"
set OUTPUT_DIR="./temp/"
set SIX_LINK="https://drive.usercontent.google.com/download?id=1GqFpIk_bkxFb0tNN3x9LxnN-Zh_oDUX5&export=download&authuser=0&confirm=t&uuid=43108c0a-bd53-4465-86f3-80aaceaa7a38&at=APZUnTVNS_BV9cNyC_iicDInosmz%3A1718921284514"
set EIGHT_LINK="https://drive.usercontent.google.com/download?id=1X8jjtYYos8aDfZKwehGS9B3zFQa-sCb-&export=download&authuser=0&confirm=t&uuid=07b24a6c-5352-4ba5-9fb8-cff151a6d91e&at=APZUnTUfw26NBAl0nCMn6HBKgHwK%3A1718922303598"
echo MAKING TEMP
mkdir %OUTPUT_DIR%
echo MADE TEMP
echo INSTALLING ANDROID BUILD TOOLS
call .\asclt\bin\sdkmanager "build-tools;32.0.0" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
call .\asclt\bin\sdkmanager "build-tools;32.1.0-rc1" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo INSTALLED ANDROID BUILD TOOLS
echo INSTALLING ANDROID SDK
REM First install the sdks
call .\asclt\bin\sdkmanager "platforms;android-29" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo ANDROID SDK INSTALLED
echo INSTALLING ANDROID NDK
REM then the ndks
call ./asclt/bin/sdkmanager "ndk;21.4.7075529" --sdk_root="%LOCALAPPDATA%/Android/Sdk/"
echo ANDROID NDK INSTALLED
echo DOWNLOADING JDK
call curl -o %ZIP_FILE% %EIGHT_LINK%
echo DOWNLOADED JDK
echo UNZIPPING JDK
call powershell -Command "Expand-Archive -Path '%ZIP_FILE%' -DestinationPath '%OUTPUT_DIR%' -Force"
echo UNZIPPED JDK
echo MAKING JDK PATH
mkdir "%LOCALAPPDATA%/Android/jdk"
echo MADE JDK PATH
echo MOVING JDK TO PROPER PATH
call move "%OUTPUT_DIR%/jdk-17.0.11+9" "%LOCALAPPDATA%/Android/jdk/"
echo MOVED JDK
echo LIME SETTING UP
haxelib run lime config ANDROID_SDK %LOCALAPPDATA%\Android\Sdk
haxelib run lime config ANDROID_NDK_ROOT %LOCALAPPDATA%\Android\Sdk\ndk\21.4.7075529
haxelib run lime config JAVA_HOME %LOCALAPPDATA%\Android\Sdk\jdk\jdk-17.0.11+9
haxelib run lime config ANDROID_SETUP true
echo DONE
pause

View file

@ -6,7 +6,7 @@
"name": "EliteMasterEric"
}
],
"api_version": "0.7.0",
"api_version": "0.5.0",
"mod_version": "1.0.0",
"license": "Apache-2.0"
}

View file

@ -6,7 +6,7 @@
"name": "EliteMasterEric"
}
],
"api_version": "0.7.0",
"api_version": "0.5.0",
"mod_version": "1.0.0",
"license": "Apache-2.0"
}

View file

@ -4,48 +4,14 @@
"name": "FlxPartialSound",
"type": "git",
"dir": null,
"ref": "3c9f63e3501c20c0b60442089dc05306f5a87968",
"url": "https://github.com/FunkinDroidTeam/FlxPartialSound.git"
},
{
"name": "astc-compressor",
"type": "git",
"dir": null,
"ref": "8c9f56927c523df7b849352c6951f04112fe15cc",
"url": "https://github.com/KarimAkra/astc-compressor"
},
{
"name": "extension-admob",
"type": "git",
"dir": null,
"ref": "02334589ff9603a5f483077a44395009644f6274",
"url": "https://github.com/FunkinCrew/extension-admob"
},
{
"name": "extension-androidtools",
"type": "haxelib",
"version": "2.2.2"
},
{
"name": "extension-haptics",
"type": "haxelib",
"version": "1.0.4"
},
{
"name": "extension-iapcore",
"type": "haxelib",
"version": "1.0.4"
},
{
"name": "extension-iarcore",
"type": "haxelib",
"version": "1.0.3"
"ref": "41f35ddb1eb9d10bc742e6f8b5bcc62f9ef8ad84",
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
},
{
"name": "flixel",
"type": "git",
"dir": null,
"ref": "08fc955ca87f192a971719a675f1d3b21709725d",
"ref": "fffb1a74cf08f63dacc2ab09976340563f5b6e6d",
"url": "https://github.com/FunkinCrew/flixel"
},
{
@ -55,11 +21,18 @@
"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": "b1faf19885dad06c899cb71ffe07b4e40b8c6d0c",
"ref": "f8842cea9883d6112a2c854cf93fa72856171428",
"url": "https://github.com/FunkinCrew/flxanimate"
},
{
@ -78,8 +51,8 @@
"name": "grig.audio",
"type": "git",
"dir": "src",
"ref": "8567c4dad34cfeaf2ff23fe12c3796f5db80685e",
"url": "https://github.com/FunkinCrew/grig.audio"
"ref": "57f5d47f2533fd0c3dcd025a86cb86c0dfa0b6d2",
"url": "https://gitlab.com/haxe-grig/grig.audio.git"
},
{
"name": "hamcrest",
@ -90,29 +63,29 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "47ee9f5fa02d422e186e844f87e68220cabfcc5b",
"ref": "07fc7aa8098deaea633b0726d01f83eb4ef8a832",
"url": "https://github.com/haxeui/haxeui-core"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "100f2c96beab619cfe72c567a058c41c71e3e998",
"ref": "b899a4c7d7318c5ff2b1bb645fbc73728fad1ac9",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
"name": "hscript",
"type": "git",
"dir": null,
"ref": "d60bb2947fa609fdc875ccfae89666a6984eeaf2",
"ref": "27c86f9a761c1d16d4433c4cf252eccb7b2e18de",
"url": "https://github.com/FunkinCrew/hscript"
},
{
"name": "hxcpp",
"type": "git",
"dir": null,
"ref": "5a0dc3f644dc676a4a092b7e6c8edc8be941f024",
"url": "https://github.com/FunkinCrew/hxcpp"
"ref": "v4.3.75",
"url": "https://github.com/HaxeFoundation/hxcpp"
},
{
"name": "hxcpp-debug-server",
@ -143,7 +116,7 @@
{
"name": "hxvlc",
"type": "haxelib",
"version": "2.2.2"
"version": "2.1.2"
},
{
"name": "json2object",
@ -170,7 +143,7 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "e5f8c27124598505917a001588b560244731adfb",
"ref": "be81ad7e4e1a92c3482bcc009648a4ac892cfa35",
"url": "https://github.com/FunkinCrew/lime"
},
{
@ -212,14 +185,14 @@
"name": "openfl",
"type": "git",
"dir": null,
"ref": "a0df7c3afe360c9af59a76e45007dbf4e53b5131",
"ref": "d061c936b462f040304ec2bd42d9f59d2e59e285",
"url": "https://github.com/FunkinCrew/openfl"
},
{
"name": "polymod",
"type": "git",
"dir": null,
"ref": "d4142dd15a3b57ed4eb149f9f6a2c3ad9935bf7b",
"ref": "0fbdf27fe124549730accd540cec8a183f8652c0",
"url": "https://github.com/larsiusprime/polymod"
},
{

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,12 @@
package;
import flixel.FlxG;
import flixel.FlxGame;
import flixel.FlxState;
import funkin.ui.FullScreenScaleMode;
import funkin.Preferences;
import funkin.util.logging.CrashHandler;
import funkin.ui.debug.MemoryCounter;
import funkin.save.Save;
import haxe.ui.Toolkit;
#if hxvlc
import hxvlc.util.Handle;
#end
import openfl.display.FPS;
import openfl.display.Sprite;
import openfl.events.Event;
@ -36,15 +31,6 @@ class Main extends Sprite
public static function main():Void
{
// Set the current working directory for Android and iOS devices
#if android
// On Android use External Files Dir.
Sys.setCwd(haxe.io.Path.addTrailingSlash(extension.androidtools.content.Context.getExternalFilesDir()));
#elseif ios
// On iOS use Documents Dir.
Sys.setCwd(haxe.io.Path.addTrailingSlash(lime.system.System.documentsDirectory));
#end
// We need to make the crash handler LITERALLY FIRST so nothing EVER gets past it.
CrashHandler.initialize();
CrashHandler.queryStatus();
@ -111,19 +97,9 @@ class Main extends Sprite
memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
#end
#if mobile
// Add this signal so we can reposition and resize the memory and fps counter.
FlxG.signals.preUpdate.add(repositionCounters.bind(true));
#end
// George recommends binding the save before FlxGame is created.
Save.load();
#if hxvlc
// Initialize hxvlc's Handle here so the videos are loading faster.
Handle.init();
#end
// 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
@ -133,6 +109,7 @@ class Main extends Sprite
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
@ -147,15 +124,6 @@ class Main extends Sprite
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
#end
#if !html5
FlxG.scaleMode = new FullScreenScaleMode();
#end
#if mobile
// Reposition and resize the memory and fps counter without lerping.
repositionCounters(false);
#end
#if hxcpp_debug_server
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
#else
@ -177,56 +145,4 @@ class Main extends Sprite
funkin.input.Cursor.registerHaxeUICursors();
haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
}
#if mobile
function repositionCounters(lerp:Bool):Void
{
// Calling this so it gets scaled based on the resolution of the game and device's resolution.
var scale:Float = Math.min(FlxG.stage.stageWidth / FlxG.width, FlxG.stage.stageHeight / FlxG.height);
#if android
scale = Math.max(scale, 1);
#else
scale = Math.min(scale, 1);
#end
final thypos:Float = Math.max(FullScreenScaleMode.notchSize.x, 10);
if (fpsCounter != null)
{
fpsCounter.scaleX = fpsCounter.scaleY = scale;
if (FlxG.game != null)
{
if (lerp)
{
fpsCounter.x = flixel.math.FlxMath.lerp(fpsCounter.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
}
else
{
fpsCounter.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
}
fpsCounter.y = FlxG.game.y + (3 * scale);
}
}
if (memoryCounter != null)
{
memoryCounter.scaleX = memoryCounter.scaleY = scale;
if (FlxG.game != null)
{
if (lerp)
{
memoryCounter.x = flixel.math.FlxMath.lerp(memoryCounter.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
}
else
{
memoryCounter.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
}
memoryCounter.y = FlxG.game.y + (13 * scale);
}
}
}
#end
}

View file

@ -29,7 +29,7 @@ class Postbuild
var buildTime:Float = roundToTwoDecimals(end - start);
Sys.println('[INFO] Build took: ${buildTime} seconds');
trace('Build took: ${buildTime} seconds');
}
}

View file

@ -9,24 +9,48 @@ 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
{
var start:Float = Sys.time();
Sys.println('[INFO] Performing pre-build tasks...');
trace('Building...');
saveBuildTime();
var end:Float = Sys.time();
var duration:Float = end - start;
Sys.println('[INFO] Finished pre-build tasks in $duration seconds.');
buildCredsFile();
}
static function saveBuildTime():Void
{
// PostBuild.hx reads this file and computes the total build duration.
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
}
}

View file

@ -24,27 +24,6 @@ extern class Native_TracyProfiler
@:native('::__hxcpp_tracy_framemark')
public static function frameMark():Void;
/**
Mark a named frame. Allows creating multiple frame sets for different timing categories.
Each unique name creates a separate frame set in the Tracy timeline.
**/
@:native('::__hxcpp_tracy_framemark_named')
public static function frameMarkNamed(_name:String):Void;
/**
Mark the start of a discontinuous frame. Use for periodic work with gaps.
Must be paired with frameMarkEnd() using the same name.
**/
@:native('::__hxcpp_tracy_framemark_start')
public static function frameMarkStart(_name:String):Void;
/**
Mark the end of a discontinuous frame. Use for periodic work with gaps.
Must be paired with frameMarkStart() using the same name.
**/
@:native('::__hxcpp_tracy_framemark_end')
public static function frameMarkEnd(_name:String):Void;
/**
Print a message into Tracy's log.
**/
@ -92,18 +71,6 @@ class Cppia_TracyProfiler
public static function frameMark()
Native_TracyProfiler.frameMark();
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.frameMarkNamed)
public static function frameMarkNamed(_name:String)
Native_TracyProfiler.frameMarkNamed(_name);
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.frameMarkStart)
public static function frameMarkStart(_name:String)
Native_TracyProfiler.frameMarkStart(_name);
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.frameMarkEnd)
public static function frameMarkEnd(_name:String)
Native_TracyProfiler.frameMarkEnd(_name);
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.message)
public static function message(_msg:String, ?_color:Int = 0x000000)
Native_TracyProfiler.message(_msg, _color);

View file

@ -9,11 +9,6 @@ import openfl.utils.Future;
@:nullSafety
class Assets
{
/**
* The assets cache.
*/
public static var cache:openfl.utils.IAssetCache = openfl.utils.Assets.cache;
/**
* Get the file system path for an asset
* @param path The asset path to load from, relative to the assets folder

View file

@ -6,7 +6,6 @@ import flixel.math.FlxMath;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongDataUtils;
import funkin.save.Save;
import funkin.util.TimerUtil.SongSequence;
import haxe.Timer;
import flixel.sound.FlxSound;
@ -93,12 +92,6 @@ class Conductor
*/
public var songPosition(default, null):Float = 0;
/**
* The offset between frame time and music time.
* Used in `getTimeWithDelta()` to get a more accurate music time when on higher framerates.
*/
var songPositionDelta(default, null):Float = 0;
var prevTimestamp:Float = 0;
var prevTime:Float = 0;
@ -249,18 +242,25 @@ class Conductor
* No matter if you're using a local conductor or not, this always loads
* to/from the save file
*/
public var globalOffset(get, never):Int;
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, never):Int;
public var audioVisualOffset(get, set):Int;
function get_globalOffset():Int
function get_inputOffset():Int
{
return Preferences.globalOffset;
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
@ -268,11 +268,18 @@ class Conductor
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 + formatOffset + globalOffset;
return instrumentalOffset + audioVisualOffset + inputOffset;
}
/**
@ -394,10 +401,8 @@ class Conductor
* @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
* @param forceDispatch If it should force the dispatch of onStepHit, onBeatHit, and onMeasureHit
* even if the current step, beat, or measure hasn't changed.
*/
public function update(?songPos:Float, applyOffsets:Bool = true, forceDispatch:Bool = false):Void
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;
@ -417,8 +422,7 @@ class Conductor
// 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 = FlxMath.bound(Math.min(this.combinedOffset, 0), songPos, currentLength);
this.songPositionDelta += FlxG.elapsed * 1000 * FlxG.sound.music.pitch;
this.songPosition = Math.min(currentLength, Math.max(0, songPos));
}
else
{
@ -484,23 +488,10 @@ class Conductor
// which it doesn't do every frame!
if (prevTime != this.songPosition)
{
this.songPositionDelta = 0;
// Update the timestamp for use in-between frames
prevTime = this.songPosition;
prevTimestamp = Std.int(Timer.stamp() * 1000);
}
if (this == Conductor.instance) @:privateAccess SongSequence.update.dispatch();
}
/**
* Returns a more accurate music time for higher framerates.
* @return Float
*/
public function getTimeWithDelta():Float
{
return this.songPosition + this.songPositionDelta;
}
/**

View file

@ -1,428 +0,0 @@
package funkin;
import flixel.graphics.FlxGraphic;
import flixel.FlxG;
import funkin.play.notes.notestyle.NoteStyle;
import openfl.utils.AssetType;
import openfl.Assets;
import openfl.system.System;
import openfl.media.Sound;
import lime.app.Future;
import lime.app.Promise;
/**
* Handles caching of textures and sounds for the game.
* TODO: Remove this once Eric finishes the memory system.
*/
@:nullSafety
class FunkinMemory
{
static var permanentCachedTextures:Map<String, FlxGraphic> = [];
static var currentCachedTextures:Map<String, FlxGraphic> = [];
static var previousCachedTextures:Map<String, FlxGraphic> = [];
// waow
static var permanentCachedSounds:Map<String, Sound> = [];
static var currentCachedSounds:Map<String, Sound> = [];
static var previousCachedSounds:Map<String, Sound> = [];
static var purgeFilter:Array<String> = ["/week", "/characters", "/charSelect", "/results"];
/**
* Caches textures that are always required.
*/
public static inline function initialCache():Void
{
var allImages:Array<String> = Assets.list();
for (file in allImages)
{
if (!(file.endsWith(".png") #if FEATURE_COMPRESSED_TEXTURES || file.endsWith(".astc") #end)
|| file.contains("chart-editor")
|| !file.contains("ui/"))
{
continue;
}
file = file.replace(" ", ""); // Handle stray spaces.
if (file.contains("shared") || Assets.exists('shared:$file', AssetType.IMAGE))
{
file = 'shared:$file';
}
permanentCacheTexture(file);
}
permanentCacheTexture(Paths.image("healthBar"));
permanentCacheTexture(Paths.image("menuDesat"));
permanentCacheTexture(Paths.image("notes", "shared"));
permanentCacheTexture(Paths.image("noteSplashes", "shared"));
permanentCacheTexture(Paths.image("noteStrumline", "shared"));
permanentCacheTexture(Paths.image("NOTE_hold_assets"));
// dude
permanentCacheTexture(Paths.image("fonts/bold", null));
permanentCacheTexture(Paths.image("fonts/default", null));
permanentCacheTexture(Paths.image("fonts/freeplay-clear", null));
var allSounds:Array<String> = Assets.list(AssetType.SOUND);
for (file in allSounds)
{
if (!file.endsWith(".ogg") || !file.contains("countdown/")) continue;
file = file.replace(" ", "");
if (file.contains("shared") || Assets.exists('shared:$file', AssetType.SOUND))
{
file = 'shared:$file';
}
permanentCacheSound(file);
}
permanentCacheSound(Paths.sound("cancelMenu"));
permanentCacheSound(Paths.sound("confirmMenu"));
permanentCacheSound(Paths.sound("screenshot"));
permanentCacheSound(Paths.sound("scrollMenu"));
permanentCacheSound(Paths.sound("soundtray/Voldown"));
permanentCacheSound(Paths.sound("soundtray/VolMAX"));
permanentCacheSound(Paths.sound("soundtray/Volup"));
permanentCacheSound(Paths.music("freakyMenu/freakyMenu"));
permanentCacheSound(Paths.music("offsetsLoop/offsetsLoop"));
permanentCacheSound(Paths.music("offsetsLoop/drumsLoop"));
permanentCacheSound(Paths.sound("missnote1", "shared"));
permanentCacheSound(Paths.sound("missnote2", "shared"));
permanentCacheSound(Paths.sound("missnote3", "shared"));
}
/**
* Clears the current texture and sound caches.
*/
public static inline function purgeCache(callGarbageCollector:Bool = false):Void
{
preparePurgeTextureCache();
purgeTextureCache();
preparePurgeSoundCache();
purgeSoundCache();
#if (cpp || neko || hl)
if (callGarbageCollector) funkin.util.MemoryUtil.collect(true);
#end
}
///// TEXTURES /////
/**
* Ensures a texture with the given key is cached.
* @param key The key of the texture to cache.
*/
public static function cacheTexture(key:String):Void
{
if (currentCachedTextures.exists(key))
{
return; // Already cached.
}
if (previousCachedTextures.exists(key))
{
// Move the texture from the previous cache to the current cache.
var graphic:Null<FlxGraphic> = previousCachedTextures.get(key);
previousCachedTextures.remove(key);
if (graphic != null) currentCachedTextures.set(key, graphic);
return;
}
var graphic:Null<FlxGraphic> = FlxGraphic.fromAssetKey(key, false, null, true);
if (graphic == null)
{
FlxG.log.warn('Failed to cache graphic: $key');
}
else
{
trace('Successfully cached graphic: $key');
graphic.persist = true;
currentCachedTextures.set(key, graphic);
}
}
/**
* Permanently caches a texture with the given key.
* @param key The key of the texture to cache.
*/
static function permanentCacheTexture(key:String):Void
{
if (permanentCachedTextures.exists(key))
{
return; // Already cached.
}
var graphic:Null<FlxGraphic> = FlxGraphic.fromAssetKey(key, false, null, true);
if (graphic == null)
{
FlxG.log.warn('Failed to cache graphic: $key');
}
else
{
trace('Successfully cached graphic: $key');
graphic.persist = true;
permanentCachedTextures.set(key, graphic);
}
currentCachedTextures = permanentCachedTextures;
}
/**
* Prepares the cache for purging unused textures.
*/
public inline static function preparePurgeTextureCache():Void
{
previousCachedTextures = currentCachedTextures;
for (graphicKey in previousCachedTextures.keys())
{
if (permanentCachedTextures.exists(graphicKey))
{
previousCachedTextures.remove(graphicKey);
}
}
currentCachedTextures = permanentCachedTextures;
}
/**
* Purges unused textures from the cache.
*/
public static function purgeTextureCache():Void
{
for (graphicKey in previousCachedTextures.keys())
{
if (permanentCachedTextures.exists(graphicKey))
{
previousCachedTextures.remove(graphicKey);
continue;
}
if (graphicKey.contains("fonts")) continue;
var graphic:Null<FlxGraphic> = previousCachedTextures.get(graphicKey);
if (graphic != null)
{
FlxG.bitmap.remove(graphic);
graphic.destroy();
previousCachedTextures.remove(graphicKey);
Assets.cache.clear(graphicKey);
}
}
@:privateAccess
if (FlxG.bitmap._cache == null)
{
@:privateAccess
FlxG.bitmap._cache = new Map();
}
@:privateAccess
for (key in FlxG.bitmap._cache.keys())
{
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj == null || obj.persist || permanentCachedTextures.exists(key) || key.contains("fonts"))
{
continue;
}
if (obj.useCount > 0)
{
for (purgeEntry in purgeFilter)
{
if (key.contains(purgeEntry))
{
FlxG.bitmap.removeKey(key);
obj.destroy();
}
}
}
}
}
///// NOTE STYLE //////
public static function cacheNoteStyle(style:NoteStyle):Void
{
// TODO: Texture paths should fall back to the default values.
cacheTexture(Paths.image(style.getNoteAssetPath() ?? "note"));
cacheTexture(style.getHoldNoteAssetPath() ?? "noteHold");
cacheTexture(Paths.image(style.getStrumlineAssetPath() ?? "strumline"));
cacheTexture(Paths.image(style.getSplashAssetPath() ?? "noteSplash"));
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(LEFT) ?? "LEFT"));
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(RIGHT) ?? "RIGHT"));
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(UP) ?? "UP"));
cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(DOWN) ?? "DOWN"));
// cacheTexture(Paths.image(style.buildCountdownSpritePath(THREE) ?? "THREE"));
cacheTexture(Paths.image(style.buildCountdownSpritePath(TWO) ?? "TWO"));
cacheTexture(Paths.image(style.buildCountdownSpritePath(ONE) ?? "ONE"));
cacheTexture(Paths.image(style.buildCountdownSpritePath(GO) ?? "GO"));
cacheSound(style.getCountdownSoundPath(THREE) ?? "THREE");
cacheSound(style.getCountdownSoundPath(TWO) ?? "TWO");
cacheSound(style.getCountdownSoundPath(ONE) ?? "ONE");
cacheSound(style.getCountdownSoundPath(GO) ?? "GO");
cacheTexture(Paths.image(style.buildJudgementSpritePath("sick") ?? 'sick'));
cacheTexture(Paths.image(style.buildJudgementSpritePath("good") ?? 'good'));
cacheTexture(Paths.image(style.buildJudgementSpritePath("bad") ?? 'bad'));
cacheTexture(Paths.image(style.buildJudgementSpritePath("shit") ?? 'shit'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(0) ?? '0'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(1) ?? '1'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(2) ?? '2'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(3) ?? '3'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(4) ?? '4'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(5) ?? '5'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(6) ?? '6'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(7) ?? '7'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(8) ?? '8'));
cacheTexture(Paths.image(style.buildComboNumSpritePath(9) ?? '9'));
}
///// SOUND //////
public static function cacheSound(key:String):Void
{
if (currentCachedSounds.exists(key)) return;
if (previousCachedSounds.exists(key))
{
// Move the texture from the previous cache to the current cache.
var sound:Null<Sound> = previousCachedSounds.get(key);
previousCachedSounds.remove(key);
if (sound != null) currentCachedSounds.set(key, sound);
return;
}
var sound:Null<Sound> = Assets.getSound(key, true);
if (sound == null) return;
else
currentCachedSounds.set(key, sound);
}
public static function permanentCacheSound(key:String):Void
{
if (permanentCachedSounds.exists(key)) return;
var sound:Null<Sound> = Assets.getSound(key, true);
if (sound == null) return;
else
permanentCachedSounds.set(key, sound);
if (sound != null) currentCachedSounds.set(key, sound);
}
public static function preparePurgeSoundCache():Void
{
previousCachedSounds = currentCachedSounds;
for (key in previousCachedSounds.keys())
{
if (permanentCachedSounds.exists(key))
{
previousCachedSounds.remove(key);
}
}
currentCachedSounds = permanentCachedSounds;
}
/**
* Purges unused sounds from the cache.
*/
public static inline function purgeSoundCache():Void
{
for (key in previousCachedSounds.keys())
{
if (permanentCachedSounds.exists(key))
{
previousCachedSounds.remove(key);
continue;
}
var sound:Null<Sound> = previousCachedSounds.get(key);
if (sound != null)
{
Assets.cache.removeSound(key);
previousCachedSounds.remove(key);
}
}
Assets.cache.clear("songs");
Assets.cache.clear("music");
// Felt lazy.
var key = Paths.music("freakyMenu/freakyMenu");
var sound:Null<Sound> = Assets.getSound(key, true);
if (sound != null)
{
permanentCachedSounds.set(key, sound);
currentCachedSounds.set(key, sound);
}
}
///// MISC /////
public static inline function clearFreeplay():Void
{
var keysToRemove:Array<String> = [];
@:privateAccess
for (key in FlxG.bitmap._cache.keys())
{
if (!key.contains("freeplay")) continue;
if (permanentCachedTextures.exists(key) || key.contains("fonts")) continue;
keysToRemove.push(key);
}
@:privateAccess
for (key in keysToRemove)
{
trace('Cleaning up $key');
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj != null)
{
obj.destroy();
}
FlxG.bitmap.removeKey(key);
if (currentCachedTextures.exists(key)) currentCachedTextures.remove(key);
Assets.cache.clear(key);
}
preparePurgeSoundCache();
purgeSoundCache();
}
public static inline function clearStickers():Void
{
var keysToRemove:Array<String> = [];
@:privateAccess
for (key in FlxG.bitmap._cache.keys())
{
if (!key.contains("stickers")) continue;
if (permanentCachedTextures.exists(key) || key.contains("fonts")) continue;
keysToRemove.push(key);
}
@:privateAccess
for (key in keysToRemove)
{
trace('Cleaning up $key');
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj != null)
{
obj.destroy();
}
FlxG.bitmap.removeKey(key);
if (currentCachedTextures.exists(key)) currentCachedTextures.remove(key);
Assets.cache.clear(key);
}
}
}

View file

@ -3,7 +3,6 @@ package funkin;
/**
* A core class which handles tracking score and combo for the current song.
*/
@:nullSafety
class Highscore
{
/**

View file

@ -10,7 +10,6 @@ import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.system.debug.log.LogStyle;
import flixel.util.FlxColor;
import funkin.graphics.FunkinSprite;
import funkin.data.dialogue.conversation.ConversationRegistry;
import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry;
import funkin.data.dialogue.speaker.SpeakerRegistry;
@ -28,12 +27,12 @@ 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.debug.stageeditor.StageEditorState;
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;
@ -52,7 +51,6 @@ import funkin.api.newgrounds.NewgroundsClient;
*
* It should not contain any sprites or rendering.
*/
@:nullSafety
class InitState extends FlxState
{
/**
@ -92,31 +90,6 @@ class InitState extends FlxState
funkin.util.WindowUtil.initTracy();
#end
#if FEATURE_HAPTICS
// Setup Haptic feedback
extension.haptics.Haptic.initialize();
#end
#if FEATURE_MOBILE_ADVERTISEMENTS
// Setup Admob
funkin.mobile.util.AdMobUtil.init();
#end
#if FEATURE_MOBILE_IAP
// Setup In-App purchases
funkin.mobile.util.InAppPurchasesUtil.init();
#end
#if FEATURE_MOBILE_IAR
// Setup In-App purchases
funkin.mobile.util.InAppReviewUtil.init();
#end
#if ios
// Setup Audio session
funkin.mobile.external.ios.AudioSession.initialize();
#end
// This ain't a pixel art game! (most of the time)
FlxSprite.defaultAntialiasing = true;
@ -129,15 +102,13 @@ class InitState extends FlxState
// 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
#if !mobile
FlxG.game.soundTray.active = true;
#end
// 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;
FlxG.fixedTimestep = false;
setupFlixelDebug();
@ -160,25 +131,6 @@ class InitState extends FlxState
// Don't play transition in when entering the title state.
FlxTransitionableState.skipNextTransIn = true;
FlxG.signals.gameResized.add(function(width:Int, height:Int) {
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));
});
// SDL for some reason enables VSync on focus lost/gained in Android
// Since we don't really need VSync on Android we're gonna forcefully disable it on these signals for now
// This is fixed on SDL3 from what I've heared but that doodoo isn't working poperly for Android
#if android
FlxG.signals.focusLost.add(function() {
WindowUtil.setVSyncMode(lime.ui.WindowVSyncMode.OFF);
});
FlxG.signals.focusGained.add(function() {
WindowUtil.setVSyncMode(lime.ui.WindowVSyncMode.OFF);
});
#end
//
// NEWGROUNDS API SETUP
//
@ -202,7 +154,6 @@ class InitState extends FlxState
//
#if android
FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
funkin.mobile.external.android.CallbackUtil.init();
#end
//
@ -217,20 +168,12 @@ class InitState extends FlxState
#if FEATURE_SCREENSHOTS
funkin.util.plugins.ScreenshotPlugin.initialize();
#end
#if FEATURE_NEWGROUNDS
funkin.util.plugins.NewgroundsMedalPlugin.initialize();
#end
funkin.util.plugins.EvacuateDebugPlugin.initialize();
funkin.util.plugins.ForceCrashPlugin.initialize();
funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
#if !mobile
funkin.util.plugins.VolumePlugin.initialize();
#end
funkin.util.plugins.WatchPlugin.initialize();
#if mobile
funkin.util.plugins.TouchPointerPlugin.initialize();
funkin.mobile.input.ControlsHandler.initInputTrackers();
#end
//
// GAME DATA PARSING
@ -239,6 +182,7 @@ class InitState extends FlxState
// 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();
@ -264,10 +208,7 @@ class InitState extends FlxState
funkin.input.Cursor.hide();
#if !html5
// This fucking breaks on HTML5 builds because the "shared" library isn't loaded yet.
funkin.FunkinMemory.initialCache();
#end
trace('Parsing game data took: ${TimerUtil.ms(perfStart)}');
}
/**
@ -300,9 +241,6 @@ class InitState extends FlxState
#elseif CHARTING
// -DCHARTING
FlxG.switchState(() -> new funkin.ui.debug.charting.ChartEditorState());
#elseif STAGING
// -DSTAGING
FlxG.switchState(() -> new funkin.ui.debug.stageeditor.StageEditorState());
#elseif STAGEBUILD
// -DSTAGEBUILD
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
@ -365,16 +303,9 @@ class InitState extends FlxState
fnfcTargetPath: params.chart.chartPath,
}));
}
else if (params.stage.shouldLoadStage)
{
FlxG.switchState(() -> new StageEditorState(
{
fnfsTargetPath: params.stage.stagePath,
}));
}
else
{
// FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
FlxG.switchState(() -> new TitleState());
}
}
@ -386,7 +317,7 @@ class InitState extends FlxState
*/
function startSong(songId:String, difficultyId:String = 'normal'):Void
{
var songData:Null<funkin.play.song.Song> = funkin.data.song.SongRegistry.instance.fetchEntry(songId);
var songData:funkin.play.song.Song = funkin.data.song.SongRegistry.instance.fetchEntry(songId);
if (songData == null)
{
@ -423,7 +354,6 @@ class InitState extends FlxState
PlayStatePlaylist.campaignId = 'weekend1';
}
@:nullSafety(Off) // Cannot unify?
LoadingState.loadPlayState(
{
targetSong: songData,
@ -438,7 +368,7 @@ class InitState extends FlxState
*/
function startLevel(levelId:String, difficultyId:String = 'normal'):Void
{
var currentLevel:Null<funkin.ui.story.Level> = funkin.data.story.level.LevelRegistry.instance.fetchEntry(levelId);
var currentLevel:funkin.ui.story.Level = funkin.data.story.level.LevelRegistry.instance.fetchEntry(levelId);
if (currentLevel == null)
{
@ -454,19 +384,10 @@ class InitState extends FlxState
PlayStatePlaylist.isStoryMode = true;
PlayStatePlaylist.campaignScore = 0;
var targetSongId:Null<String> = PlayStatePlaylist.playlistSongIds.shift();
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
var targetSong:Null<funkin.play.song.Song> = null;
var targetSong:funkin.play.song.Song = SongRegistry.instance.fetchEntry(targetSongId);
if (targetSongId != null) targetSong = SongRegistry.instance.fetchEntry(targetSongId);
if (targetSongId == null)
{
startGameNormally();
return;
}
@:nullSafety(Off)
LoadingState.loadPlayState(
{
targetSong: targetSong,
@ -474,7 +395,6 @@ class InitState extends FlxState
});
}
@:nullSafety(Off) // Meh, remove when flixel.system.debug.log.LogStyle is null safe
function setupFlixelDebug():Void
{
//
@ -554,17 +474,17 @@ class InitState extends FlxState
#end
}
function defineSong():Null<String>
function defineSong():String
{
return MacroUtil.getDefine('SONG');
}
function defineLevel():Null<String>
function defineLevel():String
{
return MacroUtil.getDefine('LEVEL');
}
function defineDifficulty():Null<String>
function defineDifficulty():String
{
return MacroUtil.getDefine('DIFFICULTY');
}

View file

@ -6,7 +6,6 @@ import openfl.utils.AssetType;
/**
* A core class which handles determining asset paths.
*/
@:nullSafety
class Paths
{
static var currentLevel:Null<String> = null;
@ -137,7 +136,7 @@ class Paths
* @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
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';

View file

@ -9,17 +9,12 @@ import flixel.util.FlxSignal.FlxTypedSignal;
/**
* A core class which represents the current player(s) and their controls and other configuration.
*/
@:nullSafety
class PlayerSettings
{
// TODO: Finish implementation of second player.
public static var numPlayers(default, null) = 0;
public static var numAvatars(default, null) = 0;
// TODO: Making both of these null makes a lot of errors with the controls.
// That'd explain why unplugging input devices can cause the game to crash?
@:nullSafety(Off)
public static var player1(default, null):PlayerSettings;
@:nullSafety(Off)
public static var player2(default, null):PlayerSettings;
public static var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
@ -75,7 +70,6 @@ class PlayerSettings
/**
* Forcibly destroy the PlayerSettings singletons for each player.
*/
@:nullSafety(Off)
public static function reset():Void
{
player1 = null;

View file

@ -1,22 +1,15 @@
package funkin;
#if mobile
import funkin.mobile.ui.FunkinHitbox;
import funkin.mobile.util.InAppPurchasesUtil;
#end
import funkin.save.Save;
import funkin.util.WindowUtil;
import funkin.util.HapticUtil.HapticsMode;
/**
* A core class which provides a store of user-configurable, globally relevant values.
*/
@:nullSafety
class Preferences
{
/**
* FPS
* Always the refresh rate of the display on mobile, or 60 on web.
* @default `60`
*/
public static var framerate(get, set):Int;
@ -25,12 +18,6 @@ class Preferences
{
#if web
return 60;
#elseif mobile
var refreshRate:Int = FlxG.stage.window.displayMode.refreshRate;
if (refreshRate < 60) refreshRate = 60;
return refreshRate;
#else
return Save?.instance?.options?.framerate ?? 60;
#end
@ -58,19 +45,11 @@ class Preferences
static function get_naughtyness():Bool
{
#if NO_FEATURE_NAUGHTYNESS
return false;
#else
return Save?.instance?.options?.naughtyness ?? true;
#end
return Save?.instance?.options?.naughtyness;
}
static function set_naughtyness(value:Bool):Bool
{
#if NO_FEATURE_NAUGHTYNESS
value = false;
#end
var save:Save = Save.instance;
save.options.naughtyness = value;
save.flush();
@ -85,7 +64,7 @@ class Preferences
static function get_downscroll():Bool
{
return Save?.instance?.options?.downscroll #if mobile ?? true #else ?? false #end;
return Save?.instance?.options?.downscroll;
}
static function set_downscroll(value:Bool):Bool
@ -123,7 +102,7 @@ class Preferences
static function get_zoomCamera():Bool
{
return Save?.instance?.options?.zoomCamera ?? true;
return Save?.instance?.options?.zoomCamera;
}
static function set_zoomCamera(value:Bool):Bool
@ -136,17 +115,13 @@ class Preferences
/**
* If enabled, an FPS and memory counter will be displayed even if this is not a debug build.
* Always disabled on mobile.
* @default `false`
*/
public static var debugDisplay(get, set):Bool;
static function get_debugDisplay():Bool
{
#if mobile
return false;
#end
return Save?.instance?.options?.debugDisplay ?? false;
return Save?.instance?.options?.debugDisplay;
}
static function set_debugDisplay(value:Bool):Bool
@ -162,78 +137,14 @@ class Preferences
return value;
}
/**
* If enabled, haptic feedback will be enabled.
* @default `All`
*/
public static var hapticsMode(get, set):HapticsMode;
static function get_hapticsMode():HapticsMode
{
var value = Save?.instance?.options?.hapticsMode ?? "All";
return switch (value)
{
case "None":
HapticsMode.NONE;
case "Notes Only":
HapticsMode.NOTES_ONLY;
default:
HapticsMode.ALL;
};
}
static function set_hapticsMode(value:HapticsMode):HapticsMode
{
var string;
switch (value)
{
case HapticsMode.NONE:
string = "None";
case HapticsMode.NOTES_ONLY:
string = "Notes Only";
default:
string = "All";
};
var save:Save = Save.instance;
save.options.hapticsMode = string;
save.flush();
return value;
}
/**
* Multiplier of intensity for all the haptic feedback effects.
* @default `2.5`
*/
public static var hapticsIntensityMultiplier(get, set):Float;
static function get_hapticsIntensityMultiplier():Float
{
return Save?.instance?.options?.hapticsIntensityMultiplier ?? 1;
}
static function set_hapticsIntensityMultiplier(value:Float):Float
{
var save:Save = Save.instance;
save.options.hapticsIntensityMultiplier = value;
save.flush();
return value;
}
/**
* If enabled, the game will automatically pause when tabbing out.
* Always enabled on mobile.
* @default `true`
*/
public static var autoPause(get, set):Bool;
static function get_autoPause():Bool
{
#if mobile
return true;
#end
return Save?.instance?.options?.autoPause ?? true;
}
@ -266,26 +177,6 @@ class Preferences
return value;
}
/**
* A global audio offset in milliseconds.
* This is used to sync the audio.
* @default `0`
*/
public static var globalOffset(get, set):Int;
static function get_globalOffset():Int
{
return Save?.instance?.options?.globalOffset ?? 0;
}
static function set_globalOffset(value:Int):Int
{
var save:Save = Save.instance;
save.options.globalOffset = value;
save.flush();
return value;
}
/**
* If enabled, the game will utilize VSync (or adaptive VSync) on startup.
* @default `OFF`
@ -337,7 +228,7 @@ class Preferences
static function get_unlockedFramerate():Bool
{
return Save?.instance?.options?.unlockedFramerate ?? false;
return Save?.instance?.options?.unlockedFramerate;
}
static function set_unlockedFramerate(value:Bool):Bool
@ -452,6 +343,44 @@ class Preferences
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.
*/
@ -465,16 +394,8 @@ class Preferences
#if web
toggleFramerateCap(Preferences.unlockedFramerate);
#end
#if desktop
// Apply the autoFullscreen setting (launches the game in fullscreen automatically)
FlxG.fullscreen = Preferences.autoFullscreen;
#end
#if mobile
// Apply the allowScreenTimeout setting.
lime.system.System.allowScreenTimeout = Preferences.screenTimeout;
#end
}
static function toggleFramerateCap(unlocked:Bool):Void
@ -490,88 +411,18 @@ class Preferences
if (show)
{
// Enable the debug display.
FlxG.game.parent.addChild(Main.fpsCounter);
FlxG.stage.addChild(Main.fpsCounter);
#if !html5
FlxG.game.parent.addChild(Main.memoryCounter);
FlxG.stage.addChild(Main.memoryCounter);
#end
}
else
{
// Disable the debug display.
FlxG.game.parent.removeChild(Main.fpsCounter);
FlxG.stage.removeChild(Main.fpsCounter);
#if !html5
FlxG.game.parent.removeChild(Main.memoryCounter);
FlxG.stage.removeChild(Main.memoryCounter);
#end
}
}
#if mobile
/**
* If enabled, device will be able to sleep on its own.
* @default `false`
*/
public static var screenTimeout(get, set):Bool;
static function get_screenTimeout():Bool
{
return Save?.instance?.mobileOptions?.screenTimeout ?? false;
}
static function set_screenTimeout(value:Bool):Bool
{
if (value != Save.instance.mobileOptions.screenTimeout) lime.system.System.allowScreenTimeout = value;
var save:Save = Save.instance;
save.mobileOptions.screenTimeout = value;
save.flush();
return value;
}
/**
* Controls Scheme for the hitbox.
* @default `4 Lanes`
*/
public static var controlsScheme(get, set):String;
static function get_controlsScheme():String
{
return Save?.instance?.mobileOptions?.controlsScheme ?? FunkinHitboxControlSchemes.Arrows;
}
static function set_controlsScheme(value:String):String
{
var save:Save = Save.instance;
save.mobileOptions.controlsScheme = value;
save.flush();
return value;
}
#if FEATURE_MOBILE_IAP
/**
* If bought, the game will not show any ads.
* @default `false`
*/
@:unreflective
public static var noAds(get, set):Bool;
@:unreflective
static function get_noAds():Bool
{
if (InAppPurchasesUtil.hasInitialized) noAds = InAppPurchasesUtil.isPurchased(InAppPurchasesUtil.UPGRADE_PRODUCT_ID);
var returnedValue = Save?.instance?.mobileOptions?.noAds ?? false;
return returnedValue;
}
@:unreflective
static function set_noAds(value:Bool):Bool
{
var save:Save = Save.instance;
save.mobileOptions.noAds = value;
save.flush();
return value;
}
#end
#end
}

View file

@ -1,6 +1,5 @@
package funkin.api.discord;
import funkin.util.macro.EnvironmentConfigMacro;
#if FEATURE_DISCORD_RPC
import hxdiscord_rpc.Discord;
import hxdiscord_rpc.Types.DiscordButton;
@ -9,10 +8,9 @@ import hxdiscord_rpc.Types.DiscordRichPresence;
import hxdiscord_rpc.Types.DiscordUser;
import sys.thread.Thread;
@:nullSafety
class DiscordClient
{
static final CLIENT_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("DESKTOP_DISCORD_CLIENT_ID");
static final CLIENT_ID:String = "816168432860790794";
public static var instance(get, never):DiscordClient;
static var _instance:Null<DiscordClient> = null;
@ -41,29 +39,13 @@ class DiscordClient
{
trace('[DISCORD] Initializing connection...');
if (!hasValidCredentials())
{
FlxG.log.warn("Tried to initialize Discord connection, but credentials are invalid!");
return;
}
@:nullSafety(Off)
{
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, "");
}
// Discord.initialize(CLIENT_ID, handlers, true, null);
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, null);
createDaemon();
}
/**
* @returns `false` if the client ID is invalid.
*/
static function hasValidCredentials():Bool
{
return !(CLIENT_ID == null || CLIENT_ID == "" || (CLIENT_ID != null && CLIENT_ID.contains(" ")));
}
var daemon:Null<Thread> = null;
var daemon:Thread = null;
function createDaemon():Void
{
@ -74,6 +56,8 @@ class DiscordClient
{
while (true)
{
trace('[DISCORD] Performing client update...');
#if DISCORD_DISABLE_IO_THREAD
Discord.updateConnection();
#end
@ -92,6 +76,8 @@ class DiscordClient
public function setPresence(params:DiscordClientPresenceParams):Void
{
trace('[DISCORD] Updating presence... (${params})');
Discord.updatePresence(buildPresence(params));
}
@ -106,15 +92,17 @@ class DiscordClient
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>) ?? "";
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>) ?? "";
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";
@ -122,7 +110,7 @@ class DiscordClient
// 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>) ?? "";
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;
@ -148,9 +136,9 @@ class DiscordClient
final username:String = request[0].username;
final globalName:String = request[0].username;
final discriminator:Null<Int> = Std.parseInt(request[0].discriminator);
final discriminator:Int = Std.parseInt(request[0].discriminator);
if (discriminator != null && discriminator != 0)
if (discriminator != 0)
{
trace('[DISCORD] User: ${username}#${discriminator} (${globalName})');
}
@ -216,30 +204,4 @@ typedef DiscordClientPresenceParams =
*/
var ?smallImageKey:String;
}
class DiscordClientSandboxed
{
public static function setPresence(params:DiscordClientPresenceParams):Void
{
DiscordClient.instance.setPresence(params);
}
public static function shutdown():Void
{
DiscordClient.instance.shutdown();
}
}
#else
class DiscordClientSandboxed
{
public static function setPresence(params:Dynamic):Void
{
// Do nothing.
}
public static function shutdown():Void
{
// Do nothing.
}
}
#end

View file

@ -1,11 +1,9 @@
package funkin.api.newgrounds;
#if FEATURE_NEWGROUNDS_EVENTS
import io.newgrounds.Call.CallOutcome;
import io.newgrounds.NG;
import io.newgrounds.objects.events.Outcome;
import io.newgrounds.objects.events.Result;
#end
/**
* Use Newgrounds to perform basic telemetry. Ignore if not logged in to Newgrounds.
@ -33,7 +31,6 @@ class Events
#end
}
#if FEATURE_NEWGROUNDS_EVENTS
static function onEventLogged(eventName:String, outcome:CallOutcome<LogEventData>)
{
switch (outcome)
@ -58,7 +55,6 @@ class Events
}
}
}
#end
public static inline function logStartGame():Void
{

View file

@ -2,14 +2,10 @@ package funkin.api.newgrounds;
#if FEATURE_NEWGROUNDS
import io.newgrounds.Call.CallError;
import io.newgrounds.components.ScoreBoardComponent;
import io.newgrounds.objects.Score;
import io.newgrounds.objects.ScoreBoard as LeaderboardData;
import io.newgrounds.objects.User;
import io.newgrounds.objects.events.Outcome;
import io.newgrounds.utils.ScoreBoardList;
@:nullSafety
class Leaderboards
{
public static function listLeaderboardData():Map<Leaderboard, LeaderboardData>
@ -20,8 +16,21 @@ class Leaderboards
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
return [];
}
else
{
var result:Map<Leaderboard, LeaderboardData> = [];
return @:privateAccess leaderboardList._map?.copy() ?? [];
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;
}
}
/**
@ -57,41 +66,6 @@ class Leaderboards
}
}
/**
* Request to receive scores from Newgrounds.
* @param leaderboard The leaderboard to fetch scores from.
* @param params Additional parameters for fetching the score.
*/
public static function requestScores(leaderboard:Leaderboard, ?params:RequestScoresParams)
{
// Silently reject retrieving scores from unknown leaderboards.
if (leaderboard == Leaderboard.Unknown) return;
var leaderboardList = NewgroundsClient.instance.leaderboards;
if (leaderboardList == null) return;
var leaderboardData:Null<LeaderboardData> = leaderboardList.get(leaderboard.getId());
if (leaderboardData == null) return;
var user:Null<User> = null;
if ((params?.useCurrentUser ?? false) && NewgroundsClient.instance.isLoggedIn()) user = NewgroundsClient.instance.user;
leaderboardData.requestScores(params?.limit ?? 10, params?.skip ?? 0, params?.period ?? ALL, params?.social ?? false, params?.tag, user,
function(outcome:Outcome<CallError>):Void {
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Fetched scores!');
if (params != null && params.onComplete != null) params.onComplete(leaderboardData.scores);
case FAIL(error):
trace('[NEWGROUNDS] Failed to fetch scores!');
trace(error);
if (params != null && params.onFail != null) params.onFail();
}
});
}
/**
* Submit a score for a Story Level to Newgrounds.
*/
@ -110,77 +84,9 @@ class Leaderboards
Leaderboards.submitScore(Leaderboard.getLeaderboardBySong(songId, difficultyId), score, tag);
}
}
/**
* Wrapper for `Leaderboards` that prevents submitting scores.
*/
@:nullSafety
class LeaderboardsSandboxed
{
public static function getLeaderboardBySong(songId:String, difficultyId:String)
{
return Leaderboard.getLeaderboardBySong(songId, difficultyId);
}
public static function getLeaderboardByLevel(levelId:String)
{
return Leaderboard.getLeaderboardByLevel(levelId);
}
public function requestScores(leaderboard:Leaderboard, params:RequestScoresParams)
{
Leaderboards.requestScores(leaderboard, params);
}
}
/**
* Additional parameters for `Leaderboards.requestScores()`
*/
typedef RequestScoresParams =
{
/**
* How many scores to include in a list.
* @default `10`
*/
var ?limit:Int;
/**
* How many scores to skip before starting the list.
* @default `0`
*/
var ?skip:Int;
/**
* The time-frame to pull the scores from.
* @default `Period.ALL`
*/
var ?period:Period;
/**
* If true, only scores by the user and their friends will be loaded. Ignored if no user is set.
* @default `false`
*/
var ?social:Bool;
/**
* An optional tag to filter the results by.
* @default `null`
*/
var ?tag:String;
/**
* If true, only the scores from the currently logged in user will be loaded.
* Additionally, if `social` is set to true, the scores of the user's friend will be loaded.
* @default `false`
*/
var ?useCurrentUser:Bool;
var ?onComplete:Array<Score>->Void;
var ?onFail:Void->Void;
}
#end
enum abstract Leaderboard(Int) from Int to Int
enum abstract Leaderboard(Int)
{
/**
* Represents an undefined or invalid leaderboard.
@ -379,7 +285,7 @@ enum abstract Leaderboard(Int) from Int to Int
{
case "darnell":
return DarnellBFMix;
case "lit-up":
case "litup":
return LitUpBFMix;
default:
return Unknown;
@ -473,7 +379,7 @@ enum abstract Leaderboard(Int) from Int to Int
return Stress;
case "darnell":
return Darnell;
case "lit-up":
case "litup":
return LitUp;
case "2hot":
return TwoHot;

View file

@ -8,7 +8,6 @@ import openfl.display.BitmapData;
import io.newgrounds.utils.MedalList;
import haxe.Json;
@:nullSafety
class Medals
{
public static var medalJSON:Array<MedalJSON> = [];
@ -22,8 +21,22 @@ class Medals
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> = [];
return @:privateAccess medalList._map?.copy() ?? [];
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
@ -118,79 +131,33 @@ class Medals
}
}
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
{
var medalList = NewgroundsClient.instance.medals;
@:privateAccess
if (medalList == null || medalList._map == null) return null;
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 null;
}
return {
id: medalData.id,
name: medalData.name,
description: medalData.description,
icon: medalData.icon,
value: medalData.value,
difficulty: medalData.difficulty,
secret: medalData.secret,
unlocked: medalData.unlocked
}
}
public static function awardStoryLevel(id:String):Void
{
var medal:Medal = Medal.getMedalByStoryLevel(id);
if (medal == Medal.Unknown)
switch (id)
{
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
return;
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}).');
}
Medals.award(medal);
}
}
/**
* Wrapper for `Medals` that prevents awarding medals.
*/
class MedalsSandboxed
{
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
{
return Medals.fetchMedalData(medal);
}
public static function getMedalByStoryLevel(id:String):Medal
{
return Medal.getMedalByStoryLevel(id);
}
public static function getAllMedals():Array<Medal>
{
return Medal.getAllMedals();
}
}
/**
* Contains data for a Medal, but excludes functions like `sendUnlock()`.
*/
typedef FetchedMedalData =
{
var id:Int;
var name:String;
var description:String;
var icon:String;
var value:Int;
var difficulty:Int;
var secret:Bool;
var unlocked:Bool;
}
#end
/**
@ -357,8 +324,6 @@ enum abstract Medal(Int) from Int to Int
{
switch (levelId)
{
case "tutorial":
return StoryTutorial;
case "week1":
return StoryWeek1;
case "week2":
@ -379,33 +344,4 @@ enum abstract Medal(Int) from Int to Int
return Unknown;
}
}
/**
* Lists all medals aside from the `Unknown` one.
*/
public static function getAllMedals()
{
return [
StartGame,
StoryTutorial,
StoryWeek1,
StoryWeek2,
StoryWeek3,
StoryWeek4,
StoryWeek5,
StoryWeek6,
StoryWeek7,
StoryWeekend1,
CharSelect,
FreeplayPicoMix,
FreeplayStressPico,
LossRating,
PerfectRatingHard,
GoldPerfectRatingHard,
ErectDifficulty,
GoldPerfectRatingNightmare,
FridayNight,
Nice
];
}
}

View file

@ -1,156 +0,0 @@
package funkin.api.newgrounds;
#if FEATURE_NEWGROUNDS
import io.newgrounds.utils.SaveSlotList;
import io.newgrounds.objects.SaveSlot;
import io.newgrounds.Call.CallError;
import io.newgrounds.objects.events.Outcome;
import funkin.save.Save;
@:nullSafety
@:access(funkin.save.Save)
class NGSaveSlot
{
public static var instance(get, never):NGSaveSlot;
static var _instance:Null<NGSaveSlot> = null;
static function get_instance():NGSaveSlot
{
if (_instance == null)
{
return loadInstance();
}
return _instance;
}
public static function loadInstance():NGSaveSlot
{
var loadedSave:NGSaveSlot = loadSlot(Save.BASE_SAVE_SLOT);
if (_instance == null) _instance = loadedSave;
return loadedSave;
}
static function loadSlot(slot:Int):NGSaveSlot
{
trace('[NEWGROUNDS] Getting save slot from ID $slot');
var saveSlot:Null<SaveSlot> = NewgroundsClient.instance.saveSlots?.getById(slot);
var saveSlotObj:NGSaveSlot = new NGSaveSlot(saveSlot);
return saveSlotObj;
}
public var ngSaveSlot:Null<SaveSlot> = null;
public function new(?ngSaveSlot:Null<SaveSlot>)
{
this.ngSaveSlot = ngSaveSlot;
#if FLX_DEBUG
FlxG.console.registerClass(NGSaveSlot);
FlxG.console.registerClass(Save);
#end
}
/**
* Saves `data` to the newgrounds save slot.
* @param data The raw save data.
*/
public function save(data:RawSaveData):Void
{
var encodedData:String = haxe.Serializer.run(data);
try
{
ngSaveSlot?.save(encodedData, function(outcome:Outcome<CallError>) {
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Successfully saved save data to save slot!');
case FAIL(error):
trace('[NEWGROUNDS] Failed to save data to save slot!');
trace(error);
}
});
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to save data to save slot!');
trace(error);
}
}
public function load(?onComplete:Null<Dynamic->Void>, ?onError:Null<CallError->Void>):Void
{
try
{
ngSaveSlot?.load(function(outcome:SaveSlotOutcome):Void {
switch (outcome)
{
case SUCCESS(value):
trace('[NEWGROUNDS] Loaded save slot with the ID of ${ngSaveSlot?.id}!');
#if FEATURE_DEBUG_FUNCTIONS
trace('Save Slot Data:');
trace(value);
#end
if (onComplete != null && value != null)
{
var decodedData:Dynamic = haxe.Unserializer.run(value);
onComplete(decodedData);
}
case FAIL(error):
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(error);
if (onError != null)
{
onError(error);
}
}
});
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(error);
if (onError != null)
{
onError(RESPONSE({message: error, code: 500}));
}
}
}
public function clear():Void
{
try
{
ngSaveSlot?.clear(function(outcome:Outcome<CallError>) {
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Successfully cleared save slot!');
case FAIL(error):
trace('[NEWGROUNDS] Failed to clear save slot!');
trace(error);
}
});
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to clear save slot!');
trace(error);
}
}
public function checkSlot():Void
{
trace('[NEWGROUNDS] Checking save slot with the ID of ${ngSaveSlot?.id}...');
trace(' Is null? ${ngSaveSlot == null}');
trace(' Is empty? ${ngSaveSlot?.isEmpty() ?? false}');
}
}
#end

View file

@ -1,6 +1,5 @@
package funkin.api.newgrounds;
import funkin.util.macro.EnvironmentConfigMacro;
import funkin.save.Save;
import funkin.api.newgrounds.Medals.Medal;
#if FEATURE_NEWGROUNDS
@ -11,18 +10,13 @@ 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.SaveSlotList;
import io.newgrounds.utils.ScoreBoardList;
import io.newgrounds.objects.User;
@:nullSafety
class NewgroundsClient
{
static final APP_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_APP_ID");
static final ENCRYPTION_KEY:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_ENC_KEY");
public static var instance(get, never):NewgroundsClient;
static var _instance:Null<NewgroundsClient> = null;
static function get_instance():NewgroundsClient
@ -35,15 +29,14 @@ class NewgroundsClient
public var user(get, never):Null<User>;
public var medals(get, never):Null<MedalList>;
public var leaderboards(get, never):Null<ScoreBoardList>;
public var saveSlots(get, never):Null<SaveSlotList>;
private function new()
{
trace('[NEWGROUNDS] Initializing client...');
#if FEATURE_NEWGROUNDS_DEBUG
trace('[NEWGROUNDS] App ID: ${APP_ID}');
trace('[NEWGROUNDS] Encryption Key: ${ENCRYPTION_KEY}');
trace('[NEWGROUNDS] App ID: ${NewgroundsCredentials.APP_ID}');
trace('[NEWGROUNDS] Encryption Key: ${NewgroundsCredentials.ENCRYPTION_KEY}');
#end
if (!hasValidCredentials())
@ -52,12 +45,9 @@ class NewgroundsClient
return;
}
@:nullSafety(Off)
{
NG.create(APP_ID, getSessionId(), #if FEATURE_NEWGROUNDS_DEBUG true #else false #end, onLoginResolved);
NG.core.setupEncryption(ENCRYPTION_KEY);
}
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()
@ -176,12 +166,12 @@ class NewgroundsClient
*/
static function hasValidCredentials():Bool
{
return !(APP_ID == null
|| APP_ID == ""
|| (APP_ID != null && APP_ID.contains(" "))
|| ENCRYPTION_KEY == null
|| ENCRYPTION_KEY == ""
|| (ENCRYPTION_KEY != null && ENCRYPTION_KEY.contains(" ")));
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
@ -246,8 +236,6 @@ class NewgroundsClient
trace('[NEWGROUNDS] Submitting leaderboard request...');
NG.core.scoreBoards.loadList(onFetchedLeaderboards);
trace('[NEWGROUNDS] Submitting save slot request...');
NG.core.saveSlots.loadList(onFetchedSaveSlots);
}
function onLoginFailed(result:LoginFail):Void
@ -313,13 +301,6 @@ class NewgroundsClient
// trace(funkin.api.newgrounds.Leaderboards.listLeaderboardData());
}
function onFetchedSaveSlots(outcome:Outcome<CallError>):Void
{
trace('[NEWGROUNDS] Fetched save slots!');
NGSaveSlot.instance.checkSlot();
}
function get_user():Null<User>
{
if (NG.core == null || !this.isLoggedIn()) return null;
@ -338,12 +319,6 @@ class NewgroundsClient
return NG.core.scoreBoards;
}
function get_saveSlots():Null<SaveSlotList>
{
if (NG.core == null || !this.isLoggedIn()) return null;
return NG.core.saveSlots;
}
static function getSessionId():Null<String>
{
#if js
@ -356,22 +331,4 @@ class NewgroundsClient
return Save.instance.ngSessionId;
}
}
/**
* Wrapper for `NewgroundsClient` that prevents submitting cheated data.
*/
class NewgroundsClientSandboxed
{
public static var user(get, never):Null<User>;
static function get_user()
{
return NewgroundsClient.instance.user;
}
public static function isLoggedIn()
{
return NewgroundsClient.instance.isLoggedIn();
}
}
#end

View file

@ -11,7 +11,6 @@ import openfl.utils.AssetType;
/**
* a FlxSound that just overrides loadEmbedded to allow for "streamed" sounds to load with better performance!
*/
@:nullSafety
class FlxStreamSound extends FlxSound
{
public function new()
@ -19,7 +18,7 @@ class FlxStreamSound extends FlxSound
super();
}
override public function loadEmbedded(EmbeddedSound:Null<FlxSoundAsset>, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound
override public function loadEmbedded(EmbeddedSound:FlxSoundAsset, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound
{
if (EmbeddedSound == null) return this;

View file

@ -20,6 +20,9 @@ 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.
@ -42,9 +45,9 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
if (_onVolumeChanged == null)
{
_onVolumeChanged = new FlxTypedSignal<Float->Void>();
FlxG.sound.onVolumeChange.add(function(volume:Float) {
FlxG.sound.volumeHandler = function(volume:Float) {
_onVolumeChanged.dispatch(volume);
});
}
}
return _onVolumeChanged;
}
@ -475,7 +478,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
if (autoPlay) sound.play();
sound.volume = volume;
FlxG.sound.defaultSoundGroup.add(sound);
sound.group = FlxG.sound.defaultSoundGroup;
sound.persist = persist;
sound.important = important;
@ -548,7 +551,6 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
}
FlxTween.cancelTweensOf(this);
this._label = 'unknown';
this._waveformData = null;
}
@:access(openfl.media.Sound)

View file

@ -7,7 +7,6 @@ import flixel.tweens.FlxTween;
* A group of FunkinSounds that are all synced together.
* Unlike FlxSoundGroup, you can also control their time and pitch.
*/
@:nullSafety
class SoundGroup extends FlxTypedGroup<FunkinSound>
{
public var time(get, set):Float;
@ -37,7 +36,6 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
return result;
}
@:nullSafety(Off)
for (sndFile in files)
{
var snd:FunkinSound = FunkinSound.load(Paths.voices(song, '$sndFile'));
@ -72,7 +70,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
/**
* Add a sound to the group.
*/
public override function add(sound:FunkinSound):Null<FunkinSound>
public override function add(sound:FunkinSound):FunkinSound
{
var result:FunkinSound = super.add(sound);
@ -136,7 +134,6 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
/**
* Fade in all the sounds in the group.
*/
@:nullSafety(Off)
public function fadeIn(duration:Float, ?from:Float = 0.0, ?to:Float = 1.0, ?onComplete:FlxTween->Void):Void
{
forEachAlive(function(sound:FunkinSound) {
@ -147,7 +144,6 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
/**
* Fade out all the sounds in the group.
*/
@:nullSafety(Off)
public function fadeOut(duration:Float, ?to:Float = 0.0, ?onComplete:FlxTween->Void):Void
{
forEachAlive(function(sound:FunkinSound) {
@ -242,7 +238,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
function get_muted():Bool
{
if (getFirstAlive() != null) return getFirstAlive()?.muted ?? false;
if (getFirstAlive() != null) return getFirstAlive().muted;
else
return false;
}

View file

@ -3,11 +3,10 @@ package funkin.audio;
import flixel.group.FlxGroup.FlxTypedGroup;
import funkin.audio.waveform.WaveformData;
@:nullSafety
class VoicesGroup extends SoundGroup
{
var playerVoices:Null<FlxTypedGroup<FunkinSound>>;
var opponentVoices:Null<FlxTypedGroup<FunkinSound>>;
var playerVoices:FlxTypedGroup<FunkinSound>;
var opponentVoices:FlxTypedGroup<FunkinSound>;
/**
* Control the volume of only the sounds in the player group.
@ -42,12 +41,12 @@ class VoicesGroup extends SoundGroup
public function addPlayerVoice(sound:FunkinSound):Void
{
super.add(sound);
playerVoices?.add(sound);
playerVoices.add(sound);
}
function set_playerVolume(volume:Float):Float
{
playerVoices?.forEachAlive(function(voice:FunkinSound) {
playerVoices.forEachAlive(function(voice:FunkinSound) {
voice.volume = volume;
});
return playerVolume = volume;
@ -60,10 +59,10 @@ class VoicesGroup extends SoundGroup
snd.time = time;
});
playerVoices?.forEachAlive(function(voice:FunkinSound) {
playerVoices.forEachAlive(function(voice:FunkinSound) {
voice.time -= playerVoicesOffset;
});
opponentVoices?.forEachAlive(function(voice:FunkinSound) {
opponentVoices.forEachAlive(function(voice:FunkinSound) {
voice.time -= opponentVoicesOffset;
});
@ -72,7 +71,7 @@ class VoicesGroup extends SoundGroup
function set_playerVoicesOffset(offset:Float):Float
{
playerVoices?.forEachAlive(function(voice:FunkinSound) {
playerVoices.forEachAlive(function(voice:FunkinSound) {
voice.time += playerVoicesOffset;
voice.time -= offset;
});
@ -81,7 +80,7 @@ class VoicesGroup extends SoundGroup
function set_opponentVoicesOffset(offset:Float):Float
{
opponentVoices?.forEachAlive(function(voice:FunkinSound) {
opponentVoices.forEachAlive(function(voice:FunkinSound) {
voice.time += opponentVoicesOffset;
voice.time -= offset;
});
@ -94,12 +93,12 @@ class VoicesGroup extends SoundGroup
public function addOpponentVoice(sound:FunkinSound):Void
{
super.add(sound);
opponentVoices?.add(sound);
opponentVoices.add(sound);
}
function set_opponentVolume(volume:Float):Float
{
opponentVoices?.forEachAlive(function(voice:FunkinSound) {
opponentVoices.forEachAlive(function(voice:FunkinSound) {
voice.volume = volume;
});
return opponentVolume = volume;
@ -107,26 +106,26 @@ class VoicesGroup extends SoundGroup
public function getPlayerVoice(index:Int = 0):Null<FunkinSound>
{
return playerVoices?.members[index];
return playerVoices.members[index];
}
public function getOpponentVoice(index:Int = 0):Null<FunkinSound>
{
return opponentVoices?.members[index];
return opponentVoices.members[index];
}
public function getPlayerVoiceWaveform():Null<WaveformData>
{
if (playerVoices?.members.length == 0) return null;
if (playerVoices.members.length == 0) return null;
return playerVoices?.members[0].waveformData;
return playerVoices.members[0].waveformData;
}
public function getOpponentVoiceWaveform():Null<WaveformData>
{
if (opponentVoices?.members.length == 0) return null;
if (opponentVoices.members.length == 0) return null;
return opponentVoices?.members[0].waveformData;
return opponentVoices.members[0].waveformData;
}
/**
@ -134,9 +133,9 @@ class VoicesGroup extends SoundGroup
*/
public function getPlayerVoiceLength():Float
{
if (playerVoices?.members.length == 0) return 0.0;
if (playerVoices.members.length == 0) return 0.0;
return playerVoices?.members[0]?.length ?? 0.0;
return playerVoices.members[0].length;
}
/**
@ -144,15 +143,15 @@ class VoicesGroup extends SoundGroup
*/
public function getOpponentVoiceLength():Float
{
if (opponentVoices?.members.length == 0) return 0.0;
if (opponentVoices.members.length == 0) return 0.0;
return opponentVoices?.members[0]?.length ?? 0.0;
return opponentVoices.members[0].length;
}
public override function clear():Void
{
playerVoices?.clear();
opponentVoices?.clear();
playerVoices.clear();
opponentVoices.clear();
super.clear();
}
@ -160,13 +159,13 @@ class VoicesGroup extends SoundGroup
{
if (playerVoices != null)
{
playerVoices?.destroy();
playerVoices.destroy();
playerVoices = null;
}
if (opponentVoices != null)
{
opponentVoices?.destroy();
opponentVoices.destroy();
opponentVoices = null;
}

View file

@ -3,7 +3,6 @@ package funkin.audio.visualize;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
@:nullSafety
class ABot extends FlxTypedSpriteGroup<FlxSprite>
{
public function new()

View file

@ -72,8 +72,8 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
// we use a very low minFreq since some songs use low low subbass like a boss
analyzer.minFreq = 10;
#if sys
// On native it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5
#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
@ -116,7 +116,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
for (i in 0...min(group.members.length, levels.length))
{
var animFrame:Int = (FlxG.sound.volume == 0 || FlxG.sound.muted) ? 0 : Math.round(levels[i].value * 6);
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;

View file

@ -3,12 +3,11 @@ package funkin.audio.visualize;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.sound.FlxSound;
@:nullSafety
class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
{
public var playerVis:Null<PolygonSpectogram>;
public var opponentVis:Null<PolygonSpectogram>;
public var instVis:Null<PolygonSpectogram>;
public var playerVis:PolygonSpectogram;
public var opponentVis:PolygonSpectogram;
public var instVis:PolygonSpectogram;
public function new()
{
@ -100,14 +99,8 @@ class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
public override function destroy():Void
{
if (playerVis != null)
{
playerVis.destroy();
}
if (opponentVis != null)
{
opponentVis.destroy();
}
playerVis.destroy();
opponentVis.destroy();
super.destroy();
}
}

View file

@ -4,7 +4,6 @@ package funkin.audio.visualize.dsp;
Complex number representation.
**/
@:forward(real, imag) @:notNull @:pure
@:nullSafety
abstract Complex({
final real:Float;
final imag:Float;

View file

@ -8,7 +8,6 @@ using funkin.audio.visualize.dsp.Signal;
/**
Fast/Finite Fourier Transforms.
**/
@:nullSafety
class FFT
{
/**

View file

@ -6,7 +6,6 @@ package funkin.audio.visualize.dsp;
Usages include 1-indexed sequences or zero-centered buffers with negative indexing.
**/
@:forward(array, offset)
@:nullSafety
abstract OffsetArray<T>({
final array:Array<T>;
final offset:Int;

View file

@ -5,13 +5,12 @@ using Lambda;
/**
Signal processing miscellaneous utilities.
**/
@:nullSafety
class Signal
{
/**
Returns a smoothed version of the input array using a moving average.
**/
public static function smooth(y:Array<Float>, n:Int):Null<Array<Float>>
public static function smooth(y:Array<Float>, n:Int):Array<Float>
{
if (n <= 0)
{

View file

@ -243,10 +243,9 @@ class WaveformDataChannel
}
/**
* @param i Index
* @return minimum point at an index.
* Retrieve a given minimum point at an index.
*/
public function minSample(i:Int):Int
public function minSample(i:Int)
{
var offset = (i * parent.channels + this.channelId) * 2;
return inline parent.get(offset);

View file

@ -1,6 +1,7 @@
package funkin.audio.waveform;
@:nullSafety
import funkin.util.TimerUtil;
class WaveformDataParser
{
static final INT16_MAX:Int = 32767;
@ -9,7 +10,7 @@ class WaveformDataParser
static final INT8_MAX:Int = 127;
static final INT8_MIN:Int = -128;
public static function interpretFlxSound(sound:Null<flixel.sound.FlxSound>):Null<WaveformData>
public static function interpretFlxSound(sound:flixel.sound.FlxSound):Null<WaveformData>
{
if (sound == null) return null;
@ -43,58 +44,73 @@ class WaveformDataParser
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 soundDataSampleCount:Int = Std.int(Math.ceil(soundData.length / channels / (bitsPerSample == 16 ? 2 : 1)));
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));
// Pre-allocate Vector with exact final size for better performance and memory efficiency
var outputDataLength:Int = outputPointCount * channels * 2;
var outputData = new haxe.ds.Vector<Int>(outputDataLength);
// 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}');
// Reusable min/max tracking arrays to avoid allocation in the loop
var minValues = new haxe.ds.Vector<Int>(channels);
var maxValues = new haxe.ds.Vector<Int>(channels);
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)
{
var rangeStart:Int = pointIndex * samplesPerPoint;
var rangeEnd:Int = Std.int(Math.min(rangeStart + samplesPerPoint, soundDataSampleCount));
// minChannel1, maxChannel1, minChannel2, maxChannel2, ...
var values:Array<Int> = [];
// Reset min/max values for this range
for (i in 0...channels)
{
minValues[i] = bitsPerSample == 16 ? INT16_MAX : INT8_MAX;
maxValues[i] = bitsPerSample == 16 ? INT16_MIN : INT8_MIN;
values.push(bitsPerSample == 16 ? INT16_MAX : INT8_MAX);
values.push(bitsPerSample == 16 ? INT16_MIN : INT8_MIN);
}
// Process all samples in this range
var rangeStart = pointIndex * samplesPerPoint;
var rangeEnd = rangeStart + samplesPerPoint;
if (rangeEnd > soundDataSampleCount) rangeEnd = soundDataSampleCount;
for (sampleIndex in rangeStart...rangeEnd)
{
for (channelIndex in 0...channels)
{
var sampleValue:Int = soundData[sampleIndex * channels + channelIndex];
var sampleIndex:Int = sampleIndex * channels + channelIndex;
var sampleValue = soundData[sampleIndex];
if (sampleValue < minValues[channelIndex]) minValues[channelIndex] = sampleValue;
if (sampleValue > maxValues[channelIndex]) maxValues[channelIndex] = sampleValue;
if (sampleValue < values[channelIndex * 2]) values[(channelIndex * 2)] = sampleValue;
if (sampleValue > values[channelIndex * 2 + 1]) values[(channelIndex * 2) + 1] = sampleValue;
}
}
// Write directly to final positions in output Vector
var baseIndex:Int = pointIndex * channels * 2;
for (channelIndex in 0...channels)
{
outputData[baseIndex + channelIndex * 2] = minValues[channelIndex];
outputData[baseIndex + channelIndex * 2 + 1] = maxValues[channelIndex];
}
// We now have the min and max values for the range.
for (value in values)
outputData.push(value);
}
var result = new WaveformData(null, channels, soundBuffer.sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData.toArray());
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;
}

View file

@ -15,7 +15,6 @@ typedef EntryConstructorFunction = String->Void;
* @param T The type to construct. Must implement `IRegistryEntry`.
* @param J The type of the JSON data used when constructing.
*/
@:nullSafety
@:generic
@:autoBuild(funkin.util.macro.DataRegistryMacro.buildRegistry())
abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J>
@ -116,7 +115,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
{
try
{
var entry:Null<T> = createEntry(entryId);
var entry:T = createEntry(entryId);
if (entry != null)
{
trace(' Loaded entry data: ${entry}');
@ -166,7 +165,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param id The ID of the entry.
* @return The class name, or `null` if it does not exist.
*/
public function getScriptedEntryClassName(id:String):Null<String>
public function getScriptedEntryClassName(id:String):String
{
return scriptedEntryIds.get(id);
}
@ -217,7 +216,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
public function fetchEntryVersion(id:String):Null<thx.semver.Version>
{
var entryStr:String = loadEntryFile(id).contents;
var entryVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(entryStr);
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
return entryVersion;
}

View file

@ -4,7 +4,6 @@ import json2object.Position;
import json2object.Position.Line;
import json2object.Error;
@:nullSafety
class DataError
{
public static function printError(error:Error):Void

View file

@ -19,7 +19,6 @@ import thx.semver.VersionRule;
*
* 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.
*/
@:nullSafety
class DataParse
{
/**
@ -147,6 +146,7 @@ class DataParse
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}.';
}
@ -310,7 +310,6 @@ class DataParse
var length:Null<Float> = values[2] == null ? null : Tools.getValue(values[2]);
var alt:Null<Bool> = values[3] == null ? null : Tools.getValue(values[3]);
if (time == null || data == null) throw 'Property $name note is missing time and/or data values.';
return new LegacyNote(time, data, length, alt);
// return null;
default:

View file

@ -12,7 +12,6 @@ import haxe.ds.Either;
*
* NOTE: Result must include quotation marks if the value is a string! json2object will not add them for you!
*/
@:nullSafety
class DataWrite
{
/**

View file

@ -1,9 +1,8 @@
package funkin.data.animation;
@:nullSafety
class AnimationDataUtil
{
public static function toNamed(data:UnnamedAnimationData, name:String = ""):AnimationData
public static function toNamed(data:UnnamedAnimationData, ?name:String = ""):AnimationData
{
return {
name: name,
@ -23,7 +22,7 @@ class AnimationDataUtil
* @param name (adds index to name)
* @return Array<AnimationData>
*/
public static function toNamedArray(data:Array<UnnamedAnimationData>, name:String = ""):Array<AnimationData>
public static function toNamedArray(data:Array<UnnamedAnimationData>, ?name:String = ""):Array<AnimationData>
{
return data.mapi(function(animItem, ind) return toNamed(animItem, '$name$ind'));
}

View file

@ -5,7 +5,6 @@ import funkin.play.cutscene.dialogue.ScriptedConversation;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class ConversationRegistry extends BaseRegistry<Conversation, ConversationData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -6,7 +6,6 @@ import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class DialogueBoxRegistry extends BaseRegistry<DialogueBox, DialogueBoxData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -5,7 +5,6 @@ import funkin.play.cutscene.dialogue.ScriptedSpeaker;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class SpeakerRegistry extends BaseRegistry<Speaker, SpeakerData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -8,7 +8,6 @@ import funkin.play.event.ScriptedSongEvent;
/**
* This class statically handles the parsing of internal and scripted song event handlers.
*/
@:nullSafety
class SongEventRegistry
{
/**
@ -88,14 +87,14 @@ class SongEventRegistry
return eventCache.values();
}
public static function getEvent(id:String):Null<SongEvent>
public static function getEvent(id:String):SongEvent
{
return eventCache.get(id);
}
public static function getEventSchema(id:String):Null<SongEventSchema>
public static function getEventSchema(id:String):SongEventSchema
{
var event:Null<SongEvent> = getEvent(id);
var event:SongEvent = getEvent(id);
if (event == null) return null;
return event.getEventSchema();
@ -109,7 +108,7 @@ class SongEventRegistry
public static function handleEvent(data:SongEventData):Void
{
var eventKind:String = data.eventKind;
var eventHandler:Null<SongEvent> = eventCache.get(eventKind);
var eventHandler:SongEvent = eventCache.get(eventKind);
if (eventHandler != null)
{

View file

@ -6,7 +6,6 @@ import funkin.ui.freeplay.ScriptedAlbum;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class AlbumRegistry extends BaseRegistry<Album, AlbumData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -7,7 +7,6 @@ import funkin.save.Save;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> implements ISingleton implements DefaultRegistryImpl
{
/**
@ -57,11 +56,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
var player = fetchEntry(charId);
if (player == null) continue;
#if UNLOCK_EVERYTHING
count++;
#else
if (player.isUnlocked()) count++;
#end
}
return count;
@ -69,7 +64,6 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
public function hasNewCharacter():Bool
{
#if (!UNLOCK_EVERYTHING)
var charactersSeen = Save.instance.charactersSeen.clone();
for (charId in listEntryIds())
@ -83,7 +77,6 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
// This character is unlocked but we haven't seen them in Freeplay yet.
return true;
}
#end
// Fallthrough case.
return false;
@ -91,10 +84,9 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
public function listNewCharacters():Array<String>
{
var charactersSeen = Save.instance.charactersSeen.clone();
var result = [];
#if (!UNLOCK_EVERYTHING)
var charactersSeen = Save.instance.charactersSeen.clone();
for (charId in listEntryIds())
{
var player = fetchEntry(charId);
@ -106,7 +98,6 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
// This character is unlocked but we haven't seen them in Freeplay yet.
result.push(charId);
}
#end
return result;
}
@ -125,7 +116,6 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
/**
* Return true if the given stage character is associated with a specific playable character.
* If so, the level should only appear if that character is selected in Freeplay.
* NOTE: This is NOT THE SAME as `player.isUnlocked()`!
* @param characterId The stage character ID.
* @return Whether the character is owned by any one character.
*/
@ -133,17 +123,4 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> impleme
{
return ownedCharacterIds.exists(characterId);
}
/**
* @param characterId The character ID to check.
* @return Whether the player saw the character unlock animation in Character Select.
*/
public function isCharacterSeen(characterId:String):Bool
{
#if UNLOCK_EVERYTHING
return true;
#else
return Save.instance.charactersSeen.contains(characterId);
#end
}
}

View file

@ -6,7 +6,6 @@ import funkin.ui.freeplay.ScriptedFreeplayStyle;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class FreeplayStyleRegistry extends BaseRegistry<FreeplayStyle, FreeplayStyleData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -6,7 +6,6 @@ import funkin.data.notestyle.NoteStyleData;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData> implements ISingleton implements DefaultRegistryImpl
{
/**
@ -25,8 +24,6 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData> implement
public function fetchDefault():NoteStyle
{
var notestyle:Null<NoteStyle> = fetchEntry(Constants.DEFAULT_NOTE_STYLE);
if (notestyle == null) throw 'Default notestyle was null! This should not happen!';
return notestyle;
return fetchEntry(Constants.DEFAULT_NOTE_STYLE);
}
}

View file

@ -68,12 +68,11 @@ class SongMetadata implements ICloneable<SongMetadata>
@:jignored
public var variation:String;
public function new(songName:String, artist:String, ?charter:String, ?variation:String)
public function new(songName:String, artist:String, ?variation:String)
{
this.version = SongRegistry.SONG_METADATA_VERSION;
this.songName = songName;
this.artist = artist;
this.charter = (charter == null) ? null : charter;
this.timeFormat = 'ms';
this.divisions = null;
this.offsets = new SongOffsets();
@ -97,7 +96,7 @@ class SongMetadata implements ICloneable<SongMetadata>
*/
public function clone():SongMetadata
{
var result:SongMetadata = new SongMetadata(this.songName, this.artist, this.charter, this.variation);
var result:SongMetadata = new SongMetadata(this.songName, this.artist, this.variation);
result.version = this.version;
result.timeFormat = this.timeFormat;
result.divisions = this.divisions;
@ -140,7 +139,7 @@ class SongMetadata implements ICloneable<SongMetadata>
*/
public function toString():String
{
return 'SongMetadata(${this.songName} by ${this.artist}, charted by ${this.charter}, variation ${this.variation})';
return 'SongMetadata(${this.songName} by ${this.artist}, variation ${this.variation})';
}
}
@ -1117,23 +1116,6 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}'
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
}
public function buildTooltip():String
{
if ((this.kind?.length ?? 0) == 0) return "";
var result:String = 'Kind: ${this.kind}';
if (this.params.length == 0) return result;
result += "\nParams:";
for (param in params)
{
result += '\n- ${param.name}: ${param.value}';
}
return result;
}
}
/**

View file

@ -5,13 +5,13 @@ import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.util.ClipboardUtil;
import funkin.util.SerializerUtil;
using Lambda;
/**
* Utility functions for working with song data, including note data, event data, metadata, etc.
*/
@:nullSafety
class SongDataUtils
{
/**
@ -28,7 +28,7 @@ class SongDataUtils
var time:Float = note.time + offset;
var data:Int = note.data;
var length:Float = note.length;
var kind:Null<String> = note.kind;
var kind:String = note.kind;
return new SongNoteData(time, data, length, kind);
};
@ -132,7 +132,7 @@ class SongDataUtils
* Create an array of notes whose note data is flipped (player becomes opponent and vice versa)
* Does not mutate the original array.
*/
public static function flipNotes(notes:Array<SongNoteData>, strumlineSize:Int = 4):Array<SongNoteData>
public static function flipNotes(notes:Array<SongNoteData>, ?strumlineSize:Int = 4):Array<SongNoteData>
{
return notes.map(function(note:SongNoteData):SongNoteData {
var newData = note.data;
@ -150,7 +150,7 @@ class SongDataUtils
*
* Offset the provided array of notes such that the first note is at 0 milliseconds.
*/
public static function buildNoteClipboard(notes:Array<SongNoteData>, ?timeOffset:Int):Array<SongNoteData>
public static function buildNoteClipboard(notes:Array<SongNoteData>, ?timeOffset:Int = null):Array<SongNoteData>
{
if (notes.length == 0) return notes;
if (timeOffset == null) timeOffset = Std.int(notes[0].time);
@ -162,7 +162,7 @@ class SongDataUtils
*
* Offset the provided array of events such that the first event is at 0 milliseconds.
*/
public static function buildEventClipboard(events:Array<SongEventData>, ?timeOffset:Int):Array<SongEventData>
public static function buildEventClipboard(events:Array<SongEventData>, ?timeOffset:Int = null):Array<SongEventData>
{
if (events.length == 0) return events;
if (timeOffset == null) timeOffset = Std.int(events[0].time);

View file

@ -1,138 +0,0 @@
package funkin.data.song;
using SongData.SongNoteData;
/**
* Utility class for extra handling of song notes
*/
@:nullSafety
class SongNoteDataUtils
{
static final CHUNK_INTERVAL_MS:Float = 2500;
/**
* Retrieves all stacked notes. It does this by cycling through "chunks" of notes within a certain interval.
*
* @param notes Sorted notes by time.
* @param threshold The note stack threshold. Refer to `doNotesStack` for more details.
* @param includeOverlapped (Optional) If overlapped notes should be included.
* @param overlapped (Optional) An array that gets populated with overlapped notes.
* Note that it's only guaranteed to work properly if the provided notes are sorted.
* @return Stacked notes.
*/
public static function listStackedNotes(notes:Array<SongNoteData>, threshold:Float, includeOverlapped:Bool = true,
?overlapped:Array<SongNoteData>):Array<SongNoteData>
{
var stackedNotes:Array<SongNoteData> = [];
var chunkTime:Float = 0;
var chunks:Array<Array<SongNoteData>> = [[]];
for (note in notes)
{
if (note == null)
{
continue;
}
while (note.time >= chunkTime + CHUNK_INTERVAL_MS)
{
chunkTime += CHUNK_INTERVAL_MS;
chunks.push([]);
}
chunks[chunks.length - 1].push(note);
}
for (chunk in chunks)
{
for (i in 0...(chunk.length - 1))
{
for (j in (i + 1)...chunk.length)
{
var noteI:SongNoteData = chunk[i];
var noteJ:SongNoteData = chunk[j];
if (doNotesStack(noteI, noteJ, threshold))
{
if (!stackedNotes.fastContains(noteI))
{
if (includeOverlapped) stackedNotes.push(noteI);
if (overlapped != null && !overlapped.contains(noteI)) overlapped.push(noteI);
}
if (!stackedNotes.fastContains(noteJ))
{
stackedNotes.push(noteJ);
}
}
}
}
}
return stackedNotes;
}
/**
* Concatenates two arrays of notes but overwrites notes in `lhs` that are overlapped by notes in `rhs`.
* Hold notes are only overwritten by longer hold notes.
* This operation only modifies the second array and `overwrittenNotes`.
*
* @param lhs An array of notes
* @param rhs An array of notes to concatenate into `lhs`
* @param overwrittenNotes An optional array that is modified in-place with the notes in `lhs` that were overwritten.
* @param threshold The note stack threshold. Refer to `doNotesStack` for more details.
* @return The unsorted resulting array.
*/
public static function concatOverwrite(lhs:Array<SongNoteData>, rhs:Array<SongNoteData>, ?overwrittenNotes:Array<SongNoteData>,
threshold:Float = 0):Array<SongNoteData>
{
if (lhs == null || rhs == null || rhs.length == 0) return lhs;
if (lhs.length == 0) return rhs;
var result = lhs.copy();
for (i in 0...rhs.length)
{
var noteB:SongNoteData = rhs[i];
var hasOverlap:Bool = false;
for (j in 0...lhs.length)
{
var noteA:SongNoteData = lhs[j];
if (doNotesStack(noteA, noteB, threshold))
{
// Long hold notes should have priority over shorter hold notes
if (noteA.length <= noteB.length)
{
overwrittenNotes?.push(result[j].clone());
result[j] = noteB;
}
hasOverlap = true;
break;
}
}
if (!hasOverlap) result.push(noteB);
}
return result;
}
/**
* @param noteA First note.
* @param noteB Second note.
* @param threshold The note stack threshold, in steps.
* @return Returns `true` if both notes are on the same strumline, have the same direction
* and their time difference in steps is less than the step-based threshold.
* A threshold of 0 will return `true` if notes are nearly perfectly aligned.
*/
public static function doNotesStack(noteA:SongNoteData, noteB:SongNoteData, threshold:Float = 0):Bool
{
if (noteA.data != noteB.data) return false;
else if (threshold == 0) return Math.ffloor(Math.abs(noteA.time - noteB.time)) < 1;
final stepDiff:Float = Math.abs(noteA.getStepTime() - noteB.getStepTime());
return stepDiff <= threshold + 0.001;
}
}

View file

@ -324,7 +324,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleto
}
else
{
throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.';
throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
}
}
@ -337,7 +337,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleto
}
else
{
throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.';
throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
}
}

View file

@ -9,10 +9,9 @@ import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.importer.FNFLegacyData;
import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection;
@:nullSafety
class FNFLegacyImporter
{
public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):Null<FNFLegacyData>
public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):FNFLegacyData
{
var parser = new json2object.JsonParser<FNFLegacyData>();
parser.ignoreUnknownVariables = true; // Set to true to ignore extra variables that might be included in the JSON.
@ -37,16 +36,18 @@ class FNFLegacyImporter
{
trace('Migrating song metadata from FNF Legacy.');
var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, Constants.DEFAULT_CHARTER, Constants.DEFAULT_VARIATION);
var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, 'default');
var hadError:Bool = false;
// Set generatedBy string for debugging.
songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)';
songMetadata.playData.stage = songData.song?.stageDefault ?? 'mainStage';
songMetadata.songName = songData.song?.song ?? 'Import';
songMetadata.playData.stage = songData?.song?.stageDefault ?? 'mainStage';
songMetadata.songName = songData?.song?.song ?? 'Import';
songMetadata.playData.difficulties = [];
if (songData.song?.notes != null)
if (songData?.song?.notes != null)
{
switch (songData.song.notes)
{
@ -64,7 +65,7 @@ class FNFLegacyImporter
songMetadata.timeChanges = rebuildTimeChanges(songData);
songMetadata.playData.characters = new SongCharacterData(songData.song?.player1 ?? 'bf', 'gf', songData.song?.player2 ?? 'dad');
songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad');
return songMetadata;
}
@ -75,7 +76,7 @@ class FNFLegacyImporter
var songChartData:SongChartData = new SongChartData([difficulty => 1.0], [], [difficulty => []]);
if (songData.song?.notes != null)
if (songData?.song?.notes != null)
{
switch (songData.song.notes)
{
@ -83,6 +84,7 @@ class FNFLegacyImporter
// One difficulty of notes.
songChartData.notes.set(difficulty, migrateNoteSections(notes));
case Right(difficulties):
var baseDifficulty = null;
if (difficulties.easy != null) songChartData.notes.set('easy', migrateNoteSections(difficulties.easy));
if (difficulties.normal != null) songChartData.notes.set('normal', migrateNoteSections(difficulties.normal));
if (difficulties.hard != null) songChartData.notes.set('hard', migrateNoteSections(difficulties.hard));
@ -122,8 +124,8 @@ class FNFLegacyImporter
noteSections = notes;
case Right(difficulties):
if (difficulties.normal != null) noteSections = difficulties.normal;
if (difficulties.hard != null) noteSections = difficulties.hard;
if (difficulties.easy != null) noteSections = difficulties.easy;
if (difficulties.hard != null) noteSections = difficulties.normal;
if (difficulties.easy != null) noteSections = difficulties.normal;
}
if (noteSections == null || noteSections.length == 0) return result;
@ -156,7 +158,7 @@ class FNFLegacyImporter
{
var result:Array<SongTimeChange> = [];
result.push(new SongTimeChange(0, songData.song?.bpm ?? Constants.DEFAULT_BPM));
result.push(new SongTimeChange(0, songData?.song?.bpm ?? Constants.DEFAULT_BPM));
var noteSections = [];
switch (songData.song.notes)
@ -166,8 +168,8 @@ class FNFLegacyImporter
noteSections = notes;
case Right(difficulties):
if (difficulties.normal != null) noteSections = difficulties.normal;
if (difficulties.hard != null) noteSections = difficulties.hard;
if (difficulties.easy != null) noteSections = difficulties.easy;
if (difficulties.hard != null) noteSections = difficulties.normal;
if (difficulties.easy != null) noteSections = difficulties.normal;
}
if (noteSections == null || noteSections.length == 0) return result;
@ -177,7 +179,7 @@ class FNFLegacyImporter
if (noteSection.changeBPM ?? false)
{
var firstNote:LegacyNote = noteSection.sectionNotes[0];
if (firstNote != null) result.push(new SongTimeChange(firstNote.time, noteSection.bpm ?? Constants.DEFAULT_BPM));
if (firstNote != null) result.push(new SongTimeChange(firstNote.time, noteSection.bpm));
}
}

View file

@ -24,7 +24,7 @@ class SongDataMigrator
public static function migrate_SongMetadata_v2_1_0(input:SongData_v2_1_0.SongMetadata_v2_1_0):SongMetadata
{
var result:SongMetadata = new SongMetadata(input.songName, input.artist, Constants.DEFAULT_CHARTER, input.variation);
var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation);
result.version = SongRegistry.SONG_METADATA_VERSION;
result.timeFormat = input.timeFormat;
result.divisions = input.divisions;
@ -66,7 +66,7 @@ class SongDataMigrator
public static function migrate_SongMetadata_v2_0_0(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata
{
var result:SongMetadata = new SongMetadata(input.songName, input.artist, Constants.DEFAULT_CHARTER, input.variation);
var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation);
result.version = SongRegistry.SONG_METADATA_VERSION;
result.timeFormat = input.timeFormat;
result.divisions = input.divisions;

View file

@ -205,7 +205,7 @@ typedef StageDataProp =
/**
* The angle of the prop, as a float.
* @default 0.0
* @default 1.0
*/
@:optional
@:default(0.0)
@ -284,7 +284,7 @@ typedef StageDataCharacter =
/**
* The angle of the character, as a float.
* @default 0.0
* @default 1.0
*/
@:optional
@:default(0.0)

View file

@ -5,7 +5,6 @@ import funkin.play.stage.ScriptedStage;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class StageRegistry extends BaseRegistry<Stage, StageData> implements ISingleton implements DefaultRegistryImpl
{
/**
@ -13,7 +12,7 @@ class StageRegistry extends BaseRegistry<Stage, StageData> implements ISingleton
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.2";
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.3";
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0";

View file

@ -4,7 +4,6 @@ import funkin.data.stickers.StickerData;
import funkin.ui.transition.stickers.StickerPack;
import funkin.ui.transition.stickers.ScriptedStickerPack;
@:nullSafety
class StickerRegistry extends BaseRegistry<StickerPack, StickerData>
{
/**
@ -25,9 +24,7 @@ class StickerRegistry extends BaseRegistry<StickerPack, StickerData>
public function fetchDefault():StickerPack
{
var stickerPack:Null<StickerPack> = fetchEntry(Constants.DEFAULT_STICKER_PACK);
if (stickerPack == null) throw 'Default sticker pack was null! This should not happen!';
return stickerPack;
return fetchEntry(Constants.DEFAULT_STICKER_PACK);
}
/**

View file

@ -6,7 +6,6 @@ import funkin.ui.story.ScriptedLevel;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@:nullSafety
class LevelRegistry extends BaseRegistry<Level, LevelData> implements ISingleton implements DefaultRegistryImpl
{
/**

View file

@ -32,7 +32,6 @@ import openfl.display._internal.CairoGraphics as GfxRenderer;
* A modified `FlxSprite` that supports filters.
* The name's pretty much self-explanatory.
*/
@:nullSafety
@:access(openfl.geom.Rectangle)
@:access(openfl.filters.BitmapFilter)
@:access(flixel.graphics.frames.FlxFrame)
@ -40,12 +39,12 @@ class FlxFilteredSprite extends FlxSprite
{
@:noCompletion var _renderer:FlxAnimateFilterRenderer = new FlxAnimateFilterRenderer();
@:noCompletion var _filterMatrix:FlxMatrix = new FlxMatrix();
@:noCompletion var _filterMatrix:FlxMatrix;
/**
* An `Array` of shader filters (aka `BitmapFilter`).
*/
public var filters(default, set):Null<Array<BitmapFilter>>;
public var filters(default, set):Array<BitmapFilter>;
/**
* a flag to update the image with the filters.
@ -53,15 +52,11 @@ class FlxFilteredSprite extends FlxSprite
*/
public var filterDirty:Bool = false;
@:noCompletion var filtered:Bool = false;
@:noCompletion var filtered:Bool;
// These appear to be a little troublesome to null safe.
@:nullSafety(Off)
@:noCompletion var _blankFrame:FlxFrame;
@:nullSafety(Off)
var _filterBmp1:BitmapData;
@:nullSafety(Off)
var _filterBmp2:BitmapData;
override public function update(elapsed:Float)
@ -167,7 +162,6 @@ class FlxFilteredSprite extends FlxSprite
}
_flashRect.width += frameWidth;
_flashRect.height += frameHeight;
@:nullSafety(Off)
if (_blankFrame == null) _blankFrame = new FlxFrame(null);
if (_blankFrame.parent == null || _flashRect.width > _blankFrame.parent.width || _flashRect.height > _blankFrame.parent.height)
@ -184,7 +178,6 @@ class FlxFilteredSprite extends FlxSprite
_filterBmp2 = new BitmapData(_blankFrame.parent.width, _blankFrame.parent.height, 0);
}
_blankFrame.offset.copyFrom(_frame.offset);
@:nullSafety(Off)
_blankFrame.parent.bitmap = _renderer.applyFilter(_blankFrame.parent.bitmap, _filterBmp1, _filterBmp2, frame.parent.bitmap, filters, _flashRect,
frame.frame.copyToFlash());
_blankFrame.frame = FlxRect.get(0, 0, _blankFrame.parent.bitmap.width, _blankFrame.parent.bitmap.height);
@ -200,7 +193,7 @@ class FlxFilteredSprite extends FlxSprite
}
@:noCompletion
function set_filters(value:Null<Array<BitmapFilter>>)
function set_filters(value:Array<BitmapFilter>)
{
if (filters != value) filterDirty = true;

View file

@ -24,7 +24,6 @@ import openfl.filters.ShaderFilter;
* - NOTE: Several other blend modes work without FunkinCamera. Some still do not work.
* - NOTE: Framerate-independent camera tweening is fixed in Flixel 6.x. Rest in peace, SwagCamera.
*/
@:nullSafety
@:access(openfl.display.DisplayObject)
@:access(openfl.display.BitmapData)
@:access(openfl.display3D.Context3D)
@ -51,12 +50,11 @@ class FunkinCamera extends FlxCamera
// Used to identify the camera during debugging.
final id:String = 'unknown';
@:nullSafety(Off)
public function new(id:String = 'unknown', x:Int = 0, y:Int = 0, width:Int = 0, height:Int = 0, zoom:Float = 0)
{
super(x, y, width, height, zoom);
this.id = id;
bgTexture = @:nullSafety(Off) pickTexture(width, height);
bgTexture = pickTexture(width, height);
bgBitmap = FixedBitmapData.fromTexture(bgTexture);
bgFrame = new FlxFrame(new FlxGraphic('', null));
bgFrame.parent.bitmap = bgBitmap;
@ -76,15 +74,12 @@ class FunkinCamera extends FlxCamera
* and the grabbed bitmap will not include any previously rendered sprites
* @return the grabbed bitmap data
*/
public function grabScreen(applyFilters:Bool, isolate:Bool = false):Null<BitmapData>
public function grabScreen(applyFilters:Bool, isolate:Bool = false):BitmapData
{
final texture = pickTexture(width, height);
final bitmap = FixedBitmapData.fromTexture(texture);
if (bitmap != null)
{
squashTo(bitmap, applyFilters, isolate);
grabbed.push(bitmap);
}
squashTo(bitmap, applyFilters, isolate);
grabbed.push(bitmap);
return bitmap;
}
@ -131,14 +126,12 @@ class FunkinCamera extends FlxCamera
if (applyFilters)
{
bitmap.draw(flashSprite, matrix);
@:nullSafety(Off) // TODO: Remove this once openfl.display.Sprite has been null safed.
flashSprite.filters = null;
filtersApplied = true;
}
else
{
final tmp = flashSprite.filters;
@:nullSafety(Off)
flashSprite.filters = null;
bitmap.draw(flashSprite, matrix);
flashSprite.filters = tmp;
@ -204,7 +197,6 @@ class FunkinCamera extends FlxCamera
final isolated = grabScreen(false, true);
// apply fullscreen blend
customBlendShader.blendSwag = blend;
@:nullSafety(Off) // I hope this doesn't cause issues
customBlendShader.sourceSwag = isolated;
customBlendShader.updateViewInfo(FlxG.width, FlxG.height, this);
applyFilter(customBlendFilter);
@ -236,7 +228,7 @@ class FunkinCamera extends FlxCamera
bgItemCount = 0;
}
function pickTexture(width:Int, height:Int):Null<TextureBase>
function pickTexture(width:Int, height:Int):TextureBase
{
// zero-sized textures will be problematic
width = width < 1 ? 1 : width;
@ -244,9 +236,7 @@ class FunkinCamera extends FlxCamera
if (texturePool.length > 0)
{
final res = texturePool.pop();
if (res != null) BitmapDataUtil.resizeTexture(res, width, height);
else
trace('huh? why is this null? $texturePool');
BitmapDataUtil.resizeTexture(res, width, height);
return res;
}
return Lib.current.stage.context3D.createTexture(width, height, BGRA, true);

View file

@ -11,16 +11,12 @@ import flixel.math.FlxRect;
import flixel.math.FlxPoint;
import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
import flixel.FlxCamera;
import openfl.system.System;
using StringTools;
/**
* An FlxSprite with additional functionality.
* - A more efficient method for creating solid color sprites.
* - TODO: Better cache handling for textures.
*/
@:nullSafety
class FunkinSprite extends FlxSprite
{
/**
@ -35,8 +31,6 @@ class FunkinSprite extends FlxSprite
*/
static var previousCachedTextures:Map<String, FlxGraphic> = [];
static var permanentCachedTextures:Map<String, FlxGraphic> = [];
/**
* @param x Starting X position
* @param y Starting Y position
@ -164,14 +158,9 @@ class FunkinSprite extends FlxSprite
* @param input The OpenFL `TextureBase` to apply
* @return This sprite, for chaining
*/
public function loadTextureBase(input:TextureBase):Null<FunkinSprite>
public function loadTextureBase(input:TextureBase):FunkinSprite
{
var inputBitmap:Null<FixedBitmapData> = FixedBitmapData.fromTexture(input);
if (inputBitmap == null)
{
FlxG.log.warn('loadTextureBase - input resulted in null bitmap! $input');
return null;
}
var inputBitmap:FixedBitmapData = FixedBitmapData.fromTexture(input);
return loadBitmapData(inputBitmap);
}
@ -230,7 +219,7 @@ class FunkinSprite extends FlxSprite
// Move the graphic from the previous cache to the current cache.
var graphic = previousCachedTextures.get(key);
previousCachedTextures.remove(key);
if (graphic != null) currentCachedTextures.set(key, graphic);
currentCachedTextures.set(key, graphic);
return;
}
@ -248,27 +237,6 @@ class FunkinSprite extends FlxSprite
}
}
public static function permanentCacheTexture(key:String):Void
{
// We don't want to cache the same texture twice.
if (permanentCachedTextures.exists(key)) return;
// Else, texture is currently uncached.
var graphic:FlxGraphic = FlxGraphic.fromAssetKey(key, false, null, true);
if (graphic == null)
{
FlxG.log.warn('Failed to cache graphic: $key');
}
else
{
trace('Successfully cached graphic: $key');
graphic.persist = true;
permanentCachedTextures.set(key, graphic);
}
currentCachedTextures = permanentCachedTextures;
}
public static function cacheSparrow(key:String):Void
{
cacheTexture(Paths.image(key));
@ -285,14 +253,7 @@ class FunkinSprite extends FlxSprite
public static function preparePurgeCache():Void
{
previousCachedTextures = currentCachedTextures;
for (graphicKey in previousCachedTextures.keys())
{
if (!permanentCachedTextures.exists(graphicKey)) continue;
previousCachedTextures.remove(graphicKey);
}
currentCachedTextures = permanentCachedTextures;
currentCachedTextures = [];
}
public static function purgeCache():Void
@ -306,39 +267,12 @@ class FunkinSprite extends FlxSprite
graphic.destroy();
previousCachedTextures.remove(graphicKey);
}
@:privateAccess
if (FlxG.bitmap._cache == null)
{
@:privateAccess
FlxG.bitmap._cache = new Map();
System.gc();
return;
}
@:privateAccess
for (key in FlxG.bitmap._cache.keys())
{
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj == null) continue;
if (obj.persist) continue;
if (permanentCachedTextures.exists(key)) continue;
if (!(obj.useCount <= 0 || key.contains("characters") || key.contains("charSelect") || key.contains("results"))) continue;
FlxG.bitmap.removeKey(key);
obj.destroy();
}
openfl.Assets.cache.clear("songs");
openfl.Assets.cache.clear("sounds");
openfl.Assets.cache.clear("music");
System.gc();
}
static function isGraphicCached(graphic:FlxGraphic):Bool
{
var result = null;
if (graphic == null) return false;
result = FlxG.bitmap.get(graphic.key);
var result = FlxG.bitmap.get(graphic.key);
if (result == null) return false;
if (result != graphic)
{
@ -354,9 +288,8 @@ class FunkinSprite extends FlxSprite
*/
public function isAnimationDynamic(id:String):Bool
{
var animData = null;
if (this.animation == null) return false;
animData = this.animation.getByName(id);
var animData = this.animation.getByName(id);
if (animData == null) return false;
return animData.numFrames > 1;
}
@ -495,7 +428,6 @@ class FunkinSprite extends FlxSprite
public override function destroy():Void
{
@:nullSafety(Off) // TODO: Remove when flixel.FlxSprite is null safed.
frames = null;
// Cancel all tweens so they don't continue to run on a destroyed sprite.
// This prevents crashes.

View file

@ -11,7 +11,6 @@ import flxanimate.animate.FlxKeyFrame;
/**
* A sprite which provides convenience functions for rendering a texture atlas with animations.
*/
@:nullSafety
class FlxAtlasSprite extends FlxAnimate
{
static final SETTINGS:Settings =
@ -41,11 +40,10 @@ class FlxAtlasSprite extends FlxAnimate
*/
public var onAnimationLoop:FlxTypedSignal<String->Void> = new FlxTypedSignal();
var currentAnimation:String = '';
var currentAnimation:String;
var canPlayOtherAnims:Bool = true;
@:nullSafety(Off) // null safety HATES new classes atm, it'll be fixed in haxe 4.0.0?
public function new(x:Float, y:Float, ?path:String, ?settings:Settings)
{
if (settings == null) settings = SETTINGS;
@ -112,7 +110,7 @@ class FlxAtlasSprite extends FlxAnimate
var _completeAnim:Bool = false;
var fr:Null<FlxKeyFrame> = null;
var fr:FlxKeyFrame = null;
var looping:Bool = false;
@ -197,9 +195,8 @@ class FlxAtlasSprite extends FlxAnimate
fr = null;
}
var frameLabelNames = getFrameLabelNames();
// Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings!
if (frameLabelNames != null && frameLabelNames.indexOf(id) != -1)
if (getFrameLabelNames().indexOf(id) != -1)
{
goToFrameLabel(id);
fr = anim.getFrameLabel(id);
@ -269,7 +266,7 @@ class FlxAtlasSprite extends FlxAnimate
this.anim.goToFrameLabel(label);
}
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String>):Null<Array<String>>
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String>):Array<String>
{
var labels = this.anim.getFrameLabels(layer);
var array = [];
@ -377,7 +374,6 @@ class FlxAtlasSprite extends FlxAnimate
var prevFrame:FlxFrame = prevFrames.get(index) ?? frames.getByIndex(index).copyTo();
prevFrames.set(index, prevFrame);
@:nullSafety(Off) // TODO: Remove this once flixel.system.frontEnds.BitmapFrontEnd has been null safed
var frame = FlxG.bitmap.add(graphic).imageFrame.frame;
frame.copyTo(frames.getByIndex(index));

View file

@ -12,7 +12,6 @@ import openfl.filters.BitmapFilter;
/**
* Provides cool stuff for `BitmapData`s that have a hardware texture internally.
*/
@:nullSafety
@:access(openfl.display.BitmapData)
@:access(openfl.display3D.textures.TextureBase)
@:access(openfl.display3D.Context3D)
@ -20,7 +19,7 @@ class BitmapDataUtil
{
static function getCache():{sprite:Sprite, bitmap:Bitmap}
{
static var cache:Null<{sprite:Sprite, bitmap:Bitmap}> = null;
static var cache:{sprite:Sprite, bitmap:Bitmap} = null;
if (cache == null)
{
final sprite = new Sprite();
@ -57,7 +56,7 @@ class BitmapDataUtil
* @param format the format if the internal texture
* @return the bitmap
*/
public static function create(width:Int, height:Int, format:Context3DTextureFormat = BGRA):Null<FixedBitmapData>
public static function create(width:Int, height:Int, format:Context3DTextureFormat = BGRA):FixedBitmapData
{
final texture = Lib.current.stage.context3D.createTexture(width, height, format, true);
return FixedBitmapData.fromTexture(texture);
@ -84,7 +83,6 @@ class BitmapDataUtil
* @param width the width
* @param height the height
*/
@:nullSafety(Off) // the final context there is causing an error, idk how to fix it
public static function resizeTexture(texture:TextureBase, width:Int, height:Int):Void
{
if (texture.__width == width && texture.__height == height) return;
@ -103,7 +101,6 @@ class BitmapDataUtil
* @param dst the destination bitmap
* @param src the source bitmap
*/
@:nullSafety(Off) // TODO: Remove this once openfl.display.Sprite has been null safed.
public static function copy(dst:BitmapData, src:BitmapData):Void
{
hardwareCheck(dst);

View file

@ -10,7 +10,6 @@ import openfl.display3D.textures.TextureBase;
/**
* `BitmapData` is kinda broken so I fixed it.
*/
@:nullSafety
@:access(openfl.display3D.textures.TextureBase)
@:access(openfl.display.OpenGLRenderer)
class FixedBitmapData extends BitmapData
@ -30,7 +29,7 @@ class FixedBitmapData extends BitmapData
* @param texture the texture
* @return the bitmap data
*/
public static function fromTexture(texture:Null<TextureBase>):Null<FixedBitmapData>
public static function fromTexture(texture:TextureBase):FixedBitmapData
{
if (texture == null) return null;
final bitmapData:FixedBitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0);

View file

@ -7,7 +7,6 @@ import flixel.util.FlxColor;
* Yoinked from AustinEast, thanks hopefully u dont mind me using some of ur good code
* instead of my dumbass ugly code bro
*/
@:nullSafety
class MeshRender extends FlxStrip
{
public var vertex_count(default, null):Int = 0;

View file

@ -2,13 +2,12 @@ package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
@:nullSafety
class AdjustColorShader extends FlxRuntimeShader
{
public var hue(default, set):Float = 0;
public var saturation(default, set):Float = 0;
public var brightness(default, set):Float = 0;
public var contrast(default, set):Float = 0;
public var hue(default, set):Float;
public var saturation(default, set):Float;
public var brightness(default, set):Float;
public var contrast(default, set):Float;
public function new()
{

View file

@ -8,13 +8,12 @@ typedef BlendModeShader =
var uBlendColor:ShaderParameter<Float>;
}
@:nullSafety
class BlendModeEffect
{
public var shader(default, null):BlendModeShader;
@:isVar
public var color(default, set):FlxColor = new FlxColor();
public var color(default, set):FlxColor;
public function new(shader:BlendModeShader, color:FlxColor):Void
{

View file

@ -4,11 +4,10 @@ import flixel.addons.display.FlxRuntimeShader;
import openfl.display.BitmapData;
import openfl.display.ShaderInput;
@:nullSafety
class BlendModesShader extends FlxRuntimeShader
{
public var camera:Null<ShaderInput<BitmapData>>;
public var cameraData:Null<BitmapData>;
public var camera:ShaderInput<BitmapData>;
public var cameraData:BitmapData;
public function new()
{

View file

@ -5,7 +5,7 @@ import flixel.tweens.FlxTween;
class BlueFade extends FlxShader
{
public var fadeVal(default, set):Float = 1;
public var fadeVal(default, set):Float;
function set_fadeVal(val:Float):Float
{

View file

@ -136,9 +136,6 @@ class DropShadowScreenspace extends DropShadowShader
// essentially just stole this from the AngleMask shader but repurposed it to smooth
// the threshold because without any sort of smoothing it produces horrible edges
float antialias(vec2 fragCoord, float curThreshold, bool useMask) {
if (AA_STAGES == 0.0) {
return intensityPass(fragCoord, curThreshold, useMask);
}
// In GLSL 100, we need to use constant loop bounds
// Well assume a reasonable maximum for AA_STAGES and use a fixed loop

View file

@ -198,7 +198,7 @@ class DropShadowShader extends FlxShader
/*
Loads an image for the mask.
While you *could* directly set the value of the mask, this function works for both HTML5 and native targets.
While you *could* directly set the value of the mask, this function works for both HTML5 and desktop targets.
*/
public function loadAltMask(path:String)
{
@ -362,9 +362,6 @@ class DropShadowShader extends FlxShader
// essentially just stole this from the AngleMask shader but repurposed it to smooth
// the threshold because without any sort of smoothing it produces horrible edges
float antialias(vec2 fragCoord, float curThreshold, bool useMask) {
if (AA_STAGES == 0.0) {
return intensityPass(fragCoord, curThreshold, useMask);
}
// In GLSL 100, we need to use constant loop bounds
// Well assume a reasonable maximum for AA_STAGES and use a fixed loop

View file

@ -5,10 +5,9 @@ import flixel.addons.display.FlxRuntimeShader;
/**
* Note... not actually gaussian!
*/
@:nullSafety
class GaussianBlurShader extends FlxRuntimeShader
{
public var amount:Float = 1;
public var amount:Float;
public function new(amount:Float = 1.0)
{

View file

@ -2,7 +2,6 @@ package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
@:nullSafety
class Grayscale extends FlxRuntimeShader
{
public var amount:Float = 1;

Some files were not shown because too many files have changed in this diff Show more