Merge branch 'rewrite/master' into blazin-rain

This commit is contained in:
Cameron Taylor 2024-04-30 15:25:03 -04:00
commit ab74cc2d5a
72 changed files with 961 additions and 338 deletions

0
.github/hooks/post-checkout vendored Normal file → Executable file
View file

0
.github/hooks/post-merge vendored Normal file → Executable file
View file

0
.github/hooks/pre-push vendored Normal file → Executable file
View file

22
.vscode/tasks.json vendored
View file

@ -1,13 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "lime",
"command": "test",
"group": {
"kind": "build",
"isDefault": true
}
}
]
"version": "2.0.0",
"tasks": [
{
"type": "lime",
"command": "test",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View file

@ -124,7 +124,7 @@
<haxelib name="polymod" /> <!-- Modding framework -->
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
<haxelib name="hxCodec" if="desktop" unless="hl" /> <!-- Video playback -->
<haxelib name="funkVis"/>
<haxelib name="funkin.vis"/>
<haxelib name="json2object" /> <!-- JSON parsing -->

2
art

@ -1 +1 @@
Subproject commit 00463685fa570f0c853d08e250b46ef80f30bc48
Subproject commit f72947b65fe0555821f827dccd562f01d308486d

View file

@ -16,5 +16,5 @@
- Mac: [`lime setup mac` Documentation](https://lime.openfl.org/docs/advanced-setup/macos/)
- Linux: [`lime setup linux` Documentation](https://lime.openfl.org/docs/advanced-setup/linux/)
- HTML5: Compiles without any extra setup
6. If you are targeting for native, you likely need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
7. `lime test PLATFORM` !
6. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
7. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State).

186
docs/FNFC-SPEC.md Normal file
View file

@ -0,0 +1,186 @@
# .fnfc File Specification
*Updated 2024-04-29*
- Manifest version: `1.0.0`
- Metadata version: `2.2.2`
- Chart data version: `2.0.0`
## Introduction
This document describes the structure of the FNFC file format used for saving and loading charts in the Chart Editor for Friday Night Funkin'. It is designed to refactor how the game stores levels, as the original format was clunky and poorly extensible.
FNFC files are a store of all the required files for crediting and editing charts. This includes any relevant audio files, which is perfect for collaboration between charters.
## Overview
Friday Night Funkin' charts utilize a concept called "variations"; these are groups of difficulties which share gameplay data. This is done to prevent significant redundancy; if each chart
If a difficulty for a song should have the same events as another difficulty, those difficulties should be in the same variation (for example, Normal and Hard). If a particular difficulty for a song should have different song events, characters, stages, or music (including instruments or vocals) from the base variation, they should use a different variation. Difficulties for a song start in the `default` variation.
An example of this is the Erect and Nightmare difficulties. These are defined in an alternative variation, which allows for those difficulties to utilize different events and music from the base variation.
The Chart Editor is made aware of which variations are available in the current chart file by querying the `playData.songVariations` key in the `default` variation's metadata file.
The chart data itself is split into two files. The `<songid>-metadata.json` file should contain all the information the game needs to display a song in menus such as Freeplay, including but not limited to the song name, artist, and BPM. The `<songid>-chart.json` should only contain the note data for each difficulty of that variation, and the song event data for the variation. This allows the game to, when it first loads, parse and cache the metadata for all available songs for use in menus, while keeping the bulky chart file data unloaded until that chart specifically is played.
Note that the game itself does not store its songs as FNFC files; rather, it stores the `metadata.json`, `chart.json`, and `.ogg` files separately.
Note also that the files may include some values whose functionality are not yet fully implemented into the game itself.
## File Contents
FNFC files are standard ZIP files containing the following:
- `manifest.json`: This file contains minimal information to minimize work in the parsing of chart files.
- `Inst.ogg`: Song instrumental for the default variation.
- `Inst-<instid>.ogg`: *(optional)* An alternative instrumental which can be used.
- `Voices-<charid>.ogg`: *(optional)* Song vocals for a specific character, for the default variation.
- `Voices-<charid>-<variation>.ogg`: *(optional)* Song vocals for a specific character, for an alternate variation.
- `<songid>-metadata.json`: Song metadata for the `default` variation.
- `<songid>-metadata-<variation>.json`: *(optional)* Song metadata for alternate variations.
- `<songid>-chart.json`: Song chart data for the `default` variation.
- `<songid>-chart-<variation>.json`: *(optional)* Song chart data for alternate variations.
## Files
Note that each component file contains its own separate [Semantic Version](https://semver.org/) number which the game adheres to when parsing. New functionality (with backwards compatibility) should be represented by a `1.x.0` change and breaking changes should be kept to a minimum, and represented by a `x.0.0` change.
### manifest.json
`manifest.json` is a JSON-formatted text file, containing a single object with the following keys:
- `version`: The Semantic Version string for the manifest file.
- `songId`: The song ID associated with this chart. Used to allow the Chart Editor to easily determine the proper filenames for the `metadata` and `chart` files.
#### manifest.json Example
```jsonc
{
"version": "1.0.0", // The Semantic Version string.
"songId": "dadbattle" // The song ID.
}
```
### Inst.ogg
This is an audio file in the OGG container format with the Vorbis audio codec.
This file is mandatory. A chart file without an instrumental track is considered invalid.
This file is used as the default backing track for the song.
### Inst-<instid>.ogg
This is an audio file in the OGG container format with the Vorbis audio codec.
This file is optional if no alternative instrumental is specified in any variation.
This file is used as an alternate backing track for the song. It is specified using the `playData.characters.instrumental` key in the current variation's metadata file.
### Voices-<charid>.ogg
This is an audio file in the OGG container format with the Vorbis audio codec.
This file is optional. The game will look for the specified file but will ignore if it is missing.
This file is used for the character vocal track for the song. The game will look for and play the vocal track for the player (using the ID specified by the `playData.characters.player` key in the current variation's metadata file), and the opponent (using the ID specified by the `playData.characters.opponent` key in the current variation's metadata file).
### Voices-<charid>-<variation>.ogg
This is an audio file in the OGG container format with the Vorbis audio codec.
This file is optional. The game will look for the specified file but will ignore if it is missing.
This file is used for the character vocal track for the song, for the given variation. The game will look for and play the vocal track for the player and the opponent using the same JSON keys as the `Voices-<charid>.ogg` files for the default variation, while also applying the current variation ID.
### <songid>-metadata.json
`<songid>-metadata.json` is a JSON-formatted text file, specifying metadata about the default variation for this song. It has the following keys:
- `version`: The Semantic Version string for the metadata file.
- `songName`: The human readable name for the song, as a string.
- `artist`: The human readable artist(s) for the song, as a string.
- `timeFormat`: The time format. In the future, this will allow chart files to define the timestamps for BPM changes, note data, or event data, in fractional beats and steps, but for now the only supported value is the string `"ms"`.
- `timeChanges`: An array of Song Time Change objects. Note that at least one Song Time Change object must be specified, with a timestamp of `0`.
- `playData`: A Song Play Data object.
- `offsets`: A Song Offset Data object.
- `generatedBy`: A string specified when creating a chart. Should only be used for debugging purposes, and not read or used by the game. Custom engines should modify `Constants.hx` to ensure unique values in case of issues with the metadata or chart data.
The Song Time Change objects have the following keys:
- `t`: The timestamp for the BPM change, in milliseconds, as a float.
- `bpm`: The new song timing, in beats per minute, as a float.
- `n`: *optional* Time signature numerator, as an integer. Defaults to 4. (int). Optional, defaults to `4`.
- `d`: *optional* Time signature denominator, as an integer. Should only ever be a power of two. Defaults to `4`.
- `bt`: *optional* Beat tuplets. This defines how many steps each beat is divided into. Defaults to `[4, 4, 4, 4]`
The Song Play Data objects have the following keys:
- `album`: The album ID to display in the Freeplay menu, as a string ID.
- `previewStart`: The timestamp to begin the audio preview for this track in the Freeplay Menu, in milliseconds, as a float.
- `previewEnd`: The timestamp to end the audio preview for this track in the Freeplay Menu, in milliseconds, as a float.
- `ratings`: An map object; each key is a difficulty ID and each value is an integer difficulty rating value for display in Freeplay.
- `songVariations`: An array of string variation IDs this song has. The game will attempt to read `<songid>-metadata-<variationid>.json` and `<songid>-chart-<variationid>.json` files for each variation ID included in this list.
- `difficulties`: An array of string difficulties this song has available to play. Any difficulties in this list will be made available to players in-game, and any difficulties not in this list will be ignored.
- `characters`: A Song Character Data object.
- `stage`: The stage to use for this chart, as a string ID.
- `noteStyle`: The note style to use for this chart, as a string ID.
The Song Character Data objects have the following keys:
- `player`: The player character to use, as a string ID.
- `girlfriend`: The girlfriend character to use, as a string ID.
- `opponent`: The opponent character to use, as a string ID.
- `instrumental`: The instrumental ID to use. Defaults to a blank string to use `Inst.ogg`
- `altInstrumentals`:
The Song Offset Data objects have the following keys:
#### <songid>-metadata.json Example
```jsonc
{
"version": "2.2.2", // Semantic Version string
"songName": "DadBattle", // Readable song name
"artist": "Kawai Sprite", // Song artist(s)
"timeFormat": "ms", // Time format to use
"timeChanges": [{ // List of BPM changes. Must have at least one.
"t": 0, // BPM change timestamp in milliseconds.
"bpm": 180 // The target BPM.
}],
"playData": {
"album": "volume1", // The album to display in Freeplay.
"previewStart": 0, // Time (ms) to start preview at
"previewEnd": 15000, // Time (ms) to end preview at
"ratings": { // Rating data for each difficulty
"easy": 1, // Rating for Easy difficulty
"normal": 3, // Rating for Normal difficulty
"hard": 5 // Rating for Hard difficulty
},
"songVariations": [ // Available variation files.
"erect" // This says dadbattle-metadata-erect.json exists.
],
"difficulties": [ // Available difficulties.
"easy",
"normal",
"hard"
],
"characters": { // Characters to use for this variation.
"player": "bf", // Boyfriend
"girlfriend": "gf", // Girlfriend
"opponent": "dad" // Daddy Dearest
},
"stage": "mainStage", // Week 1 stage.
"noteStyle": "funkin" // Default note style.
},
"generatedBy": "EliteMasterEric (by hand)" // Unique string.
}
```
### <songid>-chart.json
Song chart data for the `default` variation.
### <songid>-metadata-<variation>.json
*(optional)* Song metadata for alternate variations.
### <songid>-chart-<variation>.json
*(optional)* Song chart data for alternate variations.

View file

@ -46,10 +46,10 @@
"version": "3.5.0"
},
{
"name": "funkVis",
"name": "funkin.vis",
"type": "git",
"dir": null,
"ref": "7fc9901553fbe9b8fcf6e2e84bc86eabeaf29701",
"ref": "98c9db09f0bbfedfe67a84538a5814aaef80bdea",
"url": "https://github.com/FunkinCrew/funkVis"
},
{
@ -80,7 +80,7 @@
"name": "hxCodec",
"type": "git",
"dir": null,
"ref": "387e1665d6feb5762358134f168e6ebfe46acec8",
"ref": "c0c7f2680cc190c932a549c2e2fdd9b0ba2bd10e",
"url": "https://github.com/FunkinCrew/hxCodec"
},
{
@ -171,4 +171,4 @@
"url": "https://github.com/FunkinCrew/thx.semver"
}
]
}
}

View file

@ -113,7 +113,9 @@ class Main extends Sprite
addChild(game);
#if debug
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
#end
addChild(fpsCounter);

View file

@ -5,6 +5,9 @@ import flixel.util.FlxSignal;
import flixel.math.FlxMath;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongDataUtils;
import funkin.save.Save;
import haxe.Timer;
import flixel.sound.FlxSound;
/**
* A core class which handles musical timing throughout the game,
@ -89,6 +92,9 @@ class Conductor
*/
public var songPosition(default, null):Float = 0;
var prevTimestamp:Float = 0;
var prevTime:Float = 0;
/**
* Beats per minute of the current song at the current time.
*/
@ -233,8 +239,41 @@ class Conductor
/**
* An offset set by the user to compensate for input lag.
* No matter if you're using a local conductor or not, this always loads
* to/from the save file
*/
public var inputOffset:Float = 0;
public var inputOffset(get, set):Int;
/**
* An offset set by the user to compensate for audio/visual lag
* No matter if you're using a local conductor or not, this always loads
* to/from the save file
*/
public var audioVisualOffset(get, set):Int;
function get_inputOffset():Int
{
return Save.instance.options.inputOffset;
}
function set_inputOffset(value:Int):Int
{
Save.instance.options.inputOffset = value;
Save.instance.flush();
return Save.instance.options.inputOffset;
}
function get_audioVisualOffset():Int
{
return Save.instance.options.audioVisualOffset;
}
function set_audioVisualOffset(value:Int):Int
{
Save.instance.options.audioVisualOffset = value;
Save.instance.flush();
return Save.instance.options.audioVisualOffset;
}
/**
* The number of beats in a measure. May be fractional depending on the time signature.
@ -353,16 +392,19 @@ class Conductor
* BPM, current step, etc. will be re-calculated based on the song position.
*
* @param songPosition The current position in the song in milliseconds.
* Leave blank to use the `FlxG.sound.music` position.
* Leave blank to use the FlxG.sound.music position.
* @param applyOffsets If it should apply the instrumentalOffset + formatOffset + audioVisualOffset
*/
public function update(?songPos:Float):Void
public function update(?songPos:Float, applyOffsets:Bool = true, forceDispatch:Bool = false)
{
if (songPos == null)
{
// Take into account instrumental and file format song offsets.
songPos = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0;
songPos = (FlxG.sound.music != null) ? FlxG.sound.music.time : 0.0;
}
// Take into account instrumental and file format song offsets.
songPos += applyOffsets ? (instrumentalOffset + formatOffset + audioVisualOffset) : 0;
var oldMeasure:Float = this.currentMeasure;
var oldBeat:Float = this.currentBeat;
var oldStep:Float = this.currentStep;
@ -421,6 +463,35 @@ class Conductor
{
this.onMeasureHit.dispatch();
}
// only update the timestamp if songPosition actually changed
// which it doesn't do every frame!
if (prevTime != this.songPosition)
{
// Update the timestamp for use in-between frames
prevTime = this.songPosition;
prevTimestamp = Std.int(Timer.stamp() * 1000);
}
}
/**
* Can be called in-between frames, usually for input related things
* that can potentially get processed on exact milliseconds/timestmaps.
* If you need song position, use `Conductor.instance.songPosition` instead
* for use in update() related functions.
* @param soundToCheck Which FlxSound object to check, defaults to FlxG.sound.music if no input
* @return Float
*/
public function getTimeWithDiff(?soundToCheck:FlxSound):Float
{
if (soundToCheck == null) soundToCheck = FlxG.sound.music;
// trace(this.songPosition);
@:privateAccess
this.songPosition = soundToCheck._channel.position;
// return this.songPosition + (Std.int(Timer.stamp() * 1000) - prevTimestamp);
// trace("\t--> " + this.songPosition);
return this.songPosition;
}
/**
@ -468,7 +539,7 @@ class Conductor
}
// Update currentStepTime
this.update(Conductor.instance.songPosition);
this.update(this.songPosition, false);
}
/**
@ -587,7 +658,8 @@ class Conductor
}
/**
* Add variables of the current Conductor instance to the Flixel debugger.
* Adds Conductor fields to the Flixel debugger variable display.
* @param conductorToUse The conductor to use. Defaults to `Conductor.instance`.
*/
public static function watchQuick(?target:Conductor):Void
{

View file

@ -16,14 +16,14 @@ import funkin.util.macro.MacroUtil;
import funkin.util.WindowUtil;
import funkin.play.PlayStatePlaylist;
import openfl.display.BitmapData;
import funkin.data.level.LevelRegistry;
import funkin.data.story.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.freeplay.AlbumRegistry;
import funkin.data.dialogue.conversation.ConversationRegistry;
import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry;
import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.freeplay.album.AlbumRegistry;
import funkin.data.song.SongRegistry;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.modding.module.ModuleHandler;
@ -74,6 +74,7 @@ class InitState extends FlxState
//
// Setup window events (like callbacks for onWindowClose)
// and fullscreen keybind setup
WindowUtil.initWindowEvents();
// Disable the thing on Windows where it tries to send a bug report to Microsoft because why do they care?
WindowUtil.disableCrashHandler();
@ -304,7 +305,7 @@ class InitState extends FlxState
*/
function startLevel(levelId:String, difficultyId:String = 'normal'):Void
{
var currentLevel:funkin.ui.story.Level = funkin.data.level.LevelRegistry.instance.fetchEntry(levelId);
var currentLevel:funkin.ui.story.Level = funkin.data.story.level.LevelRegistry.instance.fetchEntry(levelId);
if (currentLevel == null)
{

View file

@ -53,7 +53,7 @@ class Preferences
static function get_flashingLights():Bool
{
return Save.instance.options.flashingLights;
return Save?.instance?.options?.flashingLights ?? true;
}
static function set_flashingLights(value:Bool):Bool
@ -115,7 +115,7 @@ class Preferences
static function get_autoPause():Bool
{
return Save.instance.options.autoPause;
return Save?.instance?.options?.autoPause ?? true;
}
static function set_autoPause(value:Bool):Bool

View file

@ -8,8 +8,8 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import funkin.util.MathUtil;
import funkVis.dsp.SpectralAnalyzer;
import funkVis.audioclip.frontends.LimeAudioClip;
import funkin.vis.dsp.SpectralAnalyzer;
import funkin.vis.audioclip.frontends.LimeAudioClip;
using Lambda;
@ -90,7 +90,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
}
/**
* TJW funkVis based visualizer! updateFFT() is the old nasty shit that dont worky!
* TJW funkin.vis based visualizer! updateFFT() is the old nasty shit that dont worky!
*/
function drawFFT():Void
{

View file

@ -120,7 +120,7 @@ class DataParse
}
}
public static function backdropData(json:Json, name:String):funkin.data.dialogue.ConversationData.BackdropData
public static function backdropData(json:Json, name:String):funkin.data.dialogue.conversation.ConversationData.BackdropData
{
switch (json.value)
{
@ -152,7 +152,7 @@ class DataParse
}
}
public static function outroData(json:Json, name:String):Null<funkin.data.dialogue.ConversationData.OutroData>
public static function outroData(json:Json, name:String):Null<funkin.data.dialogue.conversation.ConversationData.OutroData>
{
switch (json.value)
{

View file

@ -0,0 +1,9 @@
# Dialogue Conversation Data Schema Changelog
All notable changes to this project 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).
## [1.0.0]
Initial release.

View file

@ -1,4 +1,4 @@
package funkin.data.dialogue;
package funkin.data.dialogue.conversation;
import funkin.data.animation.AnimationData;

View file

@ -1,7 +1,7 @@
package funkin.data.dialogue;
package funkin.data.dialogue.conversation;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.data.dialogue.ConversationData;
import funkin.data.dialogue.conversation.ConversationData;
import funkin.play.cutscene.dialogue.ScriptedConversation;
class ConversationRegistry extends BaseRegistry<Conversation, ConversationData>

View file

@ -0,0 +1,13 @@
# Dialogue Box Data Schema Changelog
All notable changes to this project 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).
## [1.1.0]
### Added
- Added an option to specify the font used by the dialogue box. Defaults to `Arial` if unspecified.
## [1.0.0]
Initial release.

View file

@ -1,4 +1,4 @@
package funkin.data.dialogue;
package funkin.data.dialogue.dialoguebox;
import funkin.data.animation.AnimationData;

View file

@ -1,7 +1,7 @@
package funkin.data.dialogue;
package funkin.data.dialogue.dialoguebox;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.data.dialogue.DialogueBoxData;
import funkin.data.dialogue.dialoguebox.DialogueBoxData;
import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
class DialogueBoxRegistry extends BaseRegistry<DialogueBox, DialogueBoxData>

View file

@ -0,0 +1,9 @@
# Dialogue Speaker Data Schema Changelog
All notable changes to this project 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).
## [1.0.0]
Initial release.

View file

@ -1,4 +1,4 @@
package funkin.data.dialogue;
package funkin.data.dialogue.speaker;
import funkin.data.animation.AnimationData;

View file

@ -1,7 +1,7 @@
package funkin.data.dialogue;
package funkin.data.dialogue.speaker;
import funkin.play.cutscene.dialogue.Speaker;
import funkin.data.dialogue.SpeakerData;
import funkin.data.dialogue.speaker.SpeakerData;
import funkin.play.cutscene.dialogue.ScriptedSpeaker;
class SpeakerRegistry extends BaseRegistry<Speaker, SpeakerData>

View file

@ -1,4 +1,4 @@
package funkin.data.freeplay;
package funkin.data.freeplay.album;
import funkin.data.animation.AnimationData;

View file

@ -1,7 +1,7 @@
package funkin.data.freeplay;
package funkin.data.freeplay.album;
import funkin.ui.freeplay.Album;
import funkin.data.freeplay.AlbumData;
import funkin.data.freeplay.album.AlbumData;
import funkin.ui.freeplay.ScriptedAlbum;
class AlbumRegistry extends BaseRegistry<Album, AlbumData>

View file

@ -0,0 +1,9 @@
# Freeplay Album Data Schema Changelog
All notable changes to this project 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).
## [1.0.0]
Initial release.

View file

@ -0,0 +1,36 @@
# Song Chart Data Schema Changelog
All notable changes to this project 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).
## [2.2.2]
### Added
- Added `playData.previewStart` and `playData.previewEnd` fields to specify when in the song should the song's audio should be played as a preview in Freeplay.
## [2.2.1]
### Added
- Added `playData.offsets` field to specify instrumental and vocal offsets.
## [2.2.0]
### Added
- Added `playData.album` to specify the album art to display in Freeplay.
- Added `playData.ratings` for difficulty ratings displayed in Freeplay.
### Changed
- Renamed `playData.noteSkin` to `playData.noteStyle`.
## [2.1.0]
### Changed
- Rearranged the `playData` field.
- Refactored the `playableChars`
### Removed
- Removed the `variation` field.
## [2.0.0]
Full refactor of the chart format for improved structure.
### Added
- Added a semantic version field for migration tracking.
## [1.0.0]
Initial version from 2020.

View file

@ -0,0 +1,14 @@
# Story Mode Level Data Schema Changelog
All notable changes to this project 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).
## [1.0.1]
### Added
- Added the ability to specify a hexadecimal color in the `assetPath` field instead of a texture key.
- In this case, the `scale` property will be used to determine the size of the rectangle in pixels.
## [1.0.0]
Initial release.

View file

@ -11,7 +11,7 @@ class StageRegistry extends BaseRegistry<Stage, StageData>
* 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.1";
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.0";
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";

View file

@ -0,0 +1,9 @@
# Story Mode Level Data Schema Changelog
All notable changes to this project 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).
## [1.0.0]
Initial release.

View file

@ -1,4 +1,4 @@
package funkin.data.level;
package funkin.data.story.level;
import funkin.data.animation.AnimationData;
@ -13,7 +13,7 @@ typedef LevelData =
* When making changes to the level data format, this should be incremented,
* and a migration function should be added to LevelDataParser to handle old versions.
*/
@:default(funkin.data.level.LevelRegistry.LEVEL_DATA_VERSION)
@:default(funkin.data.story.level.LevelRegistry.LEVEL_DATA_VERSION)
var version:String;
/**

View file

@ -1,7 +1,7 @@
package funkin.data.level;
package funkin.data.story.level;
import funkin.ui.story.Level;
import funkin.data.level.LevelData;
import funkin.data.story.level.LevelData;
import funkin.ui.story.ScriptedLevel;
class LevelRegistry extends BaseRegistry<Level, LevelData>

View file

@ -29,7 +29,7 @@ class HSVShader extends FlxRuntimeShader
function set_saturation(value:Float):Float
{
this.setFloat('sat', value);
this.setFloat('_sat', value);
this.saturation = value;
return this.saturation;
@ -37,7 +37,7 @@ class HSVShader extends FlxRuntimeShader
function set_value(value:Float):Float
{
this.setFloat('val', value);
this.setFloat('_val', value);
this.value = value;
return this.value;

View file

@ -66,6 +66,7 @@ class Controls extends FlxActionSet
var _volume_up = new FunkinAction(Action.VOLUME_UP);
var _volume_down = new FunkinAction(Action.VOLUME_DOWN);
var _volume_mute = new FunkinAction(Action.VOLUME_MUTE);
var _fullscreen = new FunkinAction(Action.FULLSCREEN);
var byName:Map<String, FunkinAction> = new Map<String, FunkinAction>();
@ -272,6 +273,11 @@ class Controls extends FlxActionSet
inline function get_VOLUME_MUTE()
return _volume_mute.check();
public var FULLSCREEN(get, never):Bool;
inline function get_FULLSCREEN()
return _fullscreen.check();
public function new(name, scheme:KeyboardScheme = null)
{
super(name);
@ -296,6 +302,7 @@ class Controls extends FlxActionSet
add(_volume_up);
add(_volume_down);
add(_volume_mute);
add(_fullscreen);
for (action in digitalActions) {
if (Std.isOfType(action, FunkinAction)) {
@ -399,6 +406,7 @@ class Controls extends FlxActionSet
case VOLUME_UP: _volume_up;
case VOLUME_DOWN: _volume_down;
case VOLUME_MUTE: _volume_mute;
case FULLSCREEN: _fullscreen;
}
}
@ -474,6 +482,8 @@ class Controls extends FlxActionSet
func(_volume_down, JUST_PRESSED);
case VOLUME_MUTE:
func(_volume_mute, JUST_PRESSED);
case FULLSCREEN:
func(_fullscreen, JUST_PRESSED);
}
}
@ -668,6 +678,7 @@ class Controls extends FlxActionSet
bindKeys(Control.VOLUME_UP, getDefaultKeybinds(scheme, Control.VOLUME_UP));
bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN));
bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE));
bindKeys(Control.FULLSCREEN, getDefaultKeybinds(scheme, Control.FULLSCREEN));
bindMobileLol();
}
@ -696,6 +707,8 @@ class Controls extends FlxActionSet
case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS];
case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO];
case Control.FULLSCREEN: return [FlxKey.F];
}
case Duo(true):
switch (control) {
@ -719,6 +732,8 @@ class Controls extends FlxActionSet
case Control.VOLUME_UP: return [PLUS];
case Control.VOLUME_DOWN: return [MINUS];
case Control.VOLUME_MUTE: return [ZERO];
case Control.FULLSCREEN: return [FlxKey.F];
}
case Duo(false):
switch (control) {
@ -742,6 +757,8 @@ class Controls extends FlxActionSet
case Control.VOLUME_UP: return [NUMPADPLUS];
case Control.VOLUME_DOWN: return [NUMPADMINUS];
case Control.VOLUME_MUTE: return [NUMPADZERO];
case Control.FULLSCREEN: return [];
}
default:
// Fallthrough.
@ -876,6 +893,7 @@ class Controls extends FlxActionSet
case Control.CUTSCENE_ADVANCE: return [A];
case Control.DEBUG_MENU: return [];
case Control.DEBUG_CHART: return [];
case Control.FULLSCREEN: return [];
default:
// Fallthrough.
}
@ -1398,6 +1416,7 @@ enum Control
ACCEPT;
BACK;
PAUSE;
FULLSCREEN;
// CUTSCENE
CUTSCENE_ADVANCE;
// SCREENSHOT
@ -1443,6 +1462,7 @@ enum abstract Action(String) to String from String
var BACK = "back";
var PAUSE = "pause";
var RESET = "reset";
var FULLSCREEN = "fullscreen";
// SCREENSHOT
var SCREENSHOT = "screenshot";
// CUTSCENE

View file

@ -293,8 +293,9 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
timestamp *= Constants.NS_PER_MS; // 18126000000 38367000000
timestamp -= Conductor.instance.inputOffset * Constants.NS_PER_MS;
// trace(timestamp);
updateKeyStates(key, true);
if (getInputByKey(key)?.justPressed ?? false)

View file

@ -1,14 +1,14 @@
package funkin.modding;
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.dialogue.conversation.ConversationRegistry;
import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry;
import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.level.LevelRegistry;
import funkin.data.story.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.song.SongRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.freeplay.AlbumRegistry;
import funkin.data.freeplay.album.AlbumRegistry;
import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.save.Save;

View file

@ -2,14 +2,16 @@ package funkin.play;
import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxG;
import flixel.util.FlxTimer;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.data.song.SongRegistry;
import funkin.ui.freeplay.FreeplayState;
@ -17,6 +19,7 @@ import funkin.graphics.FunkinSprite;
import funkin.play.cutscene.VideoCutscene;
import funkin.play.PlayState;
import funkin.ui.AtlasText;
import funkin.ui.debug.latency.LatencyState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.transition.StickerSubState;

View file

@ -19,7 +19,7 @@ import flixel.util.FlxTimer;
import funkin.api.newgrounds.NGio;
import funkin.audio.FunkinSound;
import funkin.audio.VoicesGroup;
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.dialogue.conversation.ConversationRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.notestyle.NoteStyleData;
import funkin.data.notestyle.NoteStyleRegistry;
@ -568,7 +568,7 @@ class PlayState extends MusicBeatSubState
var generatedMusic:Bool = false;
var perfectMode:Bool = false;
static final BACKGROUND_COLOR:FlxColor = FlxColor.MAGENTA;
static final BACKGROUND_COLOR:FlxColor = FlxColor.BLACK;
/**
* Instantiate a new PlayState.
@ -890,7 +890,8 @@ class PlayState extends MusicBeatSubState
{
if (isInCountdown)
{
Conductor.instance.update(Conductor.instance.songPosition + elapsed * 1000);
// Do NOT apply offsets at this point, because they already got applied the previous frame!
Conductor.instance.update(Conductor.instance.songPosition + elapsed * 1000, false);
if (Conductor.instance.songPosition >= (startTimestamp)) startSong();
}
}
@ -2049,10 +2050,10 @@ class PlayState extends MusicBeatSubState
{
if (note == null) continue;
// TODO: Does this properly account for offsets?
var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
var hitWindowCenter = note.strumTime;
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
// TODO: Are offsets being accounted for in the correct direction?
var hitWindowStart = note.strumTime + Conductor.instance.inputOffset - Constants.HIT_WINDOW_MS;
var hitWindowCenter = note.strumTime + Conductor.instance.inputOffset;
var hitWindowEnd = note.strumTime + Conductor.instance.inputOffset + Constants.HIT_WINDOW_MS;
if (Conductor.instance.songPosition > hitWindowEnd)
{

View file

@ -42,7 +42,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
if (PlayState.instance.currentStageId.startsWith('school'))
{
rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.65));
rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7));
rating.antialiasing = false;
}
else
@ -133,7 +133,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
if (PlayState.instance.currentStageId.startsWith('school'))
{
numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE));
numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE * 0.7));
numScore.antialiasing = false;
}
else

View file

@ -8,13 +8,13 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxSort;
import funkin.audio.FunkinSound;
import funkin.data.dialogue.ConversationData;
import funkin.data.dialogue.ConversationData.DialogueEntryData;
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.dialogue.DialogueBoxData;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerData;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.dialogue.conversation.ConversationData;
import funkin.data.dialogue.conversation.ConversationData.DialogueEntryData;
import funkin.data.dialogue.conversation.ConversationRegistry;
import funkin.data.dialogue.dialoguebox.DialogueBoxData;
import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry;
import funkin.data.dialogue.speaker.SpeakerData;
import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.IRegistryEntry;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEvent;

View file

@ -11,8 +11,8 @@ import funkin.modding.events.ScriptEvent;
import funkin.audio.FunkinSound;
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import flixel.util.FlxColor;
import funkin.data.dialogue.DialogueBoxData;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.dialoguebox.DialogueBoxData;
import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry;
class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry<DialogueBoxData>
{

View file

@ -6,8 +6,8 @@ import funkin.modding.events.ScriptEvent;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.util.assets.FlxAnimationUtil;
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import funkin.data.dialogue.SpeakerData;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.dialogue.speaker.SpeakerData;
import funkin.data.dialogue.speaker.SpeakerRegistry;
/**
* The character sprite which displays during dialogue.

View file

@ -127,7 +127,7 @@ class FocusCameraSongEvent extends SongEvent
switch (ease)
{
case 'CLASSIC': // Old-school. No ease. Just set follow point.
PlayState.instance.resetCamera();
PlayState.instance.resetCamera(false, true);
PlayState.instance.cameraFollowPoint.setPosition(targetX, targetY);
case 'INSTANT': // Instant ease. Duration is automatically 0.
PlayState.instance.tweenCameraToPosition(targetX, targetY, 0);

View file

@ -15,6 +15,7 @@ import funkin.play.notes.SustainTrail;
import funkin.data.song.SongData.SongNoteData;
import funkin.ui.options.PreferencesMenu;
import funkin.util.SortUtil;
import funkin.modding.events.ScriptEvent;
/**
* A group of sprites which handles the receptor, the note splashes, and the notes (with sustains) for a given player.
@ -45,6 +46,25 @@ class Strumline extends FlxSpriteGroup
*/
public var isPlayer:Bool;
/**
* Usually you want to keep this as is, but if you are using a Strumline and
* playing a sound that has it's own conductor, set this (LatencyState for example)
*/
public var conductorInUse(get, set):Conductor;
var _conductorInUse:Null<Conductor>;
function get_conductorInUse():Conductor
{
if (_conductorInUse == null) return Conductor.instance;
return _conductorInUse;
}
function set_conductorInUse(value:Conductor):Conductor
{
return _conductorInUse = value;
}
/**
* The notes currently being rendered on the strumline.
* This group iterates over this every frame to update note positions.
@ -151,24 +171,10 @@ class Strumline extends FlxSpriteGroup
updateNotes();
}
var frameMax:Int;
var animFinishedEver:Bool;
/**
* Get a list of notes within + or - the given strumtime.
* @param strumTime The current time.
* @param hitWindow The hit window to check.
* Return notes that are within `Constants.HIT_WINDOW` ms of the strumline.
* @return An array of `NoteSprite` objects.
*/
public function getNotesInRange(strumTime:Float, hitWindow:Float):Array<NoteSprite>
{
var hitWindowStart:Float = strumTime - hitWindow;
var hitWindowEnd:Float = strumTime + hitWindow;
return notes.members.filter(function(note:NoteSprite) {
return note != null && note.alive && !note.hasBeenHit && note.strumTime >= hitWindowStart && note.strumTime <= hitWindowEnd;
});
}
public function getNotesMayHit():Array<NoteSprite>
{
return notes.members.filter(function(note:NoteSprite) {
@ -176,6 +182,10 @@ class Strumline extends FlxSpriteGroup
});
}
/**
* Return hold notes that are within `Constants.HIT_WINDOW` ms of the strumline.
* @return An array of `SustainTrail` objects.
*/
public function getHoldNotesHitOrMissed():Array<SustainTrail>
{
return holdNotes.members.filter(function(holdNote:SustainTrail) {
@ -183,19 +193,6 @@ class Strumline extends FlxSpriteGroup
});
}
public function getHoldNotesInRange(strumTime:Float, hitWindow:Float):Array<SustainTrail>
{
var hitWindowStart:Float = strumTime - hitWindow;
var hitWindowEnd:Float = strumTime + hitWindow;
return holdNotes.members.filter(function(note:SustainTrail) {
return note != null
&& note.alive
&& note.strumTime >= hitWindowStart
&& (note.strumTime + note.fullSustainLength) <= hitWindowEnd;
});
}
public function getNoteSprite(noteData:SongNoteData):NoteSprite
{
if (noteData == null) return null;
@ -280,7 +277,7 @@ class Strumline extends FlxSpriteGroup
* @param strumTime
* @return Float
*/
static function calculateNoteYPos(strumTime:Float, vwoosh:Bool = true):Float
public function calculateNoteYPos(strumTime:Float, vwoosh:Bool = true):Float
{
// Make the note move faster visually as it moves offscreen.
// var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
@ -288,7 +285,8 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Float = 1.0;
var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
return Constants.PIXELS_PER_MS * (Conductor.instance.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
return
Constants.PIXELS_PER_MS * (conductorInUse.songPosition - strumTime - Conductor.instance.inputOffset) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
}
function updateNotes():Void
@ -301,8 +299,8 @@ class Strumline extends FlxSpriteGroup
// if (conductorInUse.currentStep == 0) nextNoteIndex = 0;
var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0;
var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS;
var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS;
var hitWindowStart:Float = conductorInUse.songPosition - Constants.HIT_WINDOW_MS;
var renderWindowStart:Float = conductorInUse.songPosition + RENDER_DISTANCE_MS;
for (noteIndex in nextNoteIndex...noteData.length)
{
@ -351,7 +349,7 @@ class Strumline extends FlxSpriteGroup
{
if (holdNote == null || !holdNote.alive) continue;
if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote)
if (conductorInUse.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote)
{
if (isPlayer && !isKeyHeld(holdNote.noteDirection))
{
@ -365,7 +363,7 @@ class Strumline extends FlxSpriteGroup
var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Constants.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8;
if (holdNote.missedNote && Conductor.instance.songPosition >= renderWindowEnd)
if (holdNote.missedNote && conductorInUse.songPosition >= renderWindowEnd)
{
// Hold note is offscreen, kill it.
holdNote.visible = false;
@ -422,13 +420,13 @@ class Strumline extends FlxSpriteGroup
holdNote.cover.kill();
}
}
else if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote)
else if (conductorInUse.songPosition > holdNote.strumTime && holdNote.hitNote)
{
// Hold note is currently being hit, clip it off.
holdConfirm(holdNote.noteDirection);
holdNote.visible = true;
holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.instance.songPosition;
holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - conductorInUse.songPosition;
if (holdNote.sustainLength <= 10)
{

View file

@ -87,6 +87,8 @@ class Save
zoomCamera: true,
debugDisplay: false,
autoPause: true,
inputOffset: 0,
audioVisualOffset: 0,
controls:
{
@ -866,6 +868,18 @@ typedef SaveDataOptions =
*/
var autoPause:Bool;
/**
* Offset the users inputs by this many ms.
* @default `0`
*/
var inputOffset:Int;
/**
* Affects the delay between the audio and the visuals during gameplay
* @default `0`
*/
var audioVisualOffset:Int;
var controls:
{
var p1:

View file

@ -0,0 +1,12 @@
# Changelog
All notable changes to this project 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).
## [2.0.3] - 2024-01-09
### Added
- `inputOffset:Float` to `SongDataOptions`
- `audioVisualOffset:Float` to `SongDataOptions`

View file

@ -29,6 +29,21 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
public var leftWatermarkText:FlxText = null;
public var rightWatermarkText:FlxText = null;
public var conductorInUse(get, set):Conductor;
var _conductorInUse:Null<Conductor>;
function get_conductorInUse():Conductor
{
if (_conductorInUse == null) return Conductor.instance;
return _conductorInUse;
}
function set_conductorInUse(value:Conductor):Conductor
{
return _conductorInUse = value;
}
public function new()
{
super();
@ -111,7 +126,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
public function stepHit():Bool
{
var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
var event = new SongTimeScriptEvent(SONG_STEP_HIT, conductorInUse.currentBeat, conductorInUse.currentStep);
dispatchEvent(event);
@ -122,7 +137,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
public function beatHit():Bool
{
var event = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
var event = new SongTimeScriptEvent(SONG_BEAT_HIT, conductorInUse.currentBeat, conductorInUse.currentStep);
dispatchEvent(event);

View file

@ -21,6 +21,21 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
public var leftWatermarkText:FlxText = null;
public var rightWatermarkText:FlxText = null;
public var conductorInUse(get, set):Conductor;
var _conductorInUse:Null<Conductor>;
function get_conductorInUse():Conductor
{
if (_conductorInUse == null) return Conductor.instance;
return _conductorInUse;
}
function set_conductorInUse(value:Conductor):Conductor
{
return _conductorInUse = value;
}
public function new(bgColor:FlxColor = FlxColor.TRANSPARENT)
{
super();
@ -51,7 +66,6 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
override function update(elapsed:Float):Void
{
// 3.59% CPU Usage (100% is FlxTypedGroup#update() and most of that is updating each member.)
super.update(elapsed);
// Emergency exit button.
@ -62,11 +76,8 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
// Display Conductor info in the watch window.
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
Conductor.watchQuick(conductorInUse);
// 0.09% CPU Usage?
Conductor.watchQuick();
// 4.31% CPU Usage
dispatchEvent(new UpdateScriptEvent(elapsed));
}
@ -94,7 +105,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
*/
public function stepHit():Bool
{
var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, conductorInUse.currentBeat, conductorInUse.currentStep);
dispatchEvent(event);
@ -110,7 +121,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
*/
public function beatHit():Bool
{
var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, conductorInUse.currentBeat, conductorInUse.currentStep);
dispatchEvent(event);

View file

@ -23,7 +23,9 @@ class DebugMenuSubState extends MusicBeatSubState
override function create():Void
{
FlxTransitionableState.skipNextTransIn = true;
super.create();
bgColor = 0x00000000;
// Create an object for the camera to track.
@ -48,9 +50,12 @@ class DebugMenuSubState extends MusicBeatSubState
items.onChange.add(onMenuChange);
add(items);
FlxTransitionableState.skipNextTransIn = true;
// Create each menu item.
// Call onMenuChange when the first item is created to move the camera .
onMenuChange(createItem("CHART EDITOR", openChartEditor));
createItem("Input Offset Testing", openInputOffsetTesting);
createItem("ANIMATION EDITOR", openAnimationEditor);
createItem("STAGE EDITOR", openStageEditor);
createItem("TEST STICKERS", testStickers);
@ -92,6 +97,12 @@ class DebugMenuSubState extends MusicBeatSubState
FlxG.switchState(() -> new ChartEditorState());
}
function openInputOffsetTesting()
{
openSubState(new funkin.ui.debug.latency.LatencyState());
trace('Input Offset Testing');
}
function openAnimationEditor()
{
FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());

View file

@ -5,13 +5,13 @@ import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.events.ScriptEvent;
import flixel.util.FlxColor;
import funkin.ui.MusicBeatState;
import funkin.data.dialogue.ConversationData;
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.dialogue.DialogueBoxData;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerData;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.freeplay.AlbumRegistry;
import funkin.data.dialogue.conversation.ConversationData;
import funkin.data.dialogue.conversation.ConversationRegistry;
import funkin.data.dialogue.dialoguebox.DialogueBoxData;
import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry;
import funkin.data.dialogue.speaker.SpeakerData;
import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.freeplay.album.AlbumRegistry;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.play.cutscene.dialogue.Speaker;

View file

@ -7,7 +7,6 @@ import flash.text.TextField;
import flash.text.TextFormatAlign;
import flixel.math.FlxMath;
import flixel.system.debug.DebuggerUtil;
import flixel.system.debug.stats.Stats;
import flixel.util.FlxColor;
import flixel.util.FlxDestroyUtil;
@ -16,13 +15,31 @@ import flixel.util.FlxDestroyUtil;
* SHAMELESSLY STOLEN FROM FLIXEL
* https://github.com/HaxeFlixel/flixel/blob/master/flixel/system/debug/stats/StatsGraph.hx
*/
#if FLX_DEBUG
class CoolStatsGraph extends Sprite
{
static inline var AXIS_COLOR:FlxColor = 0xffffff;
static inline var AXIS_ALPHA:Float = 0.5;
static inline var HISTORY_MAX:Int = 500;
/**
* How often to update the stats, in ms. The lower, the more performance-intense!
*/
static inline var UPDATE_DELAY:Int = 250;
/**
* The initial width of the stats window.
*/
static inline var INITIAL_WIDTH:Int = 160;
static inline var FPS_COLOR:FlxColor = 0xff96ff00;
static inline var MEMORY_COLOR:FlxColor = 0xff009cff;
static inline var DRAW_TIME_COLOR:FlxColor = 0xffA60004;
static inline var UPDATE_TIME_COLOR:FlxColor = 0xffdcd400;
public static inline var LABEL_COLOR:FlxColor = 0xaaffffff;
public static inline var TEXT_SIZE:Int = 11;
public static inline var DECIMALS:Int = 1;
public var minLabel:TextField;
public var curLabel:TextField;
public var maxLabel:TextField;
@ -45,6 +62,7 @@ class CoolStatsGraph extends Sprite
public function new(X:Int, Y:Int, Width:Int, Height:Int, GraphColor:FlxColor, Unit:String, LabelWidth:Int = 45, ?Label:String)
{
super();
x = X;
y = Y;
_width = Width - LabelWidth;
@ -57,11 +75,11 @@ class CoolStatsGraph extends Sprite
_axis = new Shape();
_axis.x = _labelWidth + 10;
maxLabel = DebuggerUtil.createTextField(0, 0, Stats.LABEL_COLOR, Stats.TEXT_SIZE);
curLabel = DebuggerUtil.createTextField(0, (_height / 2) - (Stats.TEXT_SIZE / 2), graphColor, Stats.TEXT_SIZE);
minLabel = DebuggerUtil.createTextField(0, _height - Stats.TEXT_SIZE, Stats.LABEL_COLOR, Stats.TEXT_SIZE);
maxLabel = DebuggerUtil.createTextField(0, 0, LABEL_COLOR, TEXT_SIZE);
curLabel = DebuggerUtil.createTextField(0, (_height / 2) - (TEXT_SIZE / 2), graphColor, TEXT_SIZE);
minLabel = DebuggerUtil.createTextField(0, _height - TEXT_SIZE, LABEL_COLOR, TEXT_SIZE);
avgLabel = DebuggerUtil.createTextField(_labelWidth + 20, (_height / 2) - (Stats.TEXT_SIZE / 2) - 10, Stats.LABEL_COLOR, Stats.TEXT_SIZE);
avgLabel = DebuggerUtil.createTextField(_labelWidth + 20, (_height / 2) - (TEXT_SIZE / 2) - 10, LABEL_COLOR, TEXT_SIZE);
avgLabel.width = _width;
avgLabel.defaultTextFormat.align = TextFormatAlign.CENTER;
avgLabel.alpha = 0.5;
@ -136,7 +154,7 @@ class CoolStatsGraph extends Sprite
function formatValue(value:Float):String
{
return FlxMath.roundDecimal(value, Stats.DECIMALS) + " " + _unit;
return FlxMath.roundDecimal(value, DECIMALS) + " " + _unit;
}
public function average():Float
@ -157,4 +175,3 @@ class CoolStatsGraph extends Sprite
history = null;
}
}
#end

View file

@ -8,20 +8,27 @@ import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.math.FlxMath;
import funkin.ui.MusicBeatSubState;
import flixel.sound.FlxSound;
import flixel.system.debug.stats.StatsGraph;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.play.notes.NoteSprite;
import funkin.ui.debug.latency.CoolStatsGraph;
import haxe.Timer;
import openfl.events.KeyboardEvent;
import funkin.input.PreciseInputManager;
import funkin.play.notes.Strumline;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.data.notestyle.NoteStyleData;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.song.SongData.SongNoteData;
import haxe.Timer;
import flixel.FlxCamera;
class LatencyState extends MusicBeatSubState
{
var visualOffsetText:FlxText;
var offsetText:FlxText;
var noteGrp:FlxTypedGroup<NoteSprite>;
var strumLine:FlxSprite;
var noteGrp:Array<SongNoteData> = [];
var strumLine:Strumline;
var blocks:FlxTypedGroup<FlxSprite>;
@ -31,76 +38,81 @@ class LatencyState extends MusicBeatSubState
var beatTrail:FlxSprite;
var diffGrp:FlxTypedGroup<FlxText>;
var offsetsPerBeat:Array<Int> = [];
var offsetsPerBeat:Array<Null<Int>> = [];
var swagSong:HomemadeMusic;
#if FLX_DEBUG
var funnyStatsGraph:CoolStatsGraph;
var realStats:CoolStatsGraph;
#end
var previousVolume:Float;
var stateCamera:FlxCamera;
/**
* A local conductor instance for this testing class, in-case we are in a PlayState
* because I'm too lazy to set the old variables for conductor stuff !
*/
var localConductor:Conductor;
// stores values of what the previous persistent draw/update stuff was, example if opened
// from pause menu, we want to NOT draw persistently, but then resume drawing once closed
var prevPersistentDraw:Bool;
var prevPersistentUpdate:Bool;
override function create()
{
super.create();
prevPersistentDraw = FlxG.state.persistentDraw;
prevPersistentUpdate = FlxG.state.persistentUpdate;
FlxG.state.persistentDraw = false;
FlxG.state.persistentUpdate = false;
localConductor = new Conductor();
conductorInUse = localConductor;
stateCamera = new FlxCamera(0, 0, FlxG.width, FlxG.height);
stateCamera.bgColor = FlxColor.BLACK;
FlxG.cameras.add(stateCamera);
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
add(bg);
if (FlxG.sound.music != null)
{
previousVolume = FlxG.sound.music.volume;
FlxG.sound.music.volume = 0; // only want to mute the volume, incase we are coming from pause menu
}
else
previousVolume = 1; // defaults to 1 if no music is playing 🤔 also fuck it, emoji in code comment
swagSong = new HomemadeMusic();
swagSong.loadEmbedded(Paths.sound('soundTest'), true);
swagSong.looped = true;
swagSong.play();
FlxG.sound.list.add(swagSong);
FlxG.sound.music = swagSong;
FlxG.sound.music.play();
PreciseInputManager.instance.onInputPressed.add(preciseInputPressed);
#if FLX_DEBUG
funnyStatsGraph = new CoolStatsGraph(0, Std.int(FlxG.height / 2), FlxG.width, Std.int(FlxG.height / 2), FlxColor.PINK, "time");
FlxG.addChildBelowMouse(funnyStatsGraph);
PreciseInputManager.instance.onInputReleased.add(preciseInputReleased);
realStats = new CoolStatsGraph(0, Std.int(FlxG.height / 2), FlxG.width, Std.int(FlxG.height / 2), FlxColor.YELLOW, "REAL");
FlxG.addChildBelowMouse(realStats);
#end
FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, key -> {
trace(key.charCode);
if (key.charCode == 120) generateBeatStuff();
trace("\tEVENT PRESS: \t" + FlxG.sound.music.time + " " + Timer.stamp());
// trace(FlxG.sound.music.prevTimestamp);
trace(FlxG.sound.music.time);
trace("\tFR FR PRESS: \t" + swagSong.getTimeWithDiff());
// trace("\tREDDIT: \t" + swagSong.frfrTime + " " + Timer.stamp());
@:privateAccess
trace("\tREDDIT: \t" + FlxG.sound.music._channel.position + " " + Timer.stamp());
// trace("EVENT LISTENER: " + key);
});
// funnyStatsGraph.hi
localConductor.forceBPM(60);
Conductor.instance.forceBPM(60);
noteGrp = new FlxTypedGroup<NoteSprite>();
add(noteGrp);
diffGrp = new FlxTypedGroup<FlxText>();
add(diffGrp);
// var musSpec:PolygonSpectogram = new PolygonSpectogram(FlxG.sound.music, FlxColor.RED, FlxG.height, Math.floor(FlxG.height / 2));
// musSpec.x += 170;
// musSpec.scrollFactor.set();
// musSpec.waveAmplitude = 100;
// musSpec.realtimeVisLenght = 0.45;
// // musSpec.visType = FREQUENCIES;
// add(musSpec);
for (beat in 0...Math.floor(FlxG.sound.music.length / Conductor.instance.beatLengthMs))
for (beat in 0...Math.floor(swagSong.length / (localConductor.stepLengthMs * 2)))
{
var beatTick:FlxSprite = new FlxSprite(songPosToX(beat * Conductor.instance.beatLengthMs), FlxG.height - 15);
var beatTick:FlxSprite = new FlxSprite(songPosToX(beat * (localConductor.stepLengthMs * 2)), FlxG.height - 15);
beatTick.makeGraphic(2, 15);
beatTick.alpha = 0.3;
add(beatTick);
var offsetTxt:FlxText = new FlxText(songPosToX(beat * Conductor.instance.beatLengthMs), FlxG.height - 26, 0, "swag");
var offsetTxt:FlxText = new FlxText(songPosToX(beat * (localConductor.stepLengthMs * 2)), FlxG.height - 26, 0, "");
offsetTxt.alpha = 0.5;
diffGrp.add(offsetTxt);
offsetsPerBeat.push(0);
offsetsPerBeat.push(null);
}
songVisFollowAudio = new FlxSprite(0, FlxG.height - 20).makeGraphic(2, 20, FlxColor.YELLOW);
@ -121,32 +133,92 @@ class LatencyState extends MusicBeatSubState
for (i in 0...8)
{
var block = new FlxSprite(2, 50 * i).makeGraphic(48, 48);
block.alpha = 0;
var block = new FlxSprite(2, ((FlxG.height / 8) + 2) * i).makeGraphic(Std.int(FlxG.height / 8), Std.int((FlxG.height / 8) - 4));
block.alpha = 0.1;
blocks.add(block);
}
for (i in 0...32)
{
var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault());
noteGrp.add(note);
}
var strumlineBG:FlxSprite = new FlxSprite();
add(strumlineBG);
offsetText = new FlxText();
offsetText.screenCenter();
add(offsetText);
strumLine = new FlxSprite(FlxG.width / 2, 100).makeGraphic(FlxG.width, 5);
strumLine = new Strumline(NoteStyleRegistry.instance.fetchDefault(), true);
strumLine.conductorInUse = localConductor;
strumLine.screenCenter();
add(strumLine);
super.create();
strumlineBG.x = strumLine.x;
strumlineBG.makeGraphic(Std.int(strumLine.width), FlxG.height, 0xFFFFFFFF);
strumlineBG.alpha = 0.1;
visualOffsetText = new FlxText();
visualOffsetText.setFormat(Paths.font("vcr.ttf"), 20);
visualOffsetText.x = (FlxG.height / 8) + 10;
visualOffsetText.y = 10;
visualOffsetText.fieldWidth = strumLine.x - visualOffsetText.x - 10;
add(visualOffsetText);
offsetText = new FlxText();
offsetText.setFormat(Paths.font("vcr.ttf"), 20);
offsetText.x = strumLine.x + strumLine.width + 10;
offsetText.y = 10;
offsetText.fieldWidth = FlxG.width - offsetText.x - 10;
add(offsetText);
var helpText:FlxText = new FlxText();
helpText.setFormat(Paths.font("vcr.ttf"), 20);
helpText.text = "Press BACK to return to main menu";
helpText.x = FlxG.width - helpText.width;
helpText.y = FlxG.height - (helpText.height * 2) - 2;
add(helpText);
regenNoteData();
}
function preciseInputPressed(event:PreciseInputEvent)
{
generateBeatStuff(event);
strumLine.pressKey(event.noteDirection);
strumLine.playPress(event.noteDirection);
}
function preciseInputReleased(event:PreciseInputEvent)
{
strumLine.playStatic(event.noteDirection);
strumLine.releaseKey(event.noteDirection);
}
override public function close():Void
{
PreciseInputManager.instance.onInputPressed.remove(preciseInputPressed);
PreciseInputManager.instance.onInputReleased.remove(preciseInputReleased);
FlxG.sound.music.volume = previousVolume;
swagSong.stop();
FlxG.sound.list.remove(swagSong);
FlxG.cameras.remove(stateCamera);
FlxG.state.persistentDraw = prevPersistentDraw;
FlxG.state.persistentUpdate = prevPersistentUpdate;
super.close();
}
function regenNoteData()
{
for (i in 0...32)
{
var note:SongNoteData = new SongNoteData((localConductor.stepLengthMs * 2) * i, 1);
noteGrp.push(note);
}
strumLine.applyNoteData(noteGrp);
}
override function stepHit():Bool
{
if (Conductor.instance.currentStep % 4 == 2)
if (localConductor.currentStep % 4 == 2)
{
blocks.members[((Conductor.instance.currentBeat % 8) + 1) % 8].alpha = 0.5;
blocks.members[((localConductor.currentBeat % 8) + 1) % 8].alpha = 0.5;
}
return super.stepHit();
@ -154,11 +226,11 @@ class LatencyState extends MusicBeatSubState
override function beatHit():Bool
{
if (Conductor.instance.currentBeat % 8 == 0) blocks.forEach(blok -> {
blok.alpha = 0;
if (localConductor.currentBeat % 8 == 0) blocks.forEach(blok -> {
blok.alpha = 0.1;
});
blocks.members[Conductor.instance.currentBeat % 8].alpha = 1;
blocks.members[localConductor.currentBeat % 8].alpha = 1;
// block.visible = !block.visible;
return super.beatHit();
@ -171,117 +243,114 @@ class LatencyState extends MusicBeatSubState
trace(FlxG.sound.music._channel.position);
*/
// localConductor.update(swagSong.time, false);
localConductor.update(swagSong.time, false);
if (FlxG.keys.justPressed.S)
{
trace("\tUPDATE PRESS: \t" + FlxG.sound.music.time + " " + Timer.stamp());
}
// localConductor.songPosition += (Timer.stamp() * 1000) - FlxG.sound.music.prevTimestamp;
if (FlxG.keys.justPressed.SPACE)
{
if (FlxG.sound.music.playing) FlxG.sound.music.pause();
else
FlxG.sound.music.resume();
}
songPosVis.x = songPosToX(localConductor.songPosition);
songVisFollowAudio.x = songPosToX(localConductor.songPosition - localConductor.audioVisualOffset);
songVisFollowVideo.x = songPosToX(localConductor.songPosition - localConductor.inputOffset);
if (FlxG.keys.pressed.D) FlxG.sound.music.time += 1000 * FlxG.elapsed;
visualOffsetText.text = "Visual Offset: " + localConductor.audioVisualOffset + "ms";
visualOffsetText.text += "\n\nYou can press SPACE+Left/Right to change this value.";
visualOffsetText.text += "\n\nYou can hold SHIFT to step 1ms at a time";
Conductor.instance.update(swagSong.getTimeWithDiff() - Conductor.instance.inputOffset);
// Conductor.instance.songPosition += (Timer.stamp() * 1000) - FlxG.sound.music.prevTimestamp;
songPosVis.x = songPosToX(Conductor.instance.songPosition);
songVisFollowAudio.x = songPosToX(Conductor.instance.songPosition - Conductor.instance.instrumentalOffset);
songVisFollowVideo.x = songPosToX(Conductor.instance.songPosition - Conductor.instance.inputOffset);
offsetText.text = "INST Offset: " + Conductor.instance.instrumentalOffset + "ms";
offsetText.text += "\nINPUT Offset: " + Conductor.instance.inputOffset + "ms";
offsetText.text += "\ncurrentStep: " + Conductor.instance.currentStep;
offsetText.text += "\ncurrentBeat: " + Conductor.instance.currentBeat;
offsetText.text = "INPUT Offset (Left/Right to change): " + localConductor.inputOffset + "ms";
offsetText.text += "\n\nYou can hold SHIFT to step 1ms at a time";
var avgOffsetInput:Float = 0;
var loopInd:Int = 0;
for (offsetThing in offsetsPerBeat)
{
if (offsetThing == null) continue;
avgOffsetInput += offsetThing;
loopInd++;
}
avgOffsetInput /= offsetsPerBeat.length;
avgOffsetInput /= loopInd;
offsetText.text += "\naverage input offset needed: " + avgOffsetInput;
offsetText.text += "\n\nEstimated average input offset needed: " + avgOffsetInput;
var multiply:Float = 10;
var multiply:Int = 10;
if (FlxG.keys.pressed.SHIFT) multiply = 1;
if (FlxG.keys.pressed.CONTROL)
if (FlxG.keys.pressed.CONTROL || FlxG.keys.pressed.SPACE)
{
if (FlxG.keys.justPressed.RIGHT)
{
Conductor.instance.instrumentalOffset += 1.0 * multiply;
localConductor.audioVisualOffset += 1 * multiply;
}
if (FlxG.keys.justPressed.LEFT)
{
Conductor.instance.instrumentalOffset -= 1.0 * multiply;
localConductor.audioVisualOffset -= 1 * multiply;
}
}
else
{
if (FlxG.keys.justPressed.RIGHT)
if (FlxG.keys.anyJustPressed([LEFT, RIGHT]))
{
Conductor.instance.inputOffset += 1.0 * multiply;
}
if (FlxG.keys.justPressed.RIGHT)
{
localConductor.inputOffset += 1 * multiply;
}
if (FlxG.keys.justPressed.LEFT)
{
Conductor.instance.inputOffset -= 1.0 * multiply;
if (FlxG.keys.justPressed.LEFT)
{
localConductor.inputOffset -= 1 * multiply;
}
// reset the average, so you don't need to wait a full loop to start getting averages
// also reset each text member
offsetsPerBeat = [];
diffGrp.forEach(memb -> memb.text = "");
}
}
noteGrp.forEach(function(daNote:NoteSprite) {
daNote.y = (strumLine.y - ((Conductor.instance.songPosition - Conductor.instance.instrumentalOffset) - daNote.noteData.time) * 0.45);
daNote.x = strumLine.x + 30;
if (daNote.y < strumLine.y) daNote.alpha = 0.5;
if (daNote.y < 0 - daNote.height)
{
daNote.alpha = 1;
// daNote.data.strumTime += Conductor.instance.beatLengthMs * 8;
}
});
if (controls.BACK)
{
close();
}
super.update(elapsed);
}
function generateBeatStuff()
function generateBeatStuff(event:PreciseInputEvent)
{
Conductor.instance.update(swagSong.getTimeWithDiff());
// localConductor.update(swagSong.getTimeWithDiff());
var closestBeat:Int = Math.round(Conductor.instance.songPosition / Conductor.instance.beatLengthMs) % diffGrp.members.length;
var getDiff:Float = Conductor.instance.songPosition - (closestBeat * Conductor.instance.beatLengthMs);
getDiff -= Conductor.instance.inputOffset;
var inputLatencyMs:Float = haxe.Int64.toInt(PreciseInputManager.getCurrentTimestamp() - event.timestamp) / 1000.0 / 1000.0;
// trace("input latency: " + inputLatencyMs + "ms");
// trace("cur timestamp: " + PreciseInputManager.getCurrentTimestamp() + "ns");
// trace("event timestamp: " + event.timestamp + "ns");
// trace("songtime: " + localConductor.getTimeWithDiff(swagSong) + "ms");
var closestBeat:Int = Math.round(localConductor.getTimeWithDiff(swagSong) / (localConductor.stepLengthMs * 2)) % diffGrp.members.length;
var getDiff:Float = localConductor.getTimeWithDiff(swagSong) - (closestBeat * (localConductor.stepLengthMs * 2));
// getDiff -= localConductor.inputOffset;
getDiff -= inputLatencyMs;
getDiff -= localConductor.audioVisualOffset;
// lil fix for end of song
if (closestBeat == 0 && getDiff >= Conductor.instance.beatLengthMs * 2) getDiff -= FlxG.sound.music.length;
if (closestBeat == 0 && getDiff >= localConductor.stepLengthMs * 2) getDiff -= swagSong.length;
trace("\tDISTANCE TO CLOSEST BEAT: " + getDiff + "ms");
trace("\tCLOSEST BEAT: " + closestBeat);
beatTrail.x = songPosVis.x;
diffGrp.members[closestBeat].text = getDiff + "ms";
offsetsPerBeat[closestBeat] = Std.int(getDiff);
offsetsPerBeat[closestBeat] = Math.round(getDiff);
}
function songPosToX(pos:Float):Float
{
return FlxMath.remapToRange(pos, 0, FlxG.sound.music.length, 0, FlxG.width);
return FlxMath.remapToRange(pos, 0, swagSong.length, 0, FlxG.width);
}
}
class HomemadeMusic extends FlxSound
{
public var prevTimestamp:Int = 0;
public var timeWithDiff:Float = 0;
public function new()
{

View file

@ -1,8 +1,8 @@
package funkin.ui.freeplay;
import funkin.data.freeplay.AlbumData;
import funkin.data.freeplay.album.AlbumData;
import funkin.data.freeplay.album.AlbumRegistry;
import funkin.data.animation.AnimationData;
import funkin.data.freeplay.AlbumRegistry;
import funkin.data.IRegistryEntry;
import flixel.graphics.FlxGraphic;

View file

@ -7,7 +7,7 @@ import flixel.util.FlxSort;
import flixel.tweens.FlxTween;
import flixel.util.FlxTimer;
import flixel.tweens.FlxEase;
import funkin.data.freeplay.AlbumRegistry;
import funkin.data.freeplay.album.AlbumRegistry;
import funkin.util.assets.FlxAnimationUtil;
import funkin.graphics.FunkinSprite;
import funkin.util.SortUtil;

View file

@ -18,7 +18,7 @@ import flixel.util.FlxColor;
import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.data.level.LevelRegistry;
import funkin.data.story.level.LevelRegistry;
import funkin.data.song.SongRegistry;
import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite;
@ -336,13 +336,12 @@ class FreeplayState extends MusicBeatSubState
exitMovers.set([blackOverlayBullshitLOLXD, bgDad],
{
x: FlxG.width * 1.5,
y: bgDad.height,
speed: 0.4,
wait: 0
});
add(bgDad);
FlxTween.tween(blackOverlayBullshitLOLXD, {x: pinkBack.width * 0.75}, 1, {ease: FlxEase.quintOut});
FlxTween.tween(blackOverlayBullshitLOLXD, {x: pinkBack.width * 0.75}, 0.7, {ease: FlxEase.quintOut});
blackOverlayBullshitLOLXD.shader = bgDad.shader;
@ -409,6 +408,7 @@ class FreeplayState extends MusicBeatSubState
var sillyStroke:StrokeShader = new StrokeShader(0xFFFFFFFF, 2, 2);
fnfFreeplay.shader = sillyStroke;
ostName.shader = sillyStroke;
add(fnfFreeplay);
add(ostName);
@ -430,6 +430,7 @@ class FreeplayState extends MusicBeatSubState
add(fp);
var clearBoxSprite:FlxSprite = new FlxSprite(1165, 65).loadGraphic(Paths.image('freeplay/clearBox'));
clearBoxSprite.visible = false;
add(clearBoxSprite);
txtCompletion = new AtlasText(1185, 87, '69', AtlasFont.FREEPLAY_CLEAR);
@ -456,9 +457,17 @@ class FreeplayState extends MusicBeatSubState
default:
generateSongList({filterType: REGEXP, filterData: str}, true);
}
// We want to land on the first song of the group, rather than random song when changing letter sorts
// that is, only if there's more than one song in the group!
if (grpCapsules.members.length > 0)
{
curSelected = 1;
changeSelection();
}
};
exitMovers.set([fp, txtCompletion, fnfHighscoreSpr],
exitMovers.set([fp, txtCompletion, fnfHighscoreSpr, txtCompletion, clearBoxSprite],
{
x: FlxG.width,
speed: 0.3
@ -501,6 +510,7 @@ class FreeplayState extends MusicBeatSubState
fp.visible = true;
fp.updateScore(0);
clearBoxSprite.visible = true;
txtCompletion.visible = true;
intendedCompletion = 0;
@ -744,13 +754,13 @@ class FreeplayState extends MusicBeatSubState
switch (txtCompletion.text.length)
{
case 3:
txtCompletion.x = 1185 - 10;
txtCompletion.offset.x = 10;
case 2:
txtCompletion.x = 1185;
txtCompletion.offset.x = 0;
case 1:
txtCompletion.x = 1185 + 24;
txtCompletion.offset.x = -24;
default:
txtCompletion.x = 1185;
txtCompletion.offset.x = 0;
}
handleInputs(elapsed);

View file

@ -54,7 +54,7 @@ class MainMenuState extends MusicBeatState
playMenuMusic();
persistentUpdate = false;
persistentUpdate = true;
persistentDraw = true;
var bg:FlxSprite = new FlxSprite(Paths.image('menuBG'));
@ -103,6 +103,7 @@ class MainMenuState extends MusicBeatState
// Freeplay has its own custom transition
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
openSubState(new FreeplayState());
});
@ -274,6 +275,8 @@ class MainMenuState extends MusicBeatState
public function openPrompt(prompt:Prompt, onClose:Void->Void):Void
{
menuItems.enabled = false;
persistentUpdate = false;
prompt.closeCallback = function() {
menuItems.enabled = true;
if (onClose != null) onClose();
@ -326,6 +329,8 @@ class MainMenuState extends MusicBeatState
#if CHART_EDITOR_SUPPORTED
if (controls.DEBUG_MENU)
{
persistentUpdate = false;
FlxG.state.openSubState(new DebugMenuSubState());
}
#end

View file

@ -20,6 +20,7 @@ class FunkinSoundTray extends FlxSoundTray
{
var graphicScale:Float = 0.30;
var lerpYPos:Float = 0;
var alphaTarget:Float = 0;
var volumeMaxSound:String;
@ -40,7 +41,7 @@ class FunkinSoundTray extends FlxSoundTray
// makes an alpha'd version of all the bars (bar_10.png)
var backingBar:Bitmap = new Bitmap(Assets.getBitmapData(Paths.image("soundtray/bars_10")));
backingBar.x = 10;
backingBar.x = 9;
backingBar.y = 5;
backingBar.scaleX = graphicScale;
backingBar.scaleY = graphicScale;
@ -56,7 +57,7 @@ class FunkinSoundTray extends FlxSoundTray
for (i in 1...11)
{
var bar:Bitmap = new Bitmap(Assets.getBitmapData(Paths.image("soundtray/bars_" + i)));
bar.x = 10;
bar.x = 9;
bar.y = 5;
bar.scaleX = graphicScale;
bar.scaleY = graphicScale;
@ -77,31 +78,34 @@ class FunkinSoundTray extends FlxSoundTray
override public function update(MS:Float):Void
{
y = MathUtil.coolLerp(y, lerpYPos, 0.1);
alpha = MathUtil.coolLerp(alpha, alphaTarget, 0.25);
// Animate sound tray thing
if (_timer > 0)
{
_timer -= (MS / 1000);
alphaTarget = 1;
}
else if (y > -height)
else if (y >= -height)
{
lerpYPos = -height - 10;
alphaTarget = 0;
}
if (y <= -height)
if (y <= -height)
{
visible = false;
active = false;
#if FLX_SAVE
// Save sound preferences
if (FlxG.save.isBound)
{
visible = false;
active = false;
#if FLX_SAVE
// Save sound preferences
if (FlxG.save.isBound)
{
FlxG.save.data.mute = FlxG.sound.muted;
FlxG.save.data.volume = FlxG.sound.volume;
FlxG.save.flush();
}
#end
FlxG.save.data.mute = FlxG.sound.muted;
FlxG.save.data.volume = FlxG.sound.volume;
FlxG.save.flush();
}
#end
}
}

View file

@ -1,5 +1,6 @@
package funkin.ui.options;
import funkin.ui.debug.latency.LatencyState;
import flixel.FlxSprite;
import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState;
@ -190,6 +191,9 @@ class OptionsMenu extends Page
add(items = new TextMenuList());
createItem("PREFERENCES", function() switchPage(Preferences));
createItem("CONTROLS", function() switchPage(Controls));
createItem("INPUT OFFSETS", function() {
FlxG.state.openSubState(new LatencyState());
});
#if newgrounds
if (NGio.isLoggedIn) createItem("LOGOUT", selectLogout);

View file

@ -6,8 +6,8 @@ import flixel.util.FlxColor;
import funkin.play.song.Song;
import funkin.data.IRegistryEntry;
import funkin.data.song.SongRegistry;
import funkin.data.level.LevelRegistry;
import funkin.data.level.LevelData;
import funkin.data.story.level.LevelRegistry;
import funkin.data.story.level.LevelData;
/**
* An object used to retrieve data about a story mode level (also known as "weeks").

View file

@ -2,7 +2,7 @@ package funkin.ui.story;
import funkin.play.stage.Bopper;
import funkin.util.assets.FlxAnimationUtil;
import funkin.data.level.LevelData;
import funkin.data.story.level.LevelData;
class LevelProp extends Bopper
{

View file

@ -9,7 +9,7 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.data.level.LevelRegistry;
import funkin.data.story.level.LevelRegistry;
import funkin.data.song.SongRegistry;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEvent;

View file

@ -12,12 +12,12 @@ import funkin.ui.MusicBeatState;
* After about 2 minutes of inactivity on the title screen,
* the game will enter the Attract state, as a reference to physical arcade machines.
*
* In the current version, this just plays the Kickstarter trailer, but this can be changed to
* In the current version, this just plays the ~~Kickstarter trailer~~ Erect teaser, but this can be changed to
* gameplay footage, a generic game trailer, or something more elaborate.
*/
class AttractState extends MusicBeatState
{
static final ATTRACT_VIDEO_PATH:String = Paths.stripLibrary(Paths.videos('kickstarterTrailer', 'shared'));
static final ATTRACT_VIDEO_PATH:String = Paths.stripLibrary(Paths.videos('erectTeaser'));
public override function create():Void
{

View file

@ -251,7 +251,7 @@ class TitleState extends MusicBeatState
var transitioning:Bool = false;
override function update(elapsed:Float)
override function update(elapsed:Float):Void
{
FlxG.bitmapLog.add(FlxG.camera.buffer);
@ -286,7 +286,6 @@ class TitleState extends MusicBeatState
}
if (FlxG.sound.music != null) Conductor.instance.update(FlxG.sound.music.time);
if (FlxG.keys.justPressed.F) FlxG.fullscreen = !FlxG.fullscreen;
// do controls.PAUSE | controls.ACCEPT instead?
var pressedEnter:Bool = FlxG.keys.justPressed.ENTER;

View file

@ -326,15 +326,20 @@ class LoadingState extends MusicBeatSubState
// I will fix this properly later I swear -eric
if (!path.endsWith('.png')) continue;
FunkinSprite.cacheTexture(path);
new Future<String>(function() {
FunkinSprite.cacheTexture(path);
// Another dumb hack: FlxAnimate fetches from OpenFL's BitmapData cache directly and skips the FlxGraphic cache.
// Since FlxGraphic tells OpenFL to not cache it, we have to do it manually.
if (path.endsWith('spritemap1.png'))
{
trace('Preloading FlxAnimate asset: ${path}');
openfl.Assets.getBitmapData(path, true);
}
return 'Done precaching ${path}';
}, true);
// Another dumb hack: FlxAnimate fetches from OpenFL's BitmapData cache directly and skips the FlxGraphic cache.
// Since FlxGraphic tells OpenFL to not cache it, we have to do it manually.
if (path.endsWith('spritemap1.png'))
{
trace('Preloading FlxAnimate asset: ${path}');
openfl.Assets.getBitmapData(path, true);
}
trace("Queued ${path} for precaching");
// FunkinSprite.cacheTexture(path);
}
// FunkinSprite.cacheAllNoteStyleTextures(noteStyle) // This will replace the stuff above!

View file

@ -112,6 +112,7 @@ class FunkinPreloader extends FlxBasePreloader
var logo:Bitmap;
#if TOUCH_HERE_TO_PLAY
var touchHereToPlay:Bitmap;
var touchHereSprite:Sprite;
#end
var progressBarPieces:Array<Sprite>;
var progressBar:Bitmap;
@ -119,10 +120,12 @@ class FunkinPreloader extends FlxBasePreloader
var progressRightText:TextField;
var dspText:TextField;
var fnfText:TextField;
var enhancedText:TextField;
var stereoText:TextField;
var vfdShader:VFDOverlay;
var vfdBitmap:Bitmap;
var box:Sprite;
var progressLines:Sprite;
@ -162,18 +165,6 @@ class FunkinPreloader extends FlxBasePreloader
});
// addChild(logo);
#if TOUCH_HERE_TO_PLAY
touchHereToPlay = createBitmap(TouchHereToPlayImage, function(bmp:Bitmap) {
// Scale and center the touch to start image.
// We have to do this inside the async call, after the image size is known.
bmp.scaleX = bmp.scaleY = ratio;
bmp.x = (this._width - bmp.width) / 2;
bmp.y = (this._height - bmp.height) / 2;
});
touchHereToPlay.alpha = 0.0;
addChild(touchHereToPlay);
#end
var amountOfPieces:Int = 16;
progressBarPieces = [];
var maxBarWidth = this._width - BAR_PADDING * 2;
@ -212,6 +203,7 @@ class FunkinPreloader extends FlxBasePreloader
// Create the progress message.
progressLeftText = new TextField();
dspText = new TextField();
fnfText = new TextField();
enhancedText = new TextField();
stereoText = new TextField();
@ -262,6 +254,15 @@ class FunkinPreloader extends FlxBasePreloader
dspText.y = -5;
box.addChild(dspText);
fnfText.selectable = false;
fnfText.textColor = 0x000000;
fnfText.width = this._width;
fnfText.height = 20;
fnfText.x = 75;
fnfText.y = -5;
fnfText.text = 'FNF';
box.addChild(fnfText);
enhancedText.selectable = false;
enhancedText.textColor = Constants.COLOR_PRELOADER_BAR;
enhancedText.width = this._width;
@ -289,11 +290,27 @@ class FunkinPreloader extends FlxBasePreloader
// gradient.graphics.endFill();
// addChild(gradient);
var vfdBitmap:Bitmap = new Bitmap(new BitmapData(this._width, this._height, true, 0xFFFFFFFF));
vfdBitmap = new Bitmap(new BitmapData(this._width, this._height, true, 0xFFFFFFFF));
addChild(vfdBitmap);
vfdShader = new VFDOverlay();
vfdBitmap.shader = vfdShader;
#if TOUCH_HERE_TO_PLAY
touchHereToPlay = createBitmap(TouchHereToPlayImage, function(bmp:Bitmap) {
// Scale and center the touch to start image.
// We have to do this inside the async call, after the image size is known.
bmp.scaleX = bmp.scaleY = ratio;
bmp.x = (this._width - bmp.width) / 2;
bmp.y = (this._height - bmp.height) / 2;
});
touchHereToPlay.alpha = 0.0;
touchHereSprite = new Sprite();
touchHereSprite.buttonMode = false;
touchHereSprite.addChild(touchHereToPlay);
addChild(touchHereSprite);
#end
}
var lastElapsed:Float = 0.0;
@ -802,9 +819,14 @@ class FunkinPreloader extends FlxBasePreloader
if (touchHereToPlay.alpha < 1.0)
{
touchHereSprite.buttonMode = true;
touchHereToPlay.alpha = 1.0;
removeChild(vfdBitmap);
addEventListener(MouseEvent.CLICK, onTouchHereToPlay);
touchHereSprite.addEventListener(MouseEvent.MOUSE_OVER, overTouchHereToPlay);
touchHereSprite.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownTouchHereToPlay);
touchHereSprite.addEventListener(MouseEvent.MOUSE_OUT, outTouchHereToPlay);
}
return 1.0;
@ -818,9 +840,34 @@ class FunkinPreloader extends FlxBasePreloader
}
#if TOUCH_HERE_TO_PLAY
function overTouchHereToPlay(e:MouseEvent):Void
{
touchHereToPlay.scaleX = touchHereToPlay.scaleY = ratio * 1.1;
touchHereToPlay.x = (this._width - touchHereToPlay.width) / 2;
touchHereToPlay.y = (this._height - touchHereToPlay.height) / 2;
}
function outTouchHereToPlay(e:MouseEvent):Void
{
touchHereToPlay.scaleX = touchHereToPlay.scaleY = ratio * 1;
touchHereToPlay.x = (this._width - touchHereToPlay.width) / 2;
touchHereToPlay.y = (this._height - touchHereToPlay.height) / 2;
}
function mouseDownTouchHereToPlay(e:MouseEvent):Void
{
touchHereToPlay.y += 10;
}
function onTouchHereToPlay(e:MouseEvent):Void
{
touchHereToPlay.x = (this._width - touchHereToPlay.width) / 2;
touchHereToPlay.y = (this._height - touchHereToPlay.height) / 2;
removeEventListener(MouseEvent.CLICK, onTouchHereToPlay);
touchHereSprite.removeEventListener(MouseEvent.MOUSE_OVER, overTouchHereToPlay);
touchHereSprite.removeEventListener(MouseEvent.MOUSE_OUT, outTouchHereToPlay);
touchHereSprite.removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownTouchHereToPlay);
// This is the actual thing that makes the game load.
immediatelyStartGame();
@ -931,9 +978,13 @@ class FunkinPreloader extends FlxBasePreloader
progressLeftText.text = text;
dspText.defaultTextFormat = new TextFormat("Quantico", 20, 0x000000, false);
dspText.text = 'DSP\t\t\t\t\tFNF'; // fukin dum....
dspText.text = 'DSP'; // fukin dum....
dspText.textColor = 0x000000;
fnfText.defaultTextFormat = new TextFormat("Quantico", 20, 0x000000, false);
fnfText.text = 'FNF';
fnfText.textColor = 0x000000;
enhancedText.defaultTextFormat = new TextFormat("Inconsolata Black", 16, Constants.COLOR_PRELOADER_BAR, false);
enhancedText.text = 'ENHANCED';
enhancedText.textColor = Constants.COLOR_PRELOADER_BAR;
@ -971,6 +1022,7 @@ class FunkinPreloader extends FlxBasePreloader
progressRightText.alpha = logo.alpha;
box.alpha = logo.alpha;
dspText.alpha = logo.alpha;
fnfText.alpha = logo.alpha;
enhancedText.alpha = logo.alpha;
stereoText.alpha = logo.alpha;
progressLines.alpha = logo.alpha;
@ -999,9 +1051,9 @@ class FunkinPreloader extends FlxBasePreloader
*/
override function createSiteLockFailureScreen():Void
{
addChild(createSiteLockFailureBackground(Constants.COLOR_PRELOADER_LOCK_BG, Constants.COLOR_PRELOADER_LOCK_BG));
addChild(createSiteLockFailureIcon(Constants.COLOR_PRELOADER_LOCK_FG, 0.9));
addChild(createSiteLockFailureText(30));
// addChild(createSiteLockFailureBackground(Constants.COLOR_PRELOADER_LOCK_BG, Constants.COLOR_PRELOADER_LOCK_BG));
// addChild(createSiteLockFailureIcon(Constants.COLOR_PRELOADER_LOCK_FG, 0.9));
// addChild(createSiteLockFailureText(30));
}
/**

View file

@ -295,6 +295,8 @@ class Constants
/**
* Constant for the number of seconds in a minute.
*
* sex per min
*/
public static final SECS_PER_MIN:Int = 60;

View file

@ -90,6 +90,16 @@ class WindowUtil
openfl.Lib.current.stage.application.onExit.add(function(exitCode:Int) {
windowExit.dispatch(exitCode);
});
openfl.Lib.current.stage.addEventListener(openfl.events.KeyboardEvent.KEY_DOWN, (e:openfl.events.KeyboardEvent) -> {
for (key in PlayerSettings.player1.controls.getKeysForAction(FULLSCREEN))
{
if (e.keyCode == key)
{
openfl.Lib.application.window.fullscreen = !openfl.Lib.application.window.fullscreen;
}
}
});
}
/**

View file

@ -1,6 +1,6 @@
package funkin.data.level;
package funkin.data.story.level;
import funkin.data.level.LevelRegistry;
import funkin.data.story.level.LevelRegistry;
import funkin.ui.story.Level;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
@ -8,7 +8,7 @@ import massive.munit.util.Timer;
@:nullSafety
@:access(funkin.ui.story.Level)
@:access(funkin.data.level.LevelRegistry)
@:access(funkin.data.story.level.LevelRegistry)
class LevelRegistryTest extends FunkinTest
{
public function new()