Modify time changes in the metadata toolbox

This commit is contained in:
Lasercar
2025-09-27 03:57:37 +08:00
committed by Hundrec
parent f3c0541ade
commit 1dce731aa4
5 changed files with 302 additions and 32 deletions
@@ -0,0 +1,76 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongTimeChange;
/**
* A command which adds a new timechange to the current song's timechanges, after the currently selected timechange.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class AddNewTimeChangeCommand implements ChartEditorCommand
{
var currentTimeChange:Int;
var previousTimeChanges:Null<Array<SongTimeChange>>;
public function new(currentTimeChange:Int)
{
this.currentTimeChange = currentTimeChange;
}
public function execute(state:ChartEditorState):Void
{
var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
previousTimeChanges = timeChanges;
if (timeChanges == null || timeChanges.length == 0)
{
timeChanges = [new SongTimeChange(0, 100)];
}
else
{
timeChanges.insert(currentTimeChange + 1,
new SongTimeChange(timeChanges[currentTimeChange].timeStamp + 1, timeChanges[currentTimeChange].bpm, timeChanges[currentTimeChange].timeSignatureNum,
timeChanges[currentTimeChange].timeSignatureDen));
}
state.currentSongMetadata.timeChanges = timeChanges;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true;
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
state.updateGridHeight();
}
public function undo(state:ChartEditorState):Void
{
if (previousTimeChanges == null)
{
previousTimeChanges = [new SongTimeChange(0, 100)];
}
state.currentSongMetadata.timeChanges = previousTimeChanges;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true;
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
state.updateGridHeight();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
return true;
}
public function toString():String
{
return 'Added new TimeChange after TimeChange ${currentTimeChange}';
}
}
@@ -0,0 +1,94 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongTimeChange;
/**
* A command which modifies the current time change's bpm and/or timestamp in the song.
* Note that this does not have any protection to prevent time changes from being set to invalid or troublesome values.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ModifyCurrentTimeChangeCommand implements ChartEditorCommand
{
var currentTimeChange:Int;
var targetBPM:Float;
var previousBPM:Float = 100;
var targetTimeStamp:Float;
var previousTimeStamp:Float = 0;
var previousTimeChanges:Null<Array<SongTimeChange>>;
public function new(currentTimeChange:Int, targetBPM:Float, targetTimeStamp:Float)
{
this.currentTimeChange = currentTimeChange;
this.targetBPM = targetBPM;
this.targetTimeStamp = targetTimeStamp;
}
public function execute(state:ChartEditorState):Void
{
var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
previousTimeChanges = timeChanges;
if (timeChanges == null || timeChanges.length == 0)
{
previousBPM = 100;
previousTimeStamp = 0;
timeChanges = [new SongTimeChange(previousTimeStamp, targetBPM)];
}
else
{
previousBPM = timeChanges[currentTimeChange].bpm;
previousTimeStamp = timeChanges[currentTimeChange].timeStamp;
timeChanges[currentTimeChange].bpm = targetBPM;
timeChanges[currentTimeChange].timeStamp = targetTimeStamp;
}
state.currentSongMetadata.timeChanges = timeChanges;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true;
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
state.updateGridHeight();
}
public function undo(state:ChartEditorState):Void
{
if (previousTimeChanges == null)
{
previousTimeChanges = [new SongTimeChange(previousTimeStamp, previousBPM)];
}
state.currentSongMetadata.timeChanges = previousTimeChanges;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true;
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
state.updateGridHeight();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (targetBPM != previousBPM || targetTimeStamp != previousTimeStamp);
}
public function toString():String
{
if (targetBPM == previousBPM && targetTimeStamp != previousTimeStamp) return 'Changed TimeChange ${currentTimeChange} BPM to ${targetBPM}';
else if (targetBPM != previousBPM && targetTimeStamp == previousTimeStamp) return 'Changed TimeChange ${currentTimeChange} timestamp to ${targetTimeStamp}';
else
return 'Changed TimeChange ${currentTimeChange} BPM to ${targetBPM} & timestamp to ${targetTimeStamp}';
}
}
@@ -3,33 +3,32 @@ package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongTimeChange;
/**
* A command which changes the starting BPM of the song.
* A command which removes the current timechange from the song's timechanges.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChangeStartingBPMCommand implements ChartEditorCommand
class RemoveCurrentTimeChangeCommand implements ChartEditorCommand
{
var targetBPM:Float;
var currentTimeChange:Int;
var previousBPM:Float = 100;
var previousTimeChanges:Null<Array<SongTimeChange>>;
public function new(targetBPM:Float)
public function new(currentTimeChange:Int)
{
this.targetBPM = targetBPM;
this.currentTimeChange = currentTimeChange;
}
public function execute(state:ChartEditorState):Void
{
var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
previousTimeChanges = timeChanges;
if (timeChanges == null || timeChanges.length == 0)
{
previousBPM = 100;
timeChanges = [new SongTimeChange(0, targetBPM)];
timeChanges = [new SongTimeChange(0, 100)];
}
else
{
previousBPM = timeChanges[0].bpm;
timeChanges[0].bpm = targetBPM;
timeChanges.splice(currentTimeChange , 1);
}
state.currentSongMetadata.timeChanges = timeChanges;
@@ -45,17 +44,12 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void
{
var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
if (timeChanges == null || timeChanges.length == 0)
if (previousTimeChanges == null)
{
timeChanges = [new SongTimeChange(0, previousBPM)];
}
else
{
timeChanges[0].bpm = previousBPM;
previousTimeChanges = [new SongTimeChange(0, 100)];
}
state.currentSongMetadata.timeChanges = timeChanges;
state.currentSongMetadata.timeChanges = previousTimeChanges;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
@@ -68,12 +62,11 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (targetBPM != previousBPM);
return true;
}
public function toString():String
{
return 'Change Starting BPM to ${targetBPM}';
return 'Removed TimeChange ${currentTimeChange}';
}
}
@@ -6,7 +6,9 @@ import funkin.data.song.importer.ChartManifestData;
import funkin.data.stage.StageRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
import funkin.ui.debug.charting.commands.AddNewTimeChangeCommand;
import funkin.ui.debug.charting.commands.ModifyCurrentTimeChangeCommand;
import funkin.ui.debug.charting.commands.RemoveCurrentTimeChangeCommand;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import haxe.ui.components.Button;
import haxe.ui.components.DropDown;
@@ -36,6 +38,8 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
var buttonCharacterGirlfriend:Button;
var buttonCharacterOpponent:Button;
var inputBPM:NumberStepper;
var labelTimeStamp:Label;
var inputTimeStamp:NumberStepper;
var labelScrollSpeed:Label;
var inputScrollSpeed:Slider;
var frameVariation:Frame;
@@ -140,36 +144,105 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, chartEditorState.currentSongMetadata.playData.noteStyle);
inputNoteStyle.value = startingValueNoteStyle;
inputTimeChange.onChange = function(event:UIEvent) {
var currentTimeChange = chartEditorState.currentSongMetadata.timeChanges[inputTimeChange.selectedIndex];
inputBPM.value = currentTimeChange.bpm;
inputTSNum.value = currentTimeChange.timeSignatureNum;
inputTSDen.value = currentTimeChange.timeSignatureDen;
inputTimeStamp.value = currentTimeChange.timeStamp;
// Set the min max values of the input timestamp to previous and next time change timestamps to in the array, to prevent the conductor from breaking due to wack time change timestamps
inputTimeStamp.min = (chartEditorState.currentSongMetadata.timeChanges[inputTimeChange.selectedIndex - 1]?.timeStamp) ?? 0;
if (inputTimeStamp.min != 0) inputTimeStamp.min += 1;
inputTimeStamp.max = (chartEditorState.currentSongMetadata.timeChanges[inputTimeChange.selectedIndex + 1]?.timeStamp) ?? chartEditorState.songLengthInMs;
trace(inputTimeStamp.min);
trace(inputTimeStamp.max);
// Prevent the inital time change timestamp from being modified (it should always be 0) or removed
if (inputTimeChange.selectedIndex == 0)
{
labelTimeStamp.hidden = true;
inputTimeStamp.hidden = true;
removeTimeChange.disabled = true;
}
else
{
labelTimeStamp.hidden = false;
inputTimeStamp.hidden = false;
removeTimeChange.disabled = false;
}
};
var startingTimeChange = ChartEditorDropdowns.populateDropdownWithTimeChanges(inputTimeChange, chartEditorState.currentSongMetadata.timeChanges, 0);
inputTimeChange.value = startingTimeChange;
inputBPM.onChange = function(event:UIEvent) {
if (event.value == null || event.value <= 0) return;
// Use a command so we can undo/redo this action.
if (event.value != Conductor.instance.bpm)
var currentTimeChange = chartEditorState.currentSongMetadata.timeChanges[inputTimeChange.selectedIndex];
var currentBPM = currentTimeChange.bpm;
if (event.value != currentBPM)
{
chartEditorState.performCommand(new ChangeStartingBPMCommand(event.value));
chartEditorState.performCommand(new ModifyCurrentTimeChangeCommand(inputTimeChange.selectedIndex, event.value, currentTimeChange.timeStamp));
// TODO: Figure out why the text of the time change dropdown itself isn't updating and fix it
inputTimeChange.value.text = '${currentTimeChange.timeStamp} : BPM: ${event.value} in ${currentTimeChange.timeSignatureNum}/${currentTimeChange.timeSignatureDen}';
}
};
inputTimeStamp.onChange = function(event:UIEvent) {
if (event.value == null || event.value <= 0) return;
// Use a command so we can undo/redo this action.
var currentTimeChange = chartEditorState.currentSongMetadata.timeChanges[inputTimeChange.selectedIndex];
var currentTimeStamp = currentTimeChange.timeStamp;
if (inputTimeChange.selectedIndex != 0 && event.value != currentTimeStamp)
{
chartEditorState.performCommand(new ModifyCurrentTimeChangeCommand(inputTimeChange.selectedIndex, currentTimeChange.bpm, event.value));
inputTimeChange.value.text = '${event.value} : BPM: ${currentTimeChange.bpm} in ${currentTimeChange.timeSignatureNum}/${currentTimeChange.timeSignatureDen}';
trace(inputTimeChange.dataSource.get(inputTimeChange.selectedIndex));
trace(inputTimeChange.selectedItem);
chartEditorState.updateTimeSignature();
}
};
inputTSNum.onChange = function(event:UIEvent) {
var numerator:Null<Int> = Std.parseInt(event?.data?.text);
if (numerator == null) return;
var prevNumerator:Int = chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum;
var currentTimeChange = chartEditorState.currentSongMetadata.timeChanges[inputTimeChange.selectedIndex];
var prevNumerator:Int = currentTimeChange.timeSignatureNum;
if (numerator == prevNumerator) return;
chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum = numerator;
chartEditorState.currentSongMetadata.timeChanges[inputTimeChange.selectedIndex].timeSignatureNum = numerator;
inputTimeChange.value.text = '${currentTimeChange.timeStamp} : BPM: ${currentTimeChange.bpm} in ${numerator}/${currentTimeChange.timeSignatureDen}';
chartEditorState.updateTimeSignature();
}
inputTSDen.onChange = function(event:UIEvent) {
var denominator:Null<Int> = Std.parseInt(event?.data?.text);
if (denominator == null) return;
var prevDenominator:Int = chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen;
var currentTimeChange = chartEditorState.currentSongMetadata.timeChanges[inputTimeChange.selectedIndex];
var prevDenominator:Int = currentTimeChange.timeSignatureDen;
if (denominator == prevDenominator) return;
chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen = denominator;
chartEditorState.currentSongMetadata.timeChanges[inputTimeChange.selectedIndex].timeSignatureDen = denominator;
inputTimeChange.value.text = '${currentTimeChange.timeStamp} : BPM: ${currentTimeChange.bpm} in ${currentTimeChange.timeSignatureNum}/${denominator}';
chartEditorState.updateTimeSignature();
}
addTimeChange.onClick = function(_:UIEvent) {
chartEditorState.performCommand(new AddNewTimeChangeCommand(inputTimeChange.selectedIndex));
var startingTimeChange = ChartEditorDropdowns.populateDropdownWithTimeChanges(inputTimeChange, chartEditorState.currentSongMetadata.timeChanges,
inputTimeChange.selectedIndex + 1);
inputTimeChange.value = startingTimeChange;
}
removeTimeChange.onClick = function(_:UIEvent) {
chartEditorState.performCommand(new RemoveCurrentTimeChangeCommand(inputTimeChange.selectedIndex));
var startingTimeChange = ChartEditorDropdowns.populateDropdownWithTimeChanges(inputTimeChange, chartEditorState.currentSongMetadata.timeChanges,
inputTimeChange.selectedIndex - 1);
inputTimeChange.value = startingTimeChange;
}
inputScrollSpeed.onChange = function(event:UIEvent) {
var valid:Bool = event.target.value != null && event.target.value > 0;
@@ -213,16 +286,16 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
inputSongArtist.value = chartEditorState.currentSongMetadata.artist;
inputSongCharter.value = chartEditorState.currentSongMetadata.charter;
inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
inputNoteStyle.value = chartEditorState.currentSongNoteStyle;
inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle;
inputDifficultyRating.value = chartEditorState.currentSongChartDifficultyRating;
inputScrollSpeed.value = chartEditorState.currentSongChartScrollSpeed;
labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x';
frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';
frameDifficulty.text = 'Difficulty: ${chartEditorState.selectedDifficulty.toTitleCase()}';
inputTSNum.value = '${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum}';
inputTSDen.value = '${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen}';
// Reset time change dropdown and the associated inputs
var startingTimeChange = ChartEditorDropdowns.populateDropdownWithTimeChanges(inputTimeChange, chartEditorState.currentSongMetadata.timeChanges, 0);
inputTimeChange.value = startingTimeChange;
trace('Setting time signature to ${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum}/${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen}');
var stageId:String = chartEditorState.currentSongMetadata.playData.stage;
@@ -2,6 +2,7 @@ package funkin.ui.debug.charting.util;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.data.song.SongData.SongTimeChange;
import funkin.play.event.SongEvent;
import funkin.data.stage.StageRegistry;
import funkin.data.character.CharacterData;
@@ -82,6 +83,39 @@ class ChartEditorDropdowns
return returnValue;
}
/**
* Populate a dropdown with a list of time changes.
*/
public static function populateDropdownWithTimeChanges(dropDown:DropDown, timeChanges:Array<SongTimeChange>, startingTimeChange:Int = 0):DropDownEntry
{
dropDown.dataSource.clear();
var returnValue:DropDownEntry =
{
id: "0",
text: '${timeChanges[0].timeStamp} : BPM: ${timeChanges[0].bpm} in ${timeChanges[0].timeSignatureNum}/${timeChanges[0].timeSignatureDen}'
};
for (index in 0...timeChanges.length)
{
var value =
{
id: '$index',
text: '${timeChanges[index].timeStamp} : BPM: ${timeChanges[index].bpm} in ${timeChanges[index].timeSignatureNum}/${timeChanges[index].timeSignatureDen}'
};
if (startingTimeChange == index) returnValue = value;
dropDown.dataSource.add(value);
}
dropDown.dataSource.sort('id', ASCENDING);
return returnValue;
}
/**
* Populate a dropdown with a list of song events.
*/
public static function populateDropdownWithSongEvents(dropDown:DropDown, startingEventId:String):DropDownEntry
{
dropDown.dataSource.clear();