Merge branch 'rewrite/master' into feature/chart-editor-selection-tweak

This commit is contained in:
Cameron Taylor 2024-01-12 04:43:17 -05:00
commit 2539b90ab8
65 changed files with 2107 additions and 978 deletions

View file

@ -111,6 +111,7 @@
<haxelib name="tink_json" /> <!-- JSON parsing (DEPRECATED) --> <haxelib name="tink_json" /> <!-- JSON parsing (DEPRECATED) -->
<haxelib name="thx.semver" /> <!-- Version string handling --> <haxelib name="thx.semver" /> <!-- Version string handling -->
<haxelib name="hmm" if="debug" /> <!-- Read library version data at compile time -->
<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support --> <haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
<!--Disable the Flixel core focus lost screen--> <!--Disable the Flixel core focus lost screen-->
@ -127,7 +128,7 @@
<haxeflag name="-w" value="-WDeprecated" /> <haxeflag name="-w" value="-WDeprecated" />
<!-- Haxe 4.3.0+: Enable pretty syntax errors and stuff. --> <!-- Haxe 4.3.0+: Enable pretty syntax errors and stuff. -->
<haxedef name="message-reporting" value="pretty" /> <haxedef name="message.reporting" value="pretty" />
<!-- _________________________________ Custom _______________________________ --> <!-- _________________________________ Custom _______________________________ -->
<!-- Disable trace() calls in release builds to bump up performance. --> <!-- Disable trace() calls in release builds to bump up performance. -->

2
art

@ -1 +1 @@
Subproject commit 1656bea5370c65879aaeb323e329f403c78071c5 Subproject commit 03e7c2a2353b184e45955c96d763b7cdf1acbc34

2
assets

@ -1 +1 @@
Subproject commit 198c3ab87e401e595be50814af0f667eb1be25e7 Subproject commit a3e2277e6f12208f9a976b80883db67c54a2a897

View file

@ -54,14 +54,14 @@
"name": "haxeui-core", "name": "haxeui-core",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "e765a3e0b7a653823e8dec765e04623f27f573f8", "ref": "5086e59e7551d775ed4d1fb0188e31de22d1312b",
"url": "https://github.com/haxeui/haxeui-core" "url": "https://github.com/haxeui/haxeui-core"
}, },
{ {
"name": "haxeui-flixel", "name": "haxeui-flixel",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "7a517d561eff49d8123c128bf9f5c1123b84d014", "ref": "2b9cff727999b53ed292b1675ac1c9089ac77600",
"url": "https://github.com/haxeui/haxeui-flixel" "url": "https://github.com/haxeui/haxeui-flixel"
}, },
{ {

View file

@ -112,5 +112,6 @@ class Main extends Sprite
Toolkit.theme = 'dark'; // don't be cringe Toolkit.theme = 'dark'; // don't be cringe
Toolkit.autoScale = false; Toolkit.autoScale = false;
funkin.input.Cursor.registerHaxeUICursors(); funkin.input.Cursor.registerHaxeUICursors();
haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
} }
} }

View file

@ -11,6 +11,7 @@ import funkin.data.song.SongDataUtils;
* A core class which handles musical timing throughout the game, * A core class which handles musical timing throughout the game,
* both in gameplay and in menus. * both in gameplay and in menus.
*/ */
@:nullSafety
class Conductor class Conductor
{ {
// onBeatHit is called every quarter note // onBeatHit is called every quarter note
@ -28,29 +29,53 @@ class Conductor
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second // 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
// 7/8 = 3.5 beats per measure = 14 steps per measure // 7/8 = 3.5 beats per measure = 14 steps per measure
/**
* The current instance of the Conductor.
* If one doesn't currently exist, a new one will be created.
*
* You can also do stuff like store a reference to the Conductor and pass it around or temporarily replace it,
* or have a second Conductor running at the same time, or other weird stuff like that if you need to.
*/
public static var instance:Conductor = new Conductor();
/**
* Signal fired when the current Conductor instance advances to a new measure.
*/
public static var measureHit(default, null):FlxSignal = new FlxSignal();
/**
* Signal fired when the current Conductor instance advances to a new beat.
*/
public static var beatHit(default, null):FlxSignal = new FlxSignal();
/**
* Signal fired when the current Conductor instance advances to a new step.
*/
public static var stepHit(default, null):FlxSignal = new FlxSignal();
/** /**
* The list of time changes in the song. * The list of time changes in the song.
* There should be at least one time change (at the beginning of the song) to define the BPM. * There should be at least one time change (at the beginning of the song) to define the BPM.
*/ */
static var timeChanges:Array<SongTimeChange> = []; var timeChanges:Array<SongTimeChange> = [];
/** /**
* The most recent time change for the current song position. * The most recent time change for the current song position.
*/ */
public static var currentTimeChange(default, null):Null<SongTimeChange>; public var currentTimeChange(default, null):Null<SongTimeChange>;
/** /**
* The current position in the song in milliseconds. * The current position in the song in milliseconds.
* Update this every frame based on the audio position using `Conductor.update()`. * Update this every frame based on the audio position using `Conductor.instance.update()`.
*/ */
public static var songPosition(default, null):Float = 0; public var songPosition(default, null):Float = 0;
/** /**
* Beats per minute of the current song at the current time. * Beats per minute of the current song at the current time.
*/ */
public static var bpm(get, never):Float; public var bpm(get, never):Float;
static function get_bpm():Float function get_bpm():Float
{ {
if (bpmOverride != null) return bpmOverride; if (bpmOverride != null) return bpmOverride;
@ -62,9 +87,9 @@ class Conductor
/** /**
* Beats per minute of the current song at the start time. * Beats per minute of the current song at the start time.
*/ */
public static var startingBPM(get, never):Float; public var startingBPM(get, never):Float;
static function get_startingBPM():Float function get_startingBPM():Float
{ {
if (bpmOverride != null) return bpmOverride; if (bpmOverride != null) return bpmOverride;
@ -78,14 +103,14 @@ class Conductor
* The current value set by `forceBPM`. * The current value set by `forceBPM`.
* If false, BPM is determined by time changes. * If false, BPM is determined by time changes.
*/ */
static var bpmOverride:Null<Float> = null; var bpmOverride:Null<Float> = null;
/** /**
* Duration of a measure in milliseconds. Calculated based on bpm. * Duration of a measure in milliseconds. Calculated based on bpm.
*/ */
public static var measureLengthMs(get, never):Float; public var measureLengthMs(get, never):Float;
static function get_measureLengthMs():Float function get_measureLengthMs():Float
{ {
return beatLengthMs * timeSignatureNumerator; return beatLengthMs * timeSignatureNumerator;
} }
@ -93,9 +118,9 @@ class Conductor
/** /**
* Duration of a beat (quarter note) in milliseconds. Calculated based on bpm. * Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
*/ */
public static var beatLengthMs(get, never):Float; public var beatLengthMs(get, never):Float;
static function get_beatLengthMs():Float function get_beatLengthMs():Float
{ {
// Tied directly to BPM. // Tied directly to BPM.
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC); return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
@ -104,25 +129,25 @@ class Conductor
/** /**
* Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm. * Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm.
*/ */
public static var stepLengthMs(get, never):Float; public var stepLengthMs(get, never):Float;
static function get_stepLengthMs():Float function get_stepLengthMs():Float
{ {
return beatLengthMs / timeSignatureNumerator; return beatLengthMs / timeSignatureNumerator;
} }
public static var timeSignatureNumerator(get, never):Int; public var timeSignatureNumerator(get, never):Int;
static function get_timeSignatureNumerator():Int function get_timeSignatureNumerator():Int
{ {
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM; if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM;
return currentTimeChange.timeSignatureNum; return currentTimeChange.timeSignatureNum;
} }
public static var timeSignatureDenominator(get, never):Int; public var timeSignatureDenominator(get, never):Int;
static function get_timeSignatureDenominator():Int function get_timeSignatureDenominator():Int
{ {
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN; if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN;
@ -132,44 +157,44 @@ class Conductor
/** /**
* Current position in the song, in measures. * Current position in the song, in measures.
*/ */
public static var currentMeasure(default, null):Int = 0; public var currentMeasure(default, null):Int = 0;
/** /**
* Current position in the song, in beats. * Current position in the song, in beats.
*/ */
public static var currentBeat(default, null):Int = 0; public var currentBeat(default, null):Int = 0;
/** /**
* Current position in the song, in steps. * Current position in the song, in steps.
*/ */
public static var currentStep(default, null):Int = 0; public var currentStep(default, null):Int = 0;
/** /**
* Current position in the song, in measures and fractions of a measure. * Current position in the song, in measures and fractions of a measure.
*/ */
public static var currentMeasureTime(default, null):Float = 0; public var currentMeasureTime(default, null):Float = 0;
/** /**
* Current position in the song, in beats and fractions of a measure. * Current position in the song, in beats and fractions of a measure.
*/ */
public static var currentBeatTime(default, null):Float = 0; public var currentBeatTime(default, null):Float = 0;
/** /**
* Current position in the song, in steps and fractions of a step. * Current position in the song, in steps and fractions of a step.
*/ */
public static var currentStepTime(default, null):Float = 0; public var currentStepTime(default, null):Float = 0;
/** /**
* An offset tied to the current chart file to compensate for a delay in the instrumental. * An offset tied to the current chart file to compensate for a delay in the instrumental.
*/ */
public static var instrumentalOffset:Float = 0; public var instrumentalOffset:Float = 0;
/** /**
* The instrumental offset, in terms of steps. * The instrumental offset, in terms of steps.
*/ */
public static var instrumentalOffsetSteps(get, never):Float; public var instrumentalOffsetSteps(get, never):Float;
static function get_instrumentalOffsetSteps():Float function get_instrumentalOffsetSteps():Float
{ {
var startingStepLengthMs:Float = ((Constants.SECS_PER_MIN / startingBPM) * Constants.MS_PER_SEC) / timeSignatureNumerator; var startingStepLengthMs:Float = ((Constants.SECS_PER_MIN / startingBPM) * Constants.MS_PER_SEC) / timeSignatureNumerator;
@ -179,19 +204,19 @@ class Conductor
/** /**
* An offset tied to the file format of the audio file being played. * An offset tied to the file format of the audio file being played.
*/ */
public static var formatOffset:Float = 0; public var formatOffset:Float = 0;
/** /**
* An offset set by the user to compensate for input lag. * An offset set by the user to compensate for input lag.
*/ */
public static var inputOffset:Float = 0; public var inputOffset:Float = 0;
/** /**
* The number of beats in a measure. May be fractional depending on the time signature. * The number of beats in a measure. May be fractional depending on the time signature.
*/ */
public static var beatsPerMeasure(get, never):Float; public var beatsPerMeasure(get, never):Float;
static function get_beatsPerMeasure():Float function get_beatsPerMeasure():Float
{ {
// NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure // NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure
return stepsPerMeasure / Constants.STEPS_PER_BEAT; return stepsPerMeasure / Constants.STEPS_PER_BEAT;
@ -201,30 +226,15 @@ class Conductor
* The number of steps in a measure. * The number of steps in a measure.
* TODO: I don't think this can be fractional? * TODO: I don't think this can be fractional?
*/ */
public static var stepsPerMeasure(get, never):Int; public var stepsPerMeasure(get, never):Int;
static function get_stepsPerMeasure():Int function get_stepsPerMeasure():Int
{ {
// TODO: Is this always an integer? // TODO: Is this always an integer?
return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT); return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT);
} }
/** public function new() {}
* Signal fired when the Conductor advances to a new measure.
*/
public static var measureHit(default, null):FlxSignal = new FlxSignal();
/**
* Signal fired when the Conductor advances to a new beat.
*/
public static var beatHit(default, null):FlxSignal = new FlxSignal();
/**
* Signal fired when the Conductor advances to a new step.
*/
public static var stepHit(default, null):FlxSignal = new FlxSignal();
function new() {}
/** /**
* Forcibly defines the current BPM of the song. * Forcibly defines the current BPM of the song.
@ -235,7 +245,7 @@ class Conductor
* WARNING: Avoid this for things like setting the BPM of the title screen music, * WARNING: Avoid this for things like setting the BPM of the title screen music,
* you should have a metadata file for it instead. * you should have a metadata file for it instead.
*/ */
public static function forceBPM(?bpm:Float = null) public function forceBPM(?bpm:Float = null)
{ {
if (bpm != null) if (bpm != null)
{ {
@ -246,7 +256,7 @@ class Conductor
// trace('[CONDUCTOR] Resetting BPM to default'); // trace('[CONDUCTOR] Resetting BPM to default');
} }
Conductor.bpmOverride = bpm; this.bpmOverride = bpm;
} }
/** /**
@ -256,29 +266,29 @@ class Conductor
* @param songPosition The current position in the song in milliseconds. * @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.
*/ */
public static function update(?songPosition:Float) public function update(?songPos:Float)
{ {
if (songPosition == null) if (songPos == null)
{ {
// Take into account instrumental and file format song offsets. // Take into account instrumental and file format song offsets.
songPosition = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0; songPos = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0;
} }
var oldMeasure = currentMeasure; var oldMeasure = this.currentMeasure;
var oldBeat = currentBeat; var oldBeat = this.currentBeat;
var oldStep = currentStep; var oldStep = this.currentStep;
// Set the song position we are at (for purposes of calculating note positions, etc). // Set the song position we are at (for purposes of calculating note positions, etc).
Conductor.songPosition = songPosition; this.songPosition = songPos;
currentTimeChange = timeChanges[0]; currentTimeChange = timeChanges[0];
if (Conductor.songPosition > 0.0) if (this.songPosition > 0.0)
{ {
for (i in 0...timeChanges.length) for (i in 0...timeChanges.length)
{ {
if (songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i]; if (this.songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i];
if (songPosition < timeChanges[i].timeStamp) break; if (this.songPosition < timeChanges[i].timeStamp) break;
} }
} }
@ -286,45 +296,49 @@ class Conductor
{ {
trace('WARNING: Conductor is broken, timeChanges is empty.'); trace('WARNING: Conductor is broken, timeChanges is empty.');
} }
else if (currentTimeChange != null && Conductor.songPosition > 0.0) else if (currentTimeChange != null && this.songPosition > 0.0)
{ {
// roundDecimal prevents representing 8 as 7.9999999 // roundDecimal prevents representing 8 as 7.9999999
currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6); this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT; this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
currentMeasureTime = currentStepTime / stepsPerMeasure; this.currentMeasureTime = currentStepTime / stepsPerMeasure;
currentStep = Math.floor(currentStepTime); this.currentStep = Math.floor(currentStepTime);
currentBeat = Math.floor(currentBeatTime); this.currentBeat = Math.floor(currentBeatTime);
currentMeasure = Math.floor(currentMeasureTime); this.currentMeasure = Math.floor(currentMeasureTime);
} }
else else
{ {
// Assume a constant BPM equal to the forced value. // Assume a constant BPM equal to the forced value.
currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4); this.currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4);
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT; this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
currentMeasureTime = currentStepTime / stepsPerMeasure; this.currentMeasureTime = currentStepTime / stepsPerMeasure;
currentStep = Math.floor(currentStepTime); this.currentStep = Math.floor(currentStepTime);
currentBeat = Math.floor(currentBeatTime); this.currentBeat = Math.floor(currentBeatTime);
currentMeasure = Math.floor(currentMeasureTime); this.currentMeasure = Math.floor(currentMeasureTime);
} }
// Only fire the signal if we are THE Conductor.
if (this == Conductor.instance)
{
// FlxSignals are really cool. // FlxSignals are really cool.
if (currentStep != oldStep) if (currentStep != oldStep)
{ {
stepHit.dispatch(); Conductor.stepHit.dispatch();
} }
if (currentBeat != oldBeat) if (currentBeat != oldBeat)
{ {
beatHit.dispatch(); Conductor.beatHit.dispatch();
} }
if (currentMeasure != oldMeasure) if (currentMeasure != oldMeasure)
{ {
measureHit.dispatch(); Conductor.measureHit.dispatch();
}
} }
} }
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>) public function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
{ {
timeChanges = []; timeChanges = [];
@ -338,8 +352,6 @@ class Conductor
// Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`. // Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`.
if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0; if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0;
if (currentTimeChange.beatTime == null)
{
if (currentTimeChange.timeStamp <= 0.0) if (currentTimeChange.timeStamp <= 0.0)
{ {
currentTimeChange.beatTime = 0.0; currentTimeChange.beatTime = 0.0;
@ -357,7 +369,6 @@ class Conductor
4); 4);
} }
} }
}
timeChanges.push(currentTimeChange); timeChanges.push(currentTimeChange);
} }
@ -368,13 +379,13 @@ class Conductor
} }
// Update currentStepTime // Update currentStepTime
Conductor.update(Conductor.songPosition); this.update(Conductor.instance.songPosition);
} }
/** /**
* Given a time in milliseconds, return a time in steps. * Given a time in milliseconds, return a time in steps.
*/ */
public static function getTimeInSteps(ms:Float):Float public function getTimeInSteps(ms:Float):Float
{ {
if (timeChanges.length == 0) if (timeChanges.length == 0)
{ {
@ -411,7 +422,7 @@ class Conductor
/** /**
* Given a time in steps and fractional steps, return a time in milliseconds. * Given a time in steps and fractional steps, return a time in milliseconds.
*/ */
public static function getStepTimeInMs(stepTime:Float):Float public function getStepTimeInMs(stepTime:Float):Float
{ {
if (timeChanges.length == 0) if (timeChanges.length == 0)
{ {
@ -447,7 +458,7 @@ class Conductor
/** /**
* Given a time in beats and fractional beats, return a time in milliseconds. * Given a time in beats and fractional beats, return a time in milliseconds.
*/ */
public static function getBeatTimeInMs(beatTime:Float):Float public function getBeatTimeInMs(beatTime:Float):Float
{ {
if (timeChanges.length == 0) if (timeChanges.length == 0)
{ {
@ -480,13 +491,20 @@ class Conductor
} }
} }
public static function watchQuick():Void
{
FlxG.watch.addQuick("songPosition", Conductor.instance.songPosition);
FlxG.watch.addQuick("bpm", Conductor.instance.bpm);
FlxG.watch.addQuick("currentMeasureTime", Conductor.instance.currentMeasureTime);
FlxG.watch.addQuick("currentBeatTime", Conductor.instance.currentBeatTime);
FlxG.watch.addQuick("currentStepTime", Conductor.instance.currentStepTime);
}
/**
* Reset the Conductor, replacing the current instance with a fresh one.
*/
public static function reset():Void public static function reset():Void
{ {
beatHit.removeAll(); Conductor.instance = new Conductor();
stepHit.removeAll();
mapTimeChanges([]);
forceBPM(null);
update(0);
} }
} }

View file

@ -19,7 +19,7 @@ import funkin.play.PlayStatePlaylist;
import openfl.display.BitmapData; import openfl.display.BitmapData;
import funkin.data.level.LevelRegistry; import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.event.SongEventData.SongEventParser; import funkin.data.event.SongEventRegistry;
import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser; import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
import funkin.play.cutscene.dialogue.SpeakerDataParser; import funkin.play.cutscene.dialogue.SpeakerDataParser;
@ -197,6 +197,13 @@ class InitState extends FlxState
FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK]; FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
#end #end
//
// FLIXEL PLUGINS
//
funkin.util.plugins.EvacuateDebugPlugin.initialize();
funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
funkin.util.plugins.WatchPlugin.initialize();
// //
// GAME DATA PARSING // GAME DATA PARSING
// //
@ -206,7 +213,7 @@ class InitState extends FlxState
SongRegistry.instance.loadEntries(); SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries();
SongEventParser.loadEventCache(); SongEventRegistry.loadEventCache();
ConversationDataParser.loadConversationCache(); ConversationDataParser.loadConversationCache();
DialogueBoxDataParser.loadDialogueBoxCache(); DialogueBoxDataParser.loadDialogueBoxCache();
SpeakerDataParser.loadSpeakerCache(); SpeakerDataParser.loadSpeakerCache();

View file

@ -64,7 +64,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples)); if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples));
else else
remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, vis.numSamples)); remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, vis.numSamples));
var fftSamples:Array<Float> = []; var fftSamples:Array<Float> = [];

View file

@ -164,7 +164,7 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples)); if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples));
else else
remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, numSamples)); remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, numSamples));
var fftSamples:Array<Float> = []; var fftSamples:Array<Float> = [];
var i = remappedShit; var i = remappedShit;
@ -235,15 +235,15 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples)); if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples));
else else
{ {
if (curTime == Conductor.songPosition) if (curTime == Conductor.instance.songPosition)
{ {
wavOptimiz = 3; wavOptimiz = 3;
return; // already did shit, so finishes function early return; // already did shit, so finishes function early
} }
curTime = Conductor.songPosition; curTime = Conductor.instance.songPosition;
remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, numSamples)); remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, numSamples));
} }
wavOptimiz = 8; wavOptimiz = 8;

View file

@ -1,7 +1,7 @@
package funkin.data.event; package funkin.data.event;
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.data.event.SongEventData.SongEventSchema; import funkin.data.event.SongEventSchema;
import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.util.macro.ClassMacro; import funkin.util.macro.ClassMacro;
import funkin.play.event.ScriptedSongEvent; import funkin.play.event.ScriptedSongEvent;
@ -9,7 +9,7 @@ import funkin.play.event.ScriptedSongEvent;
/** /**
* This class statically handles the parsing of internal and scripted song event handlers. * This class statically handles the parsing of internal and scripted song event handlers.
*/ */
class SongEventParser class SongEventRegistry
{ {
/** /**
* Every built-in event class must be added to this list. * Every built-in event class must be added to this list.
@ -160,84 +160,3 @@ class SongEventParser
} }
} }
} }
enum abstract SongEventFieldType(String) from String to String
{
/**
* The STRING type will display as a text field.
*/
var STRING = "string";
/**
* The INTEGER type will display as a text field that only accepts numbers.
*/
var INTEGER = "integer";
/**
* The FLOAT type will display as a text field that only accepts numbers.
*/
var FLOAT = "float";
/**
* The BOOL type will display as a checkbox.
*/
var BOOL = "bool";
/**
* The ENUM type will display as a dropdown.
* Make sure to specify the `keys` field in the schema.
*/
var ENUM = "enum";
}
typedef SongEventSchemaField =
{
/**
* The name of the property as it should be saved in the event data.
*/
name:String,
/**
* The title of the field to display in the UI.
*/
title:String,
/**
* The type of the field.
*/
type:SongEventFieldType,
/**
* Used only for ENUM values.
* The key is the display name and the value is the actual value.
*/
?keys:Map<String, Dynamic>,
/**
* Used for INTEGER and FLOAT values.
* The minimum value that can be entered.
* @default No minimum
*/
?min:Float,
/**
* Used for INTEGER and FLOAT values.
* The maximum value that can be entered.
* @default No maximum
*/
?max:Float,
/**
* Used for INTEGER and FLOAT values.
* The step value that will be used when incrementing/decrementing the value.
* @default `0.1`
*/
?step:Float,
/**
* An optional default value for the field.
*/
?defaultValue:Dynamic,
}
typedef SongEventSchema = Array<SongEventSchemaField>;

View file

@ -0,0 +1,125 @@
package funkin.data.event;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.song.SongData.SongEventData;
import funkin.util.macro.ClassMacro;
import funkin.play.event.ScriptedSongEvent;
@:forward(name, tittlte, type, keys, min, max, step, defaultValue, iterator)
abstract SongEventSchema(SongEventSchemaRaw)
{
public function new(?fields:Array<SongEventSchemaField>)
{
this = fields;
}
@:arrayAccess
public inline function getByName(name:String):SongEventSchemaField
{
for (field in this)
{
if (field.name == name) return field;
}
return null;
}
public function getFirstField():SongEventSchemaField
{
return this[0];
}
@:arrayAccess
public inline function get(key:Int)
{
return this[key];
}
@:arrayAccess
public inline function arrayWrite(k:Int, v:SongEventSchemaField):SongEventSchemaField
{
return this[k] = v;
}
}
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
typedef SongEventSchemaField =
{
/**
* The name of the property as it should be saved in the event data.
*/
name:String,
/**
* The title of the field to display in the UI.
*/
title:String,
/**
* The type of the field.
*/
type:SongEventFieldType,
/**
* Used only for ENUM values.
* The key is the display name and the value is the actual value.
*/
?keys:Map<String, Dynamic>,
/**
* Used for INTEGER and FLOAT values.
* The minimum value that can be entered.
* @default No minimum
*/
?min:Float,
/**
* Used for INTEGER and FLOAT values.
* The maximum value that can be entered.
* @default No maximum
*/
?max:Float,
/**
* Used for INTEGER and FLOAT values.
* The step value that will be used when incrementing/decrementing the value.
* @default `0.1`
*/
?step:Float,
/**
* An optional default value for the field.
*/
?defaultValue:Dynamic,
}
enum abstract SongEventFieldType(String) from String to String
{
/**
* The STRING type will display as a text field.
*/
var STRING = "string";
/**
* The INTEGER type will display as a text field that only accepts numbers.
*/
var INTEGER = "integer";
/**
* The FLOAT type will display as a text field that only accepts numbers.
*/
var FLOAT = "float";
/**
* The BOOL type will display as a checkbox.
*/
var BOOL = "bool";
/**
* The ENUM type will display as a dropdown.
* Make sure to specify the `keys` field in the schema.
*/
var ENUM = "enum";
}

View file

@ -1,7 +1,10 @@
package funkin.data.song; package funkin.data.song;
import funkin.data.event.SongEventRegistry;
import funkin.data.event.SongEventSchema;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
import thx.semver.Version; import thx.semver.Version;
import funkin.util.tools.ICloneable;
/** /**
* Data containing information about a song. * Data containing information about a song.
@ -9,7 +12,7 @@ import thx.semver.Version;
* Data which is only necessary in-game should be stored in the SongChartData. * Data which is only necessary in-game should be stored in the SongChartData.
*/ */
@:nullSafety @:nullSafety
class SongMetadata class SongMetadata implements ICloneable<SongMetadata>
{ {
/** /**
* A semantic versioning string for the song data format. * A semantic versioning string for the song data format.
@ -84,16 +87,16 @@ class SongMetadata
* @param newVariation Set to a new variation ID to change the new metadata. * @param newVariation Set to a new variation ID to change the new metadata.
* @return The cloned SongMetadata * @return The cloned SongMetadata
*/ */
public function clone(?newVariation:String = null):SongMetadata public function clone():SongMetadata
{ {
var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation); var result:SongMetadata = new SongMetadata(this.songName, this.artist, this.variation);
result.version = this.version; result.version = this.version;
result.timeFormat = this.timeFormat; result.timeFormat = this.timeFormat;
result.divisions = this.divisions; result.divisions = this.divisions;
result.offsets = this.offsets; result.offsets = this.offsets.clone();
result.timeChanges = this.timeChanges; result.timeChanges = this.timeChanges.deepClone();
result.looped = this.looped; result.looped = this.looped;
result.playData = this.playData; result.playData = this.playData.clone();
result.generatedBy = this.generatedBy; result.generatedBy = this.generatedBy;
return result; return result;
@ -128,7 +131,7 @@ enum abstract SongTimeFormat(String) from String to String
var MILLISECONDS = 'ms'; var MILLISECONDS = 'ms';
} }
class SongTimeChange class SongTimeChange implements ICloneable<SongTimeChange>
{ {
public static final DEFAULT_SONGTIMECHANGE:SongTimeChange = new SongTimeChange(0, 100); public static final DEFAULT_SONGTIMECHANGE:SongTimeChange = new SongTimeChange(0, 100);
@ -149,7 +152,7 @@ class SongTimeChange
*/ */
@:optional @:optional
@:alias("b") @:alias("b")
public var beatTime:Null<Float>; public var beatTime:Float;
/** /**
* Quarter notes per minute (float). Cannot be empty in the first element of the list, * Quarter notes per minute (float). Cannot be empty in the first element of the list,
@ -195,6 +198,11 @@ class SongTimeChange
this.beatTuplets = beatTuplets == null ? DEFAULT_BEAT_TUPLETS : beatTuplets; this.beatTuplets = beatTuplets == null ? DEFAULT_BEAT_TUPLETS : beatTuplets;
} }
public function clone():SongTimeChange
{
return new SongTimeChange(this.timeStamp, this.bpm, this.timeSignatureNum, this.timeSignatureDen, this.beatTime, this.beatTuplets);
}
/** /**
* Produces a string representation suitable for debugging. * Produces a string representation suitable for debugging.
*/ */
@ -209,7 +217,7 @@ class SongTimeChange
* These are intended to correct for issues with the chart, or with the song's audio (for example a 10ms delay before the song starts). * These are intended to correct for issues with the chart, or with the song's audio (for example a 10ms delay before the song starts).
* This is independent of the offsets applied in the user's settings, which are applied after these offsets and intended to correct for the user's hardware. * This is independent of the offsets applied in the user's settings, which are applied after these offsets and intended to correct for the user's hardware.
*/ */
class SongOffsets class SongOffsets implements ICloneable<SongOffsets>
{ {
/** /**
* The offset, in milliseconds, to apply to the song's instrumental relative to the chart. * The offset, in milliseconds, to apply to the song's instrumental relative to the chart.
@ -279,6 +287,15 @@ class SongOffsets
return value; return value;
} }
public function clone():SongOffsets
{
var result:SongOffsets = new SongOffsets(this.instrumental);
result.altInstrumentals = this.altInstrumentals.clone();
result.vocals = this.vocals.clone();
return result;
}
/** /**
* Produces a string representation suitable for debugging. * Produces a string representation suitable for debugging.
*/ */
@ -292,7 +309,7 @@ class SongOffsets
* Metadata for a song only used for the music. * Metadata for a song only used for the music.
* For example, the menu music. * For example, the menu music.
*/ */
class SongMusicData class SongMusicData implements ICloneable<SongMusicData>
{ {
/** /**
* A semantic versioning string for the song data format. * A semantic versioning string for the song data format.
@ -346,13 +363,13 @@ class SongMusicData
this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation; this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
} }
public function clone(?newVariation:String = null):SongMusicData public function clone():SongMusicData
{ {
var result:SongMusicData = new SongMusicData(this.songName, this.artist, newVariation == null ? this.variation : newVariation); var result:SongMusicData = new SongMusicData(this.songName, this.artist, this.variation);
result.version = this.version; result.version = this.version;
result.timeFormat = this.timeFormat; result.timeFormat = this.timeFormat;
result.divisions = this.divisions; result.divisions = this.divisions;
result.timeChanges = this.timeChanges; result.timeChanges = this.timeChanges.clone();
result.looped = this.looped; result.looped = this.looped;
result.generatedBy = this.generatedBy; result.generatedBy = this.generatedBy;
@ -368,7 +385,7 @@ class SongMusicData
} }
} }
class SongPlayData class SongPlayData implements ICloneable<SongPlayData>
{ {
/** /**
* The variations this song has. The associated metadata files should exist. * The variations this song has. The associated metadata files should exist.
@ -417,6 +434,20 @@ class SongPlayData
ratings = new Map<String, Int>(); ratings = new Map<String, Int>();
} }
public function clone():SongPlayData
{
var result:SongPlayData = new SongPlayData();
result.songVariations = this.songVariations.clone();
result.difficulties = this.difficulties.clone();
result.characters = this.characters.clone();
result.stage = this.stage;
result.noteStyle = this.noteStyle;
result.ratings = this.ratings.clone();
result.album = this.album;
return result;
}
/** /**
* Produces a string representation suitable for debugging. * Produces a string representation suitable for debugging.
*/ */
@ -430,7 +461,7 @@ class SongPlayData
* Information about the characters used in this variation of the song. * Information about the characters used in this variation of the song.
* Create a new variation if you want to change the characters. * Create a new variation if you want to change the characters.
*/ */
class SongCharacterData class SongCharacterData implements ICloneable<SongCharacterData>
{ {
@:optional @:optional
@:default('') @:default('')
@ -460,6 +491,14 @@ class SongCharacterData
this.instrumental = instrumental; this.instrumental = instrumental;
} }
public function clone():SongCharacterData
{
var result:SongCharacterData = new SongCharacterData(this.player, this.girlfriend, this.opponent, this.instrumental);
result.altInstrumentals = this.altInstrumentals.clone();
return result;
}
/** /**
* Produces a string representation suitable for debugging. * Produces a string representation suitable for debugging.
*/ */
@ -469,7 +508,7 @@ class SongCharacterData
} }
} }
class SongChartData class SongChartData implements ICloneable<SongChartData>
{ {
@:default(funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION) @:default(funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION)
@:jcustomparse(funkin.data.DataParse.semverVersion) @:jcustomparse(funkin.data.DataParse.semverVersion)
@ -539,6 +578,24 @@ class SongChartData
return writer.write(this, pretty ? ' ' : null); return writer.write(this, pretty ? ' ' : null);
} }
public function clone():SongChartData
{
// We have to manually perform the deep clone here because Map.deepClone() doesn't work.
var noteDataClone:Map<String, Array<SongNoteData>> = new Map<String, Array<SongNoteData>>();
for (key in this.notes.keys())
{
noteDataClone.set(key, this.notes.get(key).deepClone());
}
var eventDataClone:Array<SongEventData> = this.events.deepClone();
var result:SongChartData = new SongChartData(this.scrollSpeed.clone(), eventDataClone, noteDataClone);
result.version = this.version;
result.generatedBy = this.generatedBy;
result.variation = this.variation;
return result;
}
/** /**
* Produces a string representation suitable for debugging. * Produces a string representation suitable for debugging.
*/ */
@ -548,7 +605,7 @@ class SongChartData
} }
} }
class SongEventDataRaw class SongEventDataRaw implements ICloneable<SongEventDataRaw>
{ {
/** /**
* The timestamp of the event. The timestamp is in the format of the song's time format. * The timestamp of the event. The timestamp is in the format of the song's time format.
@ -602,14 +659,19 @@ class SongEventDataRaw
{ {
if (_stepTime != null && !force) return _stepTime; if (_stepTime != null && !force) return _stepTime;
return _stepTime = Conductor.getTimeInSteps(this.time); return _stepTime = Conductor.instance.getTimeInSteps(this.time);
}
public function clone():SongEventDataRaw
{
return new SongEventDataRaw(this.time, this.event, this.value);
} }
} }
/** /**
* Wrap SongEventData in an abstract so we can overload operators. * Wrap SongEventData in an abstract so we can overload operators.
*/ */
@:forward(time, event, value, activated, getStepTime) @:forward(time, event, value, activated, getStepTime, clone)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{ {
public function new(time:Float, event:String, value:Dynamic = null) public function new(time:Float, event:String, value:Dynamic = null)
@ -617,6 +679,33 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
this = new SongEventDataRaw(time, event, value); this = new SongEventDataRaw(time, event, value);
} }
public inline function valueAsStruct(?defaultKey:String = "key"):Dynamic
{
if (this.value == null) return {};
if (Std.isOfType(this.value, Array))
{
var result:haxe.DynamicAccess<Dynamic> = {};
result.set(defaultKey, this.value);
return cast result;
}
else if (Reflect.isObject(this.value))
{
// We enter this case if the value is a struct.
return cast this.value;
}
else
{
var result:haxe.DynamicAccess<Dynamic> = {};
result.set(defaultKey, this.value);
return cast result;
}
}
public inline function getSchema():Null<SongEventSchema>
{
return SongEventRegistry.getEventSchema(this.event);
}
public inline function getDynamic(key:String):Null<Dynamic> public inline function getDynamic(key:String):Null<Dynamic>
{ {
return this.value == null ? null : Reflect.field(this.value, key); return this.value == null ? null : Reflect.field(this.value, key);
@ -662,11 +751,6 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return this.value == null ? null : cast Reflect.field(this.value, key); return this.value == null ? null : cast Reflect.field(this.value, key);
} }
public function clone():SongEventData
{
return new SongEventData(this.time, this.event, this.value);
}
@:op(A == B) @:op(A == B)
public function op_equals(other:SongEventData):Bool public function op_equals(other:SongEventData):Bool
{ {
@ -712,7 +796,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
} }
} }
class SongNoteDataRaw class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
{ {
/** /**
* The timestamp of the note. The timestamp is in the format of the song's time format. * The timestamp of the note. The timestamp is in the format of the song's time format.
@ -796,7 +880,7 @@ class SongNoteDataRaw
{ {
if (_stepTime != null && !force) return _stepTime; if (_stepTime != null && !force) return _stepTime;
return _stepTime = Conductor.getTimeInSteps(this.time); return _stepTime = Conductor.instance.getTimeInSteps(this.time);
} }
@:jignored @:jignored
@ -812,7 +896,7 @@ class SongNoteDataRaw
if (_stepLength != null && !force) return _stepLength; if (_stepLength != null && !force) return _stepLength;
return _stepLength = Conductor.getTimeInSteps(this.time + this.length) - getStepTime(); return _stepLength = Conductor.instance.getTimeInSteps(this.time + this.length) - getStepTime();
} }
public function setStepLength(value:Float):Void public function setStepLength(value:Float):Void
@ -823,11 +907,16 @@ class SongNoteDataRaw
} }
else else
{ {
var lengthMs:Float = Conductor.getStepTimeInMs(value) - this.time; var lengthMs:Float = Conductor.instance.getStepTimeInMs(value) - this.time;
this.length = lengthMs; this.length = lengthMs;
} }
_stepLength = null; _stepLength = null;
} }
public function clone():SongNoteDataRaw
{
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
}
} }
/** /**

View file

@ -8,7 +8,7 @@ import funkin.play.stage.StageData;
import polymod.Polymod; import polymod.Polymod;
import polymod.backends.PolymodAssets.PolymodAssetType; import polymod.backends.PolymodAssets.PolymodAssetType;
import polymod.format.ParseRules.TextFileFormat; import polymod.format.ParseRules.TextFileFormat;
import funkin.data.event.SongEventData.SongEventParser; import funkin.data.event.SongEventRegistry;
import funkin.util.FileUtil; import funkin.util.FileUtil;
import funkin.data.level.LevelRegistry; import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
@ -271,7 +271,7 @@ class PolymodHandler
SongRegistry.instance.loadEntries(); SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries();
SongEventParser.loadEventCache(); SongEventRegistry.loadEventCache();
ConversationDataParser.loadConversationCache(); ConversationDataParser.loadConversationCache();
DialogueBoxDataParser.loadDialogueBoxCache(); DialogueBoxDataParser.loadDialogueBoxCache();
SpeakerDataParser.loadSpeakerCache(); SpeakerDataParser.loadSpeakerCache();

View file

@ -40,7 +40,7 @@ class Countdown
stopCountdown(); stopCountdown();
PlayState.instance.isInCountdown = true; PlayState.instance.isInCountdown = true;
Conductor.update(PlayState.instance.startTimestamp + Conductor.beatLengthMs * -5); Conductor.instance.update(PlayState.instance.startTimestamp + Conductor.instance.beatLengthMs * -5);
// Handle onBeatHit events manually // Handle onBeatHit events manually
// @:privateAccess // @:privateAccess
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0)); // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
@ -48,7 +48,7 @@ class Countdown
// The timer function gets called based on the beat of the song. // The timer function gets called based on the beat of the song.
countdownTimer = new FlxTimer(); countdownTimer = new FlxTimer();
countdownTimer.start(Conductor.beatLengthMs / 1000, function(tmr:FlxTimer) { countdownTimer.start(Conductor.instance.beatLengthMs / 1000, function(tmr:FlxTimer) {
if (PlayState.instance == null) if (PlayState.instance == null)
{ {
tmr.cancel(); tmr.cancel();
@ -158,7 +158,7 @@ class Countdown
{ {
stopCountdown(); stopCountdown();
// This will trigger PlayState.startSong() // This will trigger PlayState.startSong()
Conductor.update(0); Conductor.instance.update(0);
// PlayState.isInCountdown = false; // PlayState.isInCountdown = false;
} }
@ -225,7 +225,7 @@ class Countdown
countdownSprite.screenCenter(); countdownSprite.screenCenter();
// Fade sprite in, then out, then destroy it. // Fade sprite in, then out, then destroy it.
FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.beatLengthMs / 1000, FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.instance.beatLengthMs / 1000,
{ {
ease: FlxEase.cubeInOut, ease: FlxEase.cubeInOut,
onComplete: function(twn:FlxTween) { onComplete: function(twn:FlxTween) {

View file

@ -129,7 +129,7 @@ class GameOverSubState extends MusicBeatSubState
gameOverMusic.stop(); gameOverMusic.stop();
// The conductor now represents the BPM of the game over music. // The conductor now represents the BPM of the game over music.
Conductor.update(0); Conductor.instance.update(0);
} }
var hasStartedAnimation:Bool = false; var hasStartedAnimation:Bool = false;
@ -204,7 +204,7 @@ class GameOverSubState extends MusicBeatSubState
{ {
// Match the conductor to the music. // Match the conductor to the music.
// This enables the stepHit and beatHit events. // This enables the stepHit and beatHit events.
Conductor.update(gameOverMusic.time); Conductor.instance.update(gameOverMusic.time);
} }
else else
{ {

View file

@ -42,7 +42,7 @@ import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.VanillaCutscenes; import funkin.play.cutscene.VanillaCutscenes;
import funkin.play.cutscene.VideoCutscene; import funkin.play.cutscene.VideoCutscene;
import funkin.data.event.SongEventData.SongEventParser; import funkin.data.event.SongEventRegistry;
import funkin.play.notes.NoteSprite; import funkin.play.notes.NoteSprite;
import funkin.play.notes.NoteDirection; import funkin.play.notes.NoteDirection;
import funkin.play.notes.Strumline; import funkin.play.notes.Strumline;
@ -561,15 +561,15 @@ class PlayState extends MusicBeatSubState
} }
// Prepare the Conductor. // Prepare the Conductor.
Conductor.forceBPM(null); Conductor.instance.forceBPM(null);
if (currentChart.offsets != null) if (currentChart.offsets != null)
{ {
Conductor.instrumentalOffset = currentChart.offsets.getInstrumentalOffset(); Conductor.instance.instrumentalOffset = currentChart.offsets.getInstrumentalOffset();
} }
Conductor.mapTimeChanges(currentChart.timeChanges); Conductor.instance.mapTimeChanges(currentChart.timeChanges);
Conductor.update((Conductor.beatLengthMs * -5) + startTimestamp); Conductor.instance.update((Conductor.instance.beatLengthMs * -5) + startTimestamp);
// The song is now loaded. We can continue to initialize the play state. // The song is now loaded. We can continue to initialize the play state.
initCameras(); initCameras();
@ -734,7 +734,7 @@ class PlayState extends MusicBeatSubState
// Reset music properly. // Reset music properly.
FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instrumentalOffset); FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instance.instrumentalOffset);
FlxG.sound.music.pause(); FlxG.sound.music.pause();
if (!overrideMusic) if (!overrideMusic)
@ -785,22 +785,22 @@ class PlayState extends MusicBeatSubState
{ {
if (isInCountdown) if (isInCountdown)
{ {
Conductor.update(Conductor.songPosition + elapsed * 1000); Conductor.instance.update(Conductor.instance.songPosition + elapsed * 1000);
if (Conductor.songPosition >= (startTimestamp)) startSong(); if (Conductor.instance.songPosition >= (startTimestamp)) startSong();
} }
} }
else else
{ {
if (Constants.EXT_SOUND == 'mp3') if (Constants.EXT_SOUND == 'mp3')
{ {
Conductor.formatOffset = Constants.MP3_DELAY_MS; Conductor.instance.formatOffset = Constants.MP3_DELAY_MS;
} }
else else
{ {
Conductor.formatOffset = 0.0; Conductor.instance.formatOffset = 0.0;
} }
Conductor.update(); // Normal conductor update. Conductor.instance.update(); // Normal conductor update.
} }
var androidPause:Bool = false; var androidPause:Bool = false;
@ -942,7 +942,7 @@ class PlayState extends MusicBeatSubState
// TODO: Check that these work even when songPosition is less than 0. // TODO: Check that these work even when songPosition is less than 0.
if (songEvents != null && songEvents.length > 0) if (songEvents != null && songEvents.length > 0)
{ {
var songEventsToActivate:Array<SongEventData> = SongEventParser.queryEvents(songEvents, Conductor.songPosition); var songEventsToActivate:Array<SongEventData> = SongEventRegistry.queryEvents(songEvents, Conductor.instance.songPosition);
if (songEventsToActivate.length > 0) if (songEventsToActivate.length > 0)
{ {
@ -950,7 +950,7 @@ class PlayState extends MusicBeatSubState
for (event in songEventsToActivate) for (event in songEventsToActivate)
{ {
// If an event is trying to play, but it's over 5 seconds old, skip it. // If an event is trying to play, but it's over 5 seconds old, skip it.
if (event.time - Conductor.songPosition < -5000) if (event.time - Conductor.instance.songPosition < -5000)
{ {
event.activated = true; event.activated = true;
continue; continue;
@ -961,7 +961,7 @@ class PlayState extends MusicBeatSubState
// Calling event.cancelEvent() skips the event. Neat! // Calling event.cancelEvent() skips the event. Neat!
if (!eventEvent.eventCanceled) if (!eventEvent.eventCanceled)
{ {
SongEventParser.handleEvent(event); SongEventRegistry.handleEvent(event);
} }
} }
} }
@ -1052,7 +1052,7 @@ class PlayState extends MusicBeatSubState
if (startTimer.finished) if (startTimer.finished)
{ {
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true,
currentSongLengthMs - Conductor.songPosition); currentSongLengthMs - Conductor.instance.songPosition);
} }
else else
{ {
@ -1076,12 +1076,12 @@ class PlayState extends MusicBeatSubState
{ {
if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause)
{ {
if (Conductor.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song if (Conductor.instance.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song
+ ' (' + ' ('
+ storyDifficultyText + storyDifficultyText
+ ')', iconRPC, true, + ')', iconRPC, true,
currentSongLengthMs currentSongLengthMs
- Conductor.songPosition); - Conductor.instance.songPosition);
else else
DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
} }
@ -1154,17 +1154,17 @@ class PlayState extends MusicBeatSubState
if (!startingSong if (!startingSong
&& FlxG.sound.music != null && FlxG.sound.music != null
&& (Math.abs(FlxG.sound.music.time - (Conductor.songPosition + Conductor.instrumentalOffset)) > 200 && (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200
|| Math.abs(vocals.checkSyncError(Conductor.songPosition + Conductor.instrumentalOffset)) > 200)) || Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200))
{ {
trace("VOCALS NEED RESYNC"); trace("VOCALS NEED RESYNC");
if (vocals != null) trace(vocals.checkSyncError(Conductor.songPosition + Conductor.instrumentalOffset)); if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
trace(FlxG.sound.music.time - (Conductor.songPosition + Conductor.instrumentalOffset)); trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
resyncVocals(); resyncVocals();
} }
if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.currentStep)); if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep));
if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.currentStep)); if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep));
return true; return true;
} }
@ -1185,14 +1185,14 @@ class PlayState extends MusicBeatSubState
} }
// Only zoom camera if we are zoomed by less than 35%. // Only zoom camera if we are zoomed by less than 35%.
if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.currentBeat % cameraZoomRate == 0) if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.instance.currentBeat % cameraZoomRate == 0)
{ {
// Zoom camera in (1.5%) // Zoom camera in (1.5%)
FlxG.camera.zoom += cameraZoomIntensity * defaultCameraZoom; FlxG.camera.zoom += cameraZoomIntensity * defaultCameraZoom;
// Hud zooms double (3%) // Hud zooms double (3%)
camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom; camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom;
} }
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.currentBeat} % ${cameraZoomRate} == ${Conductor.currentBeat % cameraZoomRate}}'); // trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}');
// That combo milestones that got spoiled that one time. // That combo milestones that got spoiled that one time.
// Comes with NEAT visual and audio effects. // Comes with NEAT visual and audio effects.
@ -1205,13 +1205,13 @@ class PlayState extends MusicBeatSubState
// TODO: Re-enable combo text (how to do this without sections?). // TODO: Re-enable combo text (how to do this without sections?).
// if (currentSong != null) // if (currentSong != null)
// { // {
// shouldShowComboText = (Conductor.currentBeat % 8 == 7); // shouldShowComboText = (Conductor.instance.currentBeat % 8 == 7);
// var daSection = .getSong()[Std.int(Conductor.currentBeat / 16)]; // var daSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16)];
// shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection); // shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection);
// shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5); // shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5);
// //
// var daNextSection = .getSong()[Std.int(Conductor.currentBeat / 16) + 1]; // var daNextSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16) + 1];
// var isEndOfSong = .getSong().length < Std.int(Conductor.currentBeat / 16); // var isEndOfSong = .getSong().length < Std.int(Conductor.instance.currentBeat / 16);
// shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection)); // shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection));
// } // }
@ -1224,7 +1224,7 @@ class PlayState extends MusicBeatSubState
var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
new FlxTimer().start(((Conductor.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) { new FlxTimer().start(((Conductor.instance.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) {
animShit.forceFinish(); animShit.forceFinish();
}); });
} }
@ -1261,10 +1261,10 @@ class PlayState extends MusicBeatSubState
if (currentStage == null) return; if (currentStage == null) return;
// TODO: Add HEY! song events to Tutorial. // TODO: Add HEY! song events to Tutorial.
if (Conductor.currentBeat % 16 == 15 if (Conductor.instance.currentBeat % 16 == 15
&& currentStage.getDad().characterId == 'gf' && currentStage.getDad().characterId == 'gf'
&& Conductor.currentBeat > 16 && Conductor.instance.currentBeat > 16
&& Conductor.currentBeat < 48) && Conductor.instance.currentBeat < 48)
{ {
currentStage.getBoyfriend().playAnimation('hey', true); currentStage.getBoyfriend().playAnimation('hey', true);
currentStage.getDad().playAnimation('cheer', true); currentStage.getDad().playAnimation('cheer', true);
@ -1575,7 +1575,7 @@ class PlayState extends MusicBeatSubState
trace('Song difficulty could not be loaded.'); trace('Song difficulty could not be loaded.');
} }
// Conductor.forceBPM(currentChart.getStartingBPM()); // Conductor.instance.forceBPM(currentChart.getStartingBPM());
if (!overrideMusic) if (!overrideMusic)
{ {
@ -1607,7 +1607,7 @@ class PlayState extends MusicBeatSubState
// Reset song events. // Reset song events.
songEvents = currentChart.getEvents(); songEvents = currentChart.getEvents();
SongEventParser.resetEvents(songEvents); SongEventRegistry.resetEvents(songEvents);
// Reset the notes on each strumline. // Reset the notes on each strumline.
var playerNoteData:Array<SongNoteData> = []; var playerNoteData:Array<SongNoteData> = [];
@ -1706,7 +1706,7 @@ class PlayState extends MusicBeatSubState
FlxG.sound.music.onComplete = endSong; FlxG.sound.music.onComplete = endSong;
// A negative instrumental offset means the song skips the first few milliseconds of the track. // A negative instrumental offset means the song skips the first few milliseconds of the track.
// This just gets added into the startTimestamp behavior so we don't need to do anything extra. // This just gets added into the startTimestamp behavior so we don't need to do anything extra.
FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset; FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
trace('Playing vocals...'); trace('Playing vocals...');
add(vocals); add(vocals);
@ -1722,7 +1722,7 @@ class PlayState extends MusicBeatSubState
if (startTimestamp > 0) if (startTimestamp > 0)
{ {
// FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset; // FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
handleSkippedNotes(); handleSkippedNotes();
} }
} }
@ -1800,7 +1800,7 @@ class PlayState extends MusicBeatSubState
var hitWindowCenter = note.strumTime; var hitWindowCenter = note.strumTime;
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS; var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
if (Conductor.songPosition > hitWindowEnd) if (Conductor.instance.songPosition > hitWindowEnd)
{ {
if (note.hasMissed) continue; if (note.hasMissed) continue;
@ -1810,7 +1810,7 @@ class PlayState extends MusicBeatSubState
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true; if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
} }
else if (Conductor.songPosition > hitWindowCenter) else if (Conductor.instance.songPosition > hitWindowCenter)
{ {
if (note.hasBeenHit) continue; if (note.hasBeenHit) continue;
@ -1831,7 +1831,7 @@ class PlayState extends MusicBeatSubState
opponentStrumline.playNoteHoldCover(note.holdNoteSprite); opponentStrumline.playNoteHoldCover(note.holdNoteSprite);
} }
} }
else if (Conductor.songPosition > hitWindowStart) else if (Conductor.instance.songPosition > hitWindowStart)
{ {
if (note.hasBeenHit || note.hasMissed) continue; if (note.hasBeenHit || note.hasMissed) continue;
@ -1877,14 +1877,14 @@ class PlayState extends MusicBeatSubState
var hitWindowCenter = note.strumTime; var hitWindowCenter = note.strumTime;
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS; var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
if (Conductor.songPosition > hitWindowEnd) if (Conductor.instance.songPosition > hitWindowEnd)
{ {
note.tooEarly = false; note.tooEarly = false;
note.mayHit = false; note.mayHit = false;
note.hasMissed = true; note.hasMissed = true;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true; if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
} }
else if (Conductor.songPosition > hitWindowStart) else if (Conductor.instance.songPosition > hitWindowStart)
{ {
note.tooEarly = false; note.tooEarly = false;
note.mayHit = true; note.mayHit = true;
@ -1951,7 +1951,7 @@ class PlayState extends MusicBeatSubState
if (note == null || note.hasBeenHit) continue; if (note == null || note.hasBeenHit) continue;
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS; var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
if (Conductor.songPosition > hitWindowEnd) if (Conductor.instance.songPosition > hitWindowEnd)
{ {
// We have passed this note. // We have passed this note.
// Flag the note for deletion without actually penalizing the player. // Flag the note for deletion without actually penalizing the player.
@ -2115,7 +2115,7 @@ class PlayState extends MusicBeatSubState
{ {
inputSpitter.push( inputSpitter.push(
{ {
t: Std.int(Conductor.songPosition), t: Std.int(Conductor.instance.songPosition),
d: indices[i], d: indices[i],
l: 20 l: 20
}); });
@ -2125,7 +2125,7 @@ class PlayState extends MusicBeatSubState
{ {
inputSpitter.push( inputSpitter.push(
{ {
t: Std.int(Conductor.songPosition), t: Std.int(Conductor.instance.songPosition),
d: -1, d: -1,
l: 20 l: 20
}); });
@ -2186,7 +2186,7 @@ class PlayState extends MusicBeatSubState
{ {
inputSpitter.push( inputSpitter.push(
{ {
t: Std.int(Conductor.songPosition), t: Std.int(Conductor.instance.songPosition),
d: indices[i], d: indices[i],
l: 20 l: 20
}); });
@ -2275,7 +2275,7 @@ class PlayState extends MusicBeatSubState
// Get the offset and compensate for input latency. // Get the offset and compensate for input latency.
// Round inward (trim remainder) for consistency. // Round inward (trim remainder) for consistency.
var noteDiff:Int = Std.int(Conductor.songPosition - daNote.noteData.time - inputLatencyMs); var noteDiff:Int = Std.int(Conductor.instance.songPosition - daNote.noteData.time - inputLatencyMs);
var score = Scoring.scoreNote(noteDiff, PBOT1); var score = Scoring.scoreNote(noteDiff, PBOT1);
var daRating = Scoring.judgeNote(noteDiff, PBOT1); var daRating = Scoring.judgeNote(noteDiff, PBOT1);
@ -2330,7 +2330,7 @@ class PlayState extends MusicBeatSubState
{ {
inputSpitter.push( inputSpitter.push(
{ {
t: Std.int(Conductor.songPosition), t: Std.int(Conductor.instance.songPosition),
d: indices[i], d: indices[i],
l: 20 l: 20
}); });
@ -2340,7 +2340,7 @@ class PlayState extends MusicBeatSubState
{ {
inputSpitter.push( inputSpitter.push(
{ {
t: Std.int(Conductor.songPosition), t: Std.int(Conductor.instance.songPosition),
d: -1, d: -1,
l: 20 l: 20
}); });
@ -2739,15 +2739,15 @@ class PlayState extends MusicBeatSubState
{ {
FlxG.sound.music.pause(); FlxG.sound.music.pause();
var targetTimeSteps:Float = Conductor.currentStepTime + (Conductor.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections); var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections);
var targetTimeMs:Float = Conductor.getStepTimeInMs(targetTimeSteps); var targetTimeMs:Float = Conductor.instance.getStepTimeInMs(targetTimeSteps);
FlxG.sound.music.time = targetTimeMs; FlxG.sound.music.time = targetTimeMs;
handleSkippedNotes(); handleSkippedNotes();
// regenNoteData(FlxG.sound.music.time); // regenNoteData(FlxG.sound.music.time);
Conductor.update(FlxG.sound.music.time); Conductor.instance.update(FlxG.sound.music.time);
resyncVocals(); resyncVocals();
} }

View file

@ -367,7 +367,7 @@ class BaseCharacter extends Bopper
// This lets you add frames to the end of the sing animation to ease back into the idle! // This lets you add frames to the end of the sing animation to ease back into the idle!
holdTimer += event.elapsed; holdTimer += event.elapsed;
var singTimeSec:Float = singTimeSec * (Conductor.beatLengthMs * 0.001); // x beats, to ms. var singTimeSec:Float = singTimeSec * (Conductor.instance.beatLengthMs * 0.001); // x beats, to ms.
if (getCurrentAnimation().endsWith('miss')) singTimeSec *= 2; // makes it feel more awkward when you miss if (getCurrentAnimation().endsWith('miss')) singTimeSec *= 2; // makes it feel more awkward when you miss

View file

@ -40,7 +40,7 @@ class ComboMilestone extends FlxTypedSpriteGroup<FlxSprite>
{ {
if (onScreenTime < 0.9) if (onScreenTime < 0.9)
{ {
new FlxTimer().start((Conductor.beatLengthMs / 1000) * 0.25, function(tmr) { new FlxTimer().start((Conductor.instance.beatLengthMs / 1000) * 0.25, function(tmr) {
forceFinish(); forceFinish();
}); });
} }

View file

@ -59,7 +59,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
remove(rating, true); remove(rating, true);
rating.destroy(); rating.destroy();
}, },
startDelay: Conductor.beatLengthMs * 0.001 startDelay: Conductor.instance.beatLengthMs * 0.001
}); });
} }
@ -110,7 +110,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
remove(comboSpr, true); remove(comboSpr, true);
comboSpr.destroy(); comboSpr.destroy();
}, },
startDelay: Conductor.beatLengthMs * 0.001 startDelay: Conductor.instance.beatLengthMs * 0.001
}); });
var seperatedScore:Array<Int> = []; var seperatedScore:Array<Int> = [];
@ -157,7 +157,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
remove(numScore, true); remove(numScore, true);
numScore.destroy(); numScore.destroy();
}, },
startDelay: Conductor.beatLengthMs * 0.002 startDelay: Conductor.instance.beatLengthMs * 0.002
}); });
daLoop++; daLoop++;

View file

@ -5,8 +5,8 @@ import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
// Data from the event schema // Data from the event schema
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.data.event.SongEventData.SongEventSchema; import funkin.data.event.SongEventSchema;
import funkin.data.event.SongEventData.SongEventFieldType; import funkin.data.event.SongEventSchema.SongEventFieldType;
/** /**
* This class represents a handler for a type of song event. * This class represents a handler for a type of song event.
@ -132,7 +132,7 @@ class FocusCameraSongEvent extends SongEvent
*/ */
public override function getEventSchema():SongEventSchema public override function getEventSchema():SongEventSchema
{ {
return [ return new SongEventSchema([
{ {
name: "char", name: "char",
title: "Character", title: "Character",
@ -154,6 +154,6 @@ class FocusCameraSongEvent extends SongEvent
step: 10.0, step: 10.0,
type: SongEventFieldType.FLOAT, type: SongEventFieldType.FLOAT,
} }
]; ]);
} }
} }

View file

@ -7,8 +7,8 @@ import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
// Data from the event schema // Data from the event schema
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.data.event.SongEventData.SongEventSchema; import funkin.data.event.SongEventSchema;
import funkin.data.event.SongEventData.SongEventFieldType; import funkin.data.event.SongEventSchema.SongEventFieldType;
class PlayAnimationSongEvent extends SongEvent class PlayAnimationSongEvent extends SongEvent
{ {
@ -89,7 +89,7 @@ class PlayAnimationSongEvent extends SongEvent
*/ */
public override function getEventSchema():SongEventSchema public override function getEventSchema():SongEventSchema
{ {
return [ return new SongEventSchema([
{ {
name: 'target', name: 'target',
title: 'Target', title: 'Target',
@ -108,6 +108,6 @@ class PlayAnimationSongEvent extends SongEvent
type: SongEventFieldType.BOOL, type: SongEventFieldType.BOOL,
defaultValue: false defaultValue: false
} }
]; ]);
} }
} }

View file

@ -8,8 +8,8 @@ import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
// Data from the event schema // Data from the event schema
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.data.event.SongEventData.SongEventSchema; import funkin.data.event.SongEventSchema;
import funkin.data.event.SongEventData.SongEventFieldType; import funkin.data.event.SongEventSchema.SongEventFieldType;
/** /**
* This class represents a handler for configuring camera bop intensity and rate. * This class represents a handler for configuring camera bop intensity and rate.
@ -72,7 +72,7 @@ class SetCameraBopSongEvent extends SongEvent
*/ */
public override function getEventSchema():SongEventSchema public override function getEventSchema():SongEventSchema
{ {
return [ return new SongEventSchema([
{ {
name: 'intensity', name: 'intensity',
title: 'Intensity', title: 'Intensity',
@ -87,6 +87,6 @@ class SetCameraBopSongEvent extends SongEvent
step: 1, step: 1,
type: SongEventFieldType.INTEGER, type: SongEventFieldType.INTEGER,
} }
]; ]);
} }
} }

View file

@ -1,7 +1,7 @@
package funkin.play.event; package funkin.play.event;
import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.data.event.SongEventData.SongEventSchema; import funkin.data.event.SongEventSchema;
/** /**
* This class represents a handler for a type of song event. * This class represents a handler for a type of song event.

View file

@ -8,8 +8,8 @@ import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
// Data from the event schema // Data from the event schema
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.data.event.SongEventData.SongEventFieldType; import funkin.data.event.SongEventSchema;
import funkin.data.event.SongEventData.SongEventSchema; import funkin.data.event.SongEventSchema.SongEventFieldType;
/** /**
* This class represents a handler for camera zoom events. * This class represents a handler for camera zoom events.
@ -79,7 +79,8 @@ class ZoomCameraSongEvent extends SongEvent
return; return;
} }
FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000), {ease: easeFunction}); FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.instance.stepLengthMs * duration / 1000),
{ease: easeFunction});
} }
} }
@ -99,7 +100,7 @@ class ZoomCameraSongEvent extends SongEvent
*/ */
public override function getEventSchema():SongEventSchema public override function getEventSchema():SongEventSchema
{ {
return [ return new SongEventSchema([
{ {
name: 'zoom', name: 'zoom',
title: 'Zoom Level', title: 'Zoom Level',
@ -145,6 +146,6 @@ class ZoomCameraSongEvent extends SongEvent
'Elastic In/Out' => 'elasticInOut', 'Elastic In/Out' => 'elasticInOut',
] ]
} }
]; ]);
} }
} }

View file

@ -279,7 +279,7 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Float = 1.0; var vwoosh:Float = 1.0;
var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0; var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1); return Constants.PIXELS_PER_MS * (Conductor.instance.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
} }
function updateNotes():Void function updateNotes():Void
@ -287,8 +287,8 @@ class Strumline extends FlxSpriteGroup
if (noteData.length == 0) return; if (noteData.length == 0) return;
var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0; var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0;
var hitWindowStart:Float = Conductor.songPosition - Constants.HIT_WINDOW_MS; var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS;
var renderWindowStart:Float = Conductor.songPosition + RENDER_DISTANCE_MS; var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS;
for (noteIndex in nextNoteIndex...noteData.length) for (noteIndex in nextNoteIndex...noteData.length)
{ {
@ -335,7 +335,7 @@ class Strumline extends FlxSpriteGroup
{ {
if (holdNote == null || !holdNote.alive) continue; if (holdNote == null || !holdNote.alive) continue;
if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote) if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote)
{ {
if (isPlayer && !isKeyHeld(holdNote.noteDirection)) if (isPlayer && !isKeyHeld(holdNote.noteDirection))
{ {
@ -349,7 +349,7 @@ class Strumline extends FlxSpriteGroup
var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Constants.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8; var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Constants.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8;
if (holdNote.missedNote && Conductor.songPosition >= renderWindowEnd) if (holdNote.missedNote && Conductor.instance.songPosition >= renderWindowEnd)
{ {
// Hold note is offscreen, kill it. // Hold note is offscreen, kill it.
holdNote.visible = false; holdNote.visible = false;
@ -399,13 +399,13 @@ class Strumline extends FlxSpriteGroup
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2; holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2;
} }
} }
else if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote) else if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote)
{ {
// Hold note is currently being hit, clip it off. // Hold note is currently being hit, clip it off.
holdConfirm(holdNote.noteDirection); holdConfirm(holdNote.noteDirection);
holdNote.visible = true; holdNote.visible = true;
holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.songPosition; holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.instance.songPosition;
if (holdNote.sustainLength <= 10) if (holdNote.sustainLength <= 10)
{ {

View file

@ -80,25 +80,11 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
if (FlxG.keys.justPressed.F5) debug_refreshModules(); if (FlxG.keys.justPressed.F5) debug_refreshModules();
} }
function handleQuickWatch():Void
{
// Display Conductor info in the watch window.
FlxG.watch.addQuick("songPosition", Conductor.songPosition);
FlxG.watch.addQuick("songPositionNoOffset", Conductor.songPosition + Conductor.instrumentalOffset);
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
FlxG.watch.addQuick("bpm", Conductor.bpm);
FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime);
FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime);
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
}
override function update(elapsed:Float) override function update(elapsed:Float)
{ {
super.update(elapsed); super.update(elapsed);
handleControls(); handleControls();
handleFunctionControls();
handleQuickWatch();
dispatchEvent(new UpdateScriptEvent(elapsed)); dispatchEvent(new UpdateScriptEvent(elapsed));
} }
@ -139,7 +125,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
public function stepHit():Bool public function stepHit():Bool
{ {
var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep); var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
dispatchEvent(event); dispatchEvent(event);
@ -150,7 +136,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
public function beatHit():Bool public function beatHit():Bool
{ {
var event = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep); var event = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
dispatchEvent(event); dispatchEvent(event);

View file

@ -65,12 +65,8 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
if (FlxG.keys.justPressed.F5) debug_refreshModules(); if (FlxG.keys.justPressed.F5) debug_refreshModules();
// Display Conductor info in the watch window. // Display Conductor info in the watch window.
FlxG.watch.addQuick("songPosition", Conductor.songPosition);
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0); FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
FlxG.watch.addQuick("bpm", Conductor.bpm); Conductor.watchQuick();
FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime);
FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime);
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
dispatchEvent(new UpdateScriptEvent(elapsed)); dispatchEvent(new UpdateScriptEvent(elapsed));
} }
@ -99,7 +95,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
*/ */
public function stepHit():Bool public function stepHit():Bool
{ {
var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep); var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
dispatchEvent(event); dispatchEvent(event);
@ -115,7 +111,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
*/ */
public function beatHit():Bool public function beatHit():Bool
{ {
var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep); var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
dispatchEvent(event); dispatchEvent(event);

View file

@ -15,12 +15,14 @@ import flixel.group.FlxSpriteGroup;
import flixel.input.keyboard.FlxKey; import flixel.input.keyboard.FlxKey;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
import flixel.graphics.FlxGraphic;
import flixel.math.FlxRect; import flixel.math.FlxRect;
import flixel.sound.FlxSound; import flixel.sound.FlxSound;
import flixel.system.FlxAssets.FlxSoundAsset; import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.tweens.misc.VarTween; import flixel.tweens.misc.VarTween;
import haxe.ui.Toolkit;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import flixel.util.FlxSort; import flixel.util.FlxSort;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
@ -81,6 +83,7 @@ import funkin.ui.debug.charting.components.ChartEditorEventSprite;
import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite; import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite;
import funkin.ui.debug.charting.components.ChartEditorNotePreview; import funkin.ui.debug.charting.components.ChartEditorNotePreview;
import funkin.ui.debug.charting.components.ChartEditorNoteSprite; import funkin.ui.debug.charting.components.ChartEditorNoteSprite;
import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
import funkin.ui.debug.charting.components.ChartEditorPlaybarHead; import funkin.ui.debug.charting.components.ChartEditorPlaybarHead;
import funkin.ui.debug.charting.components.ChartEditorSelectionSquareSprite; import funkin.ui.debug.charting.components.ChartEditorSelectionSquareSprite;
import funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler; import funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler;
@ -150,7 +153,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Layouts // Layouts
public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata'); public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata');
public static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata'); public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata');
public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties'); public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties');
public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata'); public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata');
public static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty'); public static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty');
@ -170,7 +173,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/** /**
* The width of the scroll area. * The width of the scroll area.
*/ */
public static final PLAYHEAD_SCROLL_AREA_WIDTH:Int = 12; public static final PLAYHEAD_SCROLL_AREA_WIDTH:Int = Std.int(GRID_SIZE);
/** /**
* The height of the playhead, in pixels. * The height of the playhead, in pixels.
@ -306,13 +309,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_songLengthInSteps():Float function get_songLengthInSteps():Float
{ {
return Conductor.getTimeInSteps(songLengthInMs); return Conductor.instance.getTimeInSteps(songLengthInMs);
} }
function set_songLengthInSteps(value:Float):Float function set_songLengthInSteps(value:Float):Float
{ {
// Getting a reasonable result from setting songLengthInSteps requires that Conductor.mapBPMChanges be called first. // Getting a reasonable result from setting songLengthInSteps requires that Conductor.instance.mapBPMChanges be called first.
songLengthInMs = Conductor.getStepTimeInMs(value); songLengthInMs = Conductor.instance.getStepTimeInMs(value);
return value; return value;
} }
@ -366,17 +369,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
this.scrollPositionInPixels = value; this.scrollPositionInPixels = value;
// Move the grid sprite to the correct position. // Move the grid sprite to the correct position.
if (gridTiledSprite != null && gridPlayheadScrollArea != null) if (gridTiledSprite != null && measureTicks != null)
{ {
if (isViewDownscroll) if (isViewDownscroll)
{ {
gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS); gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS);
gridPlayheadScrollArea.y = gridTiledSprite.y; measureTicks.y = gridTiledSprite.y;
} }
else else
{ {
gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS); gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS);
gridPlayheadScrollArea.y = gridTiledSprite.y; measureTicks.y = gridTiledSprite.y;
if (audioVisGroup != null && audioVisGroup.playerVis != null) if (audioVisGroup != null && audioVisGroup.playerVis != null)
{ {
@ -398,6 +401,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff;
// Update the note preview viewport box. // Update the note preview viewport box.
setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
// Update the measure tick display.
if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0;
return this.scrollPositionInPixels; return this.scrollPositionInPixels;
} }
@ -426,12 +431,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_scrollPositionInMs():Float function get_scrollPositionInMs():Float
{ {
return Conductor.getStepTimeInMs(scrollPositionInSteps); return Conductor.instance.getStepTimeInMs(scrollPositionInSteps);
} }
function set_scrollPositionInMs(value:Float):Float function set_scrollPositionInMs(value:Float):Float
{ {
scrollPositionInSteps = Conductor.getTimeInSteps(value); scrollPositionInSteps = Conductor.instance.getTimeInSteps(value);
return value; return value;
} }
@ -485,13 +490,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_playheadPositionInMs():Float function get_playheadPositionInMs():Float
{ {
if (audioVisGroup != null && audioVisGroup.playerVis != null) if (audioVisGroup != null && audioVisGroup.playerVis != null)
audioVisGroup.playerVis.realtimeStartOffset = -Conductor.getStepTimeInMs(playheadPositionInSteps); audioVisGroup.playerVis.realtimeStartOffset = -Conductor.instance.getStepTimeInMs(playheadPositionInSteps);
return Conductor.getStepTimeInMs(playheadPositionInSteps); return Conductor.instance.getStepTimeInMs(playheadPositionInSteps);
} }
function set_playheadPositionInMs(value:Float):Float function set_playheadPositionInMs(value:Float):Float
{ {
playheadPositionInSteps = Conductor.getTimeInSteps(value); playheadPositionInSteps = Conductor.instance.getTimeInSteps(value);
if (audioVisGroup != null && audioVisGroup.playerVis != null) audioVisGroup.playerVis.realtimeStartOffset = -value; if (audioVisGroup != null && audioVisGroup.playerVis != null) audioVisGroup.playerVis.realtimeStartOffset = -value;
return value; return value;
@ -522,17 +527,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/** /**
* The note kind to use for notes being placed in the chart. Defaults to `''`. * The note kind to use for notes being placed in the chart. Defaults to `''`.
*/ */
var selectedNoteKind:String = ''; var noteKindToPlace:String = '';
/** /**
* The event type to use for events being placed in the chart. Defaults to `''`. * The event type to use for events being placed in the chart. Defaults to `''`.
*/ */
var selectedEventKind:String = 'FocusCamera'; var eventKindToPlace:String = 'FocusCamera';
/** /**
* The event data to use for events being placed in the chart. * The event data to use for events being placed in the chart.
*/ */
var selectedEventData:DynamicAccess<Dynamic> = {}; var eventDataToPlace:DynamicAccess<Dynamic> = {};
/** /**
* The internal index of what note snapping value is in use. * The internal index of what note snapping value is in use.
@ -906,6 +911,70 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return Save.get().chartEditorHasBackup = value; return Save.get().chartEditorHasBackup = value;
} }
/**
* A list of previous working file paths.
* Also known as the "recent files" list.
* The first element is [null] if the current working file has not been saved anywhere yet.
*/
public var previousWorkingFilePaths(default, set):Array<Null<String>> = [null];
function set_previousWorkingFilePaths(value:Array<Null<String>>):Array<Null<String>>
{
// Called only when the WHOLE LIST is overridden.
previousWorkingFilePaths = value;
applyWindowTitle();
populateOpenRecentMenu();
applyCanQuickSave();
return value;
}
/**
* The current file path which the chart editor is working with.
* If `null`, the current chart has not been saved yet.
*/
public var currentWorkingFilePath(get, set):Null<String>;
function get_currentWorkingFilePath():Null<String>
{
return previousWorkingFilePaths[0];
}
function set_currentWorkingFilePath(value:Null<String>):Null<String>
{
if (value == previousWorkingFilePaths[0]) return value;
if (previousWorkingFilePaths.contains(null))
{
// Filter all instances of `null` from the array.
previousWorkingFilePaths = previousWorkingFilePaths.filter(function(x:Null<String>):Bool {
return x != null;
});
}
if (previousWorkingFilePaths.contains(value))
{
// Move the path to the front of the list.
previousWorkingFilePaths.remove(value);
previousWorkingFilePaths.unshift(value);
}
else
{
// Add the path to the front of the list.
previousWorkingFilePaths.unshift(value);
}
while (previousWorkingFilePaths.length > Constants.MAX_PREVIOUS_WORKING_FILES)
{
// Remove the last path in the list.
previousWorkingFilePaths.pop();
}
populateOpenRecentMenu();
applyWindowTitle();
return value;
}
/** /**
* Whether the difficulty tree view in the toolbox has been modified and needs to be updated. * Whether the difficulty tree view in the toolbox has been modified and needs to be updated.
* This happens when we add/remove difficulties. * This happens when we add/remove difficulties.
@ -935,6 +1004,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var commandHistoryDirty:Bool = true; var commandHistoryDirty:Bool = true;
/**
* If true, we are currently in the process of quitting the chart editor.
* Skip any update functions as most of them will call a crash.
*/
var criticalFailure:Bool = false;
// Input // Input
/** /**
@ -1680,23 +1755,28 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var notePreviewViewportBitmap:Null<BitmapData> = null; var notePreviewViewportBitmap:Null<BitmapData> = null;
/**r
* The IMAGE used for the measure ticks. Updated by ChartEditorThemeHandler.
*/
var measureTickBitmap:Null<BitmapData> = null;
/** /**
* The tiled sprite used to display the grid. * The tiled sprite used to display the grid.
* The height is the length of the song, and scrolling is done by simply the sprite. * The height is the length of the song, and scrolling is done by simply the sprite.
*/ */
var gridTiledSprite:Null<FlxSprite> = null; var gridTiledSprite:Null<FlxSprite> = null;
/**
* The measure ticks area. Includes the numbers and the background sprite.
*/
var measureTicks:Null<ChartEditorMeasureTicks> = null;
/** /**
* The playhead representing the current position in the song. * The playhead representing the current position in the song.
* Can move around on the grid independently of the view. * Can move around on the grid independently of the view.
*/ */
var gridPlayhead:FlxSpriteGroup = new FlxSpriteGroup(); var gridPlayhead:FlxSpriteGroup = new FlxSpriteGroup();
/**
* The sprite for the scroll area under
*/
var gridPlayheadScrollArea:Null<FlxSprite> = null;
/** /**
* A sprite used to indicate the note that will be placed on click. * A sprite used to indicate the note that will be placed on click.
*/ */
@ -1783,70 +1863,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var params:Null<ChartEditorParams>; var params:Null<ChartEditorParams>;
/**
* A list of previous working file paths.
* Also known as the "recent files" list.
* The first element is [null] if the current working file has not been saved anywhere yet.
*/
public var previousWorkingFilePaths(default, set):Array<Null<String>> = [null];
function set_previousWorkingFilePaths(value:Array<Null<String>>):Array<Null<String>>
{
// Called only when the WHOLE LIST is overridden.
previousWorkingFilePaths = value;
applyWindowTitle();
populateOpenRecentMenu();
applyCanQuickSave();
return value;
}
/**
* The current file path which the chart editor is working with.
* If `null`, the current chart has not been saved yet.
*/
public var currentWorkingFilePath(get, set):Null<String>;
function get_currentWorkingFilePath():Null<String>
{
return previousWorkingFilePaths[0];
}
function set_currentWorkingFilePath(value:Null<String>):Null<String>
{
if (value == previousWorkingFilePaths[0]) return value;
if (previousWorkingFilePaths.contains(null))
{
// Filter all instances of `null` from the array.
previousWorkingFilePaths = previousWorkingFilePaths.filter(function(x:Null<String>):Bool {
return x != null;
});
}
if (previousWorkingFilePaths.contains(value))
{
// Move the path to the front of the list.
previousWorkingFilePaths.remove(value);
previousWorkingFilePaths.unshift(value);
}
else
{
// Add the path to the front of the list.
previousWorkingFilePaths.unshift(value);
}
while (previousWorkingFilePaths.length > Constants.MAX_PREVIOUS_WORKING_FILES)
{
// Remove the last path in the list.
previousWorkingFilePaths.pop();
}
populateOpenRecentMenu();
applyWindowTitle();
return value;
}
public function new(?params:ChartEditorParams) public function new(?params:ChartEditorParams)
{ {
super(); super();
@ -1918,6 +1934,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
this.updateTheme(); this.updateTheme();
buildGrid(); buildGrid();
buildMeasureTicks();
buildNotePreview(); buildNotePreview();
buildSelectionBox(); buildSelectionBox();
@ -1927,6 +1944,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Setup the onClick listeners for the UI after it's been created. // Setup the onClick listeners for the UI after it's been created.
setupUIListeners(); setupUIListeners();
setupContextMenu();
setupTurboKeyHandlers(); setupTurboKeyHandlers();
setupAutoSave(); setupAutoSave();
@ -2172,15 +2190,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
buildNoteGroup(); buildNoteGroup();
gridPlayheadScrollArea = new FlxSprite(0, 0);
gridPlayheadScrollArea.makeGraphic(10, 10, PLAYHEAD_SCROLL_AREA_COLOR); // Make it 10x10px and then scale it as needed.
add(gridPlayheadScrollArea);
gridPlayheadScrollArea.setGraphicSize(PLAYHEAD_SCROLL_AREA_WIDTH, 3000);
gridPlayheadScrollArea.updateHitbox();
gridPlayheadScrollArea.x = GRID_X_POS - PLAYHEAD_SCROLL_AREA_WIDTH;
gridPlayheadScrollArea.y = GRID_INITIAL_Y_POS;
gridPlayheadScrollArea.zIndex = 25;
// The playhead that show the current position in the song. // The playhead that show the current position in the song.
add(gridPlayhead); add(gridPlayhead);
gridPlayhead.zIndex = 30; gridPlayhead.zIndex = 30;
@ -2216,6 +2225,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
add(audioVisGroup); add(audioVisGroup);
} }
function buildMeasureTicks():Void
{
measureTicks = new ChartEditorMeasureTicks(this);
var measureTicksWidth = (GRID_SIZE);
measureTicks.x = gridTiledSprite.x - measureTicksWidth;
measureTicks.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
measureTicks.zIndex = 20;
add(measureTicks);
}
function buildNotePreview():Void function buildNotePreview():Void
{ {
var playbarHeightWithPad = PLAYBAR_HEIGHT + 10; var playbarHeightWithPad = PLAYBAR_HEIGHT + 10;
@ -2301,6 +2321,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
bounds.height = MIN_HEIGHT; bounds.height = MIN_HEIGHT;
} }
trace('Note preview viewport bounds: ' + bounds.toString());
return bounds; return bounds;
} }
@ -2541,13 +2563,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
else else
{ {
Conductor.currentTimeChange.bpm += 1; Conductor.instance.currentTimeChange.bpm += 1;
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
} }
} }
playbarBPM.onRightClick = _ -> { playbarBPM.onRightClick = _ -> {
Conductor.currentTimeChange.bpm -= 1; Conductor.instance.currentTimeChange.bpm -= 1;
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
} }
@ -2590,14 +2612,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemUndo.onClick = _ -> undoLastCommand(); menubarItemUndo.onClick = _ -> undoLastCommand();
menubarItemRedo.onClick = _ -> redoLastCommand(); menubarItemRedo.onClick = _ -> redoLastCommand();
menubarItemCopy.onClick = function(_) { menubarItemCopy.onClick = function(_) {
copySelection();
}; };
menubarItemCut.onClick = _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection)); menubarItemCut.onClick = _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection));
menubarItemPaste.onClick = _ -> { menubarItemPaste.onClick = _ -> {
var targetMs:Float = scrollPositionInMs + playheadPositionInMs; var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
var targetStep:Float = Conductor.getTimeInSteps(targetMs); var targetStep:Float = Conductor.instance.getTimeInSteps(targetMs);
var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio; var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio;
var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep); var targetSnappedMs:Float = Conductor.instance.getStepTimeInMs(targetSnappedStep);
performCommand(new PasteItemsCommand(targetSnappedMs)); performCommand(new PasteItemsCommand(targetSnappedMs));
}; };
@ -2753,7 +2776,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value); menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value);
menubarItemToggleToolboxMetadata.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value); menubarItemToggleToolboxMetadata.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value);
menubarItemToggleToolboxNotes.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value); menubarItemToggleToolboxNotes.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value);
menubarItemToggleToolboxEvents.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value); menubarItemToggleToolboxEventData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT, event.value);
menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value); menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value);
menubarItemToggleToolboxPlayerPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value); menubarItemToggleToolboxPlayerPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value);
menubarItemToggleToolboxOpponentPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value); menubarItemToggleToolboxOpponentPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value);
@ -2762,6 +2785,42 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// registerContextMenu(null, Paths.ui('chart-editor/context/test')); // registerContextMenu(null, Paths.ui('chart-editor/context/test'));
} }
function setupContextMenu():Void
{
Screen.instance.registerEvent(MouseEvent.RIGHT_MOUSE_UP, function(e:MouseEvent) {
var xPos = e.screenX;
var yPos = e.screenY;
onContextMenu(xPos, yPos);
});
}
function onContextMenu(xPos:Float, yPos:Float)
{
trace('User right clicked to open menu at (${xPos}, ${yPos})');
// this.openDefaultContextMenu(xPos, yPos);
}
function copySelection():Void
{
// Doesn't use a command because it's not undoable.
// Calculate a single time offset for all the notes and events.
var timeOffset:Null<Int> = currentNoteSelection.length > 0 ? Std.int(currentNoteSelection[0].time) : null;
if (currentEventSelection.length > 0)
{
if (timeOffset == null || currentEventSelection[0].time < timeOffset)
{
timeOffset = Std.int(currentEventSelection[0].time);
}
}
SongDataUtils.writeItemsToClipboard(
{
notes: SongDataUtils.buildNoteClipboard(currentNoteSelection, timeOffset),
events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset),
});
}
/** /**
* Initialize TurboKeyHandlers and add them to the state (so `update()` is called) * Initialize TurboKeyHandlers and add them to the state (so `update()` is called)
* We can then probe `keyHandler.activated` to see if the key combo's action should be taken. * We can then probe `keyHandler.activated` to see if the key combo's action should be taken.
@ -2793,10 +2852,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
saveDataDirty = false; saveDataDirty = false;
} }
var displayAutosavePopup:Bool = false;
/** /**
* UPDATE FUNCTIONS * UPDATE FUNCTIONS
*/ */
function autoSave():Void function autoSave(?beforePlaytest:Bool = false):Void
{ {
var needsAutoSave:Bool = saveDataDirty; var needsAutoSave:Bool = saveDataDirty;
@ -2814,6 +2875,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (needsAutoSave) if (needsAutoSave)
{ {
this.exportAllSongData(true, null); this.exportAllSongData(true, null);
if (beforePlaytest)
{
displayAutosavePopup = true;
}
else
{
displayAutosavePopup = false;
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]); var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [ this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [
{ {
@ -2822,6 +2890,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
]); ]);
} }
}
#end #end
} }
@ -2888,7 +2957,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
public override function update(elapsed:Float):Void public override function update(elapsed:Float):Void
{ {
// Override F4 behavior to include the autosave. // Override F4 behavior to include the autosave.
if (FlxG.keys.justPressed.F4) if (FlxG.keys.justPressed.F4 && !criticalFailure)
{ {
quitChartEditor(); quitChartEditor();
return; return;
@ -2897,6 +2966,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// dispatchEvent gets called here. // dispatchEvent gets called here.
super.update(elapsed); super.update(elapsed);
if (criticalFailure) return;
// These ones happen even if the modal dialog is open. // These ones happen even if the modal dialog is open.
handleMusicPlayback(elapsed); handleMusicPlayback(elapsed);
handleNoteDisplay(); handleNoteDisplay();
@ -2936,9 +3007,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (metronomeVolume > 0.0 && this.subState == null && (audioInstTrack != null && audioInstTrack.isPlaying)) if (metronomeVolume > 0.0 && this.subState == null && (audioInstTrack != null && audioInstTrack.isPlaying))
{ {
playMetronomeTick(Conductor.currentBeat % 4 == 0); playMetronomeTick(Conductor.instance.currentBeat % Conductor.instance.beatsPerMeasure == 0);
} }
// Show the mouse cursor.
// Just throwing this somewhere convenient and infrequently called because sometimes Flixel's debug thing hides the cursor.
Cursor.show();
return true; return true;
} }
@ -2952,8 +3027,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (audioInstTrack != null && audioInstTrack.isPlaying) if (audioInstTrack != null && audioInstTrack.isPlaying)
{ {
if (healthIconDad != null) healthIconDad.onStepHit(Conductor.currentStep); if (healthIconDad != null) healthIconDad.onStepHit(Conductor.instance.currentStep);
if (healthIconBF != null) healthIconBF.onStepHit(Conductor.currentStep); if (healthIconBF != null) healthIconBF.onStepHit(Conductor.instance.currentStep);
} }
// Updating these every step keeps it more accurate. // Updating these every step keeps it more accurate.
@ -2981,12 +3056,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
audioInstTrack.update(elapsed); audioInstTrack.update(elapsed);
// If the song starts 50ms in, make sure we start the song there. // If the song starts 50ms in, make sure we start the song there.
if (Conductor.instrumentalOffset < 0) if (Conductor.instance.instrumentalOffset < 0)
{ {
if (audioInstTrack.time < -Conductor.instrumentalOffset) if (audioInstTrack.time < -Conductor.instance.instrumentalOffset)
{ {
trace('Resetting instrumental time to ${- Conductor.instrumentalOffset}ms'); trace('Resetting instrumental time to ${- Conductor.instance.instrumentalOffset}ms');
audioInstTrack.time = -Conductor.instrumentalOffset; audioInstTrack.time = -Conductor.instance.instrumentalOffset;
} }
} }
} }
@ -2997,16 +3072,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
// If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat!
var oldStepTime:Float = Conductor.currentStepTime; var oldStepTime:Float = Conductor.instance.currentStepTime;
var oldSongPosition:Float = Conductor.songPosition + Conductor.instrumentalOffset; var oldSongPosition:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset;
Conductor.update(audioInstTrack.time); Conductor.instance.update(audioInstTrack.time);
handleHitsounds(oldSongPosition, Conductor.songPosition + Conductor.instrumentalOffset); handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
// Resync vocals. // Resync vocals.
if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
{ {
audioVocalTrackGroup.time = audioInstTrack.time; audioVocalTrackGroup.time = audioInstTrack.time;
} }
var diffStepTime:Float = Conductor.currentStepTime - oldStepTime; var diffStepTime:Float = Conductor.instance.currentStepTime - oldStepTime;
// Move the playhead. // Move the playhead.
playheadPositionInPixels += diffStepTime * GRID_SIZE; playheadPositionInPixels += diffStepTime * GRID_SIZE;
@ -3016,9 +3091,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
else else
{ {
// Else, move the entire view. // Else, move the entire view.
var oldSongPosition:Float = Conductor.songPosition + Conductor.instrumentalOffset; var oldSongPosition:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset;
Conductor.update(audioInstTrack.time); Conductor.instance.update(audioInstTrack.time);
handleHitsounds(oldSongPosition, Conductor.songPosition + Conductor.instrumentalOffset); handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
// Resync vocals. // Resync vocals.
if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
{ {
@ -3027,7 +3102,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// We need time in fractional steps here to allow the song to actually play. // We need time in fractional steps here to allow the song to actually play.
// Also account for a potentially offset playhead. // Also account for a potentially offset playhead.
scrollPositionInPixels = (Conductor.currentStepTime + Conductor.instrumentalOffsetSteps) * GRID_SIZE - playheadPositionInPixels; scrollPositionInPixels = (Conductor.instance.currentStepTime + Conductor.instance.instrumentalOffsetSteps) * GRID_SIZE - playheadPositionInPixels;
// DO NOT move song to scroll position here specifically. // DO NOT move song to scroll position here specifically.
@ -3142,6 +3217,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Update the event sprite's position. // Update the event sprite's position.
eventSprite.updateEventPosition(renderedEvents); eventSprite.updateEventPosition(renderedEvents);
// Update the sprite's graphic. TODO: Is this inefficient?
eventSprite.playAnimation(eventSprite.eventData.event);
} }
else else
{ {
@ -3156,8 +3233,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Let's try testing only notes within a certain range of the view area. // Let's try testing only notes within a certain range of the view area.
// TODO: I don't think this messes up really long sustains, does it? // TODO: I don't think this messes up really long sustains, does it?
var viewAreaTopMs:Float = scrollPositionInMs - (Conductor.measureLengthMs * 2); // Is 2 measures enough? var viewAreaTopMs:Float = scrollPositionInMs - (Conductor.instance.measureLengthMs * 2); // Is 2 measures enough?
var viewAreaBottomMs:Float = scrollPositionInMs + (Conductor.measureLengthMs * 2); // Is 2 measures enough? var viewAreaBottomMs:Float = scrollPositionInMs + (Conductor.instance.measureLengthMs * 2); // Is 2 measures enough?
// Add notes that are now visible. // Add notes that are now visible.
for (noteData in currentSongChartNoteData) for (noteData in currentSongChartNoteData)
@ -3444,14 +3521,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// PAGE UP = Jump up to nearest measure // PAGE UP = Jump up to nearest measure
if (pageUpKeyHandler.activated) if (pageUpKeyHandler.activated)
{ {
var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var targetScrollPosition:Float = Math.floor(playheadPos / measureHeight) * measureHeight; var targetScrollPosition:Float = Math.floor(playheadPos / measureHeight) * measureHeight;
// If we would move less than one grid, instead move to the top of the previous measure. // If we would move less than one grid, instead move to the top of the previous measure.
var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos); var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos);
if (targetScrollAmount < GRID_SIZE) if (targetScrollAmount < GRID_SIZE)
{ {
targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.beatsPerMeasure; targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure;
} }
scrollAmount = targetScrollPosition - playheadPos; scrollAmount = targetScrollPosition - playheadPos;
@ -3460,21 +3537,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (playbarButtonPressed == 'playbarBack') if (playbarButtonPressed == 'playbarBack')
{ {
playbarButtonPressed = ''; playbarButtonPressed = '';
scrollAmount = -GRID_SIZE * 4 * Conductor.beatsPerMeasure; scrollAmount = -GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
shouldPause = true; shouldPause = true;
} }
// PAGE DOWN = Jump down to nearest measure // PAGE DOWN = Jump down to nearest measure
if (pageDownKeyHandler.activated) if (pageDownKeyHandler.activated)
{ {
var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var targetScrollPosition:Float = Math.ceil(playheadPos / measureHeight) * measureHeight; var targetScrollPosition:Float = Math.ceil(playheadPos / measureHeight) * measureHeight;
// If we would move less than one grid, instead move to the top of the next measure. // If we would move less than one grid, instead move to the top of the next measure.
var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos); var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos);
if (targetScrollAmount < GRID_SIZE) if (targetScrollAmount < GRID_SIZE)
{ {
targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.beatsPerMeasure; targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure;
} }
scrollAmount = targetScrollPosition - playheadPos; scrollAmount = targetScrollPosition - playheadPos;
@ -3483,7 +3560,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (playbarButtonPressed == 'playbarForward') if (playbarButtonPressed == 'playbarForward')
{ {
playbarButtonPressed = ''; playbarButtonPressed = '';
scrollAmount = GRID_SIZE * 4 * Conductor.beatsPerMeasure; scrollAmount = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
shouldPause = true; shouldPause = true;
} }
@ -3598,6 +3675,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// trace('shouldHandleCursor: $shouldHandleCursor'); // trace('shouldHandleCursor: $shouldHandleCursor');
// TODO: TBH some of this should be using FlxMouseEventManager...
if (shouldHandleCursor) if (shouldHandleCursor)
{ {
// Over the course of this big conditional block, // Over the course of this big conditional block,
@ -3641,7 +3720,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
scrollAnchorScreenPos = null; scrollAnchorScreenPos = null;
} }
else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea) && !isCursorOverHaxeUI) else if (measureTicks != null && FlxG.mouse.overlaps(measureTicks) && !isCursorOverHaxeUI)
{ {
gridPlayheadScrollAreaPressed = true; gridPlayheadScrollAreaPressed = true;
} }
@ -3686,10 +3765,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// The song position of the cursor, in steps. // The song position of the cursor, in steps.
var cursorFractionalStep:Float = cursorY / GRID_SIZE; var cursorFractionalStep:Float = cursorY / GRID_SIZE;
var cursorMs:Float = Conductor.getStepTimeInMs(cursorFractionalStep); var cursorMs:Float = Conductor.instance.getStepTimeInMs(cursorFractionalStep);
// Round the cursor step to the nearest snap quant. // Round the cursor step to the nearest snap quant.
var cursorSnappedStep:Float = Math.floor(cursorFractionalStep / noteSnapRatio) * noteSnapRatio; var cursorSnappedStep:Float = Math.floor(cursorFractionalStep / noteSnapRatio) * noteSnapRatio;
var cursorSnappedMs:Float = Conductor.getStepTimeInMs(cursorSnappedStep); var cursorSnappedMs:Float = Conductor.instance.getStepTimeInMs(cursorSnappedStep);
// The direction value for the column at the cursor. // The direction value for the column at the cursor.
var cursorGridPos:Int = Math.floor(cursorX / GRID_SIZE); var cursorGridPos:Int = Math.floor(cursorX / GRID_SIZE);
@ -3711,7 +3790,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// We released the mouse. Select the notes in the box. // We released the mouse. Select the notes in the box.
var cursorFractionalStepStart:Float = cursorYStart / GRID_SIZE; var cursorFractionalStepStart:Float = cursorYStart / GRID_SIZE;
var cursorStepStart:Int = Math.floor(cursorFractionalStepStart); var cursorStepStart:Int = Math.floor(cursorFractionalStepStart);
var cursorMsStart:Float = Conductor.getStepTimeInMs(cursorStepStart); var cursorMsStart:Float = Conductor.instance.getStepTimeInMs(cursorStepStart);
var cursorColumnBase:Int = Math.floor(cursorX / GRID_SIZE); var cursorColumnBase:Int = Math.floor(cursorX / GRID_SIZE);
var cursorColumnBaseStart:Int = Math.floor(cursorXStart / GRID_SIZE); var cursorColumnBaseStart:Int = Math.floor(cursorXStart / GRID_SIZE);
@ -3947,11 +4026,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var dragDistanceMs:Float = 0; var dragDistanceMs:Float = 0;
if (dragTargetNote != null && dragTargetNote.noteData != null) if (dragTargetNote != null && dragTargetNote.noteData != null)
{ {
dragDistanceMs = Conductor.getStepTimeInMs(dragTargetNote.noteData.getStepTime() + dragDistanceSteps) - dragTargetNote.noteData.time; dragDistanceMs = Conductor.instance.getStepTimeInMs(dragTargetNote.noteData.getStepTime() + dragDistanceSteps) - dragTargetNote.noteData.time;
} }
else if (dragTargetEvent != null && dragTargetEvent.eventData != null) else if (dragTargetEvent != null && dragTargetEvent.eventData != null)
{ {
dragDistanceMs = Conductor.getStepTimeInMs(dragTargetEvent.eventData.getStepTime() + dragDistanceSteps) - dragTargetEvent.eventData.time; dragDistanceMs = Conductor.instance.getStepTimeInMs(dragTargetEvent.eventData.getStepTime() + dragDistanceSteps) - dragTargetEvent.eventData.time;
} }
var dragDistanceColumns:Int = dragTargetCurrentColumn; var dragDistanceColumns:Int = dragTargetCurrentColumn;
@ -4011,7 +4090,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
stepTime = dragTargetEvent.eventData.getStepTime(); stepTime = dragTargetEvent.eventData.getStepTime();
} }
var dragDistanceSteps:Float = Conductor.getTimeInSteps(cursorSnappedMs).clamp(0, songLengthInSteps - (1 * noteSnapRatio)) - stepTime; var dragDistanceSteps:Float = Conductor.instance.getTimeInSteps(cursorSnappedMs).clamp(0, songLengthInSteps - (1 * noteSnapRatio)) - stepTime;
var data:Int = 0; var data:Int = 0;
var noteGridPos:Int = 0; var noteGridPos:Int = 0;
if (dragTargetNote != null && dragTargetNote.noteData != null) if (dragTargetNote != null && dragTargetNote.noteData != null)
@ -4043,8 +4122,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Handle extending the note as you drag. // Handle extending the note as you drag.
var stepTime:Float = inline currentPlaceNoteData.getStepTime(); var stepTime:Float = inline currentPlaceNoteData.getStepTime();
var dragLengthSteps:Float = Conductor.getTimeInSteps(cursorSnappedMs) - stepTime; var dragLengthSteps:Float = Conductor.instance.getTimeInSteps(cursorSnappedMs) - stepTime;
var dragLengthMs:Float = dragLengthSteps * Conductor.stepLengthMs; var dragLengthMs:Float = dragLengthSteps * Conductor.instance.stepLengthMs;
var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE; var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE;
if (gridGhostNote != null && gridGhostNote.noteData != null && gridGhostHoldNote != null) if (gridGhostNote != null && gridGhostNote.noteData != null && gridGhostHoldNote != null)
@ -4181,14 +4260,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
// Create an event and place it in the chart. // Create an event and place it in the chart.
// TODO: Figure out configuring event data. // TODO: Figure out configuring event data.
var newEventData:SongEventData = new SongEventData(cursorSnappedMs, selectedEventKind, selectedEventData.clone()); var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, eventDataToPlace.clone());
performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL)); performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL));
} }
else else
{ {
// Create a note and place it in the chart. // Create a note and place it in the chart.
var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, selectedNoteKind.clone()); var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace.clone());
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
@ -4226,14 +4305,53 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (highlightedNote != null && highlightedNote.noteData != null) if (highlightedNote != null && highlightedNote.noteData != null)
{ {
// TODO: Handle the case of clicking on a sustain piece. // TODO: Handle the case of clicking on a sustain piece.
// Remove the note. if (FlxG.keys.pressed.SHIFT)
{
// Shift + Right click opens the context menu.
// If we are clicking a large selection, open the Selection context menu, otherwise open the Note context menu.
var isHighlightedNoteSelected:Bool = isNoteSelected(highlightedNote.noteData);
var useSingleNoteContextMenu:Bool = (!isHighlightedNoteSelected)
|| (isHighlightedNoteSelected && currentNoteSelection.length == 1);
// Show the context menu connected to the note.
if (useSingleNoteContextMenu)
{
this.openNoteContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedNote.noteData);
}
else
{
this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY);
}
}
else
{
// Right click removes the note.
performCommand(new RemoveNotesCommand([highlightedNote.noteData])); performCommand(new RemoveNotesCommand([highlightedNote.noteData]));
} }
}
else if (highlightedEvent != null && highlightedEvent.eventData != null) else if (highlightedEvent != null && highlightedEvent.eventData != null)
{ {
// Remove the event. if (FlxG.keys.pressed.SHIFT)
{
// Shift + Right click opens the context menu.
// If we are clicking a large selection, open the Selection context menu, otherwise open the Event context menu.
var isHighlightedEventSelected:Bool = isEventSelected(highlightedEvent.eventData);
var useSingleEventContextMenu:Bool = (!isHighlightedEventSelected)
|| (isHighlightedEventSelected && currentEventSelection.length == 1);
if (useSingleEventContextMenu)
{
this.openEventContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedEvent.eventData);
}
else
{
this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY);
}
}
else
{
// Right click removes the event.
performCommand(new RemoveEventsCommand([highlightedEvent.eventData])); performCommand(new RemoveEventsCommand([highlightedEvent.eventData]));
} }
}
else else
{ {
// Right clicked on nothing. // Right clicked on nothing.
@ -4253,11 +4371,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (gridGhostEvent == null) throw "ERROR: Tried to handle cursor, but gridGhostEvent is null! Check ChartEditorState.buildGrid()"; if (gridGhostEvent == null) throw "ERROR: Tried to handle cursor, but gridGhostEvent is null! Check ChartEditorState.buildGrid()";
var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, selectedEventKind, null); var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, eventKindToPlace, null);
if (selectedEventKind != eventData.event) if (eventKindToPlace != eventData.event)
{ {
eventData.event = selectedEventKind; eventData.event = eventKindToPlace;
} }
eventData.time = cursorSnappedMs; eventData.time = cursorSnappedMs;
@ -4273,11 +4391,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()"; if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()";
var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, selectedNoteKind); var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace);
if (cursorColumn != noteData.data || selectedNoteKind != noteData.kind) if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind)
{ {
noteData.kind = selectedNoteKind; noteData.kind = noteKindToPlace;
noteData.data = cursorColumn; noteData.data = cursorColumn;
gridGhostNote.playNoteAnimation(); gridGhostNote.playNoteAnimation();
} }
@ -4319,7 +4437,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
targetCursorMode = Pointer; targetCursorMode = Pointer;
} }
else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea)) else if (measureTicks != null && FlxG.mouse.overlaps(measureTicks))
{ {
targetCursorMode = Pointer; targetCursorMode = Pointer;
} }
@ -4520,7 +4638,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (playbarHeadLayout.playbarHead.value != songPosPercent) playbarHeadLayout.playbarHead.value = songPosPercent; if (playbarHeadLayout.playbarHead.value != songPosPercent) playbarHeadLayout.playbarHead.value = songPosPercent;
} }
var songPos:Float = Conductor.songPosition + Conductor.instrumentalOffset; var songPos:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset;
var songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2); var songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2);
var songPosMinutes:String = Std.string(Math.floor((Math.abs(songPos) / 1000) / 60)).lpad('0', 2); var songPosMinutes:String = Std.string(Math.floor((Math.abs(songPos) / 1000) / 60)).lpad('0', 2);
if (songPos < 0) songPosMinutes = '-' + songPosMinutes; if (songPos < 0) songPosMinutes = '-' + songPosMinutes;
@ -4576,16 +4694,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio;
var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep));
var playheadPosSnappedMs:Float = playheadPosStep * Conductor.stepLengthMs * noteSnapRatio; var playheadPosSnappedMs:Float = playheadPosStep * Conductor.instance.stepLengthMs * noteSnapRatio;
// Look for notes within 1 step of the playhead. // Look for notes within 1 step of the playhead.
var notesAtPos:Array<SongNoteData> = SongDataUtils.getNotesInTimeRange(currentSongChartNoteData, playheadPosSnappedMs, var notesAtPos:Array<SongNoteData> = SongDataUtils.getNotesInTimeRange(currentSongChartNoteData, playheadPosSnappedMs,
playheadPosSnappedMs + Conductor.stepLengthMs * noteSnapRatio); playheadPosSnappedMs + Conductor.instance.stepLengthMs * noteSnapRatio);
notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]); notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]);
if (notesAtPos.length == 0) if (notesAtPos.length == 0)
{ {
var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, selectedNoteKind); var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace);
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
} }
else else
@ -4699,6 +4817,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
FlxG.switchState(new MainMenuState()); FlxG.switchState(new MainMenuState());
resetWindowTitle(); resetWindowTitle();
criticalFailure = true;
} }
/** /**
@ -4742,9 +4862,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
else else
{ {
var targetMs:Float = scrollPositionInMs + playheadPositionInMs; var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
var targetStep:Float = Conductor.getTimeInSteps(targetMs); var targetStep:Float = Conductor.instance.getTimeInSteps(targetMs);
var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio; var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio;
var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep); var targetSnappedMs:Float = Conductor.instance.getStepTimeInMs(targetSnappedStep);
targetSnappedMs; targetSnappedMs;
} }
performCommand(new PasteItemsCommand(targetMs)); performCommand(new PasteItemsCommand(targetMs));
@ -4878,11 +4998,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
#end #end
} }
override function handleQuickWatch():Void function handleQuickWatch():Void
{ {
super.handleQuickWatch(); FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0);
FlxG.watch.addQuick('musicTime', audioInstTrack?.time);
FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels); FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels);
FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels); FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels);
@ -4909,7 +5027,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
function testSongInPlayState(minimal:Bool = false):Void function testSongInPlayState(minimal:Bool = false):Void
{ {
autoSave(); autoSave(true);
stopWelcomeMusic(); stopWelcomeMusic();
@ -5109,16 +5227,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function onSongLengthChanged():Void function onSongLengthChanged():Void
{ {
if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels; if (gridTiledSprite != null)
if (gridPlayheadScrollArea != null)
{ {
gridPlayheadScrollArea.setGraphicSize(Std.int(gridPlayheadScrollArea.width), songLengthInPixels); gridTiledSprite.height = songLengthInPixels;
gridPlayheadScrollArea.updateHitbox(); }
if (measureTicks != null)
{
measureTicks.setHeight(songLengthInPixels);
} }
// Remove any notes past the end of the song. // Remove any notes past the end of the song.
var songCutoffPointSteps:Float = songLengthInSteps - 0.1; var songCutoffPointSteps:Float = songLengthInSteps - 0.1;
var songCutoffPointMs:Float = Conductor.getStepTimeInMs(songCutoffPointSteps); var songCutoffPointMs:Float = Conductor.instance.getStepTimeInMs(songCutoffPointSteps);
currentSongChartNoteData = SongDataUtils.clampSongNoteData(currentSongChartNoteData, 0.0, songCutoffPointMs); currentSongChartNoteData = SongDataUtils.clampSongNoteData(currentSongChartNoteData, 0.0, songCutoffPointMs);
currentSongChartEventData = SongDataUtils.clampSongEventData(currentSongChartEventData, 0.0, songCutoffPointMs); currentSongChartEventData = SongDataUtils.clampSongEventData(currentSongChartEventData, 0.0, songCutoffPointMs);
@ -5220,7 +5340,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var prevDifficulty = availableDifficulties[availableDifficulties.length - 1]; var prevDifficulty = availableDifficulties[availableDifficulties.length - 1];
selectedDifficulty = prevDifficulty; selectedDifficulty = prevDifficulty;
Conductor.mapTimeChanges(this.currentSongMetadata.timeChanges); Conductor.instance.mapTimeChanges(this.currentSongMetadata.timeChanges);
updateTimeSignature();
refreshDifficultyTreeSelection(); refreshDifficultyTreeSelection();
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
@ -5282,9 +5403,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Update the songPosition in the audio tracks. // Update the songPosition in the audio tracks.
if (audioInstTrack != null) if (audioInstTrack != null)
{ {
audioInstTrack.time = scrollPositionInMs + playheadPositionInMs - Conductor.instrumentalOffset; audioInstTrack.time = scrollPositionInMs + playheadPositionInMs - Conductor.instance.instrumentalOffset;
// Update the songPosition in the Conductor. // Update the songPosition in the Conductor.
Conductor.update(audioInstTrack.time); Conductor.instance.update(audioInstTrack.time);
if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = audioInstTrack.time; if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = audioInstTrack.time;
} }
@ -5344,6 +5465,20 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
this.persistentUpdate = true; this.persistentUpdate = true;
this.persistentDraw = true; this.persistentDraw = true;
if (displayAutosavePopup)
{
displayAutosavePopup = false;
Toolkit.callLater(() -> {
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [
{
text: "Take Me There",
callback: openBackupsFolder,
}
]);
});
}
moveSongToScrollPosition(); moveSongToScrollPosition();
fadeInWelcomeMusic(7, 10); fadeInWelcomeMusic(7, 10);
@ -5360,6 +5495,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = vocalTargetVolume; if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = vocalTargetVolume;
} }
function updateTimeSignature():Void
{
// Redo the grid bitmap to be 4/4.
this.updateTheme();
gridTiledSprite.loadGraphic(gridBitmap);
measureTicks.reloadTickBitmap();
}
/** /**
* HAXEUI FUNCTIONS * HAXEUI FUNCTIONS
*/ */
@ -5472,6 +5615,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (notePreviewViewportBoundsDirty) if (notePreviewViewportBoundsDirty)
{ {
setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
notePreviewViewportBoundsDirty = false;
} }
} }
@ -5603,7 +5747,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
trace('ERROR: Instrumental track is null!'); trace('ERROR: Instrumental track is null!');
} }
this.songLengthInMs = (audioInstTrack?.length ?? 1000.0) + Conductor.instrumentalOffset; this.songLengthInMs = (audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset;
// Many things get reset when song length changes. // Many things get reset when song length changes.
healthIconsDirty = true; healthIconsDirty = true;
@ -5628,6 +5772,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
cleanupAutoSave(); cleanupAutoSave();
this.closeAllMenus();
// Hide the mouse cursor on other states. // Hide the mouse cursor on other states.
Cursor.hide(); Cursor.hide();

View file

@ -34,7 +34,12 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
state.currentSongMetadata.timeChanges = timeChanges; state.currentSongMetadata.timeChanges = timeChanges;
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true;
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
} }
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
@ -51,7 +56,12 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
state.currentSongMetadata.timeChanges = timeChanges; state.currentSongMetadata.timeChanges = timeChanges;
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true;
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
} }
public function shouldAddToHistory(state:ChartEditorState):Bool public function shouldAddToHistory(state:ChartEditorState):Bool

View file

@ -33,7 +33,7 @@ class MoveEventsCommand implements ChartEditorCommand
{ {
// Clone the notes to prevent editing from affecting the history. // Clone the notes to prevent editing from affecting the history.
var resultEvent = event.clone(); var resultEvent = event.clone();
resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
movedEvents.push(resultEvent); movedEvents.push(resultEvent);
} }

View file

@ -21,8 +21,8 @@ class MoveItemsCommand implements ChartEditorCommand
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, offset:Float, columns:Int) public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, offset:Float, columns:Int)
{ {
// Clone the notes to prevent editing from affecting the history. // Clone the notes to prevent editing from affecting the history.
this.notes = [for (note in notes) note.clone()]; this.notes = notes.clone();
this.events = [for (event in events) event.clone()]; this.events = events.clone();
this.offset = offset; this.offset = offset;
this.columns = columns; this.columns = columns;
this.movedNotes = []; this.movedNotes = [];
@ -41,7 +41,7 @@ class MoveItemsCommand implements ChartEditorCommand
{ {
// Clone the notes to prevent editing from affecting the history. // Clone the notes to prevent editing from affecting the history.
var resultNote = note.clone(); var resultNote = note.clone();
resultNote.time = (resultNote.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); resultNote.time = (resultNote.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0, resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0,
ChartEditorState.STRUMLINE_SIZE * 2 - 1)); ChartEditorState.STRUMLINE_SIZE * 2 - 1));
@ -52,7 +52,7 @@ class MoveItemsCommand implements ChartEditorCommand
{ {
// Clone the notes to prevent editing from affecting the history. // Clone the notes to prevent editing from affecting the history.
var resultEvent = event.clone(); var resultEvent = event.clone();
resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
movedEvents.push(resultEvent); movedEvents.push(resultEvent);
} }

View file

@ -34,7 +34,7 @@ class MoveNotesCommand implements ChartEditorCommand
{ {
// Clone the notes to prevent editing from affecting the history. // Clone the notes to prevent editing from affecting the history.
var resultNote = note.clone(); var resultNote = note.clone();
resultNote.time = (resultNote.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); resultNote.time = (resultNote.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0, resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0,
ChartEditorState.STRUMLINE_SIZE * 2 - 1)); ChartEditorState.STRUMLINE_SIZE * 2 - 1));

View file

@ -32,9 +32,9 @@ class PasteItemsCommand implements ChartEditorCommand
return; return;
} }
var stepEndOfSong:Float = Conductor.getTimeInSteps(state.songLengthInMs); var stepEndOfSong:Float = Conductor.instance.getTimeInSteps(state.songLengthInMs);
var stepCutoff:Float = stepEndOfSong - 1.0; var stepCutoff:Float = stepEndOfSong - 1.0;
var msCutoff:Float = Conductor.getStepTimeInMs(stepCutoff); var msCutoff:Float = Conductor.instance.getStepTimeInMs(stepCutoff);
addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp)); addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp));
addedNotes = SongDataUtils.clampSongNoteData(addedNotes, 0.0, msCutoff); addedNotes = SongDataUtils.clampSongNoteData(addedNotes, 0.0, msCutoff);

View file

@ -33,6 +33,32 @@ class SelectItemsCommand implements ChartEditorCommand
state.currentEventSelection.push(event); state.currentEventSelection.push(event);
} }
// If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
if (this.notes.length == 0 && this.events.length >= 1)
{
var eventSelected = this.events[0];
state.eventKindToPlace = eventSelected.event;
// This code is here to parse event data that's not built as a struct for some reason.
// TODO: Clean this up or get rid of it.
var eventSchema = eventSelected.getSchema();
var defaultKey = null;
if (eventSchema == null)
{
trace('[WARNING] Event schema not found for event ${eventSelected.event}.');
}
else
{
defaultKey = eventSchema.getFirstField()?.name;
}
var eventData = eventSelected.valueAsStruct(defaultKey);
state.eventDataToPlace = eventData;
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
}
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
} }

View file

@ -30,6 +30,32 @@ class SetItemSelectionCommand implements ChartEditorCommand
state.currentNoteSelection = notes; state.currentNoteSelection = notes;
state.currentEventSelection = events; state.currentEventSelection = events;
// If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
if (this.notes.length == 0 && this.events.length >= 1)
{
var eventSelected = this.events[0];
state.eventKindToPlace = eventSelected.event;
// This code is here to parse event data that's not built as a struct for some reason.
// TODO: Clean this up or get rid of it.
var eventSchema = eventSelected.getSchema();
var defaultKey = null;
if (eventSchema == null)
{
trace('[WARNING] Event schema not found for event ${eventSelected.event}.');
}
else
{
defaultKey = eventSchema.getFirstField()?.name;
}
var eventData = eventSelected.valueAsStruct(defaultKey);
state.eventDataToPlace = eventData;
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
}
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
} }

View file

@ -1,6 +1,6 @@
package funkin.ui.debug.charting.components; package funkin.ui.debug.charting.components;
import funkin.data.event.SongEventData.SongEventParser; import funkin.data.event.SongEventRegistry;
import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxAtlasFrames;
import openfl.display.BitmapData; import openfl.display.BitmapData;
import openfl.utils.Assets; import openfl.utils.Assets;
@ -79,7 +79,7 @@ class ChartEditorEventSprite extends FlxSprite
} }
// Push all the other events as frames. // Push all the other events as frames.
for (eventName in SongEventParser.listEventIds()) for (eventName in SongEventRegistry.listEventIds())
{ {
var exists:Bool = Assets.exists(Paths.image('ui/chart-editor/events/$eventName')); var exists:Bool = Assets.exists(Paths.image('ui/chart-editor/events/$eventName'));
if (!exists) continue; // No graphic for this event. if (!exists) continue; // No graphic for this event.
@ -105,7 +105,7 @@ class ChartEditorEventSprite extends FlxSprite
function buildAnimations():Void function buildAnimations():Void
{ {
var eventNames:Array<String> = [DEFAULT_EVENT].concat(SongEventParser.listEventIds()); var eventNames:Array<String> = [DEFAULT_EVENT].concat(SongEventRegistry.listEventIds());
for (eventName in eventNames) for (eventName in eventNames)
{ {
this.animation.addByPrefix(eventName, '${eventName}0', 24, false); this.animation.addByPrefix(eventName, '${eventName}0', 24, false);
@ -147,8 +147,6 @@ class ChartEditorEventSprite extends FlxSprite
else else
{ {
this.visible = true; this.visible = true;
// Only play the animation if the event type has changed.
// if (this.eventData == null || this.eventData.event != value.event)
playAnimation(value.event); playAnimation(value.event);
this.eventData = value; this.eventData = value;
// Update the position to match the note data. // Update the position to match the note data.

View file

@ -0,0 +1,71 @@
package funkin.ui.debug.charting.components;
import flixel.FlxSprite;
import flixel.addons.display.FlxTiledSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.text.FlxText;
import flixel.util.FlxColor;
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorMeasureTicks extends FlxTypedSpriteGroup<FlxSprite>
{
var chartEditorState:ChartEditorState;
var tickTiledSprite:FlxTiledSprite;
var measureNumber:FlxText;
override function set_y(value:Float):Float
{
var result = super.set_y(value);
updateMeasureNumber();
return result;
}
public function new(chartEditorState:ChartEditorState)
{
super();
this.chartEditorState = chartEditorState;
tickTiledSprite = new FlxTiledSprite(chartEditorState.measureTickBitmap, chartEditorState.measureTickBitmap.width, 1000, false, true);
add(tickTiledSprite);
measureNumber = new FlxText(0, 0, ChartEditorState.GRID_SIZE, "1");
measureNumber.setFormat(Paths.font('vcr.ttf'), 20, FlxColor.WHITE);
measureNumber.borderStyle = FlxTextBorderStyle.OUTLINE;
measureNumber.borderColor = FlxColor.BLACK;
add(measureNumber);
}
public function reloadTickBitmap():Void
{
tickTiledSprite.loadGraphic(chartEditorState.measureTickBitmap);
}
/**
* At time of writing, we only have to manipulate one measure number because we can only see one measure at a time.
*/
function updateMeasureNumber()
{
if (measureNumber == null) return;
var viewTopPosition = 0 - this.y;
var viewHeight = FlxG.height - ChartEditorState.MENU_BAR_HEIGHT - ChartEditorState.PLAYBAR_HEIGHT;
var viewBottomPosition = viewTopPosition + viewHeight;
var measureNumberInViewport = Math.floor(viewTopPosition / ChartEditorState.GRID_SIZE / Conductor.instance.stepsPerMeasure) + 1;
var measureNumberPosition = measureNumberInViewport * ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure;
measureNumber.text = '${measureNumberInViewport + 1}';
measureNumber.y = measureNumberPosition + this.y;
// trace(measureNumber.text + ' at ' + measureNumber.y);
}
public function setHeight(songLengthInPixels:Float):Void
{
tickTiledSprite.height = songLengthInPixels;
}
}

View file

@ -0,0 +1,19 @@
package funkin.ui.debug.charting.contextmenus;
import haxe.ui.containers.menus.Menu;
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorBaseContextMenu extends Menu
{
var chartEditorState:ChartEditorState;
public function new(chartEditorState:ChartEditorState, xPos:Float = 0, yPos:Float = 0)
{
super();
this.chartEditorState = chartEditorState;
this.left = xPos;
this.top = yPos;
}
}

View file

@ -0,0 +1,14 @@
package funkin.ui.debug.charting.contextmenus;
import haxe.ui.containers.menus.Menu;
import haxe.ui.core.Screen;
@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/default.xml"))
class ChartEditorDefaultContextMenu extends ChartEditorBaseContextMenu
{
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0)
{
super(chartEditorState2, xPos2, yPos2);
}
}

View file

@ -0,0 +1,32 @@
package funkin.ui.debug.charting.contextmenus;
import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.core.Screen;
import funkin.data.song.SongData.SongEventData;
import funkin.ui.debug.charting.commands.RemoveEventsCommand;
@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/event.xml"))
class ChartEditorEventContextMenu extends ChartEditorBaseContextMenu
{
var contextmenuEdit:MenuItem;
var contextmenuDelete:MenuItem;
var data:SongEventData;
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongEventData)
{
super(chartEditorState2, xPos2, yPos2);
this.data = data;
initialize();
}
function initialize()
{
contextmenuDelete.onClick = function(_) {
chartEditorState.performCommand(new RemoveEventsCommand([data]));
}
}
}

View file

@ -0,0 +1,38 @@
package funkin.ui.debug.charting.contextmenus;
import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.core.Screen;
import funkin.data.song.SongData.SongNoteData;
import funkin.ui.debug.charting.commands.FlipNotesCommand;
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/note.xml"))
class ChartEditorNoteContextMenu extends ChartEditorBaseContextMenu
{
var contextmenuFlip:MenuItem;
var contextmenuDelete:MenuItem;
var data:SongNoteData;
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData)
{
super(chartEditorState2, xPos2, yPos2);
this.data = data;
initialize();
}
function initialize():Void
{
// NOTE: Remember to use commands here to ensure undo/redo works properly
contextmenuFlip.onClick = function(_) {
chartEditorState.performCommand(new FlipNotesCommand([data]));
}
contextmenuDelete.onClick = function(_) {
chartEditorState.performCommand(new RemoveNotesCommand([data]));
}
}
}

View file

@ -0,0 +1,58 @@
package funkin.ui.debug.charting.contextmenus;
import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.core.Screen;
import funkin.ui.debug.charting.commands.CutItemsCommand;
import funkin.ui.debug.charting.commands.RemoveEventsCommand;
import funkin.ui.debug.charting.commands.RemoveItemsCommand;
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/selection.xml"))
class ChartEditorSelectionContextMenu extends ChartEditorBaseContextMenu
{
var contextmenuCut:MenuItem;
var contextmenuCopy:MenuItem;
var contextmenuPaste:MenuItem;
var contextmenuDelete:MenuItem;
var contextmenuFlip:MenuItem;
var contextmenuSelectAll:MenuItem;
var contextmenuSelectInverse:MenuItem;
var contextmenuSelectNone:MenuItem;
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0)
{
super(chartEditorState2, xPos2, yPos2);
initialize();
}
function initialize():Void
{
contextmenuCut.onClick = (_) -> {
chartEditorState.performCommand(new CutItemsCommand(chartEditorState.currentNoteSelection, chartEditorState.currentEventSelection));
};
contextmenuCopy.onClick = (_) -> {
chartEditorState.copySelection();
};
contextmenuFlip.onClick = (_) -> {
if (chartEditorState.currentNoteSelection.length > 0 && chartEditorState.currentEventSelection.length > 0)
{
chartEditorState.performCommand(new RemoveItemsCommand(chartEditorState.currentNoteSelection, chartEditorState.currentEventSelection));
}
else if (chartEditorState.currentNoteSelection.length > 0)
{
chartEditorState.performCommand(new RemoveNotesCommand(chartEditorState.currentNoteSelection));
}
else if (chartEditorState.currentEventSelection.length > 0)
{
chartEditorState.performCommand(new RemoveEventsCommand(chartEditorState.currentEventSelection));
}
else
{
// Do nothing???
}
};
}
}

View file

@ -185,8 +185,8 @@ class ChartEditorAudioHandler
state.audioVocalTrackGroup.addPlayerVoice(vocalTrack); state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
state.audioVisGroup.addPlayerVis(vocalTrack); state.audioVisGroup.addPlayerVis(vocalTrack);
state.audioVisGroup.playerVis.x = 885; state.audioVisGroup.playerVis.x = 885;
state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.getStepTimeInMs(16) * 0.00195; // The height of the visualizer, in time. state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; // The height of the visualizer, in pixels. state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
state.audioVisGroup.playerVis.detail = 1; state.audioVisGroup.playerVis.detail = 1;
state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS); state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS);
@ -196,8 +196,9 @@ class ChartEditorAudioHandler
state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
state.audioVisGroup.addOpponentVis(vocalTrack); state.audioVisGroup.addOpponentVis(vocalTrack);
state.audioVisGroup.opponentVis.x = 435; state.audioVisGroup.opponentVis.x = 435;
state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.getStepTimeInMs(16) * 0.00195; // The height of the visualizer, in time.
state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; // The height of the visualizer, in pixels. state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
state.audioVisGroup.opponentVis.detail = 1; state.audioVisGroup.opponentVis.detail = 1;
state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS); state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS);

View file

@ -0,0 +1,64 @@
package funkin.ui.debug.charting.handlers;
import funkin.ui.debug.charting.contextmenus.ChartEditorDefaultContextMenu;
import funkin.ui.debug.charting.contextmenus.ChartEditorEventContextMenu;
import funkin.ui.debug.charting.contextmenus.ChartEditorNoteContextMenu;
import funkin.ui.debug.charting.contextmenus.ChartEditorSelectionContextMenu;
import haxe.ui.containers.menus.Menu;
import haxe.ui.core.Screen;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
/**
* Handles context menus (the little menus that appear when you right click on stuff) for the new Chart Editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorContextMenuHandler
{
static var existingMenus:Array<Menu> = [];
public static function openDefaultContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
{
displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos));
}
public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
{
displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos));
}
public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
{
displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data));
}
public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData)
{
displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data));
}
static function displayMenu(state:ChartEditorState, targetMenu:Menu)
{
// Close any existing menus
closeAllMenus(state);
// Show the new menu
Screen.instance.addComponent(targetMenu);
existingMenus.push(targetMenu);
}
public static function closeMenu(state:ChartEditorState, targetMenu:Menu)
{
// targetMenu.close();
existingMenus.remove(targetMenu);
}
public static function closeAllMenus(state:ChartEditorState)
{
for (existingMenu in existingMenus)
{
closeMenu(state, existingMenu);
}
}
}

View file

@ -684,8 +684,9 @@ class ChartEditorDialogHandler
state.songMetadata.set(targetVariation, newSongMetadata); state.songMetadata.set(targetVariation, newSongMetadata);
Conductor.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata. Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata.
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
state.updateTimeSignature();
state.selectedVariation = Constants.DEFAULT_VARIATION; state.selectedVariation = Constants.DEFAULT_VARIATION;
state.selectedDifficulty = state.availableDifficulties[0]; state.selectedDifficulty = state.availableDifficulties[0];

View file

@ -43,7 +43,8 @@ class ChartEditorImportExportHandler
var variation = (metadata.variation == null || metadata.variation == '') ? Constants.DEFAULT_VARIATION : metadata.variation; var variation = (metadata.variation == null || metadata.variation == '') ? Constants.DEFAULT_VARIATION : metadata.variation;
// Clone to prevent modifying the original. // Clone to prevent modifying the original.
var metadataClone:SongMetadata = metadata.clone(variation); var metadataClone:SongMetadata = metadata.clone();
metadataClone.variation = variation;
if (metadataClone != null) songMetadata.set(variation, metadataClone); if (metadataClone != null) songMetadata.set(variation, metadataClone);
var chartData:Null<SongChartData> = SongRegistry.instance.parseEntryChartData(songId, metadata.variation); var chartData:Null<SongChartData> = SongRegistry.instance.parseEntryChartData(songId, metadata.variation);
@ -114,9 +115,10 @@ class ChartEditorImportExportHandler
state.songMetadata = newSongMetadata; state.songMetadata = newSongMetadata;
state.songChartData = newSongChartData; state.songChartData = newSongChartData;
Conductor.forceBPM(null); // Disable the forced BPM. Conductor.instance.forceBPM(null); // Disable the forced BPM.
Conductor.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata. Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata.
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
state.updateTimeSignature();
state.notePreviewDirty = true; state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true; state.notePreviewViewportBoundsDirty = true;
@ -415,17 +417,35 @@ class ChartEditorImportExportHandler
]); ]);
// We have to force write because the program will die before the save dialog is closed. // We have to force write because the program will die before the save dialog is closed.
trace('Force exporting to $targetPath...'); trace('Force exporting to $targetPath...');
try
{
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode); FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode);
// On success.
if (onSaveCb != null) onSaveCb(targetPath); if (onSaveCb != null) onSaveCb(targetPath);
} }
catch (e)
{
// On failure.
if (onCancelCb != null) onCancelCb();
}
}
else else
{ {
// Force write since we know what file the user wants to overwrite. // Force write since we know what file the user wants to overwrite.
trace('Force exporting to $targetPath...'); trace('Force exporting to $targetPath...');
try
{
// On success.
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode); FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode);
state.saveDataDirty = false; state.saveDataDirty = false;
if (onSaveCb != null) onSaveCb(targetPath); if (onSaveCb != null) onSaveCb(targetPath);
} }
catch (e)
{
// On failure.
if (onCancelCb != null) onCancelCb();
}
}
} }
else else
{ {

View file

@ -81,6 +81,7 @@ class ChartEditorThemeHandler
{ {
updateBackground(state); updateBackground(state);
updateGridBitmap(state); updateGridBitmap(state);
updateMeasureTicks(state);
updateSelectionSquare(state); updateSelectionSquare(state);
updateNotePreview(state); updateNotePreview(state);
} }
@ -125,7 +126,7 @@ class ChartEditorThemeHandler
// 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall. // 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall.
// This gets reused to fill the screen. // This gets reused to fill the screen.
var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT); var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT);
var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.stepsPerMeasure); var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure);
state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1, gridColor2); state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1, gridColor2);
// Selection borders // Selection borders
@ -142,7 +143,7 @@ class ChartEditorThemeHandler
selectionBorderColor); selectionBorderColor);
// Selection borders horizontally along the middle. // Selection borders horizontally along the middle.
for (i in 1...(Conductor.stepsPerMeasure)) for (i in 1...(Conductor.instance.stepsPerMeasure))
{ {
state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2),
state.gridBitmap.width, ChartEditorState.GRID_SELECTION_BORDER_WIDTH), state.gridBitmap.width, ChartEditorState.GRID_SELECTION_BORDER_WIDTH),
@ -197,9 +198,9 @@ class ChartEditorThemeHandler
}; };
// Selection borders horizontally in the middle. // Selection borders horizontally in the middle.
for (i in 1...(Conductor.stepsPerMeasure)) for (i in 1...(Conductor.instance.stepsPerMeasure))
{ {
if ((i % Conductor.beatsPerMeasure) == 0) if ((i % Conductor.instance.beatsPerMeasure) == 0)
{ {
state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (GRID_BEAT_DIVIDER_WIDTH / 2), state.gridBitmap.width, state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (GRID_BEAT_DIVIDER_WIDTH / 2), state.gridBitmap.width,
GRID_BEAT_DIVIDER_WIDTH), GRID_BEAT_DIVIDER_WIDTH),
@ -207,9 +208,6 @@ class ChartEditorThemeHandler
} }
} }
// Divider at top
state.gridBitmap.fillRect(new Rectangle(0, 0, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
// Draw vertical dividers between the strumlines. // Draw vertical dividers between the strumlines.
var gridStrumlineDividerColor:FlxColor = switch (state.currentTheme) var gridStrumlineDividerColor:FlxColor = switch (state.currentTheme)
@ -233,6 +231,61 @@ class ChartEditorThemeHandler
// Else, gridTiledSprite will be built later. // Else, gridTiledSprite will be built later.
} }
static function updateMeasureTicks(state:ChartEditorState):Void
{
var measureTickWidth:Int = 6;
var beatTickWidth:Int = 4;
var stepTickWidth:Int = 2;
// Draw the measure ticks.
var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE); // 1 grid squares wide.
var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure); // 1 measure tall.
state.measureTickBitmap = new BitmapData(ticksWidth, ticksHeight, true);
state.measureTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), GRID_BEAT_DIVIDER_COLOR_DARK);
// Draw the measure ticks.
state.measureTickBitmap.fillRect(new Rectangle(0, 0, state.measureTickBitmap.width, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
var bottomTickY:Float = state.measureTickBitmap.height - (measureTickWidth / 2);
state.measureTickBitmap.fillRect(new Rectangle(0, bottomTickY, state.measureTickBitmap.width, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
// Draw the beat ticks.
var beatTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2);
var beatTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2);
var beatTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2);
var beatTickLength:Float = state.measureTickBitmap.width * 2 / 3;
state.measureTickBitmap.fillRect(new Rectangle(0, beatTick2Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, beatTick3Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, beatTick4Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
// Draw the step ticks.
// TODO: Make this a loop or something.
var stepTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick6Y:Float = state.measureTickBitmap.height * 5 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick7Y:Float = state.measureTickBitmap.height * 6 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick8Y:Float = state.measureTickBitmap.height * 7 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick10Y:Float = state.measureTickBitmap.height * 9 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick11Y:Float = state.measureTickBitmap.height * 10 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick12Y:Float = state.measureTickBitmap.height * 11 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick14Y:Float = state.measureTickBitmap.height * 13 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick15Y:Float = state.measureTickBitmap.height * 14 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTick16Y:Float = state.measureTickBitmap.height * 15 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
var stepTickLength:Float = state.measureTickBitmap.width * 1 / 3;
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick2Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick3Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick4Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick6Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick7Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick8Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick10Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick11Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick12Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick14Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick15Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick16Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
}
static function updateSelectionSquare(state:ChartEditorState):Void static function updateSelectionSquare(state:ChartEditorState):Void
{ {
var selectionSquareBorderColor:FlxColor = switch (state.currentTheme) var selectionSquareBorderColor:FlxColor = switch (state.currentTheme)
@ -289,15 +342,22 @@ class ChartEditorThemeHandler
ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2), ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2)), ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2), ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2)),
viewportFillColor); viewportFillColor);
if (state.notePreviewViewport != null)
{
state.notePreviewViewport.loadGraphic(state.notePreviewViewportBitmap);
}
else
{
state.notePreviewViewport = new FlxSliceSprite(state.notePreviewViewportBitmap, state.notePreviewViewport = new FlxSliceSprite(state.notePreviewViewportBitmap,
new FlxRect(SELECTION_SQUARE_BORDER_WIDTH new FlxRect(SELECTION_SQUARE_BORDER_WIDTH
+ 1, SELECTION_SQUARE_BORDER_WIDTH + 1, SELECTION_SQUARE_BORDER_WIDTH
+ 1, ChartEditorState.GRID_SIZE + 1,
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 2),
ChartEditorState.GRID_SIZE ChartEditorState.GRID_SIZE
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 2), ChartEditorState.GRID_SIZE
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 2)), - (2 * SELECTION_SQUARE_BORDER_WIDTH + 2)),
32, 32); 32, 32);
} }
}
public static function buildPlayheadBlock():FlxSprite public static function buildPlayheadBlock():FlxSprite
{ {

View file

@ -9,7 +9,7 @@ import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode; import haxe.ui.containers.TreeViewNode;
import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.data.event.SongEventData; import funkin.data.event.SongEventSchema;
import funkin.data.song.SongData.SongTimeChange; import funkin.data.song.SongData.SongTimeChange;
import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData; import funkin.play.character.CharacterData;
@ -23,6 +23,7 @@ import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.util.FileUtil; import funkin.util.FileUtil;
import haxe.ui.components.Button; import haxe.ui.components.Button;
import haxe.ui.data.ArrayDataSource;
import haxe.ui.components.CheckBox; import haxe.ui.components.CheckBox;
import haxe.ui.components.DropDown; import haxe.ui.components.DropDown;
import haxe.ui.components.HorizontalSlider; import haxe.ui.components.HorizontalSlider;
@ -36,12 +37,12 @@ import haxe.ui.containers.dialogs.Dialog.DialogButton;
import haxe.ui.containers.dialogs.Dialog.DialogEvent; import haxe.ui.containers.dialogs.Dialog.DialogEvent;
import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox;
import haxe.ui.containers.Frame; import haxe.ui.containers.Frame;
import haxe.ui.containers.Grid; import haxe.ui.containers.Grid;
import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode; import haxe.ui.containers.TreeViewNode;
import haxe.ui.core.Component; import haxe.ui.core.Component;
import haxe.ui.data.ArrayDataSource;
import haxe.ui.events.UIEvent; import haxe.ui.events.UIEvent;
/** /**
@ -79,8 +80,9 @@ class ChartEditorToolboxHandler
{ {
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
onShowToolboxNoteData(state, toolbox); onShowToolboxNoteData(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
onShowToolboxEventData(state, toolbox); // TODO: Fix this.
cast(toolbox, ChartEditorBaseToolbox).refresh();
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
onShowToolboxPlaytestProperties(state, toolbox); onShowToolboxPlaytestProperties(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
@ -119,7 +121,7 @@ class ChartEditorToolboxHandler
{ {
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
onHideToolboxNoteData(state, toolbox); onHideToolboxNoteData(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
onHideToolboxEventData(state, toolbox); onHideToolboxEventData(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
onHideToolboxPlaytestProperties(state, toolbox); onHideToolboxPlaytestProperties(state, toolbox);
@ -195,7 +197,7 @@ class ChartEditorToolboxHandler
{ {
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
toolbox = buildToolboxNoteDataLayout(state); toolbox = buildToolboxNoteDataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
toolbox = buildToolboxEventDataLayout(state); toolbox = buildToolboxEventDataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
toolbox = buildToolboxPlaytestPropertiesLayout(state); toolbox = buildToolboxPlaytestPropertiesLayout(state);
@ -283,19 +285,19 @@ class ChartEditorToolboxHandler
toolboxNotesCustomKindLabel.hidden = false; toolboxNotesCustomKindLabel.hidden = false;
toolboxNotesCustomKind.hidden = false; toolboxNotesCustomKind.hidden = false;
state.selectedNoteKind = toolboxNotesCustomKind.text; state.noteKindToPlace = toolboxNotesCustomKind.text;
} }
else else
{ {
toolboxNotesCustomKindLabel.hidden = true; toolboxNotesCustomKindLabel.hidden = true;
toolboxNotesCustomKind.hidden = true; toolboxNotesCustomKind.hidden = true;
state.selectedNoteKind = event.data.id; state.noteKindToPlace = event.data.id;
} }
} }
toolboxNotesCustomKind.onChange = function(event:UIEvent) { toolboxNotesCustomKind.onChange = function(event:UIEvent) {
state.selectedNoteKind = toolboxNotesCustomKind.text; state.noteKindToPlace = toolboxNotesCustomKind.text;
} }
return toolbox; return toolbox;
@ -305,159 +307,16 @@ class ChartEditorToolboxHandler
static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function buildToolboxEventDataLayout(state:ChartEditorState):Null<CollapsibleDialog> static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
{
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
if (toolbox == null) return null;
// Starting position.
toolbox.x = 100;
toolbox.y = 150;
toolbox.onDialogClosed = function(event:DialogEvent) {
state.menubarItemToggleToolboxEvents.selected = false;
}
var toolboxEventsEventKind:Null<DropDown> = toolbox.findComponent('toolboxEventsEventKind', DropDown);
if (toolboxEventsEventKind == null) throw 'ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Could not find toolboxEventsEventKind component.';
var toolboxEventsDataGrid:Null<Grid> = toolbox.findComponent('toolboxEventsDataGrid', Grid);
if (toolboxEventsDataGrid == null) throw 'ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Could not find toolboxEventsDataGrid component.';
toolboxEventsEventKind.dataSource = new ArrayDataSource();
var songEvents:Array<SongEvent> = SongEventParser.listEvents();
for (event in songEvents)
{
toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id});
}
toolboxEventsEventKind.onChange = function(event:UIEvent) {
var eventType:String = event.data.value;
trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType');
state.selectedEventKind = eventType;
var schema:SongEventSchema = SongEventParser.getEventSchema(eventType);
if (schema == null)
{
trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Unknown event kind: $eventType');
return;
}
buildEventDataFormFromSchema(state, toolboxEventsDataGrid, schema);
}
toolboxEventsEventKind.value = state.selectedEventKind;
return toolbox;
}
static function onShowToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function buildEventDataFormFromSchema(state:ChartEditorState, target:Box, schema:SongEventSchema):Void
{
trace(schema);
// Clear the frame.
target.removeAllComponents();
state.selectedEventData = {};
for (field in schema)
{
if (field == null) continue;
// Add a label.
var label:Label = new Label();
label.text = field.title;
label.verticalAlign = "center";
target.addComponent(label);
var input:Component;
switch (field.type)
{
case INTEGER:
var numberStepper:NumberStepper = new NumberStepper();
numberStepper.id = field.name;
numberStepper.step = field.step ?? 1.0;
numberStepper.min = field.min ?? 0.0;
numberStepper.max = field.max ?? 10.0;
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
input = numberStepper;
case FLOAT:
var numberStepper:NumberStepper = new NumberStepper();
numberStepper.id = field.name;
numberStepper.step = field.step ?? 0.1;
if (field.min != null) numberStepper.min = field.min;
if (field.max != null) numberStepper.max = field.max;
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
input = numberStepper;
case BOOL:
var checkBox:CheckBox = new CheckBox();
checkBox.id = field.name;
if (field.defaultValue != null) checkBox.selected = field.defaultValue;
input = checkBox;
case ENUM:
var dropDown:DropDown = new DropDown();
dropDown.id = field.name;
dropDown.width = 200.0;
dropDown.dataSource = new ArrayDataSource();
if (field.keys == null) throw 'Field "${field.name}" is of Enum type but has no keys.';
// Add entries to the dropdown.
for (optionName in field.keys.keys())
{
var optionValue:Null<Dynamic> = field.keys.get(optionName);
trace('$optionName : $optionValue');
dropDown.dataSource.add({value: optionValue, text: optionName});
}
dropDown.value = field.defaultValue;
input = dropDown;
case STRING:
input = new TextField();
input.id = field.name;
if (field.defaultValue != null) input.text = field.defaultValue;
default:
// Unknown type. Display a label so we know what it is.
input = new Label();
input.id = field.name;
input.text = field.type;
}
target.addComponent(input);
input.onChange = function(event:UIEvent) {
var value = event.target.value;
if (field.type == ENUM)
{
value = event.target.value.value;
}
trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${value}');
if (value == null)
{
state.selectedEventData.remove(event.target.id);
}
else
{
state.selectedEventData.set(event.target.id, value);
}
}
}
}
static function buildToolboxPlaytestPropertiesLayout(state:ChartEditorState):Null<CollapsibleDialog> static function buildToolboxPlaytestPropertiesLayout(state:ChartEditorState):Null<CollapsibleDialog>
{ {
// fill with playtest properties // fill with playtest properties
@ -586,8 +445,6 @@ class ChartEditorToolboxHandler
trace('selected node: ${treeView.selectedNode}'); trace('selected node: ${treeView.selectedNode}');
} }
static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function buildToolboxMetadataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox> static function buildToolboxMetadataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
{ {
var toolbox:ChartEditorBaseToolbox = ChartEditorMetadataToolbox.build(state); var toolbox:ChartEditorBaseToolbox = ChartEditorMetadataToolbox.build(state);
@ -597,7 +454,14 @@ class ChartEditorToolboxHandler
return toolbox; return toolbox;
} }
static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} static function buildToolboxEventDataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
{
var toolbox:ChartEditorBaseToolbox = ChartEditorEventDataToolbox.build(state);
if (toolbox == null) return null;
return toolbox;
}
static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog> static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog>
{ {

View file

@ -3,6 +3,7 @@ package funkin.ui.debug.charting;
#if !macro #if !macro
// Apply handlers so they can be called as though they were functions in ChartEditorState // Apply handlers so they can be called as though they were functions in ChartEditorState
using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler; using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler;
using funkin.ui.debug.charting.handlers.ChartEditorContextMenuHandler;
using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler; using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler;
using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler; using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler;
using funkin.ui.debug.charting.handlers.ChartEditorNotificationHandler; using funkin.ui.debug.charting.handlers.ChartEditorNotificationHandler;

View file

@ -0,0 +1,259 @@
package funkin.ui.debug.charting.toolboxes;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData;
import funkin.play.stage.StageData;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import haxe.ui.components.Button;
import haxe.ui.components.CheckBox;
import haxe.ui.components.DropDown;
import haxe.ui.components.HorizontalSlider;
import haxe.ui.components.Label;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
import haxe.ui.core.Component;
import funkin.data.event.SongEventRegistry;
import haxe.ui.components.TextField;
import haxe.ui.containers.Box;
import haxe.ui.containers.Frame;
import haxe.ui.events.UIEvent;
import haxe.ui.data.ArrayDataSource;
import haxe.ui.containers.Grid;
import haxe.ui.components.DropDown;
import haxe.ui.containers.Frame;
/**
* The toolbox which allows modifying information like Song Title, Scroll Speed, Characters/Stages, and starting BPM.
*/
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/event-data.xml"))
class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
{
var toolboxEventsEventKind:DropDown;
var toolboxEventsDataFrame:Frame;
var toolboxEventsDataGrid:Grid;
var _initializing:Bool = true;
public function new(chartEditorState2:ChartEditorState)
{
super(chartEditorState2);
initialize();
this.onDialogClosed = onClose;
this._initializing = false;
}
function onClose(event:UIEvent)
{
chartEditorState.menubarItemToggleToolboxEventData.selected = false;
}
function initialize():Void
{
toolboxEventsEventKind.dataSource = new ArrayDataSource();
var songEvents:Array<SongEvent> = SongEventRegistry.listEvents();
for (event in songEvents)
{
toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id});
}
toolboxEventsEventKind.onChange = function(event:UIEvent) {
var eventType:String = event.data.value;
trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType');
// Edit the event data to place.
chartEditorState.eventKindToPlace = eventType;
var schema:SongEventSchema = SongEventRegistry.getEventSchema(eventType);
if (schema == null)
{
trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Unknown event kind: $eventType');
return;
}
buildEventDataFormFromSchema(toolboxEventsDataGrid, schema);
if (!_initializing && chartEditorState.currentEventSelection.length > 0)
{
// Edit the event data of any selected events.
for (event in chartEditorState.currentEventSelection)
{
event.event = chartEditorState.eventKindToPlace;
event.value = chartEditorState.eventDataToPlace;
}
chartEditorState.saveDataDirty = true;
chartEditorState.noteDisplayDirty = true;
chartEditorState.notePreviewDirty = true;
}
}
toolboxEventsEventKind.value = chartEditorState.eventKindToPlace;
}
public override function refresh():Void
{
super.refresh();
toolboxEventsEventKind.value = chartEditorState.eventKindToPlace;
for (pair in chartEditorState.eventDataToPlace.keyValueIterator())
{
var fieldId:String = pair.key;
var value:Null<Dynamic> = pair.value;
var field:Component = toolboxEventsDataGrid.findComponent(fieldId);
if (field == null)
{
throw 'ChartEditorToolboxHandler.refresh() - Field "${fieldId}" does not exist in the event data form.';
}
else
{
switch (field)
{
case Std.isOfType(_, NumberStepper) => true:
var numberStepper:NumberStepper = cast field;
numberStepper.value = value;
case Std.isOfType(_, CheckBox) => true:
var checkBox:CheckBox = cast field;
checkBox.selected = value;
case Std.isOfType(_, DropDown) => true:
var dropDown:DropDown = cast field;
dropDown.value = value;
case Std.isOfType(_, TextField) => true:
var textField:TextField = cast field;
textField.text = value;
default:
throw 'ChartEditorToolboxHandler.refresh() - Field "${fieldId}" is of unknown type "${Type.getClassName(Type.getClass(field))}".';
}
}
}
}
function buildEventDataFormFromSchema(target:Box, schema:SongEventSchema):Void
{
trace(schema);
// Clear the frame.
target.removeAllComponents();
chartEditorState.eventDataToPlace = {};
for (field in schema)
{
if (field == null) continue;
// Add a label for the data field.
var label:Label = new Label();
label.text = field.title;
label.verticalAlign = "center";
target.addComponent(label);
// Add an input field for the data field.
var input:Component;
switch (field.type)
{
case INTEGER:
var numberStepper:NumberStepper = new NumberStepper();
numberStepper.id = field.name;
numberStepper.step = field.step ?? 1.0;
numberStepper.min = field.min ?? 0.0;
numberStepper.max = field.max ?? 10.0;
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
input = numberStepper;
case FLOAT:
var numberStepper:NumberStepper = new NumberStepper();
numberStepper.id = field.name;
numberStepper.step = field.step ?? 0.1;
if (field.min != null) numberStepper.min = field.min;
if (field.max != null) numberStepper.max = field.max;
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
input = numberStepper;
case BOOL:
var checkBox:CheckBox = new CheckBox();
checkBox.id = field.name;
if (field.defaultValue != null) checkBox.selected = field.defaultValue;
input = checkBox;
case ENUM:
var dropDown:DropDown = new DropDown();
dropDown.id = field.name;
dropDown.width = 200.0;
dropDown.dataSource = new ArrayDataSource();
if (field.keys == null) throw 'Field "${field.name}" is of Enum type but has no keys.';
// Add entries to the dropdown.
for (optionName in field.keys.keys())
{
var optionValue:Null<Dynamic> = field.keys.get(optionName);
trace('$optionName : $optionValue');
dropDown.dataSource.add({value: optionValue, text: optionName});
}
dropDown.value = field.defaultValue;
input = dropDown;
case STRING:
input = new TextField();
input.id = field.name;
if (field.defaultValue != null) input.text = field.defaultValue;
default:
// Unknown type. Display a label that proclaims the type so we can debug it.
input = new Label();
input.id = field.name;
input.text = field.type;
}
target.addComponent(input);
// Update the value of the event data.
input.onChange = function(event:UIEvent) {
var value = event.target.value;
if (field.type == ENUM)
{
value = event.target.value.value;
}
trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${value}');
// Edit the event data to place.
if (value == null)
{
chartEditorState.eventDataToPlace.remove(event.target.id);
}
else
{
chartEditorState.eventDataToPlace.set(event.target.id, value);
}
// Edit the event data of any existing events.
if (!_initializing && chartEditorState.currentEventSelection.length > 0)
{
for (event in chartEditorState.currentEventSelection)
{
event.event = chartEditorState.eventKindToPlace;
event.value = chartEditorState.eventDataToPlace;
}
chartEditorState.saveDataDirty = true;
chartEditorState.noteDisplayDirty = true;
chartEditorState.notePreviewDirty = true;
}
}
}
}
public static function build(chartEditorState:ChartEditorState):ChartEditorEventDataToolbox
{
return new ChartEditorEventDataToolbox(chartEditorState);
}
}

View file

@ -116,13 +116,33 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
} }
}; };
inputTimeSignature.onChange = function(event:UIEvent) {
var timeSignatureStr:String = event.data.text;
var timeSignature = timeSignatureStr.split('/');
if (timeSignature.length != 2) return;
var timeSignatureNum:Int = Std.parseInt(timeSignature[0]);
var timeSignatureDen:Int = Std.parseInt(timeSignature[1]);
var previousTimeSignatureNum:Int = chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum;
var previousTimeSignatureDen:Int = chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen;
if (timeSignatureNum == previousTimeSignatureNum && timeSignatureDen == previousTimeSignatureDen) return;
chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum = timeSignatureNum;
chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen = timeSignatureDen;
trace('Time signature changed to ${timeSignatureNum}/${timeSignatureDen}');
chartEditorState.updateTimeSignature();
};
inputOffsetInst.onChange = function(event:UIEvent) { inputOffsetInst.onChange = function(event:UIEvent) {
if (event.value == null) return; if (event.value == null) return;
chartEditorState.currentInstrumentalOffset = event.value; chartEditorState.currentInstrumentalOffset = event.value;
Conductor.instrumentalOffset = event.value; Conductor.instance.instrumentalOffset = event.value;
// Update song length. // Update song length.
chartEditorState.songLengthInMs = (chartEditorState.audioInstTrack?.length ?? 1000.0) + Conductor.instrumentalOffset; chartEditorState.songLengthInMs = (chartEditorState.audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset;
}; };
inputOffsetVocal.onChange = function(event:UIEvent) { inputOffsetVocal.onChange = function(event:UIEvent) {
@ -162,6 +182,8 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
public override function refresh():Void public override function refresh():Void
{ {
super.refresh();
inputSongName.value = chartEditorState.currentSongMetadata.songName; inputSongName.value = chartEditorState.currentSongMetadata.songName;
inputSongArtist.value = chartEditorState.currentSongMetadata.artist; inputSongArtist.value = chartEditorState.currentSongMetadata.artist;
inputStage.value = chartEditorState.currentSongMetadata.playData.stage; inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
@ -172,6 +194,10 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}'; frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';
frameDifficulty.text = 'Difficulty: ${chartEditorState.selectedDifficulty.toTitleCase()}'; frameDifficulty.text = 'Difficulty: ${chartEditorState.selectedDifficulty.toTitleCase()}';
var currentTimeSignature = '${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum}/${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen}';
trace('Setting time signature to ${currentTimeSignature}');
inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature};
var stageId:String = chartEditorState.currentSongMetadata.playData.stage; var stageId:String = chartEditorState.currentSongMetadata.playData.stage;
var stageData:Null<StageData> = StageDataParser.parseStageData(stageId); var stageData:Null<StageData> = StageDataParser.parseStageData(stageId);
if (inputStage != null) if (inputStage != null)

View file

@ -75,7 +75,7 @@ class LatencyState extends MusicBeatSubState
// funnyStatsGraph.hi // funnyStatsGraph.hi
Conductor.forceBPM(60); Conductor.instance.forceBPM(60);
noteGrp = new FlxTypedGroup<NoteSprite>(); noteGrp = new FlxTypedGroup<NoteSprite>();
add(noteGrp); add(noteGrp);
@ -91,14 +91,14 @@ class LatencyState extends MusicBeatSubState
// // musSpec.visType = FREQUENCIES; // // musSpec.visType = FREQUENCIES;
// add(musSpec); // add(musSpec);
for (beat in 0...Math.floor(FlxG.sound.music.length / Conductor.beatLengthMs)) for (beat in 0...Math.floor(FlxG.sound.music.length / Conductor.instance.beatLengthMs))
{ {
var beatTick:FlxSprite = new FlxSprite(songPosToX(beat * Conductor.beatLengthMs), FlxG.height - 15); var beatTick:FlxSprite = new FlxSprite(songPosToX(beat * Conductor.instance.beatLengthMs), FlxG.height - 15);
beatTick.makeGraphic(2, 15); beatTick.makeGraphic(2, 15);
beatTick.alpha = 0.3; beatTick.alpha = 0.3;
add(beatTick); add(beatTick);
var offsetTxt:FlxText = new FlxText(songPosToX(beat * Conductor.beatLengthMs), FlxG.height - 26, 0, "swag"); var offsetTxt:FlxText = new FlxText(songPosToX(beat * Conductor.instance.beatLengthMs), FlxG.height - 26, 0, "swag");
offsetTxt.alpha = 0.5; offsetTxt.alpha = 0.5;
diffGrp.add(offsetTxt); diffGrp.add(offsetTxt);
@ -130,7 +130,7 @@ class LatencyState extends MusicBeatSubState
for (i in 0...32) for (i in 0...32)
{ {
var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), Conductor.beatLengthMs * i); var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), Conductor.instance.beatLengthMs * i);
noteGrp.add(note); noteGrp.add(note);
} }
@ -146,9 +146,9 @@ class LatencyState extends MusicBeatSubState
override function stepHit():Bool override function stepHit():Bool
{ {
if (Conductor.currentStep % 4 == 2) if (Conductor.instance.currentStep % 4 == 2)
{ {
blocks.members[((Conductor.currentBeat % 8) + 1) % 8].alpha = 0.5; blocks.members[((Conductor.instance.currentBeat % 8) + 1) % 8].alpha = 0.5;
} }
return super.stepHit(); return super.stepHit();
@ -156,11 +156,11 @@ class LatencyState extends MusicBeatSubState
override function beatHit():Bool override function beatHit():Bool
{ {
if (Conductor.currentBeat % 8 == 0) blocks.forEach(blok -> { if (Conductor.instance.currentBeat % 8 == 0) blocks.forEach(blok -> {
blok.alpha = 0; blok.alpha = 0;
}); });
blocks.members[Conductor.currentBeat % 8].alpha = 1; blocks.members[Conductor.instance.currentBeat % 8].alpha = 1;
// block.visible = !block.visible; // block.visible = !block.visible;
return super.beatHit(); return super.beatHit();
@ -192,17 +192,17 @@ class LatencyState extends MusicBeatSubState
if (FlxG.keys.pressed.D) FlxG.sound.music.time += 1000 * FlxG.elapsed; if (FlxG.keys.pressed.D) FlxG.sound.music.time += 1000 * FlxG.elapsed;
Conductor.update(swagSong.getTimeWithDiff() - Conductor.inputOffset); Conductor.instance.update(swagSong.getTimeWithDiff() - Conductor.instance.inputOffset);
// Conductor.songPosition += (Timer.stamp() * 1000) - FlxG.sound.music.prevTimestamp; // Conductor.instance.songPosition += (Timer.stamp() * 1000) - FlxG.sound.music.prevTimestamp;
songPosVis.x = songPosToX(Conductor.songPosition); songPosVis.x = songPosToX(Conductor.instance.songPosition);
songVisFollowAudio.x = songPosToX(Conductor.songPosition - Conductor.instrumentalOffset); songVisFollowAudio.x = songPosToX(Conductor.instance.songPosition - Conductor.instance.instrumentalOffset);
songVisFollowVideo.x = songPosToX(Conductor.songPosition - Conductor.inputOffset); songVisFollowVideo.x = songPosToX(Conductor.instance.songPosition - Conductor.instance.inputOffset);
offsetText.text = "INST Offset: " + Conductor.instrumentalOffset + "ms"; offsetText.text = "INST Offset: " + Conductor.instance.instrumentalOffset + "ms";
offsetText.text += "\nINPUT Offset: " + Conductor.inputOffset + "ms"; offsetText.text += "\nINPUT Offset: " + Conductor.instance.inputOffset + "ms";
offsetText.text += "\ncurrentStep: " + Conductor.currentStep; offsetText.text += "\ncurrentStep: " + Conductor.instance.currentStep;
offsetText.text += "\ncurrentBeat: " + Conductor.currentBeat; offsetText.text += "\ncurrentBeat: " + Conductor.instance.currentBeat;
var avgOffsetInput:Float = 0; var avgOffsetInput:Float = 0;
@ -221,24 +221,24 @@ class LatencyState extends MusicBeatSubState
{ {
if (FlxG.keys.justPressed.RIGHT) if (FlxG.keys.justPressed.RIGHT)
{ {
Conductor.instrumentalOffset += 1.0 * multiply; Conductor.instance.instrumentalOffset += 1.0 * multiply;
} }
if (FlxG.keys.justPressed.LEFT) if (FlxG.keys.justPressed.LEFT)
{ {
Conductor.instrumentalOffset -= 1.0 * multiply; Conductor.instance.instrumentalOffset -= 1.0 * multiply;
} }
} }
else else
{ {
if (FlxG.keys.justPressed.RIGHT) if (FlxG.keys.justPressed.RIGHT)
{ {
Conductor.inputOffset += 1.0 * multiply; Conductor.instance.inputOffset += 1.0 * multiply;
} }
if (FlxG.keys.justPressed.LEFT) if (FlxG.keys.justPressed.LEFT)
{ {
Conductor.inputOffset -= 1.0 * multiply; Conductor.instance.inputOffset -= 1.0 * multiply;
} }
} }
@ -250,7 +250,7 @@ class LatencyState extends MusicBeatSubState
}*/ }*/
noteGrp.forEach(function(daNote:NoteSprite) { noteGrp.forEach(function(daNote:NoteSprite) {
daNote.y = (strumLine.y - ((Conductor.songPosition - Conductor.instrumentalOffset) - daNote.noteData.time) * 0.45); daNote.y = (strumLine.y - ((Conductor.instance.songPosition - Conductor.instance.instrumentalOffset) - daNote.noteData.time) * 0.45);
daNote.x = strumLine.x + 30; daNote.x = strumLine.x + 30;
if (daNote.y < strumLine.y) daNote.alpha = 0.5; if (daNote.y < strumLine.y) daNote.alpha = 0.5;
@ -258,7 +258,7 @@ class LatencyState extends MusicBeatSubState
if (daNote.y < 0 - daNote.height) if (daNote.y < 0 - daNote.height)
{ {
daNote.alpha = 1; daNote.alpha = 1;
// daNote.data.strumTime += Conductor.beatLengthMs * 8; // daNote.data.strumTime += Conductor.instance.beatLengthMs * 8;
} }
}); });
@ -267,14 +267,14 @@ class LatencyState extends MusicBeatSubState
function generateBeatStuff() function generateBeatStuff()
{ {
Conductor.update(swagSong.getTimeWithDiff()); Conductor.instance.update(swagSong.getTimeWithDiff());
var closestBeat:Int = Math.round(Conductor.songPosition / Conductor.beatLengthMs) % diffGrp.members.length; var closestBeat:Int = Math.round(Conductor.instance.songPosition / Conductor.instance.beatLengthMs) % diffGrp.members.length;
var getDiff:Float = Conductor.songPosition - (closestBeat * Conductor.beatLengthMs); var getDiff:Float = Conductor.instance.songPosition - (closestBeat * Conductor.instance.beatLengthMs);
getDiff -= Conductor.inputOffset; getDiff -= Conductor.instance.inputOffset;
// lil fix for end of song // lil fix for end of song
if (closestBeat == 0 && getDiff >= Conductor.beatLengthMs * 2) getDiff -= FlxG.sound.music.length; if (closestBeat == 0 && getDiff >= Conductor.instance.beatLengthMs * 2) getDiff -= FlxG.sound.music.length;
trace("\tDISTANCE TO CLOSEST BEAT: " + getDiff + "ms"); trace("\tDISTANCE TO CLOSEST BEAT: " + getDiff + "ms");
trace("\tCLOSEST BEAT: " + closestBeat); trace("\tCLOSEST BEAT: " + closestBeat);

View file

@ -238,7 +238,7 @@ class StoryMenuState extends MusicBeatState
var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu'); var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu');
if (freakyMenuMetadata != null) if (freakyMenuMetadata != null)
{ {
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges); Conductor.instance.mapTimeChanges(freakyMenuMetadata.timeChanges);
} }
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0); FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
@ -317,7 +317,7 @@ class StoryMenuState extends MusicBeatState
override function update(elapsed:Float) override function update(elapsed:Float)
{ {
Conductor.update(); Conductor.instance.update();
highScoreLerp = Std.int(MathUtil.coolLerp(highScoreLerp, highScore, 0.5)); highScoreLerp = Std.int(MathUtil.coolLerp(highScoreLerp, highScore, 0.5));

View file

@ -221,7 +221,7 @@ class TitleState extends MusicBeatState
var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu'); var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu');
if (freakyMenuMetadata != null) if (freakyMenuMetadata != null)
{ {
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges); Conductor.instance.mapTimeChanges(freakyMenuMetadata.timeChanges);
} }
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0); FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7); FlxG.sound.music.fadeIn(4, 0, 0.7);
@ -256,7 +256,7 @@ class TitleState extends MusicBeatState
if (FlxG.keys.pressed.DOWN) FlxG.sound.music.pitch -= 0.5 * elapsed; if (FlxG.keys.pressed.DOWN) FlxG.sound.music.pitch -= 0.5 * elapsed;
#end #end
Conductor.update(); Conductor.instance.update();
/* if (FlxG.onMobile) /* if (FlxG.onMobile)
{ {
@ -280,7 +280,7 @@ class TitleState extends MusicBeatState
FlxTween.tween(FlxG.stage.window, {y: FlxG.stage.window.y + 100}, 0.7, {ease: FlxEase.quadInOut, type: PINGPONG}); FlxTween.tween(FlxG.stage.window, {y: FlxG.stage.window.y + 100}, 0.7, {ease: FlxEase.quadInOut, type: PINGPONG});
} }
if (FlxG.sound.music != null) Conductor.update(FlxG.sound.music.time); if (FlxG.sound.music != null) Conductor.instance.update(FlxG.sound.music.time);
if (FlxG.keys.justPressed.F) FlxG.fullscreen = !FlxG.fullscreen; if (FlxG.keys.justPressed.F) FlxG.fullscreen = !FlxG.fullscreen;
// do controls.PAUSE | controls.ACCEPT instead? // do controls.PAUSE | controls.ACCEPT instead?
@ -390,7 +390,7 @@ class TitleState extends MusicBeatState
var spec:SpectogramSprite = new SpectogramSprite(FlxG.sound.music); var spec:SpectogramSprite = new SpectogramSprite(FlxG.sound.music);
add(spec); add(spec);
Conductor.forceBPM(190); Conductor.instance.forceBPM(190);
FlxG.camera.flash(FlxColor.WHITE, 1); FlxG.camera.flash(FlxColor.WHITE, 1);
FlxG.sound.play(Paths.sound('confirmMenu'), 0.7); FlxG.sound.play(Paths.sound('confirmMenu'), 0.7);
} }
@ -442,13 +442,13 @@ class TitleState extends MusicBeatState
if (!skippedIntro) if (!skippedIntro)
{ {
// FlxG.log.add(Conductor.currentBeat); // FlxG.log.add(Conductor.instance.currentBeat);
// if the user is draggin the window some beats will // if the user is draggin the window some beats will
// be missed so this is just to compensate // be missed so this is just to compensate
if (Conductor.currentBeat > lastBeat) if (Conductor.instance.currentBeat > lastBeat)
{ {
// TODO: Why does it perform ALL the previous steps each beat? // TODO: Why does it perform ALL the previous steps each beat?
for (i in lastBeat...Conductor.currentBeat) for (i in lastBeat...Conductor.instance.currentBeat)
{ {
switch (i + 1) switch (i + 1)
{ {
@ -483,11 +483,11 @@ class TitleState extends MusicBeatState
} }
} }
} }
lastBeat = Conductor.currentBeat; lastBeat = Conductor.instance.currentBeat;
} }
if (skippedIntro) if (skippedIntro)
{ {
if (cheatActive && Conductor.currentBeat % 2 == 0) swagShader.update(0.125); if (cheatActive && Conductor.instance.currentBeat % 2 == 0) swagShader.update(0.125);
if (logoBl != null && logoBl.animation != null) logoBl.animation.play('bump', true); if (logoBl != null && logoBl.animation != null) logoBl.animation.play('bump', true);

View file

@ -70,7 +70,7 @@ class Constants
public static final URL_KICKSTARTER:String = 'https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/'; public static final URL_KICKSTARTER:String = 'https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/';
/** /**
* GIT REPO DATA * REPOSITORY DATA
*/ */
// ============================== // ==============================
@ -86,6 +86,11 @@ class Constants
public static final GIT_HASH:String = funkin.util.macro.GitCommit.getGitCommitHash(); public static final GIT_HASH:String = funkin.util.macro.GitCommit.getGitCommitHash();
#end #end
/**
* The current library versions, as provided by hmm.
*/
public static final LIBRARY_VERSIONS:Array<String> = funkin.util.macro.HaxelibVersions.getLibraryVersions();
/** /**
* COLORS * COLORS
*/ */

View file

@ -123,6 +123,17 @@ class CrashHandler
fullContents += '=====================\n'; fullContents += '=====================\n';
fullContents += 'Haxelibs: \n';
for (lib in Constants.LIBRARY_VERSIONS)
{
fullContents += '- ${lib}\n';
}
fullContents += '\n';
fullContents += '=====================\n';
fullContents += '\n'; fullContents += '\n';
fullContents += message; fullContents += message;

View file

@ -0,0 +1,67 @@
package funkin.util.macro;
import haxe.io.Path;
class HaxelibVersions
{
public static macro function getLibraryVersions():haxe.macro.Expr.ExprOf<Array<String>>
{
#if !display
return macro $v{formatHmmData(readHmmData())};
#else
// `#if display` is used for code completion. In this case returning an
// empty string is good enough; We don't want to call functions on every hint.
var commitHash:String = "";
return macro $v{commitHashSplice};
#end
}
#if (debug && macro)
static function readHmmData():hmm.HmmConfig
{
return hmm.HmmConfig.HmmConfigs.readHmmJsonOrThrow();
}
static function formatHmmData(hmmData:hmm.HmmConfig):Array<String>
{
var result:Array<String> = [];
for (library in hmmData.dependencies)
{
switch (library)
{
case Haxelib(name, version):
result.push('${name} haxelib(${o(version)})');
case Git(name, url, ref, dir):
result.push('${name} git(${url}/${o(dir, '')}:${o(ref)})');
case Mercurial(name, url, ref, dir):
result.push('${name} mercurial(${url}/${o(dir, '')}:${o(ref)})');
case Dev(name, path):
result.push('${name} dev(${path})');
}
}
return result;
}
static function o(option:haxe.ds.Option<String>, defaultValue:String = 'None'):String
{
switch (option)
{
case Some(value):
return value;
case None:
return defaultValue;
}
}
static function readLibraryCurrentVersion(libraryName:String):String
{
var path = Path.join([Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.current']);
// This is compile time so we should always have Sys available.
var result = sys.io.File.getContent(path);
return result;
}
#end
}

View file

@ -0,0 +1,35 @@
package funkin.util.plugins;
import flixel.FlxBasic;
/**
* A plugin which adds functionality to press `F4` to immediately transition to the main menu.
* This is useful for debugging or if you get softlocked or something.
*/
class EvacuateDebugPlugin extends FlxBasic
{
public function new()
{
super();
}
public static function initialize():Void
{
FlxG.plugins.addPlugin(new EvacuateDebugPlugin());
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (FlxG.keys.justPressed.F4)
{
FlxG.switchState(new funkin.ui.mainmenu.MainMenuState());
}
}
public override function destroy():Void
{
super.destroy();
}
}

View file

@ -0,0 +1,5 @@
# funkin.util.plugins
Flixel plugins are objects with `update()` functions that are called from every state.
See: https://github.com/HaxeFlixel/flixel/blob/dev/flixel/system/frontEnds/PluginFrontEnd.hx

View file

@ -0,0 +1,38 @@
package funkin.util.plugins;
import flixel.FlxBasic;
/**
* A plugin which adds functionality to press `F5` to reload all game assets, then reload the current state.
* This is useful for hot reloading assets during development.
*/
class ReloadAssetsDebugPlugin extends FlxBasic
{
public function new()
{
super();
}
public static function initialize():Void
{
FlxG.plugins.addPlugin(new ReloadAssetsDebugPlugin());
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (FlxG.keys.justPressed.F5)
{
funkin.modding.PolymodHandler.forceReloadAssets();
// Create a new instance of the current state, so old data is cleared.
FlxG.resetState();
}
}
public override function destroy():Void
{
super.destroy();
}
}

View file

@ -0,0 +1,38 @@
package funkin.util.plugins;
import flixel.FlxBasic;
/**
* A plugin which adds functionality to display several universally important values
* in the Flixel variable watch window.
*/
class WatchPlugin extends FlxBasic
{
public function new()
{
super();
}
public static function initialize():Void
{
FlxG.plugins.addPlugin(new WatchPlugin());
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
FlxG.watch.addQuick("songPosition", Conductor.instance.songPosition);
FlxG.watch.addQuick("songPositionNoOffset", Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
FlxG.watch.addQuick("musicTime", FlxG.sound?.music?.time ?? 0.0);
FlxG.watch.addQuick("bpm", Conductor.instance.bpm);
FlxG.watch.addQuick("currentMeasureTime", Conductor.instance.currentMeasureTime);
FlxG.watch.addQuick("currentBeatTime", Conductor.instance.currentBeatTime);
FlxG.watch.addQuick("currentStepTime", Conductor.instance.currentStepTime);
}
public override function destroy():Void
{
super.destroy();
}
}

View file

@ -77,6 +77,22 @@ class ArrayTools
array.pop(); array.pop();
} }
/**
* Create a new array with all elements of the given array, to prevent modifying the original.
*/
public static function clone<T>(array:Array<T>):Array<T>
{
return [for (element in array) element];
}
/**
* Create a new array with clones of all elements of the given array, to prevent modifying the original.
*/
public static function deepClone<T, U:ICloneable<T>>(array:Array<U>):Array<T>
{
return [for (element in array) element.clone()];
}
/** /**
* Return true only if both arrays contain the same elements (possibly in a different order). * Return true only if both arrays contain the same elements (possibly in a different order).
* @param a The first array to compare. * @param a The first array to compare.

View file

@ -0,0 +1,10 @@
package funkin.util.tools;
/**
* Implement this on a class to enable `Array<T>.deepClone()` to work on it.
* NOTE: T should be the type of the class that implements this interface.
*/
interface ICloneable<T>
{
public function clone():T;
}

View file

@ -25,6 +25,33 @@ class MapTools
return [for (i in map.iterator()) i]; return [for (i in map.iterator()) i];
} }
/**
* Create a new array with all elements of the given array, to prevent modifying the original.
*/
public static function clone<K, T>(map:Map<K, T>):Map<K, T>
{
return map.copy();
}
/**
* Create a new array with clones of all elements of the given array, to prevent modifying the original.
*/
public static function deepClone<K, T, U:ICloneable<T>>(map:Map<K, U>):Map<K, T>
{
// TODO: This function does NOT work.
throw "Not implemented";
/*
var newMap:Map<K, T> = [];
// Replace each value with a clone of itself.
for (key in newMap.keys())
{
newMap.set(key, newMap.get(key).clone());
}
return newMap;
*/
}
/** /**
* Return a list of keys from the map (as an array, rather than an iterator). * Return a list of keys from the map (as an array, rather than an iterator).
* TODO: Rename this? * TODO: Rename this?

View file

@ -31,23 +31,23 @@ class ConductorTest extends FunkinTest
{ {
// NOTE: Expected value comes first. // NOTE: Expected value comes first.
Assert.areEqual([], Conductor.timeChanges); Assert.areEqual([], Conductor.instance.timeChanges);
Assert.areEqual(null, Conductor.currentTimeChange); Assert.areEqual(null, Conductor.instance.currentTimeChange);
Assert.areEqual(0, Conductor.songPosition); Assert.areEqual(0, Conductor.instance.songPosition);
Assert.areEqual(Constants.DEFAULT_BPM, Conductor.bpm); Assert.areEqual(Constants.DEFAULT_BPM, Conductor.instance.bpm);
Assert.areEqual(null, Conductor.bpmOverride); Assert.areEqual(null, Conductor.instance.bpmOverride);
Assert.areEqual(600, Conductor.beatLengthMs); Assert.areEqual(600, Conductor.instance.beatLengthMs);
Assert.areEqual(4, Conductor.timeSignatureNumerator); Assert.areEqual(4, Conductor.instance.timeSignatureNumerator);
Assert.areEqual(4, Conductor.timeSignatureDenominator); Assert.areEqual(4, Conductor.instance.timeSignatureDenominator);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(0, Conductor.currentStep); Assert.areEqual(0, Conductor.instance.currentStep);
Assert.areEqual(0.0, Conductor.currentStepTime); Assert.areEqual(0.0, Conductor.instance.currentStepTime);
Assert.areEqual(150, Conductor.stepLengthMs); Assert.areEqual(150, Conductor.instance.stepLengthMs);
} }
/** /**
@ -60,23 +60,23 @@ class ConductorTest extends FunkinTest
var currentConductorState:Null<ConductorState> = conductorState; var currentConductorState:Null<ConductorState> = conductorState;
Assert.isNotNull(currentConductorState); Assert.isNotNull(currentConductorState);
Assert.areEqual(0, Conductor.songPosition); Assert.areEqual(0, Conductor.instance.songPosition);
step(); // 1 step(); // 1
var BPM_100_STEP_TIME = 1 / 9; var BPM_100_STEP_TIME = 1 / 9;
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(0, Conductor.currentStep); Assert.areEqual(0, Conductor.instance.currentStep);
FunkinAssert.areNear(1 / 9, Conductor.currentStepTime); FunkinAssert.areNear(1 / 9, Conductor.instance.currentStepTime);
step(7); // 8 step(7); // 8
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 8, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 8, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(0, Conductor.currentStep); Assert.areEqual(0, Conductor.instance.currentStep);
FunkinAssert.areNear(8 / 9, Conductor.currentStepTime); FunkinAssert.areNear(8 / 9, Conductor.instance.currentStepTime);
Assert.areEqual(0, currentConductorState.beatsHit); Assert.areEqual(0, currentConductorState.beatsHit);
Assert.areEqual(0, currentConductorState.stepsHit); Assert.areEqual(0, currentConductorState.stepsHit);
@ -88,10 +88,10 @@ class ConductorTest extends FunkinTest
currentConductorState.beatsHit = 0; currentConductorState.beatsHit = 0;
currentConductorState.stepsHit = 0; currentConductorState.stepsHit = 0;
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 9, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 9, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(1, Conductor.currentStep); Assert.areEqual(1, Conductor.instance.currentStep);
FunkinAssert.areNear(1.0, Conductor.currentStepTime); FunkinAssert.areNear(1.0, Conductor.instance.currentStepTime);
step(35 - 9); // 35 step(35 - 9); // 35
@ -100,10 +100,10 @@ class ConductorTest extends FunkinTest
currentConductorState.beatsHit = 0; currentConductorState.beatsHit = 0;
currentConductorState.stepsHit = 0; currentConductorState.stepsHit = 0;
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 35, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 35, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(3, Conductor.currentStep); Assert.areEqual(3, Conductor.instance.currentStep);
FunkinAssert.areNear(3.0 + 8 / 9, Conductor.currentStepTime); FunkinAssert.areNear(3.0 + 8 / 9, Conductor.instance.currentStepTime);
step(); // 36 step(); // 36
@ -112,83 +112,83 @@ class ConductorTest extends FunkinTest
currentConductorState.beatsHit = 0; currentConductorState.beatsHit = 0;
currentConductorState.stepsHit = 0; currentConductorState.stepsHit = 0;
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 36, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 36, Conductor.instance.songPosition);
Assert.areEqual(1, Conductor.currentBeat); Assert.areEqual(1, Conductor.instance.currentBeat);
Assert.areEqual(4, Conductor.currentStep); Assert.areEqual(4, Conductor.instance.currentStep);
FunkinAssert.areNear(4.0, Conductor.currentStepTime); FunkinAssert.areNear(4.0, Conductor.instance.currentStepTime);
step(50 - 36); // 50 step(50 - 36); // 50
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 50, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 50, Conductor.instance.songPosition);
Assert.areEqual(1, Conductor.currentBeat); Assert.areEqual(1, Conductor.instance.currentBeat);
Assert.areEqual(5, Conductor.currentStep); Assert.areEqual(5, Conductor.instance.currentStep);
FunkinAssert.areNear(5.555555, Conductor.currentStepTime); FunkinAssert.areNear(5.555555, Conductor.instance.currentStepTime);
step(49); // 99 step(49); // 99
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 99, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 99, Conductor.instance.songPosition);
Assert.areEqual(2, Conductor.currentBeat); Assert.areEqual(2, Conductor.instance.currentBeat);
Assert.areEqual(11, Conductor.currentStep); Assert.areEqual(11, Conductor.instance.currentStep);
FunkinAssert.areNear(11.0, Conductor.currentStepTime); FunkinAssert.areNear(11.0, Conductor.instance.currentStepTime);
step(1); // 100 step(1); // 100
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 100, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 100, Conductor.instance.songPosition);
Assert.areEqual(2, Conductor.currentBeat); Assert.areEqual(2, Conductor.instance.currentBeat);
Assert.areEqual(11, Conductor.currentStep); Assert.areEqual(11, Conductor.instance.currentStep);
FunkinAssert.areNear(11.111111, Conductor.currentStepTime); FunkinAssert.areNear(11.111111, Conductor.instance.currentStepTime);
} }
@Test @Test
function testUpdateForcedBPM():Void function testUpdateForcedBPM():Void
{ {
Conductor.forceBPM(60); Conductor.instance.forceBPM(60);
Assert.areEqual(0, Conductor.songPosition); Assert.areEqual(0, Conductor.instance.songPosition);
// 60 beats per minute = 1 beat per second // 60 beats per minute = 1 beat per second
// 1 beat per second = 1/60 beats per frame = 4/60 steps per frame // 1 beat per second = 1/60 beats per frame = 4/60 steps per frame
step(); // Advances time 1/60 of 1 second. step(); // Advances time 1/60 of 1 second.
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(0, Conductor.currentStep); Assert.areEqual(0, Conductor.instance.currentStep);
FunkinAssert.areNear(4 / 60, Conductor.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step FunkinAssert.areNear(4 / 60, Conductor.instance.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step
step(14 - 1); // 14 step(14 - 1); // 14
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 14, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 14, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(0, Conductor.currentStep); Assert.areEqual(0, Conductor.instance.currentStep);
FunkinAssert.areNear(1.0 - 4 / 60, Conductor.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step FunkinAssert.areNear(1.0 - 4 / 60, Conductor.instance.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step
step(); // 15 step(); // 15
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 15, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 15, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(1, Conductor.currentStep); Assert.areEqual(1, Conductor.instance.currentStep);
FunkinAssert.areNear(1.0, Conductor.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step FunkinAssert.areNear(1.0, Conductor.instance.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step
step(45 - 1); // 59 step(45 - 1); // 59
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(3, Conductor.currentStep); Assert.areEqual(3, Conductor.instance.currentStep);
FunkinAssert.areNear(4.0 - 4 / 60, Conductor.currentStepTime); FunkinAssert.areNear(4.0 - 4 / 60, Conductor.instance.currentStepTime);
step(); // 60 step(); // 60
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.instance.songPosition);
Assert.areEqual(1, Conductor.currentBeat); Assert.areEqual(1, Conductor.instance.currentBeat);
Assert.areEqual(4, Conductor.currentStep); Assert.areEqual(4, Conductor.instance.currentStep);
FunkinAssert.areNear(4.0, Conductor.currentStepTime); FunkinAssert.areNear(4.0, Conductor.instance.currentStepTime);
step(); // 61 step(); // 61
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.instance.songPosition);
Assert.areEqual(1, Conductor.currentBeat); Assert.areEqual(1, Conductor.instance.currentBeat);
Assert.areEqual(4, Conductor.currentStep); Assert.areEqual(4, Conductor.instance.currentStep);
FunkinAssert.areNear(4.0 + 4 / 60, Conductor.currentStepTime); FunkinAssert.areNear(4.0 + 4 / 60, Conductor.instance.currentStepTime);
} }
@Test @Test
@ -196,50 +196,50 @@ class ConductorTest extends FunkinTest
{ {
// Start the song with a BPM of 120. // Start the song with a BPM of 120.
var songTimeChanges:Array<SongTimeChange> = [new SongTimeChange(0, 120)]; var songTimeChanges:Array<SongTimeChange> = [new SongTimeChange(0, 120)];
Conductor.mapTimeChanges(songTimeChanges); Conductor.instance.mapTimeChanges(songTimeChanges);
// All should be at 0. // All should be at 0.
FunkinAssert.areNear(0, Conductor.songPosition); FunkinAssert.areNear(0, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(0, Conductor.currentStep); Assert.areEqual(0, Conductor.instance.currentStep);
FunkinAssert.areNear(0.0, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step FunkinAssert.areNear(0.0, Conductor.instance.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step
// 120 beats per minute = 2 beat per second // 120 beats per minute = 2 beat per second
// 2 beat per second = 2/60 beats per frame = 16/120 steps per frame // 2 beat per second = 2/60 beats per frame = 16/120 steps per frame
step(); // Advances time 1/60 of 1 second. step(); // Advances time 1/60 of 1 second.
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(0, Conductor.currentStep); Assert.areEqual(0, Conductor.instance.currentStep);
FunkinAssert.areNear(16 / 120, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step FunkinAssert.areNear(16 / 120, Conductor.instance.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step
step(15 - 1); // 15 step(15 - 1); // 15
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 15, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 15, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(2, Conductor.currentStep); Assert.areEqual(2, Conductor.instance.currentStep);
FunkinAssert.areNear(2.0, Conductor.currentStepTime); // 2/60 of 1 beat = 8/60 of 1 step FunkinAssert.areNear(2.0, Conductor.instance.currentStepTime); // 2/60 of 1 beat = 8/60 of 1 step
step(45 - 1); // 59 step(45 - 1); // 59
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.instance.songPosition);
Assert.areEqual(1, Conductor.currentBeat); Assert.areEqual(1, Conductor.instance.currentBeat);
Assert.areEqual(7, Conductor.currentStep); Assert.areEqual(7, Conductor.instance.currentStep);
FunkinAssert.areNear(7.0 + 104 / 120, Conductor.currentStepTime); FunkinAssert.areNear(7.0 + 104 / 120, Conductor.instance.currentStepTime);
step(); // 60 step(); // 60
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.instance.songPosition);
Assert.areEqual(2, Conductor.currentBeat); Assert.areEqual(2, Conductor.instance.currentBeat);
Assert.areEqual(8, Conductor.currentStep); Assert.areEqual(8, Conductor.instance.currentStep);
FunkinAssert.areNear(8.0, Conductor.currentStepTime); FunkinAssert.areNear(8.0, Conductor.instance.currentStepTime);
step(); // 61 step(); // 61
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.instance.songPosition);
Assert.areEqual(2, Conductor.currentBeat); Assert.areEqual(2, Conductor.instance.currentBeat);
Assert.areEqual(8, Conductor.currentStep); Assert.areEqual(8, Conductor.instance.currentStep);
FunkinAssert.areNear(8.0 + 8 / 60, Conductor.currentStepTime); FunkinAssert.areNear(8.0 + 8 / 60, Conductor.instance.currentStepTime);
} }
@Test @Test
@ -247,57 +247,57 @@ class ConductorTest extends FunkinTest
{ {
// Start the song with a BPM of 120. // Start the song with a BPM of 120.
var songTimeChanges:Array<SongTimeChange> = [new SongTimeChange(0, 120), new SongTimeChange(3000, 90)]; var songTimeChanges:Array<SongTimeChange> = [new SongTimeChange(0, 120), new SongTimeChange(3000, 90)];
Conductor.mapTimeChanges(songTimeChanges); Conductor.instance.mapTimeChanges(songTimeChanges);
// All should be at 0. // All should be at 0.
FunkinAssert.areNear(0, Conductor.songPosition); FunkinAssert.areNear(0, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(0, Conductor.currentStep); Assert.areEqual(0, Conductor.instance.currentStep);
FunkinAssert.areNear(0.0, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step FunkinAssert.areNear(0.0, Conductor.instance.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step
// 120 beats per minute = 2 beat per second // 120 beats per minute = 2 beat per second
// 2 beat per second = 2/60 beats per frame = 16/120 steps per frame // 2 beat per second = 2/60 beats per frame = 16/120 steps per frame
step(); // Advances time 1/60 of 1 second. step(); // Advances time 1/60 of 1 second.
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(0, Conductor.currentStep); Assert.areEqual(0, Conductor.instance.currentStep);
FunkinAssert.areNear(16 / 120, Conductor.currentStepTime); // 4/120 of 1 beat = 16/120 of 1 step FunkinAssert.areNear(16 / 120, Conductor.instance.currentStepTime); // 4/120 of 1 beat = 16/120 of 1 step
step(60 - 1 - 1); // 59 step(60 - 1 - 1); // 59
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.instance.songPosition);
Assert.areEqual(1, Conductor.currentBeat); Assert.areEqual(1, Conductor.instance.currentBeat);
Assert.areEqual(7, Conductor.currentStep); Assert.areEqual(7, Conductor.instance.currentStep);
FunkinAssert.areNear(7.0 + 104 / 120, Conductor.currentStepTime); FunkinAssert.areNear(7.0 + 104 / 120, Conductor.instance.currentStepTime);
step(); // 60 step(); // 60
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.instance.songPosition);
Assert.areEqual(2, Conductor.currentBeat); Assert.areEqual(2, Conductor.instance.currentBeat);
Assert.areEqual(8, Conductor.currentStep); Assert.areEqual(8, Conductor.instance.currentStep);
FunkinAssert.areNear(8.0, Conductor.currentStepTime); FunkinAssert.areNear(8.0, Conductor.instance.currentStepTime);
step(); // 61 step(); // 61
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.instance.songPosition);
Assert.areEqual(2, Conductor.currentBeat); Assert.areEqual(2, Conductor.instance.currentBeat);
Assert.areEqual(8, Conductor.currentStep); Assert.areEqual(8, Conductor.instance.currentStep);
FunkinAssert.areNear(8.0 + 8 / 60, Conductor.currentStepTime); FunkinAssert.areNear(8.0 + 8 / 60, Conductor.instance.currentStepTime);
step(179 - 61); // 179 step(179 - 61); // 179
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 179, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 179, Conductor.instance.songPosition);
Assert.areEqual(5, Conductor.currentBeat); Assert.areEqual(5, Conductor.instance.currentBeat);
Assert.areEqual(23, Conductor.currentStep); Assert.areEqual(23, Conductor.instance.currentStep);
FunkinAssert.areNear(23.0 + 52 / 60, Conductor.currentStepTime); FunkinAssert.areNear(23.0 + 52 / 60, Conductor.instance.currentStepTime);
step(); // 180 (3 seconds) step(); // 180 (3 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 180, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 180, Conductor.instance.songPosition);
Assert.areEqual(6, Conductor.currentBeat); Assert.areEqual(6, Conductor.instance.currentBeat);
Assert.areEqual(24, Conductor.currentStep); Assert.areEqual(24, Conductor.instance.currentStep);
FunkinAssert.areNear(24.0, Conductor.currentStepTime); FunkinAssert.areNear(24.0, Conductor.instance.currentStepTime);
step(); // 181 (3 + 1/60 seconds) step(); // 181 (3 + 1/60 seconds)
// BPM has switched to 90! // BPM has switched to 90!
@ -305,24 +305,24 @@ class ConductorTest extends FunkinTest
// 1.5 beat per second = 1.5/60 beats per frame = 3/120 beats per frame // 1.5 beat per second = 1.5/60 beats per frame = 3/120 beats per frame
// = 12/120 steps per frame // = 12/120 steps per frame
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 181, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 181, Conductor.instance.songPosition);
Assert.areEqual(6, Conductor.currentBeat); Assert.areEqual(6, Conductor.instance.currentBeat);
Assert.areEqual(24, Conductor.currentStep); Assert.areEqual(24, Conductor.instance.currentStep);
FunkinAssert.areNear(24.0 + 12 / 120, Conductor.currentStepTime); FunkinAssert.areNear(24.0 + 12 / 120, Conductor.instance.currentStepTime);
step(59); // 240 (4 seconds) step(59); // 240 (4 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 240, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 240, Conductor.instance.songPosition);
Assert.areEqual(7, Conductor.currentBeat); Assert.areEqual(7, Conductor.instance.currentBeat);
Assert.areEqual(30, Conductor.currentStep); Assert.areEqual(30, Conductor.instance.currentStep);
FunkinAssert.areNear(30.0, Conductor.currentStepTime); FunkinAssert.areNear(30.0, Conductor.instance.currentStepTime);
step(); // 241 (4 + 1/60 seconds) step(); // 241 (4 + 1/60 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 241, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 241, Conductor.instance.songPosition);
Assert.areEqual(7, Conductor.currentBeat); Assert.areEqual(7, Conductor.instance.currentBeat);
Assert.areEqual(30, Conductor.currentStep); Assert.areEqual(30, Conductor.instance.currentStep);
FunkinAssert.areNear(30.0 + 12 / 120, Conductor.currentStepTime); FunkinAssert.areNear(30.0 + 12 / 120, Conductor.instance.currentStepTime);
} }
@Test @Test
@ -334,63 +334,63 @@ class ConductorTest extends FunkinTest
new SongTimeChange(3000, 90), new SongTimeChange(3000, 90),
new SongTimeChange(6000, 180) new SongTimeChange(6000, 180)
]; ];
Conductor.mapTimeChanges(songTimeChanges); Conductor.instance.mapTimeChanges(songTimeChanges);
// Verify time changes. // Verify time changes.
Assert.areEqual(3, Conductor.timeChanges.length); Assert.areEqual(3, Conductor.instance.timeChanges.length);
FunkinAssert.areNear(0, Conductor.timeChanges[0].beatTime); FunkinAssert.areNear(0, Conductor.instance.timeChanges[0].beatTime);
FunkinAssert.areNear(6, Conductor.timeChanges[1].beatTime); FunkinAssert.areNear(6, Conductor.instance.timeChanges[1].beatTime);
FunkinAssert.areNear(10.5, Conductor.timeChanges[2].beatTime); FunkinAssert.areNear(10.5, Conductor.instance.timeChanges[2].beatTime);
// All should be at 0. // All should be at 0.
FunkinAssert.areNear(0, Conductor.songPosition); FunkinAssert.areNear(0, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(0, Conductor.currentStep); Assert.areEqual(0, Conductor.instance.currentStep);
FunkinAssert.areNear(0.0, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step FunkinAssert.areNear(0.0, Conductor.instance.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step
// 120 beats per minute = 2 beat per second // 120 beats per minute = 2 beat per second
// 2 beat per second = 2/60 beats per frame = 16/120 steps per frame // 2 beat per second = 2/60 beats per frame = 16/120 steps per frame
step(); // Advances time 1/60 of 1 second. step(); // Advances time 1/60 of 1 second.
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.instance.songPosition);
Assert.areEqual(0, Conductor.currentBeat); Assert.areEqual(0, Conductor.instance.currentBeat);
Assert.areEqual(0, Conductor.currentStep); Assert.areEqual(0, Conductor.instance.currentStep);
FunkinAssert.areNear(16 / 120, Conductor.currentStepTime); // 4/120 of 1 beat = 16/120 of 1 step FunkinAssert.areNear(16 / 120, Conductor.instance.currentStepTime); // 4/120 of 1 beat = 16/120 of 1 step
step(60 - 1 - 1); // 59 step(60 - 1 - 1); // 59
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.instance.songPosition);
Assert.areEqual(1, Conductor.currentBeat); Assert.areEqual(1, Conductor.instance.currentBeat);
Assert.areEqual(7, Conductor.currentStep); Assert.areEqual(7, Conductor.instance.currentStep);
FunkinAssert.areNear(7 + 104 / 120, Conductor.currentStepTime); FunkinAssert.areNear(7 + 104 / 120, Conductor.instance.currentStepTime);
step(); // 60 step(); // 60
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.instance.songPosition);
Assert.areEqual(2, Conductor.currentBeat); Assert.areEqual(2, Conductor.instance.currentBeat);
Assert.areEqual(8, Conductor.currentStep); Assert.areEqual(8, Conductor.instance.currentStep);
FunkinAssert.areNear(8.0, Conductor.currentStepTime); FunkinAssert.areNear(8.0, Conductor.instance.currentStepTime);
step(); // 61 step(); // 61
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.instance.songPosition);
Assert.areEqual(2, Conductor.currentBeat); Assert.areEqual(2, Conductor.instance.currentBeat);
Assert.areEqual(8, Conductor.currentStep); Assert.areEqual(8, Conductor.instance.currentStep);
FunkinAssert.areNear(8.0 + 8 / 60, Conductor.currentStepTime); FunkinAssert.areNear(8.0 + 8 / 60, Conductor.instance.currentStepTime);
step(179 - 61); // 179 step(179 - 61); // 179
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 179, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 179, Conductor.instance.songPosition);
Assert.areEqual(5, Conductor.currentBeat); Assert.areEqual(5, Conductor.instance.currentBeat);
Assert.areEqual(23, Conductor.currentStep); Assert.areEqual(23, Conductor.instance.currentStep);
FunkinAssert.areNear(23.0 + 52 / 60, Conductor.currentStepTime); FunkinAssert.areNear(23.0 + 52 / 60, Conductor.instance.currentStepTime);
step(); // 180 (3 seconds) step(); // 180 (3 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 180, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 180, Conductor.instance.songPosition);
Assert.areEqual(6, Conductor.currentBeat); Assert.areEqual(6, Conductor.instance.currentBeat);
Assert.areEqual(24, Conductor.currentStep); // 23.999 => 24 Assert.areEqual(24, Conductor.instance.currentStep); // 23.999 => 24
FunkinAssert.areNear(24.0, Conductor.currentStepTime); FunkinAssert.areNear(24.0, Conductor.instance.currentStepTime);
step(); // 181 (3 + 1/60 seconds) step(); // 181 (3 + 1/60 seconds)
// BPM has switched to 90! // BPM has switched to 90!
@ -398,45 +398,45 @@ class ConductorTest extends FunkinTest
// 1.5 beat per second = 1.5/60 beats per frame = 3/120 beats per frame // 1.5 beat per second = 1.5/60 beats per frame = 3/120 beats per frame
// = 12/120 steps per frame // = 12/120 steps per frame
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 181, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 181, Conductor.instance.songPosition);
Assert.areEqual(6, Conductor.currentBeat); Assert.areEqual(6, Conductor.instance.currentBeat);
Assert.areEqual(24, Conductor.currentStep); Assert.areEqual(24, Conductor.instance.currentStep);
FunkinAssert.areNear(24.0 + 12 / 120, Conductor.currentStepTime); FunkinAssert.areNear(24.0 + 12 / 120, Conductor.instance.currentStepTime);
step(60 - 1 - 1); // 240 (4 seconds) step(60 - 1 - 1); // 240 (4 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 239, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 239, Conductor.instance.songPosition);
Assert.areEqual(7, Conductor.currentBeat); Assert.areEqual(7, Conductor.instance.currentBeat);
Assert.areEqual(29, Conductor.currentStep); Assert.areEqual(29, Conductor.instance.currentStep);
FunkinAssert.areNear(29.0 + 108 / 120, Conductor.currentStepTime); FunkinAssert.areNear(29.0 + 108 / 120, Conductor.instance.currentStepTime);
step(); // 240 (4 seconds) step(); // 240 (4 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 240, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 240, Conductor.instance.songPosition);
Assert.areEqual(7, Conductor.currentBeat); Assert.areEqual(7, Conductor.instance.currentBeat);
Assert.areEqual(30, Conductor.currentStep); Assert.areEqual(30, Conductor.instance.currentStep);
FunkinAssert.areNear(30.0, Conductor.currentStepTime); FunkinAssert.areNear(30.0, Conductor.instance.currentStepTime);
step(); // 241 (4 + 1/60 seconds) step(); // 241 (4 + 1/60 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 241, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 241, Conductor.instance.songPosition);
Assert.areEqual(7, Conductor.currentBeat); Assert.areEqual(7, Conductor.instance.currentBeat);
Assert.areEqual(30, Conductor.currentStep); Assert.areEqual(30, Conductor.instance.currentStep);
FunkinAssert.areNear(30.0 + 12 / 120, Conductor.currentStepTime); FunkinAssert.areNear(30.0 + 12 / 120, Conductor.instance.currentStepTime);
step(359 - 241); // 359 (5 + 59/60 seconds) step(359 - 241); // 359 (5 + 59/60 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 359, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 359, Conductor.instance.songPosition);
Assert.areEqual(10, Conductor.currentBeat); Assert.areEqual(10, Conductor.instance.currentBeat);
Assert.areEqual(41, Conductor.currentStep); Assert.areEqual(41, Conductor.instance.currentStep);
FunkinAssert.areNear(41 + 108 / 120, Conductor.currentStepTime); FunkinAssert.areNear(41 + 108 / 120, Conductor.instance.currentStepTime);
step(); // 360 step(); // 360
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 360, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 360, Conductor.instance.songPosition);
Assert.areEqual(10, Conductor.currentBeat); Assert.areEqual(10, Conductor.instance.currentBeat);
Assert.areEqual(42, Conductor.currentStep); // 41.999 Assert.areEqual(42, Conductor.instance.currentStep); // 41.999
FunkinAssert.areNear(42.0, Conductor.currentStepTime); FunkinAssert.areNear(42.0, Conductor.instance.currentStepTime);
step(); // 361 step(); // 361
// BPM has switched to 180! // BPM has switched to 180!
@ -444,24 +444,24 @@ class ConductorTest extends FunkinTest
// 3 beat per second = 3/60 beats per frame // 3 beat per second = 3/60 beats per frame
// = 12/60 steps per frame // = 12/60 steps per frame
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 361, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 361, Conductor.instance.songPosition);
Assert.areEqual(10, Conductor.currentBeat); Assert.areEqual(10, Conductor.instance.currentBeat);
Assert.areEqual(42, Conductor.currentStep); Assert.areEqual(42, Conductor.instance.currentStep);
FunkinAssert.areNear(42.0 + 12 / 60, Conductor.currentStepTime); FunkinAssert.areNear(42.0 + 12 / 60, Conductor.instance.currentStepTime);
step(); // 362 step(); // 362
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 362, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 362, Conductor.instance.songPosition);
Assert.areEqual(10, Conductor.currentBeat); Assert.areEqual(10, Conductor.instance.currentBeat);
Assert.areEqual(42, Conductor.currentStep); Assert.areEqual(42, Conductor.instance.currentStep);
FunkinAssert.areNear(42.0 + 24 / 60, Conductor.currentStepTime); FunkinAssert.areNear(42.0 + 24 / 60, Conductor.instance.currentStepTime);
step(3); // 365 step(3); // 365
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 365, Conductor.songPosition); FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 365, Conductor.instance.songPosition);
Assert.areEqual(10, Conductor.currentBeat); Assert.areEqual(10, Conductor.instance.currentBeat);
Assert.areEqual(43, Conductor.currentStep); // 42.999 => 42 Assert.areEqual(43, Conductor.instance.currentStep); // 42.999 => 42
FunkinAssert.areNear(43.0, Conductor.currentStepTime); FunkinAssert.areNear(43.0, Conductor.instance.currentStepTime);
} }
} }
@ -504,6 +504,6 @@ class ConductorState extends FlxState
super.update(elapsed); super.update(elapsed);
// On each step, increment the Conductor as though the song was playing. // On each step, increment the Conductor as though the song was playing.
Conductor.update(Conductor.songPosition + elapsed * Constants.MS_PER_SEC); Conductor.instance.update(Conductor.instance.songPosition + elapsed * Constants.MS_PER_SEC);
} }
} }