Clean up several messy chunks of code, and add support for dragging notes and events.

This commit is contained in:
EliteMasterEric 2023-10-26 05:46:22 -04:00
parent 8d875949b9
commit eec3ac3ced
59 changed files with 3809 additions and 2922 deletions

34
.vscode/settings.json vendored
View file

@ -71,7 +71,7 @@
"files.eol": "\n",
"haxe.displayPort": "auto",
"haxe.enableCompilationServer": true,
"haxe.enableCompilationServer": false,
"haxe.displayServer": {
"arguments": ["-v"]
},
@ -97,15 +97,35 @@
"args": ["-debug"]
},
{
"label": "Windows / Debug (DEBUG ASSETS)",
"target": "windows",
"args": ["-debug", "-DDEBUG_ASSETS"]
},
{
"label": "Windows / Debug (ANIMATE)",
"label": "Windows / Debug (FlxAnimate Test)",
"target": "windows",
"args": ["-debug", "-DANIMATE"]
},
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
"args": ["-debug", "-DFREEPLAY"]
},
{
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
"target": "windows",
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
},
{
"label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows",
"args": ["-debug", "-DCHARTING"]
},
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",
"args": ["-debug", "-DANIMDEBUG"]
},
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
"args": ["-debug", "-DLATENCY"]
},
{
"label": "HTML5 / Debug",
"target": "html5",

2
assets

@ -1 +1 @@
Subproject commit ef79a6cf1ae3dcbd86a5b798f8117a6c692c0156
Subproject commit be9d790af9c6f1f5e3afc7aed2b1d5c21823bc20

View file

@ -24,7 +24,7 @@ Example:
```
/**
* Finds the largest deviation from the desired time inside this VoicesGroup.
*
*
* @param targetTime The time to check against.
* If none is provided, it checks the time of all members against the first member of this VoicesGroup.
* @return The largest deviation from the target time found.
@ -52,3 +52,10 @@ import sys.io.File;
#end
```
## Argument Formatting
[Optional arguments](https://haxe.org/manual/types-function-optional-arguments.html) and [default arguments](https://haxe.org/manual/types-function-default-values.html) should be mutually exclusive and not used together!
For example, `myFunction(?input:Int)` should be used if you want the argument to be a `Null<Int>` whose value is `null` if no value is passed, and `myFunction(input:Int = 0)` should be used if you want the argument to be an `Int`, whose value is `0` if no value is passed.
Using both at the same time is considered valid by Haxe, but `myFunction(?input:Int = 0)` results in a `Null<Int>` whose value defaults to 0 anyway, so it's never null, but it's annotated as nullable! The biggest consequence of this is that it makes null safety more annoying.

View file

@ -266,6 +266,10 @@ class InitState extends FlxState
return;
}
// Load and cache the song's charts.
// TODO: Do this in the loading state.
songData.cacheCharts(true);
LoadingState.loadAndSwitchState(new funkin.play.PlayState(
{
targetSong: songData,

View file

@ -117,7 +117,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
public function stepHit():Bool
{
var event = new SongTimeScriptEvent(ScriptEventType.SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep);
var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep);
dispatchEvent(event);
@ -128,7 +128,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
public function beatHit():Bool
{
var event = new SongTimeScriptEvent(ScriptEventType.SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep);
var event = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep);
dispatchEvent(event);
@ -148,7 +148,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
override function startOutro(onComplete:() -> Void):Void
{
var event = new StateChangeScriptEvent(ScriptEventType.STATE_CHANGE_BEGIN, null, true);
var event = new StateChangeScriptEvent(STATE_CHANGE_BEGIN, null, true);
dispatchEvent(event);
@ -164,7 +164,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
public override function openSubState(targetSubState:FlxSubState):Void
{
var event = new SubStateScriptEvent(ScriptEventType.SUBSTATE_OPEN_BEGIN, targetSubState, true);
var event = new SubStateScriptEvent(SUBSTATE_OPEN_BEGIN, targetSubState, true);
dispatchEvent(event);
@ -175,12 +175,12 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
function onOpenSubStateComplete(targetState:FlxSubState):Void
{
dispatchEvent(new SubStateScriptEvent(ScriptEventType.SUBSTATE_OPEN_END, targetState, true));
dispatchEvent(new SubStateScriptEvent(SUBSTATE_OPEN_END, targetState, true));
}
public override function closeSubState():Void
{
var event = new SubStateScriptEvent(ScriptEventType.SUBSTATE_CLOSE_BEGIN, this.subState, true);
var event = new SubStateScriptEvent(SUBSTATE_CLOSE_BEGIN, this.subState, true);
dispatchEvent(event);
@ -191,6 +191,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
function onCloseSubStateComplete(targetState:FlxSubState):Void
{
dispatchEvent(new SubStateScriptEvent(ScriptEventType.SUBSTATE_CLOSE_END, targetState, true));
dispatchEvent(new SubStateScriptEvent(SUBSTATE_CLOSE_END, targetState, true));
}
}

View file

@ -96,7 +96,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
*/
public function stepHit():Bool
{
var event:ScriptEvent = new SongTimeScriptEvent(ScriptEventType.SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep);
var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep);
dispatchEvent(event);
@ -112,7 +112,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
*/
public function beatHit():Bool
{
var event:ScriptEvent = new SongTimeScriptEvent(ScriptEventType.SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep);
var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep);
dispatchEvent(event);

View file

@ -447,7 +447,7 @@ class SongChartData
}
}
class SongEventData
class SongEventDataRaw
{
/**
* The timestamp of the event. The timestamp is in the format of the song's time format.
@ -503,40 +503,57 @@ class SongEventData
return _stepTime = Conductor.getTimeInSteps(this.time);
}
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
@:forward(time, event, value, activated, getStepTime)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
public function new(time:Float, event:String, value:Dynamic = null)
{
this = new SongEventDataRaw(time, event, value);
}
public inline function getDynamic(key:String):Null<Dynamic>
{
return value == null ? null : Reflect.field(value, key);
return this.value == null ? null : Reflect.field(this.value, key);
}
public inline function getBool(key:String):Null<Bool>
{
return value == null ? null : cast Reflect.field(value, key);
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getInt(key:String):Null<Int>
{
return value == null ? null : cast Reflect.field(value, key);
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getFloat(key:String):Null<Float>
{
return value == null ? null : cast Reflect.field(value, key);
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getString(key:String):String
{
return value == null ? null : cast Reflect.field(value, key);
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getArray(key:String):Array<Dynamic>
{
return value == null ? null : cast Reflect.field(value, key);
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getBoolArray(key:String):Array<Bool>
{
return value == null ? null : cast Reflect.field(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)
@ -584,7 +601,7 @@ class SongEventData
}
}
class SongNoteData
class SongNoteDataRaw
{
/**
* The timestamp of the note. The timestamp is in the format of the song's time format.
@ -655,6 +672,48 @@ class SongNoteData
return _stepTime = Conductor.getTimeInSteps(this.time);
}
@:jignored
var _stepLength:Null<Float> = null;
/**
* @param force Set to `true` to force recalculation (good after BPM changes)
* @return The length of the hold note in steps, or `0` if this is not a hold note.
*/
public function getStepLength(force = false):Float
{
if (this.length <= 0) return 0.0;
if (_stepLength != null && !force) return _stepLength;
return _stepLength = Conductor.getTimeInSteps(this.time + this.length) - getStepTime();
}
public function setStepLength(value:Float):Void
{
if (value <= 0)
{
this.length = 0.0;
}
else
{
var lengthMs:Float = Conductor.getStepTimeInMs(value) - this.time;
this.length = lengthMs;
}
_stepLength = null;
}
}
/**
* Wrap SongNoteData in an abstract so we can overload operators.
*/
@:forward
abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
{
public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
{
this = new SongNoteDataRaw(time, data, length, kind);
}
/**
* The direction of the note, if applicable.
* Strips the strumline index from the data.
@ -668,7 +727,12 @@ class SongNoteData
public function getDirectionName(strumlineSize:Int = 4):String
{
switch (this.data % strumlineSize)
return SongNoteData.buildDirectionName(this.data, strumlineSize);
}
public static function buildDirectionName(data:Int, strumlineSize:Int = 4):String
{
switch (data % strumlineSize)
{
case 0:
return 'Left';
@ -705,36 +769,6 @@ class SongNoteData
return getStrumlineIndex(strumlineSize) == 0;
}
@:jignored
var _stepLength:Null<Float> = null;
/**
* @param force Set to `true` to force recalculation (good after BPM changes)
* @return The length of the hold note in steps, or `0` if this is not a hold note.
*/
public function getStepLength(force = false):Float
{
if (this.length <= 0) return 0.0;
if (_stepLength != null && !force) return _stepLength;
return _stepLength = Conductor.getTimeInSteps(this.time + this.length) - getStepTime();
}
public function setStepLength(value:Float):Void
{
if (value <= 0)
{
this.length = 0.0;
}
else
{
var lengthMs:Float = Conductor.getStepTimeInMs(value) - this.time;
this.length = lengthMs;
}
_stepLength = null;
}
@:jignored
public var isHoldNote(get, never):Bool;
@ -797,6 +831,11 @@ class SongNoteData
return this.time <= other.time;
}
public function clone():SongNoteData
{
return new SongNoteData(this.time, this.data, this.length, this.kind);
}
/**
* Produces a string representation suitable for debugging.
*/

View file

@ -66,8 +66,14 @@ class SongDataUtils
var result = notes.filter(function(note:SongNoteData):Bool {
for (x in subtrahend)
{
// The currently iterated note is in the subtrahend array.
// SongNoteData's == operation has been overridden so that this will work.
if (x == note) return false;
if (x == note)
{
return false;
}
}
return true;
});

View file

@ -12,7 +12,9 @@ using Lambda;
using StringTools;
using funkin.util.tools.ArraySortTools;
using funkin.util.tools.ArrayTools;
using funkin.util.tools.FloatTools;
using funkin.util.tools.Int64Tools;
using funkin.util.tools.IntTools;
using funkin.util.tools.IteratorTools;
using funkin.util.tools.MapTools;
using funkin.util.tools.SongEventDataArrayTools;

View file

@ -158,7 +158,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent
public function new(dir:NoteDirection, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
{
super(ScriptEventType.NOTE_GHOST_MISS, true);
super(NOTE_GHOST_MISS, true);
this.dir = dir;
this.hasPossibleNotes = hasPossibleNotes;
this.healthChange = healthChange;
@ -186,7 +186,7 @@ class SongEventScriptEvent extends ScriptEvent
public function new(event:funkin.data.song.SongData.SongEventData):Void
{
super(ScriptEventType.SONG_EVENT, true);
super(SONG_EVENT, true);
this.event = event;
}
@ -209,7 +209,7 @@ class UpdateScriptEvent extends ScriptEvent
public function new(elapsed:Float):Void
{
super(ScriptEventType.UPDATE, false);
super(UPDATE, false);
this.elapsed = elapsed;
}
@ -338,7 +338,7 @@ class SongLoadScriptEvent extends ScriptEvent
public function new(id:String, difficulty:String, notes:Array<SongNoteData>):Void
{
super(ScriptEventType.SONG_LOADED, false);
super(SONG_LOADED, false);
this.id = id;
this.difficulty = difficulty;
this.notes = notes;
@ -407,7 +407,7 @@ class PauseScriptEvent extends ScriptEvent
public function new(gitaroo:Bool):Void
{
super(ScriptEventType.PAUSE, true);
super(PAUSE, true);
this.gitaroo = gitaroo;
}
}

View file

@ -23,15 +23,16 @@ class ScriptEventDispatcher
// IScriptedClass
switch (event.type)
{
case ScriptEventType.CREATE:
case CREATE:
target.onCreate(event);
return;
case ScriptEventType.DESTROY:
case DESTROY:
target.onDestroy(event);
return;
case ScriptEventType.UPDATE:
case UPDATE:
target.onUpdate(cast event);
return;
default: // Continue;
}
if (Std.isOfType(target, IStateStageProp))
@ -39,9 +40,10 @@ class ScriptEventDispatcher
var t:IStateStageProp = cast(target, IStateStageProp);
switch (event.type)
{
case ScriptEventType.ADDED:
case ADDED:
t.onAdd(cast event);
return;
default: // Continue;
}
}
@ -50,21 +52,22 @@ class ScriptEventDispatcher
var t:IDialogueScriptedClass = cast(target, IDialogueScriptedClass);
switch (event.type)
{
case ScriptEventType.DIALOGUE_START:
case DIALOGUE_START:
t.onDialogueStart(cast event);
return;
case ScriptEventType.DIALOGUE_LINE:
case DIALOGUE_LINE:
t.onDialogueLine(cast event);
return;
case ScriptEventType.DIALOGUE_COMPLETE_LINE:
case DIALOGUE_COMPLETE_LINE:
t.onDialogueCompleteLine(cast event);
return;
case ScriptEventType.DIALOGUE_SKIP:
case DIALOGUE_SKIP:
t.onDialogueSkip(cast event);
return;
case ScriptEventType.DIALOGUE_END:
case DIALOGUE_END:
t.onDialogueEnd(cast event);
return;
default: // Continue;
}
}
@ -73,54 +76,55 @@ class ScriptEventDispatcher
var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
switch (event.type)
{
case ScriptEventType.NOTE_HIT:
case NOTE_HIT:
t.onNoteHit(cast event);
return;
case ScriptEventType.NOTE_MISS:
case NOTE_MISS:
t.onNoteMiss(cast event);
return;
case ScriptEventType.NOTE_GHOST_MISS:
case NOTE_GHOST_MISS:
t.onNoteGhostMiss(cast event);
return;
case ScriptEventType.SONG_BEAT_HIT:
case SONG_BEAT_HIT:
t.onBeatHit(cast event);
return;
case ScriptEventType.SONG_STEP_HIT:
case SONG_STEP_HIT:
t.onStepHit(cast event);
return;
case ScriptEventType.SONG_START:
case SONG_START:
t.onSongStart(event);
return;
case ScriptEventType.SONG_END:
case SONG_END:
t.onSongEnd(event);
return;
case ScriptEventType.SONG_RETRY:
case SONG_RETRY:
t.onSongRetry(event);
return;
case ScriptEventType.GAME_OVER:
case GAME_OVER:
t.onGameOver(event);
return;
case ScriptEventType.PAUSE:
case PAUSE:
t.onPause(cast event);
return;
case ScriptEventType.RESUME:
case RESUME:
t.onResume(event);
return;
case ScriptEventType.SONG_EVENT:
case SONG_EVENT:
t.onSongEvent(cast event);
return;
case ScriptEventType.COUNTDOWN_START:
case COUNTDOWN_START:
t.onCountdownStart(cast event);
return;
case ScriptEventType.COUNTDOWN_STEP:
case COUNTDOWN_STEP:
t.onCountdownStep(cast event);
return;
case ScriptEventType.COUNTDOWN_END:
case COUNTDOWN_END:
t.onCountdownEnd(cast event);
return;
case ScriptEventType.SONG_LOADED:
case SONG_LOADED:
t.onSongLoaded(cast event);
return;
default: // Continue;
}
}
@ -129,24 +133,25 @@ class ScriptEventDispatcher
var t = cast(target, IStateChangingScriptedClass);
switch (event.type)
{
case ScriptEventType.STATE_CHANGE_BEGIN:
case STATE_CHANGE_BEGIN:
t.onStateChangeBegin(cast event);
return;
case ScriptEventType.STATE_CHANGE_END:
case STATE_CHANGE_END:
t.onStateChangeEnd(cast event);
return;
case ScriptEventType.SUBSTATE_OPEN_BEGIN:
case SUBSTATE_OPEN_BEGIN:
t.onSubStateOpenBegin(cast event);
return;
case ScriptEventType.SUBSTATE_OPEN_END:
case SUBSTATE_OPEN_END:
t.onSubStateOpenEnd(cast event);
return;
case ScriptEventType.SUBSTATE_CLOSE_BEGIN:
case SUBSTATE_CLOSE_BEGIN:
t.onSubStateCloseBegin(cast event);
return;
case ScriptEventType.SUBSTATE_CLOSE_END:
case SUBSTATE_CLOSE_END:
t.onSubStateCloseEnd(cast event);
return;
default: // Continue;
}
}
else

View file

@ -9,7 +9,7 @@ enum abstract ScriptEventType(String) from String to String
*
* This event is not cancelable.
*/
public static inline final CREATE:ScriptEventType = 'CREATE';
var CREATE = 'CREATE';
/**
* Called when the relevant object is destroyed.
@ -17,7 +17,7 @@ enum abstract ScriptEventType(String) from String to String
*
* This event is not cancelable.
*/
public static inline final DESTROY:ScriptEventType = 'DESTROY';
var DESTROY = 'DESTROY';
/**
* Called when the relevent object is added to the game state.
@ -25,7 +25,7 @@ enum abstract ScriptEventType(String) from String to String
*
* This event is not cancelable.
*/
public static inline final ADDED:ScriptEventType = 'ADDED';
var ADDED = 'ADDED';
/**
* Called during the update function.
@ -33,35 +33,35 @@ enum abstract ScriptEventType(String) from String to String
*
* This event is not cancelable.
*/
public static inline final UPDATE:ScriptEventType = 'UPDATE';
var UPDATE = 'UPDATE';
/**
* Called when the player moves to pause the game.
*
* This event IS cancelable! Canceling the event will prevent the game from pausing.
*/
public static inline final PAUSE:ScriptEventType = 'PAUSE';
var PAUSE = 'PAUSE';
/**
* Called when the player moves to unpause the game while paused.
*
* This event IS cancelable! Canceling the event will prevent the game from resuming.
*/
public static inline final RESUME:ScriptEventType = 'RESUME';
var RESUME = 'RESUME';
/**
* Called once per step in the song. This happens 4 times per measure.
*
* This event is not cancelable.
*/
public static inline final SONG_BEAT_HIT:ScriptEventType = 'BEAT_HIT';
var SONG_BEAT_HIT = 'BEAT_HIT';
/**
* Called once per step in the song. This happens 16 times per measure.
*
* This event is not cancelable.
*/
public static inline final SONG_STEP_HIT:ScriptEventType = 'STEP_HIT';
var SONG_STEP_HIT = 'STEP_HIT';
/**
* Called when a character hits a note.
@ -70,7 +70,7 @@ enum abstract ScriptEventType(String) from String to String
* This event IS cancelable! Canceling this event prevents the note from being hit,
* and will likely result in a miss later.
*/
public static inline final NOTE_HIT:ScriptEventType = 'NOTE_HIT';
var NOTE_HIT = 'NOTE_HIT';
/**
* Called when a character misses a note.
@ -79,7 +79,7 @@ enum abstract ScriptEventType(String) from String to String
* This event IS cancelable! Canceling this event prevents the note from being considered missed,
* avoiding a combo break and lost health.
*/
public static inline final NOTE_MISS:ScriptEventType = 'NOTE_MISS';
var NOTE_MISS = 'NOTE_MISS';
/**
* Called when a character presses a note when there was none there, causing them to lose health.
@ -88,7 +88,7 @@ enum abstract ScriptEventType(String) from String to String
* This event IS cancelable! Canceling this event prevents the note from being considered missed,
* avoiding lost health/score and preventing the miss animation.
*/
public static inline final NOTE_GHOST_MISS:ScriptEventType = 'NOTE_GHOST_MISS';
var NOTE_GHOST_MISS = 'NOTE_GHOST_MISS';
/**
* Called when a song event is reached in the chart.
@ -96,21 +96,21 @@ enum abstract ScriptEventType(String) from String to String
* This event IS cancelable! Cancelling this event prevents the event from being triggered,
* thus blocking its normal functionality.
*/
public static inline final SONG_EVENT:ScriptEventType = 'SONG_EVENT';
var SONG_EVENT = 'SONG_EVENT';
/**
* Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin.
*
* This event is not cancelable.
*/
public static inline final SONG_START:ScriptEventType = 'SONG_START';
var SONG_START = 'SONG_START';
/**
* Called when the song ends. This happens as the instrumental and vocals end.
*
* This event is not cancelable.
*/
public static inline final SONG_END:ScriptEventType = 'SONG_END';
var SONG_END = 'SONG_END';
/**
* Called when the countdown begins. This occurs before the song starts.
@ -119,7 +119,7 @@ enum abstract ScriptEventType(String) from String to String
* - The song will not start until you call Countdown.performCountdown() later.
* - Note that calling performCountdown() will trigger this event again, so be sure to add logic to ignore it.
*/
public static inline final COUNTDOWN_START:ScriptEventType = 'COUNTDOWN_START';
var COUNTDOWN_START = 'COUNTDOWN_START';
/**
* Called when a step of the countdown happens.
@ -128,21 +128,21 @@ enum abstract ScriptEventType(String) from String to String
* This event IS cancelable! Canceling this event will pause the countdown.
* - The countdown will not resume until you call PlayState.resumeCountdown().
*/
public static inline final COUNTDOWN_STEP:ScriptEventType = 'COUNTDOWN_STEP';
var COUNTDOWN_STEP = 'COUNTDOWN_STEP';
/**
* Called when the countdown is done but just before the song starts.
*
* This event is not cancelable.
*/
public static inline final COUNTDOWN_END:ScriptEventType = 'COUNTDOWN_END';
var COUNTDOWN_END = 'COUNTDOWN_END';
/**
* Called before the game over screen triggers and the death animation plays.
*
* This event is not cancelable.
*/
public static inline final GAME_OVER:ScriptEventType = 'GAME_OVER';
var GAME_OVER = 'GAME_OVER';
/**
* Called after the player presses a key to restart the game.
@ -150,21 +150,21 @@ enum abstract ScriptEventType(String) from String to String
*
* This event IS cancelable! Canceling this event will prevent the game from restarting.
*/
public static inline final SONG_RETRY:ScriptEventType = 'SONG_RETRY';
var SONG_RETRY = 'SONG_RETRY';
/**
* Called when the player pushes down any key on the keyboard.
*
* This event is not cancelable.
*/
public static inline final KEY_DOWN:ScriptEventType = 'KEY_DOWN';
var KEY_DOWN = 'KEY_DOWN';
/**
* Called when the player releases a key on the keyboard.
*
* This event is not cancelable.
*/
public static inline final KEY_UP:ScriptEventType = 'KEY_UP';
var KEY_UP = 'KEY_UP';
/**
* Called when the game has finished loading the notes from JSON.
@ -172,56 +172,56 @@ enum abstract ScriptEventType(String) from String to String
*
* This event is not cancelable.
*/
public static inline final SONG_LOADED:ScriptEventType = 'SONG_LOADED';
var SONG_LOADED = 'SONG_LOADED';
/**
* Called when the game is about to switch the current FlxState.
*
* This event is not cancelable.
*/
public static inline final STATE_CHANGE_BEGIN:ScriptEventType = 'STATE_CHANGE_BEGIN';
var STATE_CHANGE_BEGIN = 'STATE_CHANGE_BEGIN';
/**
* Called when the game has finished switching the current FlxState.
*
* This event is not cancelable.
*/
public static inline final STATE_CHANGE_END:ScriptEventType = 'STATE_CHANGE_END';
var STATE_CHANGE_END = 'STATE_CHANGE_END';
/**
* Called when the game is about to open a new FlxSubState.
*
* This event is not cancelable.
*/
public static inline final SUBSTATE_OPEN_BEGIN:ScriptEventType = 'SUBSTATE_OPEN_BEGIN';
var SUBSTATE_OPEN_BEGIN = 'SUBSTATE_OPEN_BEGIN';
/**
* Called when the game has finished opening a new FlxSubState.
*
* This event is not cancelable.
*/
public static inline final SUBSTATE_OPEN_END:ScriptEventType = 'SUBSTATE_OPEN_END';
var SUBSTATE_OPEN_END = 'SUBSTATE_OPEN_END';
/**
* Called when the game is about to close the current FlxSubState.
*
* This event is not cancelable.
*/
public static inline final SUBSTATE_CLOSE_BEGIN:ScriptEventType = 'SUBSTATE_CLOSE_BEGIN';
var SUBSTATE_CLOSE_BEGIN = 'SUBSTATE_CLOSE_BEGIN';
/**
* Called when the game has finished closing the current FlxSubState.
*
* This event is not cancelable.
*/
public static inline final SUBSTATE_CLOSE_END:ScriptEventType = 'SUBSTATE_CLOSE_END';
var SUBSTATE_CLOSE_END = 'SUBSTATE_CLOSE_END';
/**
* Called when the game starts a conversation.
*
* This event is not cancelable.
*/
public static inline final DIALOGUE_START:ScriptEventType = 'DIALOGUE_START';
var DIALOGUE_START = 'DIALOGUE_START';
/**
* Called to display the next line of conversation.
@ -229,7 +229,7 @@ enum abstract ScriptEventType(String) from String to String
* This event IS cancelable! Canceling this event will prevent the conversation from moving to the next line.
* - This event is called when the conversation starts, or when the user presses ACCEPT to advance the conversation.
*/
public static inline final DIALOGUE_LINE:ScriptEventType = 'DIALOGUE_LINE';
var DIALOGUE_LINE = 'DIALOGUE_LINE';
/**
* Called to skip scrolling the current line of conversation.
@ -237,21 +237,21 @@ enum abstract ScriptEventType(String) from String to String
* This event IS cancelable! Canceling this event will prevent the conversation from skipping to the next line.
* - This event is called when the user presses ACCEPT to advance the conversation while it is already advancing.
*/
public static inline final DIALOGUE_COMPLETE_LINE:ScriptEventType = 'DIALOGUE_COMPLETE_LINE';
var DIALOGUE_COMPLETE_LINE = 'DIALOGUE_COMPLETE_LINE';
/**
* Called to skip the conversation.
*
* This event IS cancelable! Canceling this event will prevent the conversation from skipping.
*/
public static inline final DIALOGUE_SKIP:ScriptEventType = 'DIALOGUE_SKIP';
var DIALOGUE_SKIP = 'DIALOGUE_SKIP';
/**
* Called when the game ends a conversation.
*
* This event is not cancelable.
*/
public static inline final DIALOGUE_END:ScriptEventType = 'DIALOGUE_END';
var DIALOGUE_END = 'DIALOGUE_END';
/**
* Allow for comparing `ScriptEventType` to `String`.

View file

@ -1,7 +1,7 @@
package funkin.modding.module;
import funkin.util.SortUtil;
import funkin.modding.events.ScriptEventType.UpdateScriptEvent;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.module.Module;
@ -55,7 +55,7 @@ class ModuleHandler
static function onStateSwitchComplete():Void
{
callEvent(new StateChangeScriptEvent(ScriptEventType.STATE_CHANGE_END, FlxG.state, true));
callEvent(new StateChangeScriptEvent(STATE_CHANGE_END, FlxG.state, true));
}
static function addToModuleCache(module:Module):Void
@ -119,7 +119,7 @@ class ModuleHandler
{
if (moduleCache != null)
{
var event = new ScriptEvent(ScriptEventType.DESTROY, false);
var event = new ScriptEvent(DESTROY, false);
// Note: Ignore stopPropagation()
for (key => value in moduleCache)
@ -148,6 +148,6 @@ class ModuleHandler
public static inline function callOnCreate():Void
{
callEvent(new ScriptEvent(ScriptEventType.CREATE, false));
callEvent(new ScriptEvent(CREATE, false));
}
}

View file

@ -6,7 +6,7 @@ import flixel.FlxSprite;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventType.CountdownScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import flixel.util.FlxTimer;
class Countdown
@ -43,7 +43,7 @@ class Countdown
Conductor.update(PlayState.instance.startTimestamp + Conductor.beatLengthMs * -5);
// Handle onBeatHit events manually
// @:privateAccess
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(ScriptEventType.SONG_BEAT_HIT, 0, 0));
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
// The timer function gets called based on the beat of the song.
countdownTimer = new FlxTimer();
@ -59,7 +59,7 @@ class Countdown
// onBeatHit events are now properly dispatched by the Conductor even at negative timestamps,
// so calling this is no longer necessary.
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(ScriptEventType.SONG_BEAT_HIT, 0, 0));
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
// Countdown graphic.
showCountdownGraphic(countdownStep, isPixelStyle);
@ -94,11 +94,11 @@ class Countdown
switch (index)
{
case BEFORE:
event = new CountdownScriptEvent(ScriptEventType.COUNTDOWN_START, index);
event = new CountdownScriptEvent(COUNTDOWN_START, index);
case THREE | TWO | ONE | GO: // I didn't know you could use `|` in a switch/case block!
event = new CountdownScriptEvent(ScriptEventType.COUNTDOWN_STEP, index);
event = new CountdownScriptEvent(COUNTDOWN_STEP, index);
case AFTER:
event = new CountdownScriptEvent(ScriptEventType.COUNTDOWN_END, index, false);
event = new CountdownScriptEvent(COUNTDOWN_END, index, false);
default:
return true;
}

View file

@ -682,7 +682,7 @@ class PlayState extends MusicBeatSubState
{
if (!assertChartExists()) return;
dispatchEvent(new ScriptEvent(ScriptEventType.SONG_RETRY));
dispatchEvent(new ScriptEvent(SONG_RETRY));
resetCamera();
@ -867,7 +867,7 @@ class PlayState extends MusicBeatSubState
deathCounter += 1;
dispatchEvent(new ScriptEvent(ScriptEventType.GAME_OVER));
dispatchEvent(new ScriptEvent(GAME_OVER));
// Disable updates, preventing animations in the background from playing.
persistentUpdate = false;
@ -994,7 +994,7 @@ class PlayState extends MusicBeatSubState
{
if (Std.isOfType(subState, PauseSubState))
{
var event:ScriptEvent = new ScriptEvent(ScriptEventType.RESUME, true);
var event:ScriptEvent = new ScriptEvent(RESUME, true);
dispatchEvent(event);
@ -1097,7 +1097,7 @@ class PlayState extends MusicBeatSubState
if (this.currentStage != null)
{
remove(currentStage);
var event:ScriptEvent = new ScriptEvent(ScriptEventType.DESTROY, false);
var event:ScriptEvent = new ScriptEvent(DESTROY, false);
ScriptEventDispatcher.callEvent(currentStage, event);
currentStage = null;
}
@ -1116,7 +1116,7 @@ class PlayState extends MusicBeatSubState
super.debug_refreshModules();
var event:ScriptEvent = new ScriptEvent(ScriptEventType.CREATE, false);
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentSong, event);
}
@ -1332,7 +1332,7 @@ class PlayState extends MusicBeatSubState
if (currentStage != null)
{
// Actually create and position the sprites.
var event:ScriptEvent = new ScriptEvent(ScriptEventType.CREATE, false);
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentStage, event);
// Apply camera zoom level from stage data.
@ -1640,7 +1640,7 @@ class PlayState extends MusicBeatSubState
add(currentConversation);
refresh();
var event:ScriptEvent = new ScriptEvent(ScriptEventType.CREATE, false);
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentConversation, event);
}
@ -1664,7 +1664,7 @@ class PlayState extends MusicBeatSubState
*/
function startSong():Void
{
dispatchEvent(new ScriptEvent(ScriptEventType.SONG_START));
dispatchEvent(new ScriptEvent(SONG_START));
startingSong = false;
@ -1783,7 +1783,7 @@ class PlayState extends MusicBeatSubState
// Call an event to allow canceling the note hit.
// NOTE: This is what handles the character animations!
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEventType.NOTE_HIT, note, 0, true);
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, 0, true);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@ -1872,7 +1872,7 @@ class PlayState extends MusicBeatSubState
{
// Call an event to allow canceling the note miss.
// NOTE: This is what handles the character animations!
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEventType.NOTE_MISS, note, 0, true);
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, 0, true);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@ -2021,7 +2021,7 @@ class PlayState extends MusicBeatSubState
function goodNoteHit(note:NoteSprite, input:PreciseInputEvent):Void
{
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEventType.NOTE_HIT, note, Highscore.tallies.combo + 1, true);
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, Highscore.tallies.combo + 1, true);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@ -2053,7 +2053,7 @@ class PlayState extends MusicBeatSubState
// a MISS is when you let a note scroll past you!!
Highscore.tallies.missed++;
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEventType.NOTE_MISS, note, Highscore.tallies.combo, true);
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Highscore.tallies.combo, true);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) return;
@ -2385,7 +2385,7 @@ class PlayState extends MusicBeatSubState
*/
function endSong():Void
{
dispatchEvent(new ScriptEvent(ScriptEventType.SONG_END));
dispatchEvent(new ScriptEvent(SONG_END));
#if sys
// spitter for ravy, teehee!!
@ -2593,7 +2593,7 @@ class PlayState extends MusicBeatSubState
{
remove(currentStage);
currentStage.kill();
dispatchEvent(new ScriptEvent(ScriptEventType.DESTROY, false));
dispatchEvent(new ScriptEvent(DESTROY, false));
currentStage = null;
}

View file

@ -254,7 +254,7 @@ class CharacterDataParser
char.debug = debug;
// Call onCreate only in the fetchCharacter() function, not at application initialization.
ScriptEventDispatcher.callEvent(char, new ScriptEvent(ScriptEventType.CREATE));
ScriptEventDispatcher.callEvent(char, new ScriptEvent(CREATE));
return char;
}

View file

@ -120,7 +120,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
this.alpha = 1.0;
// Start the dialogue.
dispatchEvent(new DialogueScriptEvent(ScriptEventType.DIALOGUE_START, this, false));
dispatchEvent(new DialogueScriptEvent(DIALOGUE_START, this, false));
}
function setupMusic():Void
@ -214,7 +214,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
return;
}
ScriptEventDispatcher.callEvent(nextSpeaker, new ScriptEvent(ScriptEventType.CREATE, true));
ScriptEventDispatcher.callEvent(nextSpeaker, new ScriptEvent(CREATE, true));
currentSpeaker = nextSpeaker;
currentSpeaker.zIndex = 200;
@ -258,7 +258,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
return;
}
ScriptEventDispatcher.callEvent(nextDialogueBox, new ScriptEvent(ScriptEventType.CREATE, true));
ScriptEventDispatcher.callEvent(nextDialogueBox, new ScriptEvent(CREATE, true));
currentDialogueBox = nextDialogueBox;
currentDialogueBox.zIndex = 300;
@ -293,7 +293,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
public function startConversation():Void
{
dispatchEvent(new DialogueScriptEvent(ScriptEventType.DIALOGUE_START, this, true));
dispatchEvent(new DialogueScriptEvent(DIALOGUE_START, this, true));
}
/**
@ -308,13 +308,13 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
switch (state)
{
case ConversationState.Start:
dispatchEvent(new DialogueScriptEvent(ScriptEventType.DIALOGUE_START, this, true));
dispatchEvent(new DialogueScriptEvent(DIALOGUE_START, this, true));
case ConversationState.Opening:
dispatchEvent(new DialogueScriptEvent(ScriptEventType.DIALOGUE_COMPLETE_LINE, this, true));
dispatchEvent(new DialogueScriptEvent(DIALOGUE_COMPLETE_LINE, this, true));
case ConversationState.Speaking:
dispatchEvent(new DialogueScriptEvent(ScriptEventType.DIALOGUE_COMPLETE_LINE, this, true));
dispatchEvent(new DialogueScriptEvent(DIALOGUE_COMPLETE_LINE, this, true));
case ConversationState.Idle:
dispatchEvent(new DialogueScriptEvent(ScriptEventType.DIALOGUE_LINE, this, true));
dispatchEvent(new DialogueScriptEvent(DIALOGUE_LINE, this, true));
case ConversationState.Ending:
// Skip the outro.
endOutro();
@ -371,7 +371,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
*/
public function skipConversation():Void
{
dispatchEvent(new DialogueScriptEvent(ScriptEventType.DIALOGUE_SKIP, this, true));
dispatchEvent(new DialogueScriptEvent(DIALOGUE_SKIP, this, true));
}
static var outroTween:FlxTween;
@ -405,7 +405,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
public function endOutro():Void
{
outroTween = null;
ScriptEventDispatcher.callEvent(this, new ScriptEvent(ScriptEventType.DESTROY, false));
ScriptEventDispatcher.callEvent(this, new ScriptEvent(DESTROY, false));
}
/**
@ -445,7 +445,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
if (currentDialogueEntry >= currentDialogueEntryCount)
{
dispatchEvent(new DialogueScriptEvent(ScriptEventType.DIALOGUE_END, this, false));
dispatchEvent(new DialogueScriptEvent(DIALOGUE_END, this, false));
}
else
{
@ -485,7 +485,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
propagateEvent(event);
if (event.eventCanceled) return;
dispatchEvent(new DialogueScriptEvent(ScriptEventType.DIALOGUE_END, this, false));
dispatchEvent(new DialogueScriptEvent(DIALOGUE_END, this, false));
}
public function onDialogueEnd(event:DialogueScriptEvent):Void

View file

@ -30,7 +30,7 @@ class ConversationDebugState extends MusicBeatState
conversation.completeCallback = onConversationComplete;
add(conversation);
ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(ScriptEventType.CREATE, false));
ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(CREATE, false));
}
function onConversationComplete():Void

View file

@ -47,8 +47,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*/
public final _data:Null<SongMetadata>;
final _metadata:Array<SongMetadata>;
// key = variation id, value = metadata
final _metadata:Map<String, SongMetadata>;
final variations:Array<String>;
final difficulties:Map<String, SongDifficulty>;
@ -62,7 +62,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
function get_songName():String
{
if (_data != null) return _data?.songName ?? DEFAULT_SONGNAME;
if (_metadata.length > 0) return _metadata[0]?.songName ?? DEFAULT_SONGNAME;
if (_metadata.size() > 0) return _metadata.get(Constants.DEFAULT_VARIATION)?.songName ?? DEFAULT_SONGNAME;
return DEFAULT_SONGNAME;
}
@ -71,7 +71,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
function get_songArtist():String
{
if (_data != null) return _data?.artist ?? DEFAULT_ARTIST;
if (_metadata.length > 0) return _metadata[0]?.artist ?? DEFAULT_ARTIST;
if (_metadata.size() > 0) return _metadata.get(Constants.DEFAULT_VARIATION)?.artist ?? DEFAULT_ARTIST;
return DEFAULT_ARTIST;
}
@ -88,7 +88,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
_data = _fetchData(id);
_metadata = _data == null ? [] : [_data];
_metadata = _data == null ? [] : [Constants.DEFAULT_VARIATION => _data];
variations.clear();
variations.push(Constants.DEFAULT_VARIATION);
@ -100,9 +100,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
}
for (meta in fetchVariationMetadata(id))
_metadata.push(meta);
_metadata.set(meta.variation, meta);
if (_metadata.length == 0)
if (_metadata.size() == 0)
{
trace('[WARN] Could not find song data for songId: $id');
return;
@ -119,7 +119,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
result._metadata.clear();
for (meta in metadata)
result._metadata.push(meta);
result._metadata.set(meta.variation, meta);
result.variations.clear();
for (vari in variations)
@ -138,7 +138,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
public function getRawMetadata():Array<SongMetadata>
{
return _metadata;
return _metadata.values();
}
/**
@ -147,10 +147,10 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*/
function populateDifficulties():Void
{
if (_metadata == null || _metadata.length == 0) return;
if (_metadata == null || _metadata.size() == 0) return;
// Variations may have different artist, time format, generatedBy, etc.
for (metadata in _metadata)
for (metadata in _metadata.values())
{
if (metadata == null || metadata.playData == null) continue;

View file

@ -7,6 +7,7 @@ import flixel.system.FlxAssets.FlxShader;
import flixel.util.FlxSort;
import funkin.modding.IScriptedClass;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventType;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter;
import funkin.play.stage.StageData.StageDataCharacter;
@ -402,7 +403,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
// Add the character to the scene.
this.add(character);
ScriptEventDispatcher.callEvent(character, new ScriptEvent(ScriptEventType.ADDED, false));
ScriptEventDispatcher.callEvent(character, new ScriptEvent(ADDED, false));
#if debug
debugIconGroup.add(debugIcon);

View file

@ -1,879 +0,0 @@
package funkin.ui.debug.charting;
import haxe.ui.notifications.NotificationType;
import haxe.ui.notifications.NotificationManager;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
using Lambda;
/**
* Actions in the chart editor are backed by the Command pattern
* (see Bob Nystrom's book "Game Programming Patterns" for more info)
*
* To make a function compatible with the undo/redo history, create a new class
* that implements ChartEditorCommand, then call `ChartEditorState.performCommand(new Command())`
*/
interface ChartEditorCommand
{
/**
* Calling this function should perform the action that this command represents.
* @param state The ChartEditorState to perform the action on.
*/
public function execute(state:ChartEditorState):Void;
/**
* Calling this function should perform the inverse of the action that this command represents,
* effectively undoing the action.
* @param state The ChartEditorState to undo the action on.
*/
public function undo(state:ChartEditorState):Void;
/**
* Get a short description of the action (for the UI).
* For example, return `Add Left Note` to display `Undo Add Left Note` in the menu.
*/
public function toString():String;
}
@:nullSafety
class AddNotesCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var appendToSelection:Bool;
public function new(notes:Array<SongNoteData>, appendToSelection:Bool = false)
{
this.notes = notes;
this.appendToSelection = appendToSelection;
}
public function execute(state:ChartEditorState):Void
{
for (note in notes)
{
state.currentSongChartNoteData.push(note);
}
if (appendToSelection)
{
state.currentNoteSelection = state.currentNoteSelection.concat(notes);
}
else
{
state.currentNoteSelection = notes;
state.currentEventSelection = [];
}
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteLay'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentNoteSelection = [];
state.currentEventSelection = [];
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
if (notes.length == 1)
{
var dir:String = notes[0].getDirectionName();
return 'Add $dir Note';
}
return 'Add ${notes.length} Notes';
}
}
@:nullSafety
class RemoveNotesCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
public function new(notes:Array<SongNoteData>)
{
this.notes = notes;
}
public function execute(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentNoteSelection = [];
state.currentEventSelection = [];
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteErase'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
for (note in notes)
{
state.currentSongChartNoteData.push(note);
}
state.currentNoteSelection = notes;
state.currentEventSelection = [];
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
if (notes.length == 1 && notes[0] != null)
{
var dir:String = notes[0].getDirectionName();
return 'Remove $dir Note';
}
return 'Remove ${notes.length} Notes';
}
}
/**
* Appends one or more items to the selection.
*/
@:nullSafety
class SelectItemsCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
for (note in this.notes)
{
state.currentNoteSelection.push(note);
}
for (event in this.events)
{
state.currentEventSelection.push(event);
}
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes);
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events);
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
var len:Int = notes.length + events.length;
if (notes.length == 0)
{
if (events.length == 1)
{
return 'Select Event';
}
else
{
return 'Select ${events.length} Events';
}
}
else if (events.length == 0)
{
if (notes.length == 1)
{
return 'Select Note';
}
else
{
return 'Select ${notes.length} Notes';
}
}
return 'Select ${len} Items';
}
}
@:nullSafety
class AddEventsCommand implements ChartEditorCommand
{
var events:Array<SongEventData>;
var appendToSelection:Bool;
public function new(events:Array<SongEventData>, appendToSelection:Bool = false)
{
this.events = events;
this.appendToSelection = appendToSelection;
}
public function execute(state:ChartEditorState):Void
{
for (event in events)
{
state.currentSongChartEventData.push(event);
}
if (appendToSelection)
{
state.currentEventSelection = state.currentEventSelection.concat(events);
}
else
{
state.currentNoteSelection = [];
state.currentEventSelection = events;
}
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteLay'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var len:Int = events.length;
return 'Add $len Events';
}
}
@:nullSafety
class RemoveEventsCommand implements ChartEditorCommand
{
var events:Array<SongEventData>;
public function new(events:Array<SongEventData>)
{
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentEventSelection = [];
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteErase'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
for (event in events)
{
state.currentSongChartEventData.push(event);
}
state.currentEventSelection = events;
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
if (events.length == 1 && events[0] != null)
{
return 'Remove Event';
}
return 'Remove ${events.length} Events';
}
}
@:nullSafety
class RemoveItemsCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentNoteSelection = [];
state.currentEventSelection = [];
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteErase'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
for (note in notes)
{
state.currentSongChartNoteData.push(note);
}
for (event in events)
{
state.currentSongChartEventData.push(event);
}
state.currentNoteSelection = notes;
state.currentEventSelection = events;
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
return 'Remove ${notes.length + events.length} Items';
}
}
@:nullSafety
class SwitchDifficultyCommand implements ChartEditorCommand
{
var prevDifficulty:String;
var newDifficulty:String;
var prevVariation:String;
var newVariation:String;
public function new(prevDifficulty:String, newDifficulty:String, prevVariation:String, newVariation:String)
{
this.prevDifficulty = prevDifficulty;
this.newDifficulty = newDifficulty;
this.prevVariation = prevVariation;
this.newVariation = newVariation;
}
public function execute(state:ChartEditorState):Void
{
state.selectedVariation = newVariation != null ? newVariation : prevVariation;
state.selectedDifficulty = newDifficulty != null ? newDifficulty : prevDifficulty;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.selectedVariation = prevVariation != null ? prevVariation : newVariation;
state.selectedDifficulty = prevDifficulty != null ? prevDifficulty : newDifficulty;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
return 'Switch Difficulty';
}
}
@:nullSafety
class DeselectItemsCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes);
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events);
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
for (note in this.notes)
{
state.currentNoteSelection.push(note);
}
for (event in this.events)
{
state.currentEventSelection.push(event);
}
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
var noteCount = notes.length + events.length;
if (noteCount == 1)
{
var dir:String = notes[0].getDirectionName();
return 'Deselect $dir Items';
}
return 'Deselect ${noteCount} Items';
}
}
/**
* Sets the selection rather than appends it.
* Deselects any notes that are not in the new selection.
*/
@:nullSafety
class SetItemSelectionCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, previousNoteSelection:Array<SongNoteData>,
previousEventSelection:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
}
public function execute(state:ChartEditorState):Void
{
state.currentNoteSelection = notes;
state.currentEventSelection = events;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
return 'Select ${notes.length} Items';
}
}
@:nullSafety
class SelectAllItemsCommand implements ChartEditorCommand
{
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
{
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
}
public function execute(state:ChartEditorState):Void
{
state.currentNoteSelection = state.currentSongChartNoteData;
state.currentEventSelection = state.currentSongChartEventData;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
return 'Select All Items';
}
}
@:nullSafety
class InvertSelectedItemsCommand implements ChartEditorCommand
{
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
{
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
}
public function execute(state:ChartEditorState):Void
{
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousNoteSelection);
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection);
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
return 'Invert Selected Items';
}
}
@:nullSafety
class DeselectAllItemsCommand implements ChartEditorCommand
{
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
{
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
}
public function execute(state:ChartEditorState):Void
{
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
return 'Deselect All Items';
}
}
@:nullSafety
class CutItemsCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
// Copy the notes.
SongDataUtils.writeItemsToClipboard(
{
notes: SongDataUtils.buildNoteClipboard(notes),
events: SongDataUtils.buildEventClipboard(events)
});
// Delete the notes.
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes);
state.currentSongChartEventData = state.currentSongChartEventData.concat(events);
state.currentNoteSelection = notes;
state.currentEventSelection = events;
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var len:Int = notes.length + events.length;
if (notes.length == 0) return 'Cut $len Events to Clipboard';
else if (events.length == 0) return 'Cut $len Notes to Clipboard';
else
return 'Cut $len Items to Clipboard';
}
}
@:nullSafety
class FlipNotesCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData> = [];
var flippedNotes:Array<SongNoteData> = [];
public function new(notes:Array<SongNoteData>)
{
this.notes = notes;
this.flippedNotes = SongDataUtils.flipNotes(notes);
}
public function execute(state:ChartEditorState):Void
{
// Delete the notes.
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
// Add the flipped notes.
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(flippedNotes);
state.currentNoteSelection = flippedNotes;
state.currentEventSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, flippedNotes);
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes);
state.currentNoteSelection = notes;
state.currentEventSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var len:Int = notes.length;
return 'Flip $len Notes';
}
}
@:nullSafety
class PasteItemsCommand implements ChartEditorCommand
{
var targetTimestamp:Float;
// Notes we added with this command, for undo.
var addedNotes:Array<SongNoteData> = [];
var addedEvents:Array<SongEventData> = [];
public function new(targetTimestamp:Float)
{
this.targetTimestamp = targetTimestamp;
}
public function execute(state:ChartEditorState):Void
{
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
if (currentClipboard.valid != true)
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failed to Paste',
body: 'Could not parse clipboard contents.',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
return;
}
trace(currentClipboard.notes);
addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp));
addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp));
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes);
state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents);
state.currentNoteSelection = addedNotes.copy();
state.currentEventSelection = addedEvents.copy();
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Paste Successful',
body: 'Successfully pasted clipboard contents.',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
}
public function undo(state:ChartEditorState):Void
{
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
var len:Int = currentClipboard.notes.length + currentClipboard.events.length;
if (currentClipboard.notes.length == 0) return 'Paste $len Events';
else if (currentClipboard.events.length == 0) return 'Paste $len Notes';
else
return 'Paste $len Items';
}
}
@:nullSafety
class ExtendNoteLengthCommand implements ChartEditorCommand
{
var note:SongNoteData;
var oldLength:Float;
var newLength:Float;
public function new(note:SongNoteData, newLength:Float)
{
this.note = note;
this.oldLength = note.length;
this.newLength = newLength;
}
public function execute(state:ChartEditorState):Void
{
note.length = newLength;
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
note.length = oldLength;
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
return 'Extend Note Length';
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongDataUtils;
/**
* Adds the given events to the current chart in the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class AddEventsCommand implements ChartEditorCommand
{
var events:Array<SongEventData>;
var appendToSelection:Bool;
public function new(events:Array<SongEventData>, appendToSelection:Bool = false)
{
this.events = events;
this.appendToSelection = appendToSelection;
}
public function execute(state:ChartEditorState):Void
{
for (event in events)
{
state.currentSongChartEventData.push(event);
}
if (appendToSelection)
{
state.currentEventSelection = state.currentEventSelection.concat(events);
}
else
{
state.currentNoteSelection = [];
state.currentEventSelection = events;
}
state.playSound(Paths.sound('chartingSounds/noteLay'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var len:Int = events.length;
return 'Add $len Events';
}
}

View file

@ -0,0 +1,72 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
/**
* Adds the given notes to the current chart in the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class AddNotesCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var appendToSelection:Bool;
public function new(notes:Array<SongNoteData>, appendToSelection:Bool = false)
{
this.notes = notes;
this.appendToSelection = appendToSelection;
}
public function execute(state:ChartEditorState):Void
{
for (note in notes)
{
state.currentSongChartNoteData.push(note);
}
if (appendToSelection)
{
state.currentNoteSelection = state.currentNoteSelection.concat(notes);
}
else
{
state.currentNoteSelection = notes;
state.currentEventSelection = [];
}
state.playSound(Paths.sound('chartingSounds/noteLay'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
if (notes.length == 1)
{
var dir:String = notes[0].getDirectionName();
return 'Add $dir Note';
}
return 'Add ${notes.length} Notes';
}
}

View file

@ -0,0 +1,30 @@
package funkin.ui.debug.charting.commands;
/**
* Actions in the chart editor are backed by the Command pattern
* (see Bob Nystrom's book "Game Programming Patterns" for more info)
*
* To make a functionality compatible with the undo/redo history, create a new class
* that implements ChartEditorCommand, then call `ChartEditorState.performCommand(new Command())`
*/
interface ChartEditorCommand
{
/**
* Calling this function should perform the action that this command represents.
* @param state The ChartEditorState to perform the action on.
*/
public function execute(state:ChartEditorState):Void;
/**
* Calling this function should perform the inverse of the action that this command represents,
* effectively undoing the action. Assume that the original action was the last action performed.
* @param state The ChartEditorState to undo the action on.
*/
public function undo(state:ChartEditorState):Void;
/**
* Get a short description of the action (for the UI).
* For example, return `Add Left Note` to display `Undo Add Left Note` in the menu.
*/
public function toString():String;
}

View file

@ -0,0 +1,68 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongDataUtils;
/**
* Command that copies a given set of notes and song events to the clipboard,
* and then deletes them from the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class CutItemsCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
// Copy the notes.
SongDataUtils.writeItemsToClipboard(
{
notes: SongDataUtils.buildNoteClipboard(notes),
events: SongDataUtils.buildEventClipboard(events)
});
// Delete the notes.
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes);
state.currentSongChartEventData = state.currentSongChartEventData.concat(events);
state.currentNoteSelection = notes;
state.currentEventSelection = events;
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var len:Int = notes.length + events.length;
if (notes.length == 0) return 'Cut $len Events to Clipboard';
else if (events.length == 0) return 'Cut $len Notes to Clipboard';
else
return 'Cut $len Items to Clipboard';
}
}

View file

@ -0,0 +1,42 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
/**
* Command that deselects all selected notes and events in the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class DeselectAllItemsCommand implements ChartEditorCommand
{
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
{
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
}
public function execute(state:ChartEditorState):Void
{
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.noteDisplayDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true;
}
public function toString():String
{
return 'Deselect All Items';
}
}

View file

@ -0,0 +1,60 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongDataUtils;
/**
* Command to deselect a specific set of notes and events in the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class DeselectItemsCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes);
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events);
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
for (note in this.notes)
{
state.currentNoteSelection.push(note);
}
for (event in this.events)
{
state.currentEventSelection.push(event);
}
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
var noteCount = notes.length + events.length;
if (noteCount == 1)
{
var dir:String = notes[0].getDirectionName();
return 'Deselect $dir Items';
}
return 'Deselect ${noteCount} Items';
}
}

View file

@ -0,0 +1,52 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
/**
* Command that modifies the length of a hold note in the chart editor.
* If it is not a hold note, it will become one, and if it is already a hold note, its length will change.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ExtendNoteLengthCommand implements ChartEditorCommand
{
var note:SongNoteData;
var oldLength:Float;
var newLength:Float;
public function new(note:SongNoteData, newLength:Float)
{
this.note = note;
this.oldLength = note.length;
this.newLength = newLength;
}
public function execute(state:ChartEditorState):Void
{
note.length = newLength;
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.playSound(Paths.sound('chartingSounds/undo'));
note.length = oldLength;
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
return 'Extend Note Length';
}
}

View file

@ -0,0 +1,59 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
/**
* Command that flips a given array of notes from the player's side of the chart editor to the opponent's side, or vice versa.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class FlipNotesCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData> = [];
var flippedNotes:Array<SongNoteData> = [];
public function new(notes:Array<SongNoteData>)
{
this.notes = notes;
this.flippedNotes = SongDataUtils.flipNotes(notes);
}
public function execute(state:ChartEditorState):Void
{
// Delete the notes.
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
// Add the flipped notes.
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(flippedNotes);
state.currentNoteSelection = flippedNotes;
state.currentEventSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, flippedNotes);
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes);
state.currentNoteSelection = notes;
state.currentEventSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var len:Int = notes.length;
return 'Flip $len Notes';
}
}

View file

@ -0,0 +1,43 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongDataUtils;
/**
* Command to deselect all items that are currently selected in the chart editor,
* then select all the items that were previously unselected.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class InvertSelectedItemsCommand implements ChartEditorCommand
{
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
{
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
}
public function execute(state:ChartEditorState):Void
{
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousNoteSelection);
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection);
state.noteDisplayDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true;
}
public function toString():String
{
return 'Invert Selected Items';
}
}

View file

@ -0,0 +1,72 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
/**
* Move the given events by the given offset and shift them by the given number of columns in the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class MoveEventsCommand implements ChartEditorCommand
{
var events:Array<SongEventData>;
var movedEvents:Array<SongEventData>;
var offset:Float;
public function new(notes:Array<SongEventData>, offset:Float)
{
// Clone the notes to prevent editing from affecting the history.
this.events = [for (event in events) event.clone()];
this.offset = offset;
this.movedEvents = [];
}
public function execute(state:ChartEditorState):Void
{
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
movedEvents = [];
for (event in events)
{
// Clone the notes to prevent editing from affecting the history.
var resultEvent = event.clone();
resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
movedEvents.push(resultEvent);
}
state.currentSongChartEventData = state.currentSongChartEventData.concat(movedEvents);
state.currentEventSelection = movedEvents;
state.playSound(Paths.sound('chartingSounds/noteLay'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, movedEvents);
state.currentSongChartEventData = state.currentSongChartEventData.concat(events);
state.currentEventSelection = events;
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var len:Int = events.length;
return 'Move $len Events';
}
}

View file

@ -0,0 +1,96 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
/**
* Move the given notes by the given offset and shift them by the given number of columns in the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class MoveItemsCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var movedNotes:Array<SongNoteData>;
var events:Array<SongEventData>;
var movedEvents:Array<SongEventData>;
var offset:Float;
var 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.
this.notes = [for (note in notes) note.clone()];
this.events = [for (event in events) event.clone()];
this.offset = offset;
this.columns = columns;
this.movedNotes = [];
this.movedEvents = [];
}
public function execute(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
movedNotes = [];
movedEvents = [];
for (note in notes)
{
// Clone the notes to prevent editing from affecting the history.
var resultNote = note.clone();
resultNote.time = (resultNote.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0,
ChartEditorState.STRUMLINE_SIZE * 2 - 1));
movedNotes.push(resultNote);
}
for (event in events)
{
// Clone the notes to prevent editing from affecting the history.
var resultEvent = event.clone();
resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
movedEvents.push(resultEvent);
}
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(movedNotes);
state.currentSongChartEventData = state.currentSongChartEventData.concat(movedEvents);
state.currentNoteSelection = movedNotes;
state.currentEventSelection = movedEvents;
state.playSound(Paths.sound('chartingSounds/noteLay'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, movedNotes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, movedEvents);
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes);
state.currentSongChartEventData = state.currentSongChartEventData.concat(events);
state.currentNoteSelection = notes;
state.currentEventSelection = events;
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var len:Int = notes.length + events.length;
return 'Move $len Items';
}
}

View file

@ -0,0 +1,75 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
/**
* Move the given notes by the given offset and shift them by the given number of columns in the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class MoveNotesCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var movedNotes:Array<SongNoteData>;
var offset:Float;
var columns:Int;
public function new(notes:Array<SongNoteData>, offset:Float, columns:Int)
{
// Clone the notes to prevent editing from affecting the history.
this.notes = [for (note in notes) note.clone()];
this.offset = offset;
this.columns = columns;
this.movedNotes = [];
}
public function execute(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
movedNotes = [];
for (note in notes)
{
// Clone the notes to prevent editing from affecting the history.
var resultNote = note.clone();
resultNote.time = (resultNote.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0,
ChartEditorState.STRUMLINE_SIZE * 2 - 1));
movedNotes.push(resultNote);
}
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(movedNotes);
state.currentNoteSelection = movedNotes;
state.playSound(Paths.sound('chartingSounds/noteLay'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, movedNotes);
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes);
state.currentNoteSelection = notes;
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var len:Int = notes.length;
return 'Move $len Notes';
}
}

View file

@ -0,0 +1,99 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongDataUtils.SongClipboardItems;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
/**
* A command which inserts the contents of the clipboard into the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class PasteItemsCommand implements ChartEditorCommand
{
var targetTimestamp:Float;
// Notes we added with this command, for undo.
var addedNotes:Array<SongNoteData> = [];
var addedEvents:Array<SongEventData> = [];
public function new(targetTimestamp:Float)
{
this.targetTimestamp = targetTimestamp;
}
public function execute(state:ChartEditorState):Void
{
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
if (currentClipboard.valid != true)
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failed to Paste',
body: 'Could not parse clipboard contents.',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
return;
}
trace(currentClipboard.notes);
addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp));
addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp));
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes);
state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents);
state.currentNoteSelection = addedNotes.copy();
state.currentEventSelection = addedEvents.copy();
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Paste Successful',
body: 'Successfully pasted clipboard contents.',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
public function undo(state:ChartEditorState):Void
{
state.playSound(Paths.sound('chartingSounds/undo'));
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
var len:Int = currentClipboard.notes.length + currentClipboard.events.length;
if (currentClipboard.notes.length == 0) return 'Paste $len Events';
else if (currentClipboard.events.length == 0) return 'Paste $len Notes';
else
return 'Paste $len Items';
}
}

View file

@ -0,0 +1,60 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongDataUtils;
/**
* Deletes the given events from the current chart in the chart editor.
* Use only when ONLY events are being deleted.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class RemoveEventsCommand implements ChartEditorCommand
{
var events:Array<SongEventData>;
public function new(events:Array<SongEventData>)
{
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentEventSelection = [];
state.playSound(Paths.sound('chartingSounds/noteErase'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
for (event in events)
{
state.currentSongChartEventData.push(event);
}
state.currentEventSelection = events;
state.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
if (events.length == 1 && events[0] != null)
{
return 'Remove Event';
}
return 'Remove ${events.length} Events';
}
}

View file

@ -0,0 +1,69 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongDataUtils;
/**
* Deletes the given notes and events from the current chart in the chart editor.
* Use only when BOTH notes and events are being deleted.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class RemoveItemsCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.playSound(Paths.sound('chartingSounds/noteErase'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
for (note in notes)
{
state.currentSongChartNoteData.push(note);
}
for (event in events)
{
state.currentSongChartEventData.push(event);
}
state.currentNoteSelection = notes;
state.currentEventSelection = events;
state.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
return 'Remove ${notes.length + events.length} Items';
}
}

View file

@ -0,0 +1,63 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
/**
* Deletes the given notes from the current chart in the chart editor.
* Use only when ONLY notes are being deleted.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class RemoveNotesCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
public function new(notes:Array<SongNoteData>)
{
this.notes = notes;
}
public function execute(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.playSound(Paths.sound('chartingSounds/noteErase'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
for (note in notes)
{
state.currentSongChartNoteData.push(note);
}
state.currentNoteSelection = notes;
state.currentEventSelection = [];
state.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
if (notes.length == 1 && notes[0] != null)
{
var dir:String = notes[0].getDirectionName();
return 'Remove $dir Note';
}
return 'Remove ${notes.length} Notes';
}
}

View file

@ -0,0 +1,42 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
/**
* Command to set the selection to all notes and events in the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class SelectAllItemsCommand implements ChartEditorCommand
{
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
{
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
}
public function execute(state:ChartEditorState):Void
{
state.currentNoteSelection = state.currentSongChartNoteData;
state.currentEventSelection = state.currentSongChartEventData;
state.noteDisplayDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true;
}
public function toString():String
{
return 'Select All Items';
}
}

View file

@ -0,0 +1,78 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongDataUtils;
/**
* Appends one or more items to the selection in the chart editor.
* This does not deselect any items that are already selected, if any.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class SelectItemsCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
for (note in this.notes)
{
state.currentNoteSelection.push(note);
}
for (event in this.events)
{
state.currentEventSelection.push(event);
}
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes);
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events);
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
var len:Int = notes.length + events.length;
if (notes.length == 0)
{
if (events.length == 1)
{
return 'Select Event';
}
else
{
return 'Select ${events.length} Events';
}
}
else if (events.length == 0)
{
if (notes.length == 1)
{
return 'Select Note';
}
else
{
return 'Select ${notes.length} Notes';
}
}
return 'Select ${len} Items';
}
}

View file

@ -0,0 +1,48 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
/**
* Command to set the current selection in the chart editor (rather than appending it).
* Deselects any notes that are not in the new selection.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class SetItemSelectionCommand implements ChartEditorCommand
{
var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
var previousNoteSelection:Array<SongNoteData>;
var previousEventSelection:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, previousNoteSelection:Array<SongNoteData>,
previousEventSelection:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
}
public function execute(state:ChartEditorState):Void
{
state.currentNoteSelection = notes;
state.currentEventSelection = events;
state.noteDisplayDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true;
}
public function toString():String
{
return 'Select ${notes.length} Items';
}
}

View file

@ -0,0 +1,45 @@
package funkin.ui.debug.charting.commands;
/**
* Switch the current difficulty (and possibly variation) of the chart in the chart editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class SwitchDifficultyCommand implements ChartEditorCommand
{
var prevDifficulty:String;
var newDifficulty:String;
var prevVariation:String;
var newVariation:String;
public function new(prevDifficulty:String, newDifficulty:String, prevVariation:String, newVariation:String)
{
this.prevDifficulty = prevDifficulty;
this.newDifficulty = newDifficulty;
this.prevVariation = prevVariation;
this.newVariation = newVariation;
}
public function execute(state:ChartEditorState):Void
{
state.selectedVariation = newVariation != null ? newVariation : prevVariation;
state.selectedDifficulty = newDifficulty != null ? newDifficulty : prevDifficulty;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.selectedVariation = prevVariation != null ? prevVariation : newVariation;
state.selectedDifficulty = prevDifficulty != null ? prevDifficulty : newDifficulty;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
return 'Switch Difficulty';
}
}

View file

@ -1,4 +1,4 @@
package funkin.ui.debug.charting;
package funkin.ui.debug.charting.components;
import funkin.data.event.SongEventData.SongEventParser;
import flixel.graphics.frames.FlxAtlasFrames;
@ -13,7 +13,7 @@ import flixel.math.FlxPoint;
import funkin.data.song.SongData.SongEventData;
/**
* A event sprite that can be used to display a song event in a chart.
* A sprite that can be used to display a song event in a chart.
* Designed to be used and reused efficiently. Has no gameplay functionality.
*/
@:nullSafety
@ -34,6 +34,17 @@ class ChartEditorEventSprite extends FlxSprite
*/
static var eventSpriteBasic:Null<BitmapData> = null;
public var overrideStepTime(default, set):Null<Float> = null;
function set_overrideStepTime(value:Null<Float>):Null<Float>
{
if (overrideStepTime == value) return overrideStepTime;
overrideStepTime = value;
updateEventPosition();
return overrideStepTime;
}
public function new(parent:ChartEditorState)
{
super();
@ -146,7 +157,7 @@ class ChartEditorEventSprite extends FlxSprite
this.x = (ChartEditorState.STRUMLINE_SIZE * 2 + 1 - 1) * ChartEditorState.GRID_SIZE;
var stepTime:Float = inline eventData.getStepTime();
var stepTime:Float = (overrideStepTime != null) ? overrideStepTime : eventData.getStepTime();
this.y = stepTime * ChartEditorState.GRID_SIZE;
if (origin != null)

View file

@ -1,4 +1,4 @@
package funkin.ui.debug.charting;
package funkin.ui.debug.charting.components;
import funkin.play.notes.Strumline;
import funkin.data.notestyle.NoteStyleRegistry;
@ -11,7 +11,7 @@ import funkin.play.notes.SustainTrail;
import funkin.data.song.SongData.SongNoteData;
/**
* A hold note sprite that can be used to display a note in a chart.
* A sprite that can be used to display the trail of a hold note in a chart.
* Designed to be used and reused efficiently. Has no gameplay functionality.
*/
@:nullSafety

View file

@ -1,4 +1,4 @@
package funkin.ui.debug.charting;
package funkin.ui.debug.charting.components;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;

View file

@ -1,4 +1,4 @@
package funkin.ui.debug.charting;
package funkin.ui.debug.charting.components;
import flixel.FlxObject;
import flixel.FlxSprite;
@ -10,10 +10,11 @@ import flixel.math.FlxPoint;
import funkin.data.song.SongData.SongNoteData;
/**
* A note sprite that can be used to display a note in a chart.
* A sprite that can be used to display a note in a chart.
* Designed to be used and reused efficiently. Has no gameplay functionality.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorNoteSprite extends FlxSprite
{
/**
@ -37,6 +38,28 @@ class ChartEditorNoteSprite extends FlxSprite
*/
public var noteStyle(get, never):String;
public var overrideStepTime(default, set):Null<Float> = null;
function set_overrideStepTime(value:Null<Float>):Null<Float>
{
if (overrideStepTime == value) return overrideStepTime;
overrideStepTime = value;
updateNotePosition();
return overrideStepTime;
}
public var overrideData(default, set):Null<Int> = null;
function set_overrideData(value:Null<Int>):Null<Int>
{
if (overrideData == value) return overrideData;
overrideData = value;
playNoteAnimation();
return overrideData;
}
public function new(parent:ChartEditorState)
{
super();
@ -147,32 +170,15 @@ class ChartEditorNoteSprite extends FlxSprite
{
if (this.noteData == null) return;
var cursorColumn:Int = this.noteData.data;
var cursorColumn:Int = (overrideData != null) ? overrideData : this.noteData.data;
if (cursorColumn < 0) cursorColumn = 0;
if (cursorColumn >= (ChartEditorState.STRUMLINE_SIZE * 2 + 1))
{
cursorColumn = (ChartEditorState.STRUMLINE_SIZE * 2 + 1);
}
else
{
// Invert player and opponent columns.
if (cursorColumn >= ChartEditorState.STRUMLINE_SIZE)
{
cursorColumn -= ChartEditorState.STRUMLINE_SIZE;
}
else
{
cursorColumn += ChartEditorState.STRUMLINE_SIZE;
}
}
cursorColumn = ChartEditorState.noteDataToGridColumn(cursorColumn);
this.x = cursorColumn * ChartEditorState.GRID_SIZE;
// Notes far in the song will start far down, but the group they belong to will have a high negative offset.
// noteData.getStepTime() returns a calculated value which accounts for BPM changes
var stepTime:Float =
inline this.noteData.getStepTime();
var stepTime:Float = (overrideStepTime != null) ? overrideStepTime : noteData.getStepTime();
if (stepTime >= 0)
{
this.y = stepTime * ChartEditorState.GRID_SIZE;
@ -199,7 +205,8 @@ class ChartEditorNoteSprite extends FlxSprite
var baseAnimationName:String = 'tap';
// Play the appropriate animation for the type, direction, and skin.
var animationName:String = '${baseAnimationName}${this.noteData.getDirectionName()}${this.noteStyle.toTitleCase()}';
var dirName:String = overrideData != null ? SongNoteData.buildDirectionName(overrideData) : this.noteData.getDirectionName();
var animationName:String = '${baseAnimationName}${dirName}${this.noteStyle.toTitleCase()}';
this.animation.play(animationName);

View file

@ -0,0 +1,20 @@
package funkin.ui.debug.charting.components;
import flixel.FlxSprite;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData;
/**
* A sprite that can be used to display a square over a selected note or event in the chart.
* Designed to be used and reused efficiently. Has no gameplay functionality.
*/
class ChartEditorSelectionSquareSprite extends FlxSprite
{
public var noteData:Null<SongNoteData>;
public var eventData:Null<SongEventData>;
public function new()
{
super();
}
}

View file

@ -1,22 +1,21 @@
package funkin.ui.debug.charting;
package funkin.ui.debug.charting.handlers;
import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.system.FlxSound;
import flixel.system.FlxSound;
import funkin.audio.VoicesGroup;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.util.FileUtil;
import funkin.util.assets.SoundUtil;
import haxe.io.Bytes;
import haxe.io.Path;
import openfl.utils.Assets;
/**
* Functions for loading audio for the chart editor.
* Handlers split up the functionality of the Chart Editor into different classes based on focus to limit the amount of code in each class.
*/
@:nullSafety
@:allow(funkin.ui.debug.charting.ChartEditorState)
@:allow(funkin.ui.debug.charting.ChartEditorDialogHandler)
@:allow(funkin.ui.debug.charting.ChartEditorImportExportHandler)
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorAudioHandler
{
/**
@ -27,7 +26,7 @@ class ChartEditorAudioHandler
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = ''):Bool
public static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = ''):Bool
{
#if sys
var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
@ -46,7 +45,7 @@ class ChartEditorAudioHandler
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = ''):Bool
public static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = ''):Bool
{
var trackData:Null<Bytes> = Assets.getBytes(path);
if (trackData != null)
@ -63,7 +62,7 @@ class ChartEditorAudioHandler
* @param charId The character this vocal track will be for.
* @param instId The instrumental this vocal track will be for.
*/
static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = ''):Bool
public static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = ''):Bool
{
var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
state.audioVocalTrackData.set(trackId, bytes);
@ -77,7 +76,7 @@ class ChartEditorAudioHandler
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = ''):Bool
public static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = ''):Bool
{
#if sys
var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
@ -95,7 +94,7 @@ class ChartEditorAudioHandler
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = ''):Bool
public static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = ''):Bool
{
var trackData:Null<Bytes> = Assets.getBytes(path);
if (trackData != null)
@ -112,7 +111,7 @@ class ChartEditorAudioHandler
* @param charId The character this vocal track will be for.
* @param instId The instrumental this vocal track will be for.
*/
static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = ''):Bool
public static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = ''):Bool
{
if (instId == '') instId = 'default';
state.audioInstTrackData.set(instId, bytes);
@ -136,11 +135,11 @@ class ChartEditorAudioHandler
/**
* Tell the Chart Editor to select a specific instrumental track, that is already loaded.
*/
static function playInstrumental(state:ChartEditorState, instId:String = ''):Bool
public static function playInstrumental(state:ChartEditorState, instId:String = ''):Bool
{
if (instId == '') instId = 'default';
var instTrackData:Null<Bytes> = state.audioInstTrackData.get(instId);
var instTrack:Null<FlxSound> = buildFlxSoundFromBytes(instTrackData);
var instTrack:Null<FlxSound> = SoundUtil.buildFlxSoundFromBytes(instTrackData);
if (instTrack == null) return false;
stopExistingInstrumental(state);
@ -149,7 +148,7 @@ class ChartEditorAudioHandler
return true;
}
static function stopExistingInstrumental(state:ChartEditorState):Void
public static function stopExistingInstrumental(state:ChartEditorState):Void
{
if (state.audioInstTrack != null)
{
@ -162,11 +161,11 @@ class ChartEditorAudioHandler
/**
* Tell the Chart Editor to select a specific vocal track, that is already loaded.
*/
static function playVocals(state:ChartEditorState, charType:CharacterType, charId:String, instId:String = ''):Bool
public static function playVocals(state:ChartEditorState, charType:CharacterType, charId:String, instId:String = ''):Bool
{
var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
var vocalTrackData:Null<Bytes> = state.audioVocalTrackData.get(trackId);
var vocalTrack:Null<FlxSound> = buildFlxSoundFromBytes(vocalTrackData);
var vocalTrack:Null<FlxSound> = SoundUtil.buildFlxSoundFromBytes(vocalTrackData);
if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup();
@ -190,7 +189,7 @@ class ChartEditorAudioHandler
return false;
}
static function stopExistingVocals(state:ChartEditorState):Void
public static function stopExistingVocals(state:ChartEditorState):Void
{
if (state.audioVocalTrackGroup != null)
{
@ -203,7 +202,7 @@ class ChartEditorAudioHandler
* Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance.
* @param path The path to the sound effect. Use `Paths` to build this.
*/
public static function playSound(path:String):Void
public static function playSound(_state:ChartEditorState, path:String):Void
{
var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
var asset:Null<FlxSoundAsset> = FlxG.sound.cache(path);
@ -219,22 +218,11 @@ class ChartEditorAudioHandler
}
/**
* Convert byte data into a playable sound.
*
* @param input The byte data.
* @return The playable sound, or `null` if loading failed.
* Create a list of ZIP file entries from the current loaded instrumental tracks in the chart eidtor.
* @param state The chart editor state.
* @return `Array<haxe.zip.Entry>`
*/
public static function buildFlxSoundFromBytes(input:Null<Bytes>):Null<FlxSound>
{
if (input == null) return null;
var openflSound:openfl.media.Sound = new openfl.media.Sound();
openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(input), input.length);
var output:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
return output;
}
static function makeZIPEntriesFromInstrumentals(state:ChartEditorState):Array<haxe.zip.Entry>
public static function makeZIPEntriesFromInstrumentals(state:ChartEditorState):Array<haxe.zip.Entry>
{
var zipEntries = [];
@ -257,7 +245,12 @@ class ChartEditorAudioHandler
return zipEntries;
}
static function makeZIPEntriesFromVocals(state:ChartEditorState):Array<haxe.zip.Entry>
/**
* Create a list of ZIP file entries from the current loaded vocal tracks in the chart eidtor.
* @param state The chart editor state.
* @return `Array<haxe.zip.Entry>`
*/
public static function makeZIPEntriesFromVocals(state:ChartEditorState):Array<haxe.zip.Entry>
{
var zipEntries = [];

View file

@ -1,6 +1,5 @@
package funkin.ui.debug.charting;
package funkin.ui.debug.charting.handlers;
import funkin.ui.haxeui.components.FunkinDropDown;
import flixel.util.FlxTimer;
import funkin.data.song.importer.FNFLegacyData;
import funkin.data.song.importer.FNFLegacyImporter;
@ -15,6 +14,8 @@ import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.Song;
import funkin.play.stage.StageData;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.ui.haxeui.components.FunkinDropDown;
import funkin.ui.haxeui.components.FunkinLink;
import funkin.util.Constants;
import funkin.util.FileUtil;
@ -47,8 +48,10 @@ using Lambda;
* Handles dialogs for the new Chart Editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorDialogHandler
{
// Paths to HaxeUI layout files for each dialog.
static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT:String = Paths.ui('chart-editor/dialogs/about');
static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT:String = Paths.ui('chart-editor/dialogs/welcome');
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst');
@ -67,7 +70,7 @@ class ChartEditorDialogHandler
* @param state The current chart editor state.
* @return The dialog that was opened.
*/
public static inline function openAboutDialog(state:ChartEditorState):Null<Dialog>
public static function openAboutDialog(state:ChartEditorState):Null<Dialog>
{
return openDialog(state, CHART_EDITOR_DIALOG_ABOUT_LAYOUT, true, true);
}
@ -158,7 +161,7 @@ class ChartEditorDialogHandler
state.stopWelcomeMusic();
// Load song from template
ChartEditorImportExportHandler.loadSongAsTemplate(state, targetSongId);
state.loadSongAsTemplate(targetSongId);
}
splashTemplateContainer.addComponent(linkTemplateSong);
@ -402,7 +405,7 @@ class ChartEditorDialogHandler
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) {
if (selectedFile != null && selectedFile.bytes != null)
{
if (ChartEditorAudioHandler.loadInstFromBytes(state, selectedFile.bytes, instId))
if (state.loadInstFromBytes(selectedFile.bytes, instId))
{
#if !mac
NotificationManager.instance.addNotification(
@ -410,7 +413,7 @@ class ChartEditorDialogHandler
title: 'Success',
body: 'Loaded instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
@ -426,7 +429,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: 'Failed to load instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
@ -437,7 +440,7 @@ class ChartEditorDialogHandler
onDropFile = function(pathStr:String) {
var path:Path = new Path(pathStr);
trace('Dropped file (${path})');
if (ChartEditorAudioHandler.loadInstFromPath(state, path, instId))
if (state.loadInstFromPath(path, instId))
{
// Tell the user the load was successful.
#if !mac
@ -446,7 +449,7 @@ class ChartEditorDialogHandler
title: 'Success',
body: 'Loaded instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
@ -472,7 +475,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: message,
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
@ -483,66 +486,6 @@ class ChartEditorDialogHandler
return dialog;
}
static var dropHandlers:Array<
{
component:Component,
handler:(String->Void)
}> = [];
/**
* Add a callback for when a file is dropped on a component.
*
* On OS X you cant drop on the application window, but rather only the app icon
* (either in the dock while running or the icon on the hard drive) so this must be disabled
* and UI updated appropriately.
* @param component
* @param handler
*/
static function addDropHandler(component:Component, handler:String->Void):Void
{
#if desktop
if (!FlxG.stage.window.onDropFile.has(onDropFile)) FlxG.stage.window.onDropFile.add(onDropFile);
dropHandlers.push(
{
component: component,
handler: handler
});
#else
trace('addDropHandler not implemented for this platform');
#end
}
static function removeDropHandler(handler:String->Void):Void
{
#if desktop
FlxG.stage.window.onDropFile.remove(handler);
#end
}
static function clearDropHandlers():Void
{
#if desktop
dropHandlers = [];
FlxG.stage.window.onDropFile.remove(onDropFile);
#end
}
static function onDropFile(path:String):Void
{
// a VERY short timer to wait for the mouse position to update
new FlxTimer().start(0.01, function(_) {
for (handler in dropHandlers)
{
if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY))
{
handler.handler(path);
return;
}
}
});
}
/**
* Opens the dialog in the wizard where the user can set song metadata like name and artist and BPM.
* @param state The ChartEditorState instance.
@ -722,7 +665,7 @@ class ChartEditorDialogHandler
if (dialogNoVocals == null) throw 'Could not locate dialogNoVocals button in Upload Vocals dialog';
dialogNoVocals.onClick = function(_event) {
// Dismiss
ChartEditorAudioHandler.stopExistingVocals(state);
state.stopExistingVocals();
dialog.hideDialog(DialogButton.APPLY);
};
@ -749,10 +692,10 @@ class ChartEditorDialogHandler
if (!hasClearedVocals)
{
hasClearedVocals = true;
ChartEditorAudioHandler.stopExistingVocals(state);
state.stopExistingVocals();
}
if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey, instId))
if (state.loadVocalsFromPath(path, charKey, instId))
{
// Tell the user the load was successful.
#if !mac
@ -761,7 +704,7 @@ class ChartEditorDialogHandler
title: 'Success',
body: 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
#if FILE_DROP_SUPPORTED
@ -784,7 +727,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
@ -805,9 +748,9 @@ class ChartEditorDialogHandler
if (!hasClearedVocals)
{
hasClearedVocals = true;
ChartEditorAudioHandler.stopExistingVocals(state);
state.stopExistingVocals();
}
if (ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey, instId))
if (state.loadVocalsFromBytes(selectedFile.bytes, charKey, instId))
{
// Tell the user the load was successful.
#if !mac
@ -816,7 +759,7 @@ class ChartEditorDialogHandler
title: 'Success',
body: 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
#if FILE_DROP_SUPPORTED
@ -837,7 +780,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
@ -897,7 +840,7 @@ class ChartEditorDialogHandler
var buttonContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
if (buttonContinue == null) throw 'Could not locate dialogContinue button in Open Chart dialog';
buttonContinue.onClick = function(_event) {
ChartEditorImportExportHandler.loadSong(state, songMetadata, songChartData);
state.loadSong(songMetadata, songChartData);
dialog.hideDialog(DialogButton.APPLY);
}
@ -996,7 +939,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: 'Could not parse metadata file version (${path.file}.${path.ext})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
return;
@ -1014,7 +957,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: 'Could not load metadata file (${path.file}.${path.ext})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
return;
@ -1029,7 +972,7 @@ class ChartEditorDialogHandler
title: 'Success',
body: 'Loaded metadata file (${path.file}.${path.ext})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
@ -1061,7 +1004,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: 'Could not parse metadata file version (${selectedFile.name})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
return;
@ -1081,7 +1024,7 @@ class ChartEditorDialogHandler
title: 'Success',
body: 'Loaded metadata file (${selectedFile.name})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
@ -1102,7 +1045,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: 'Failed to load metadata file (${selectedFile.name})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
@ -1126,7 +1069,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: 'Could not parse chart data file version (${path.file}.${path.ext})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
return;
@ -1149,7 +1092,7 @@ class ChartEditorDialogHandler
title: 'Success',
body: 'Loaded chart data file (${path.file}.${path.ext})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
@ -1168,7 +1111,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: 'Failed to load chart data file (${path.file}.${path.ext})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
@ -1193,7 +1136,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: 'Could not parse chart data file version (${selectedFile.name})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
return;
@ -1216,7 +1159,7 @@ class ChartEditorDialogHandler
title: 'Success',
body: 'Loaded chart data file (${selectedFile.name})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
@ -1319,7 +1262,7 @@ class ChartEditorDialogHandler
title: 'Failure',
body: 'Failed to parse FNF chart file (${selectedFile.name})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
return;
@ -1328,7 +1271,7 @@ class ChartEditorDialogHandler
var songMetadata:SongMetadata = FNFLegacyImporter.migrateMetadata(fnfLegacyData);
var songChartData:SongChartData = FNFLegacyImporter.migrateChartData(fnfLegacyData);
ChartEditorImportExportHandler.loadSong(state, [Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
dialog.hideDialog(DialogButton.APPLY);
#if !mac
@ -1337,7 +1280,7 @@ class ChartEditorDialogHandler
title: 'Success',
body: 'Loaded chart file (${selectedFile.name})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
@ -1351,7 +1294,7 @@ class ChartEditorDialogHandler
var songMetadata:SongMetadata = FNFLegacyImporter.migrateMetadata(selectedFileData);
var songChartData:SongChartData = FNFLegacyImporter.migrateChartData(selectedFileData);
ChartEditorImportExportHandler.loadSong(state, [Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
dialog.hideDialog(DialogButton.APPLY);
#if !mac
@ -1360,7 +1303,7 @@ class ChartEditorDialogHandler
title: 'Success',
body: 'Loaded chart file (${path.file}.${path.ext})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
};
@ -1376,35 +1319,11 @@ class ChartEditorDialogHandler
* @param state The current chart editor state.
* @return The dialog that was opened.
*/
public static inline function openUserGuideDialog(state:ChartEditorState):Null<Dialog>
public static function openUserGuideDialog(state:ChartEditorState):Null<Dialog>
{
return openDialog(state, CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT, true, true);
}
/**
* Builds and opens a dialog from a given layout path.
* @param modal Makes the background uninteractable while the dialog is open.
* @param closable Hides the close button on the dialog, preventing it from being closed unless the user interacts with the dialog.
*/
static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Null<Dialog>
{
var dialog:Null<Dialog> = cast state.buildComponent(key);
if (dialog == null) return null;
dialog.destroyOnClose = true;
dialog.closable = closable;
dialog.showDialog(modal);
state.isHaxeUIDialogOpen = true;
dialog.onDialogClosed = function(event:UIEvent) {
state.isHaxeUIDialogOpen = false;
};
dialog.zIndex = 1000;
return dialog;
}
/**
* Builds and opens a dialog where the user can add a new variation for a song.
* @param state The current chart editor state.
@ -1561,4 +1480,91 @@ class ChartEditorDialogHandler
return dialog;
}
/**
* Builds and opens a dialog from a given layout path.
* @param modal Makes the background uninteractable while the dialog is open.
* @param closable Hides the close button on the dialog, preventing it from being closed unless the user interacts with the dialog.
*/
static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Null<Dialog>
{
var dialog:Null<Dialog> = cast state.buildComponent(key);
if (dialog == null) return null;
dialog.destroyOnClose = true;
dialog.closable = closable;
dialog.showDialog(modal);
state.isHaxeUIDialogOpen = true;
dialog.onDialogClosed = function(event:UIEvent) {
state.isHaxeUIDialogOpen = false;
};
dialog.zIndex = 1000;
return dialog;
}
// ==========
// DROP HANDLERS
// ==========
static var dropHandlers:Array<
{
component:Component,
handler:(String->Void)
}> = [];
/**
* Add a callback for when a file is dropped on a component.
*
* On OS X you cant drop on the application window, but rather only the app icon
* (either in the dock while running or the icon on the hard drive) so this must be disabled
* and UI updated appropriately.
* @param component
* @param handler
*/
static function addDropHandler(component:Component, handler:String->Void):Void
{
#if desktop
if (!FlxG.stage.window.onDropFile.has(onDropFile)) FlxG.stage.window.onDropFile.add(onDropFile);
dropHandlers.push(
{
component: component,
handler: handler
});
#else
trace('addDropHandler not implemented for this platform');
#end
}
static function removeDropHandler(handler:String->Void):Void
{
#if desktop
FlxG.stage.window.onDropFile.remove(handler);
#end
}
static function clearDropHandlers():Void
{
#if desktop
dropHandlers = [];
FlxG.stage.window.onDropFile.remove(onDropFile);
#end
}
static function onDropFile(path:String):Void
{
// a VERY short timer to wait for the mouse position to update
new FlxTimer().start(0.01, function(_) {
for (handler in dropHandlers)
{
if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY))
{
handler.handler(path);
return;
}
}
});
}
}

View file

@ -1,4 +1,4 @@
package funkin.ui.debug.charting;
package funkin.ui.debug.charting.handlers;
import haxe.ui.notifications.NotificationType;
import funkin.util.DateUtil;
@ -16,7 +16,7 @@ import funkin.data.song.SongRegistry;
* Contains functions for importing, loading, saving, and exporting charts.
*/
@:nullSafety
@:allow(funkin.ui.debug.charting.ChartEditorState)
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorImportExportHandler
{
/**
@ -50,18 +50,18 @@ class ChartEditorImportExportHandler
state.sortChartData();
state.clearVocals();
state.stopExistingVocals();
var variations:Array<String> = state.availableVariations;
for (variation in variations)
{
if (variation == Constants.DEFAULT_VARIATION)
{
ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId));
state.loadInstFromAsset(Paths.inst(songId));
}
else
{
ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId, '-$variation'), variation);
state.loadInstFromAsset(Paths.inst(songId, '-$variation'), variation);
}
}
@ -75,12 +75,12 @@ class ChartEditorImportExportHandler
if (voiceList.length == 2)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId);
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[1], diff.characters.opponent, instId);
state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId);
state.loadVocalsFromAsset(voiceList[1], diff.characters.opponent, instId);
}
else if (voiceList.length == 1)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId);
state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId);
}
else
{
@ -98,7 +98,7 @@ class ChartEditorImportExportHandler
title: 'Success',
body: 'Loaded song (${rawSongMetadata[0].songName})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
@ -169,8 +169,8 @@ class ChartEditorImportExportHandler
}
}
if (state.audioInstTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromInstrumentals(state));
if (state.audioVocalTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromVocals(state));
if (state.audioInstTrackData != null) zipEntries.concat(state.makeZIPEntriesFromInstrumentals());
if (state.audioVocalTrackData != null) zipEntries.concat(state.makeZIPEntriesFromVocals());
trace('Exporting ${zipEntries.length} files to ZIP...');

View file

@ -1,26 +1,19 @@
package funkin.ui.debug.charting;
package funkin.ui.debug.charting.handlers;
import flixel.FlxSprite;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.display.FlxSliceSprite;
import flixel.FlxSprite;
import flixel.math.FlxRect;
import flixel.util.FlxColor;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
/**
* Available themes for the chart editor state.
*/
enum ChartEditorTheme
{
Light;
Dark;
}
/**
* Static functions which handle building themed UI elements for a provided ChartEditorState.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorThemeHandler
{
// TODO: There's probably a better system of organization for these colors.

View file

@ -1,53 +1,43 @@
package funkin.ui.debug.charting;
package funkin.ui.debug.charting.handlers;
import funkin.ui.haxeui.components.FunkinDropDown;
import funkin.play.stage.StageData.StageDataParser;
import funkin.play.stage.StageData;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import haxe.ui.components.HorizontalSlider;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.event.SongEvent;
import funkin.play.song.SongSerializer;
import funkin.play.stage.StageData;
import funkin.play.stage.StageData.StageDataParser;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.ui.haxeui.components.FunkinDropDown;
import funkin.util.FileUtil;
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.components.TextField;
import haxe.ui.containers.Box;
import haxe.ui.containers.Grid;
import haxe.ui.containers.Group;
import haxe.ui.containers.VBox;
import haxe.ui.containers.Frame;
import haxe.ui.containers.dialogs.CollapsibleDialog;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
import haxe.ui.containers.Frame;
import haxe.ui.containers.Grid;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
import haxe.ui.core.Component;
import haxe.ui.data.ArrayDataSource;
import haxe.ui.events.UIEvent;
/**
* Available tools for the chart editor state.
*/
enum ChartEditorToolMode
{
Select;
Place;
}
/**
* Static functions which handle building themed UI elements for a provided ChartEditorState.
*/
@:nullSafety
@:allow(funkin.ui.debug.charting.ChartEditorState)
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorToolboxHandler
{
public static function setToolboxState(state:ChartEditorState, id:String, shown:Bool):Void
@ -72,12 +62,10 @@ class ChartEditorToolboxHandler
{
toolbox.showDialog(false);
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/openWindow'));
state.playSound(Paths.sound('chartingSounds/openWindow'));
switch (id)
{
case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
onShowToolboxTools(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
onShowToolboxNoteData(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
@ -111,12 +99,10 @@ class ChartEditorToolboxHandler
{
toolbox.hideDialog(DialogButton.CANCEL);
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/exitWindow'));
state.playSound(Paths.sound('chartingSounds/exitWindow'));
switch (id)
{
case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
onHideToolboxTools(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
onHideToolboxNoteData(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
@ -175,8 +161,6 @@ class ChartEditorToolboxHandler
var toolbox:Null<CollapsibleDialog> = null;
switch (id)
{
case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
toolbox = buildToolboxToolsLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
toolbox = buildToolboxNoteDataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
@ -223,44 +207,6 @@ class ChartEditorToolboxHandler
return toolbox;
}
static function buildToolboxToolsLayout(state:ChartEditorState):Null<CollapsibleDialog>
{
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT);
if (toolbox == null) return null;
// Starting position.
toolbox.x = 50;
toolbox.y = 50;
toolbox.onDialogClosed = function(event:DialogEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxTools', false);
}
var toolsGroup:Null<Group> = toolbox.findComponent('toolboxToolsGroup', Group);
if (toolsGroup == null) throw 'ChartEditorToolboxHandler.buildToolboxToolsLayout() - Could not find toolboxToolsGroup component.';
if (toolsGroup == null) return null;
toolsGroup.onChange = function(event:UIEvent) {
switch (event.target.id)
{
case 'toolboxToolsGroupSelect':
state.currentToolMode = ChartEditorToolMode.Select;
case 'toolboxToolsGroupPlace':
state.currentToolMode = ChartEditorToolMode.Place;
default:
trace('ChartEditorToolboxHandler.buildToolboxToolsLayout() - Unknown toolbox tool selected: $event.target.id');
}
}
return toolbox;
}
static function onShowToolboxTools(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxTools(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function buildToolboxNoteDataLayout(state:ChartEditorState):Null<CollapsibleDialog>
{
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
@ -483,11 +429,11 @@ class ChartEditorToolboxHandler
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadChart component.';
difficultyToolboxAddVariation.onClick = function(_:UIEvent) {
ChartEditorDialogHandler.openAddVariationDialog(state, true);
state.openAddVariationDialog(true);
};
difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) {
ChartEditorDialogHandler.openAddDifficultyDialog(state, true);
state.openAddDifficultyDialog(true);
};
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {

View file

@ -0,0 +1,10 @@
package funkin.ui.debug.charting;
#if !macro
// 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.ChartEditorDialogHandler;
using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler;
using funkin.ui.debug.charting.handlers.ChartEditorThemeHandler;
using funkin.ui.debug.charting.handlers.ChartEditorToolboxHandler;
#end

View file

@ -1,4 +1,4 @@
package funkin.ui.debug.charting;
package funkin.ui.debug.charting.util;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
@ -10,13 +10,16 @@ import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData.CharacterDataParser;
/**
* This class contains functions for populating dropdowns based on game data.
* Functions for populating dropdowns based on game data.
* These get used by both dialogs and toolboxes so they're in their own class to prevent "reaching over."
*/
@:nullSafety
@:access(ChartEditorState)
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorDropdowns
{
/**
* Populate a dropdown with a list of characters.
*/
public static function populateDropdownWithCharacters(dropDown:DropDown, charType:CharacterType, startingCharId:String):DropDownEntry
{
dropDown.dataSource.clear();
@ -50,6 +53,9 @@ class ChartEditorDropdowns
return returnValue;
}
/**
* Populate a dropdown with a list of stages.
*/
public static function populateDropdownWithStages(dropDown:DropDown, startingStageId:String):DropDownEntry
{
dropDown.dataSource.clear();
@ -74,6 +80,9 @@ class ChartEditorDropdowns
return returnValue;
}
/**
* Populate a dropdown with a list of note styles.
*/
public static function populateDropdownWithNoteStyles(dropDown:DropDown, startingStyleId:String):DropDownEntry
{
dropDown.dataSource.clear();
@ -98,6 +107,9 @@ class ChartEditorDropdowns
return returnValue;
}
/**
* Populate a dropdown with a list of song variations.
*/
public static function populateDropdownWithVariations(dropDown:DropDown, state:ChartEditorState, includeNone:Bool = true):DropDownEntry
{
dropDown.dataSource.clear();
@ -122,6 +134,9 @@ class ChartEditorDropdowns
}
}
/**
* An entry in a dropdown.
*/
typedef DropDownEntry =
{
id:String,

View file

@ -1,9 +1,9 @@
package funkin.ui.haxeui.components;
import funkin.modding.events.ScriptEventType.GhostMissNoteScriptEvent;
import funkin.modding.events.ScriptEventType.NoteScriptEvent;
import funkin.modding.events.ScriptEventType.SongTimeScriptEvent;
import funkin.modding.events.ScriptEventType.UpdateScriptEvent;
import funkin.modding.events.ScriptEvent.GhostMissNoteScriptEvent;
import funkin.modding.events.ScriptEvent.NoteScriptEvent;
import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import haxe.ui.core.IDataComponent;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser;

View file

@ -251,6 +251,16 @@ class Constants
*/
public static final NS_PER_SEC:Int = NS_PER_US * US_PER_MS * MS_PER_SEC;
/**
* Duration, in milliseconds, until toast notifications are automatically hidden.
*/
public static final NOTIFICATION_DISMISS_TIME:Int = 5 * MS_PER_SEC;
/**
* Duration to wait before autosaving the chart.
*/
public static final AUTOSAVE_TIMER_DELAY_SEC:Float = 5.0 * SECS_PER_MIN;
/**
* Number of steps in a beat.
* One step is one 16th note and one beat is one quarter note.
@ -392,7 +402,8 @@ class Constants
public static final GHOST_TAPPING:Bool = false;
/**
* The separator between an asset library and the asset path.
* The separator between an asset library and the asset path.
*/
public static final LIBRARY_SEPARATOR:String = ':';

View file

@ -0,0 +1,23 @@
package funkin.util.assets;
import haxe.io.Bytes;
import flixel.system.FlxSound;
class SoundUtil
{
/**
* Convert byte data into a playable sound.
*
* @param input The byte data.
* @return The playable sound, or `null` if loading failed.
*/
public static function buildFlxSoundFromBytes(input:Null<Bytes>):Null<FlxSound>
{
if (input == null) return null;
var openflSound:openfl.media.Sound = new openfl.media.Sound();
openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(input), input.length);
var output:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
return output;
}
}

View file

@ -0,0 +1,15 @@
package funkin.util.tools;
/**
* Utilities for performing common math operations.
*/
class FloatTools
{
/**
* Constrain a float between a minimum and maximum value.
*/
public static function clamp(value:Float, min:Float, max:Float):Float
{
return Math.max(min, Math.min(max, value));
}
}

View file

@ -0,0 +1,16 @@
package funkin.util.tools;
/**
* Utilities for performing common math operations.
*/
class IntTools
{
/**
* Constrain an integer between a minimum and maximum value.
*/
public static function clamp(value:Int, min:Int, max:Int):Int
{
// Don't use Math.min because it returns a Float.
return value < min ? min : value > max ? max : value;
}
}