diff --git a/.gitignore b/.gitignore
index 0b7c366da..2931e2d59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,4 @@ latest/windows/haxe/
pages/
docs/doc.xml
mods/*
-!mods/readme.txt
\ No newline at end of file
+!mods/readme.txt
diff --git a/assets/images/editors/charter/event-icons/BPM Change End.png b/assets/images/editors/charter/event-icons/BPM Change End.png
new file mode 100644
index 000000000..6d5dd527c
Binary files /dev/null and b/assets/images/editors/charter/event-icons/BPM Change End.png differ
diff --git a/assets/images/editors/charter/event-icons/BPM Change Start.png b/assets/images/editors/charter/event-icons/BPM Change Start.png
new file mode 100644
index 000000000..832a02357
Binary files /dev/null and b/assets/images/editors/charter/event-icons/BPM Change Start.png differ
diff --git a/assets/images/editors/charter/event-icons/BPM Change.png b/assets/images/editors/charter/event-icons/BPM Change.png
index 40dba09a6..4eefebc39 100644
Binary files a/assets/images/editors/charter/event-icons/BPM Change.png and b/assets/images/editors/charter/event-icons/BPM Change.png differ
diff --git a/assets/images/editors/charter/event-icons/Time Signature Change.png b/assets/images/editors/charter/event-icons/Time Signature Change.png
new file mode 100644
index 000000000..967146006
Binary files /dev/null and b/assets/images/editors/charter/event-icons/Time Signature Change.png differ
diff --git a/assets/images/editors/charter/event-icons/components/arrow-down.png b/assets/images/editors/charter/event-icons/components/arrow-down.png
new file mode 100644
index 000000000..f44a87114
Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/arrow-down.png differ
diff --git a/assets/images/editors/charter/event-icons/components/arrow-down.xml b/assets/images/editors/charter/event-icons/components/arrow-down.xml
new file mode 100644
index 000000000..1e58cc38e
--- /dev/null
+++ b/assets/images/editors/charter/event-icons/components/arrow-down.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/images/editors/charter/event-icons/components/arrow-right.png b/assets/images/editors/charter/event-icons/components/arrow-right.png
new file mode 100644
index 000000000..a5da3a70a
Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/arrow-right.png differ
diff --git a/assets/images/editors/charter/event-icons/components/cross.png b/assets/images/editors/charter/event-icons/components/cross.png
new file mode 100644
index 000000000..a3dee356f
Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/cross.png differ
diff --git a/assets/images/editors/charter/event-icons/components/end-plus.png b/assets/images/editors/charter/event-icons/components/end-plus.png
new file mode 100644
index 000000000..7629e754e
Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/end-plus.png differ
diff --git a/assets/images/editors/charter/event-icons/components/eventNums.png b/assets/images/editors/charter/event-icons/components/eventNums.png
new file mode 100644
index 000000000..8ae1513cb
Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/eventNums.png differ
diff --git a/assets/images/editors/charter/event-icons/components/flash.png b/assets/images/editors/charter/event-icons/components/flash.png
new file mode 100644
index 000000000..2ef5f9492
Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/flash.png differ
diff --git a/assets/images/editors/charter/event-icons/components/note.png b/assets/images/editors/charter/event-icons/components/note.png
new file mode 100644
index 000000000..4b08e4f5b
Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/note.png differ
diff --git a/assets/images/editors/charter/event-icons/components/plus.png b/assets/images/editors/charter/event-icons/components/plus.png
new file mode 100644
index 000000000..939d4c46f
Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/plus.png differ
diff --git a/assets/images/editors/charter/event-icons/components/start-plus.png b/assets/images/editors/charter/event-icons/components/start-plus.png
new file mode 100644
index 000000000..84b42f136
Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/start-plus.png differ
diff --git a/assets/images/editors/charter/event-icons/components/warning.png b/assets/images/editors/charter/event-icons/components/warning.png
new file mode 100644
index 000000000..03a423dbf
Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/warning.png differ
diff --git a/source/funkin/backend/chart/ChartData.hx b/source/funkin/backend/chart/ChartData.hx
index c46164f54..93f18ae4b 100644
--- a/source/funkin/backend/chart/ChartData.hx
+++ b/source/funkin/backend/chart/ChartData.hx
@@ -19,7 +19,7 @@ typedef ChartMetaData = {
public var ?bpm:Float;
public var ?displayName:String;
public var ?beatsPerMeasure:Float;
- public var ?stepsPerBeat:Float;
+ public var ?stepsPerBeat:Int;
public var ?needsVoices:Bool;
public var ?icon:String;
public var ?color:Dynamic;
diff --git a/source/funkin/backend/chart/EventsData.hx b/source/funkin/backend/chart/EventsData.hx
index 79f0e210b..97eefd7f1 100644
--- a/source/funkin/backend/chart/EventsData.hx
+++ b/source/funkin/backend/chart/EventsData.hx
@@ -10,7 +10,7 @@ import funkin.backend.assets.Paths;
using StringTools;
class EventsData {
- public static var defaultEventsList:Array = ["HScript Call", "Camera Movement", "Add Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"];
+ public static var defaultEventsList:Array = ["HScript Call", "Camera Movement", "Add Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Continuous BPM Change", "Time Signature Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"];
public static var defaultEventsParams:Map> = [
"HScript Call" => [
{name: "Function Name", type: TString, defValue: "myFunc"},
@@ -31,7 +31,9 @@ class EventsData {
{name: "Time (Steps)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4},
{name: "Camera", type: TDropDown(['camGame', 'camHUD']), defValue: "camHUD"}
],
- "BPM Change" => [{name: "Target BPM", type: TFloat(1.00, null, 0.001, 3), defValue: 100}],
+ "BPM Change" => [{name: "Target BPM", type: TFloat(1.00, 9999, 0.001, 3), defValue: 100}],
+ "Continuous BPM Change" => [{name: "Target BPM", type: TFloat(1.00, 9999, 0.001, 3), defValue: 100}, {name: "Time (steps)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4}],
+ "Time Signature Change" => [{name: "Target Beat Count", type: TFloat(1), defValue: 4}, {name: "Target Step Count", type: TFloat(1), defValue: 4}],
"Scroll Speed Change" => [
{name: "Tween Speed?", type: TBool, defValue: true},
{name: "New Speed", type: TFloat(0.01, 99, 0.01, 2), defValue: 1.},
@@ -48,7 +50,7 @@ class EventsData {
}
],
"Alt Animation Toggle" => [{name: "Enable On Sing Poses", type: TBool, defValue: true}, {name: "Enable On Idle", type: TBool, defValue: true}, {name: "Strumline", type: TStrumLine, defValue: 0}],
- "Play Animation" => [{name: "Character", type: TStrumLine, defValue: 0}, {name: "Animation", type: TString, defValue: "animation"}, {name: "Is forced?", type: TBool, defValue: true}],
+ "Play Animation" => [{name: "Character", type: TStrumLine, defValue: 0}, {name: "Animation", type: TString, defValue: "animation"}, {name: "Is forced?", type: TBool, defValue: true}]
];
public static var eventsList:Array = defaultEventsList.copy();
@@ -75,11 +77,12 @@ class EventsData {
hscriptParser.allowJSON = hscriptParser.allowMetadata = false;
for (file in Paths.getFolderContent('data/events/', true, BOTH)) {
- if (Path.extension(file) != "json" && Path.extension(file) != "pack") continue;
- var eventName:String = Path.withoutExtension(Path.withoutDirectory(file));
+ var ext = Path.extension(file);
+ if (ext != "json" && ext != "pack") continue;
+ var eventName:String = CoolUtil.getFilename(file);
var fileTxt:String = Assets.getText(file);
- if (Path.extension(file) == "pack") {
+ if (ext == "pack") {
var arr = fileTxt.split("________PACKSEP________");
eventName = Path.withoutExtension(arr[0]);
fileTxt = arr[2];
@@ -97,7 +100,14 @@ class EventsData {
var finalParams:Array = [];
for (paramData in cast(data.params, Array)) {
try {
- finalParams.push({name: paramData.name, type: hscriptInterp.expr(hscriptParser.parseString(paramData.type)), defValue: paramData.defaultValue});
+ finalParams.push({
+ name: paramData.name,
+ type: hscriptInterp.expr(hscriptParser.parseString(paramData.type)),
+ defValue: paramData.defaultValue,
+
+ x: paramData.x,
+ y: paramData.y
+ });
} catch (e) {trace('Error parsing event param ${paramData.name} - ${eventName}: $e'); finalParams.push(null);}
}
eventsParams.set(eventName, finalParams);
@@ -117,6 +127,9 @@ typedef EventParamInfo = {
var name:String;
var type:EventParamType;
var defValue:Dynamic;
+
+ @:optional var x:Float;
+ @:optional var y:Float;
}
enum EventParamType {
diff --git a/source/funkin/backend/chart/FNFLegacyParser.hx b/source/funkin/backend/chart/FNFLegacyParser.hx
index 61a0f837f..6bef760e8 100644
--- a/source/funkin/backend/chart/FNFLegacyParser.hx
+++ b/source/funkin/backend/chart/FNFLegacyParser.hx
@@ -51,6 +51,18 @@ class FNFLegacyParser {
continue; // Yoshi Engine charts crash fix
}
+ var newBeatsPerMeasure:Float = section.sectionBeats != null ? section.sectionBeats : data.beatsPerMeasure.getDefault(4); // Default to 4 if sectionBeats is null or undefined (oops :3)
+
+ if (newBeatsPerMeasure != beatsPerMeasure) {
+ beatsPerMeasure = newBeatsPerMeasure;
+
+ result.events.push({
+ time: curTime,
+ name: "Time Signature Change",
+ params: [newBeatsPerMeasure, 4]
+ });
+ }
+
if (camFocusedBF != (camFocusedBF = section.mustHitSection)) {
result.events.push({
time: curTime,
@@ -174,7 +186,8 @@ class FNFLegacyParser {
mustHitSection: notes[section-1] != null ? notes[section-1].mustHitSection : false,
bpm: notes[section-1] != null ? notes[section-1].bpm : chart.meta.bpm,
changeBPM: false,
- altAnim: notes[section-1] != null ? notes[section-1].altAnim : false
+ altAnim: notes[section-1] != null ? notes[section-1].altAnim : false,
+ sectionBeats: notes[section-1] != null ? notes[section-1].sectionBeats : chart.meta.beatsPerMeasure.getDefault(4)
};
var sectionEndTime:Float = Conductor.getTimeForStep(Conductor.getMeasureLength() * (section+1));
@@ -188,6 +201,8 @@ class FNFLegacyParser {
case "BPM Change":
baseSection.changeBPM = true;
baseSection.bpm = event.params[0];
+ case "Time Signature Change":
+ baseSection.sectionBeats = event.params[0];
}
}
notes[section] = baseSection;
diff --git a/source/funkin/backend/system/Conductor.hx b/source/funkin/backend/system/Conductor.hx
index bd5fca4cf..10bc8a42e 100644
--- a/source/funkin/backend/system/Conductor.hx
+++ b/source/funkin/backend/system/Conductor.hx
@@ -1,18 +1,29 @@
package funkin.backend.system;
-import funkin.backend.chart.ChartData;
import flixel.FlxState;
-import funkin.backend.system.interfaces.IBeatReceiver;
import flixel.util.FlxSignal.FlxTypedSignal;
+import funkin.backend.chart.ChartData;
+import funkin.backend.system.interfaces.IBeatReceiver;
+import funkin.editors.charter.Charter;
-typedef BPMChangeEvent =
+@:structInit
+class BPMChangeEvent
{
- var stepTime:Float;
- var songTime:Float;
- var bpm:Float;
+ public var songTime:Float;
+ public var bpm:Float;
+ public var beatsPerMeasure:Float = 4;
+ public var stepsPerBeat:Int = 4;
+
+ public var endSongTime:Float = 0;
+ public var endStepTime:Float = 0;
+ public var continuous:Bool = false;
+
+ public var stepTime:Float;
+ public var beatTime:Float;
+ public var measureTime:Float;
}
-class Conductor
+final class Conductor
{
/**
* FlxSignals
@@ -20,47 +31,76 @@ class Conductor
public static var onMeasureHit:FlxTypedSignalVoid> = new FlxTypedSignal();
public static var onBeatHit:FlxTypedSignalVoid> = new FlxTypedSignal();
public static var onStepHit:FlxTypedSignalVoid> = new FlxTypedSignal();
- public static var onBPMChange:FlxTypedSignalVoid> = new FlxTypedSignal();
+ public static var onBPMChange:FlxTypedSignal<(Float,Float)->Void> = new FlxTypedSignal();
+ public static var onTimeSignatureChange:FlxTypedSignal<(Float,Float)->Void> = new FlxTypedSignal();
/**
- * Current BPM
+ * Current position of the song, in milliseconds.
*/
- public static var bpm:Float = 100;
+ public static var songPosition(get, default):Float;
+ private static function get_songPosition() {
+ if (songOffset != Options.songOffset) songOffset = Options.songOffset;
+ return songPosition - songOffset;
+ }
/**
- * Current Crochet (time per beat), in milliseconds.
+ * Offset of the song
*/
- public static var crochet:Float = ((60 / bpm) * 1000); // beats in milliseconds
+ public static var songOffset:Float = 0;
+
/**
- * Current StepCrochet (time per step), in milliseconds.
+ * Current bpmChangeMap index
*/
- public static var stepCrochet:Float = crochet / 4; // steps in milliseconds
+ public static var curChangeIndex:Int = 0;
/**
- * Number of beats per mesure (top number in time signature). Defaults to 4.
+ * Current bpmChangeMap
*/
- public static var beatsPerMeasure:Float = 4;
+ public static var curChange(get, never):Null;
+ private static function get_curChange()
+ return bpmChangeMap[curChangeIndex];
/**
- * Number of steps per beat (bottom number in time signature). Defaults to 4.
+ * Current BPM
*/
- public static var stepsPerBeat:Float = 4;
+ public static var bpm(get, never):Float;
+ private static function get_bpm()
+ return curChangeIndex == 0 ? startingBPM : getTimeWithIndexInBPM(songPosition, curChangeIndex);
+ /**
+ * Starting BPM
+ */
+ public static var startingBPM(get, never):Float;
+ private static function get_startingBPM()
+ return bpmChangeMap.length == 0 ? 100 : bpmChangeMap[0].bpm;
/**
- * Current position of the song, in milliseconds.
+ * Current Crochet (time per beat), in milliseconds.
+ * It should be crotchet but ehhh, now it's there for backward compatibility.
*/
- public static var songPosition(get, default):Float;
- private static function get_songPosition() {
- if (songOffset != Options.songOffset) trace(songOffset = Options.songOffset);
- return songPosition - songOffset;
- }
+ public static var crochet(get, never):Float;
+ private static function get_crochet() return 15000 * stepsPerBeat / bpm;
/**
- * Offset of the song
+ * Current StepCrochet (time per step), in milliseconds.
*/
- public static var songOffset:Float = 0;
+ public static var stepCrochet(get, never):Float;
+ private static function get_stepCrochet() return 15000 / bpm;
+
+ /**
+ * Number of beats per mesure (top number in time signature). Defaults to 4.
+ */
+ public static var beatsPerMeasure(get, never):Float;
+ private static function get_beatsPerMeasure()
+ return bpmChangeMap.length == 0 ? 4 : bpmChangeMap[curChangeIndex].beatsPerMeasure;
+
+ /**
+ * Number of steps per beat (bottom number in time signature). Defaults to 4.
+ */
+ public static var stepsPerBeat(get, never):Float;
+ private static function get_stepsPerBeat()
+ return bpmChangeMap.length == 0 ? 4 : bpmChangeMap[curChangeIndex].stepsPerBeat;
/**
* Current step
@@ -77,7 +117,6 @@ class Conductor
*/
public static var curMeasure:Int = 0;
-
/**
* Current step, as a `Float` (ex: 4.94, instead of 4)
*/
@@ -100,54 +139,154 @@ class Conductor
/**
* Array of all BPM changes that have been mapped.
*/
- public static var bpmChangeMap:Array = [];
+ public static var bpmChangeMap:Array;
+
+ /**
+ * Array of all events that have been rejected by the Conductor.
+ */
+ public static var invalidEvents:Array = [];
@:dox(hide) public function new() {}
public static function reset() {
songPosition = lastSongPos = curBeatFloat = curStepFloat = curBeat = curStep = 0;
- bpmChangeMap = [];
- changeBPM(0);
+ changeBPM();
}
+ public static function changeBPM(bpm:Float = 100, beatsPerMeasure:Float = 4, stepsPerBeat:Int = 4)
+ bpmChangeMap = [{bpm: bpm, beatsPerMeasure: beatsPerMeasure, stepsPerBeat: stepsPerBeat, songTime: 0, stepTime: 0, beatTime: 0, measureTime: 0}];
+
public static function setupSong(SONG:ChartData) {
reset();
mapBPMChanges(SONG);
- changeBPM(SONG.meta.bpm, cast SONG.meta.beatsPerMeasure.getDefault(4), cast SONG.meta.stepsPerBeat.getDefault(4));
}
+
+ private static function mapBPMChange(curChange:BPMChangeEvent, time:Float, bpm:Float):BPMChangeEvent {
+ var beatTime:Float, measureTime:Float, stepTime:Float;
+ stepTime = (curChange.continuous ? curChange.endStepTime : curChange.stepTime) + (time - (curChange.continuous ? curChange.endSongTime : curChange.songTime)) / (15000 / curChange.bpm);
+
+ beatTime = curChange.beatTime + (stepTime - curChange.stepTime) / curChange.stepsPerBeat;
+ measureTime = curChange.measureTime + (beatTime - curChange.beatTime) / curChange.beatsPerMeasure;
+
+ bpmChangeMap.push(curChange = {
+ songTime: time,
+ stepTime: stepTime,
+ beatTime: beatTime,
+ measureTime: measureTime,
+ bpm: bpm,
+ beatsPerMeasure: curChange.beatsPerMeasure,
+ stepsPerBeat: curChange.stepsPerBeat
+ });
+ return curChange;
+ }
+
/**
* Maps BPM changes from a song.
* @param song Song to map BPM changes from.
*/
- public static function mapBPMChanges(song:ChartData)
- {
- bpmChangeMap = [
- {
- stepTime: 0,
- songTime: 0,
- bpm: song.meta.bpm
+ public static function mapBPMChanges(song:ChartData) {
+ var curChange:BPMChangeEvent = {
+ songTime: 0,
+ stepTime: 0,
+ beatTime: 0,
+ measureTime: 0,
+ bpm: song.meta.bpm,
+ beatsPerMeasure: song.meta.beatsPerMeasure.getDefault(4),
+ stepsPerBeat: CoolUtil.floorInt(song.meta.stepsPerBeat.getDefault(4))
+ };
+ curChangeIndex = 0;
+ bpmChangeMap = [curChange];
+ invalidEvents = [];
+ if (song.events == null) return;
+
+ // fix the sort first...
+ var events:Array = [];
+ for (e in song.events) if (e.params != null && (e.name == "BPM Change" || e.name == "Time Signature Change" || e.name == "Continuous BPM Change")) events.push(e);
+ events.sort(function(a, b) {
+ if (MathUtil.equal(a.time, b.time)) {
+ if (a.name == "Continuous BPM Change") return 1;
+ if (b.name == "Continuous BPM Change") return -1;
}
- ];
+ return Std.int(a.time - b.time);
+ });
- if (song.events == null) return;
+ for (e in events) {
+ curChange = mapEvent(e, curChange);
+ }
+ }
+
+ private static function mapEvent(e:ChartEvent, curChange:BPMChangeEvent) {
+ var name = e.name, params = e.params, time = e.time;
+ if (curChange.continuous && MathUtil.lessThan(time, curChange.endSongTime)) { //ensure that you cannot place any conductor events during a continuous change
+ invalidEvents.push(e);
+ Logs.trace('Invalid Conductor event "${e.name}" at ${e.time} (Intersecting continuous change!)', WARNING);
+ return curChange;
+ }
- var curBPM:Float = song.meta.bpm;
- var songTime:Float = 0;
- var stepTime:Float = 0;
-
- for(e in song.events) if (e.name == "BPM Change" && e.params != null && e.params[0] is Float) {
- if (e.params[0] == curBPM) continue;
- var steps = (e.time - songTime) / ((60 / curBPM) * 1000 / 4);
- stepTime += steps;
- songTime = e.time;
- curBPM = e.params[0];
-
- bpmChangeMap.push({
- stepTime: stepTime,
- songTime: songTime,
- bpm: curBPM
+ if (name == "BPM Change" && params[0] is Float && curChange.bpm != params[0])
+ curChange = mapBPMChange(curChange, time, params[0]);
+ else if (name == "Time Signature Change") {
+ //if (beatsPerMeasure == curChange.beatsPerMeasure && stepsPerBeat == curChange.stepsPerBeat) continue;
+ /* TODO: make so time sigs doesnt stop the bpm change if its in the duration of bpm change */
+
+ if (curChange.songTime != time) curChange = mapBPMChange(curChange, time, curChange.bpm);
+ curChange.beatsPerMeasure = params[0];
+ curChange.stepsPerBeat = params[1];
+
+ curChange.stepTime = CoolUtil.floorInt(curChange.stepTime + .99998);
+ curChange.beatTime = CoolUtil.floorInt(curChange.beatTime + .99998);
+ curChange.measureTime = CoolUtil.floorInt(curChange.measureTime + .99998);
+ } else if (name == "Continuous BPM Change") {
+
+ var prevBPM = curChange.bpm;
+ if (curChange.bpm == params[0]) {
+ invalidEvents.push(e);
+ return curChange; //DO NOT!!!!
+ }
+ curChange = mapBPMChange(curChange, time, params[0]);
+ var endTime = time + (params[1]) / (curChange.bpm - prevBPM) * Math.log(curChange.bpm / prevBPM) * 15000;
+ curChange.endStepTime = curChange.stepTime + params[1];
+ curChange.continuous = true;
+ curChange.endSongTime = endTime;
+ }
+ return curChange;
+ }
+
+ public static function mapCharterBPMChanges(song:ChartData) {
+ var curChange:BPMChangeEvent = {
+ songTime: 0,
+ stepTime: 0,
+ beatTime: 0,
+ measureTime: 0,
+ bpm: song.meta.bpm,
+ beatsPerMeasure: song.meta.beatsPerMeasure.getDefault(4),
+ stepsPerBeat: CoolUtil.floorInt(song.meta.stepsPerBeat.getDefault(4))
+ };
+ curChangeIndex = 0;
+ bpmChangeMap = [curChange];
+ invalidEvents = [];
+
+ for(event in Charter.instance.eventsGroup.members) {
+ event.events.sort(function(a, b) {
+ if (MathUtil.equal(a.time, b.time)) {
+ if (a.name == "Continuous BPM Change") return 1;
+ if (b.name == "Continuous BPM Change") return -1;
+ }
+ return 0;
});
+
+ var eventTime = Conductor.getTimeForStep(event.step);
+ for (e in event.events) {
+ e.time = eventTime;
+
+ if ((e.name == "BPM Change" || e.name == "Time Signature Change" || e.name == "Continuous BPM Change")) {
+ curChange = mapEvent(e, curChange);
+ }
+
+ }
}
+
+
}
private static var elapsed:Float;
@@ -177,6 +316,7 @@ class Conductor
reset();
}
private static var __lastChange:BPMChangeEvent;
+ private static var __updateStep:Bool;
private static var __updateBeat:Bool;
private static var __updateMeasure:Bool;
@@ -185,125 +325,238 @@ class Conductor
__updateSongPos(FlxG.elapsed);
- if (bpm > 0) {
- // updates curbeat and stuff
- __lastChange = {
- stepTime: 0,
- songTime: 0,
- bpm: 0
- };
- for (change in Conductor.bpmChangeMap)
- {
- if (Conductor.songPosition >= change.songTime)
- __lastChange = change;
+ var oldStep = curStep, oldBeat = curBeat, oldMeasure = curMeasure, oldChangeIndex = curChangeIndex;
+
+ if ((curChangeIndex = getTimeInChangeIndex(songPosition, curChangeIndex)) > 0) {
+ var change = curChange;
+ curStepFloat = getTimeWithBPMInSteps(songPosition, curChangeIndex, getTimeWithIndexInBPM(songPosition, curChangeIndex));
+ curBeatFloat = change.beatTime + (curStepFloat - change.stepTime) / change.stepsPerBeat;
+ curMeasureFloat = change.measureTime + (curBeatFloat - change.beatTime) / change.beatsPerMeasure;
+ }
+ else
+ curMeasureFloat = (curBeatFloat = (curStepFloat = songPosition / stepCrochet) / stepsPerBeat) / beatsPerMeasure;
+
+ if (curChangeIndex != oldChangeIndex) {
+ var prev = bpmChangeMap[oldChangeIndex];
+ if (prev != null) {
+ if (beatsPerMeasure != prev.beatsPerMeasure || stepsPerBeat != prev.stepsPerBeat)
+ onTimeSignatureChange.dispatch(beatsPerMeasure, stepsPerBeat);
+
+ if (curChange.bpm != prev.bpm) onBPMChange.dispatch(curChange.bpm, curChange.endSongTime);
+ }
+ else {
+ onTimeSignatureChange.dispatch(beatsPerMeasure, stepsPerBeat);
+ onBPMChange.dispatch(curChange.bpm, curChange.endSongTime);
}
+ }
- if (__lastChange.bpm > 0 && bpm != __lastChange.bpm) changeBPM(__lastChange.bpm);
+ if (__updateStep = (curStep != (curStep = CoolUtil.floorInt(curStepFloat)))) {
+ if (curStep > oldStep) for (i in oldStep...curStep) onStepHit.dispatch(i + 1);
+ else onStepHit.dispatch(curStep);
+ }
- curStepFloat = __lastChange.stepTime + ((Conductor.songPosition - __lastChange.songTime) / Conductor.stepCrochet);
- curBeatFloat = curStepFloat / stepsPerBeat;
- curMeasureFloat = curBeatFloat / beatsPerMeasure;
+ if (__updateBeat = (curBeat != (curBeat = CoolUtil.floorInt(curBeatFloat)))) {
+ if (curBeat > oldBeat) for (i in oldBeat...curBeat) onBeatHit.dispatch(i + 1);
+ else onBeatHit.dispatch(curBeat);
+ }
- var oldStep = curStep;
- var oldBeat = curBeat;
- var oldMeasure = curMeasure;
- if (curStep != (curStep = CoolUtil.floorInt(curStepFloat))) {
- if (curStep < oldStep && oldStep - curStep < 2) return;
- // updates step
- __updateBeat = curBeat != (curBeat = CoolUtil.floorInt(curBeatFloat));
- __updateMeasure = __updateBeat && (curMeasure != (curMeasure = CoolUtil.floorInt(curMeasureFloat)));
+ if (__updateMeasure = (curMeasure != (curMeasure = CoolUtil.floorInt(curMeasureFloat)))) {
+ if (curMeasure > oldMeasure) for (i in oldMeasure...curMeasure) onMeasureHit.dispatch(i + 1);
+ else onMeasureHit.dispatch(curMeasure);
+ }
- if (curStep > oldStep) {
- for(i in oldStep...curStep) {
- onStepHit.dispatch(i+1);
- }
- }
- if (__updateBeat && curBeat > oldBeat) {
- for(i in oldBeat...curBeat) {
- onBeatHit.dispatch(i+1);
+ if (__updateStep || __updateBeat || __updateMeasure) {
+ var state = FlxG.state;
+ while (state != null) {
+ if (state is IBeatReceiver && (state.subState == null || state.persistentUpdate)) {
+ var st = cast(state, IBeatReceiver);
+
+ if (__updateStep) {
+ if (curStep > oldStep) for (i in oldStep...curStep) st.stepHit(i + 1);
+ else st.stepHit(curStep);
}
- }
- if (__updateMeasure && curMeasure > oldMeasure) {
- for(i in oldMeasure...curMeasure) {
- onMeasureHit.dispatch(i+1);
+
+ if (__updateBeat) {
+ if (curBeat > oldBeat) for (i in oldBeat...curBeat) st.beatHit(i + 1);
+ else st.beatHit(curBeat);
}
- }
- if (FlxG.state is IBeatReceiver) {
- var state = FlxG.state;
- while(state != null) {
- if (state is IBeatReceiver && (state.subState == null || state.persistentUpdate)) {
- var st = cast(state, IBeatReceiver);
- if (curStep > oldStep) {
- for(i in oldStep...curStep) {
- st.stepHit(i+1);
- }
- }
- if (__updateBeat && curBeat > oldBeat) {
- for(i in oldBeat...curBeat) {
- st.beatHit(i+1);
- }
- }
- if (__updateMeasure && curMeasure > oldMeasure) {
- for(i in oldMeasure...curMeasure) {
- st.measureHit(i+1);
- }
- }
- }
- state = state.subState;
+ if (__updateMeasure) {
+ if (curMeasure > oldMeasure) for (i in oldMeasure...curMeasure) st.measureHit(i + 1);
+ else st.measureHit(curMeasure);
}
}
-
+ state = state.subState;
}
}
}
- public static function changeBPM(newBpm:Float, beatsPerMeasure:Float = 4, stepsPerBeat:Float = 4)
- {
- bpm = newBpm;
+ public static function getTimeInChangeIndex(time:Float, index:Int = 0):Int {
+ if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
+ else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].songTime > time) {
+ while (--index > 0) if (time > bpmChangeMap[index].songTime) return index;
+ return 0;
+ }
+ else {
+ for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].songTime > time) return i - 1;
+ return bpmChangeMap.length - 1;
+ }
+ }
- crochet = ((60 / bpm) * 1000);
- stepCrochet = crochet / stepsPerBeat;
+ public static function getStepsInChangeIndex(stepTime:Float, index:Int = 0):Int {
+ if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
+ else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].stepTime > stepTime) {
+ while (--index > 0) if (stepTime > bpmChangeMap[index].stepTime) return index;
+ return 0;
+ }
+ else {
+ for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].stepTime > stepTime) return i - 1;
+ return bpmChangeMap.length - 1;
+ }
+ }
- Conductor.beatsPerMeasure = beatsPerMeasure;
- Conductor.stepsPerBeat = stepsPerBeat;
+ public static function getBeatsInChangeIndex(beatTime:Float, index:Int = 0):Int {
+ if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
+ else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].beatTime > beatTime) {
+ while (--index > 0) if (beatTime > bpmChangeMap[index].beatTime) return index;
+ return 0;
+ }
+ else {
+ for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].beatTime > beatTime) return i - 1;
+ return bpmChangeMap.length - 1;
+ }
+ }
- onBPMChange.dispatch(bpm);
+ public static function getMeasuresInChangeIndex(measureTime:Float, index:Int = 0):Int {
+ if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
+ else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].measureTime > measureTime) {
+ while (--index > 0) if (measureTime > bpmChangeMap[index].measureTime) return index;
+ return 0;
+ }
+ else {
+ for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].measureTime > measureTime) return i - 1;
+ return bpmChangeMap.length - 1;
+ }
}
- public static function getTimeForStep(step:Float) {
- var bpmChange:BPMChangeEvent = {
- stepTime: 0,
- songTime: 0,
- bpm: bpm
- };
+ public static function getTimeWithIndexInBPM(time:Float, index:Int):Float {
+ var bpmChange = bpmChangeMap[index];
+ if (bpmChange.continuous && time < bpmChange.endSongTime && index > 0) {
+ var prevBPM = bpmChangeMap[index - 1].bpm;
+ if (time <= bpmChange.songTime) return prevBPM;
+
+ var ratio = (time - bpmChange.songTime) / (bpmChange.endSongTime - bpmChange.songTime);
+ return Math.pow(prevBPM, 1 - ratio) * Math.pow(bpmChange.bpm, ratio);
+ }
+ return bpmChange.bpm;
+ }
- for(change in bpmChangeMap)
- if (change.stepTime < step && change.stepTime >= bpmChange.stepTime)
- bpmChange = change;
+ public static function getStepsWithIndexInBPM(stepTime:Float, index:Int):Float {
+ var bpmChange = bpmChangeMap[index];
+ if (bpmChange.continuous && index > 0) {
+ var prevBPM = bpmChangeMap[index - 1].bpm;
+ if (stepTime <= bpmChange.stepTime) return prevBPM;
- return bpmChange.songTime + ((step - bpmChange.stepTime) * ((60 / bpmChange.bpm) * (1000/stepsPerBeat)));
+ var endStepTime = bpmChange.stepTime + (bpmChange.endSongTime - bpmChange.songTime) * (bpmChange.bpm - prevBPM) / Math.log(bpmChange.bpm / prevBPM) / 15000;
+ if (stepTime < endStepTime) return FlxMath.remapToRange(stepTime, bpmChange.stepTime, endStepTime, prevBPM, bpmChange.bpm);
+ }
+ return bpmChange.bpm;
}
- public static function getStepForTime(time:Float) {
- var bpmChange:BPMChangeEvent = {
- stepTime: 0,
- songTime: 0,
- bpm: bpm
- };
+ public static function getTimeInBPM(time:Float):Float {
+ if (bpmChangeMap.length == 0) return 100;
+ return getTimeWithIndexInBPM(time, getTimeInChangeIndex(time));
+ }
- for(change in bpmChangeMap)
- if (change.songTime < time && change.songTime >= bpmChange.songTime)
- bpmChange = change;
+ public static function getTimeWithBPMInSteps(time:Float, index:Int, bpm:Float):Float {
+ var bpmChange = bpmChangeMap[index];
+ if (bpmChange.continuous && time > bpmChange.songTime && index > 0) {
+ var prevBPM = bpmChangeMap[index - 1].bpm;
+ if (time > bpmChange.endSongTime)
+ return bpmChange.stepTime + (((bpmChange.endSongTime - bpmChange.songTime) * (bpmChange.bpm - prevBPM))
+ / Math.log(bpmChange.bpm / prevBPM) + (time - bpmChange.endSongTime) * bpm) / 15000;
+ else
+ return bpmChange.stepTime + (time - bpmChange.songTime) * (bpm - prevBPM) / Math.log(bpm / prevBPM) / 15000;
+ }
+ else {
+ return bpmChange.stepTime + (time - bpmChange.songTime) / (15000 / bpm);
+ }
+ }
- return bpmChange.stepTime + ((time - bpmChange.songTime) / ((60 / bpmChange.bpm) * (1000/stepsPerBeat)));
+ public static function getTimeInBeats(time:Float, from:Int = 0):Float {
+ var index = getTimeInChangeIndex(time, from);
+ if (index == -1) return time / (60000 / 100);
+ else if (index == 0) return time / (15000 / bpmChangeMap[index].bpm) / bpmChangeMap[index].stepsPerBeat;
+ else {
+ var change = bpmChangeMap[index];
+ return change.beatTime + (getTimeWithBPMInSteps(time, index, getTimeWithIndexInBPM(time, index)) - change.stepTime) / change.stepsPerBeat;
+ }
}
+ public static function getTimeInSteps(time:Float, from:Int = 0):Float {
+ var index = getTimeInChangeIndex(time, from);
+ return index < 1 ? time / (15000 / getTimeInBPM(time)) : getTimeWithBPMInSteps(time, index, getTimeWithIndexInBPM(time, index));
+ }
+
+ @:noCompletion
+ @:haxe.warning("-WDeprecated")
+ public static inline function getStepForTime(time:Float):Float return getTimeInSteps(time);
+
+ public static function getStepsWithBPMInTime(stepTime:Float, index:Int, bpm:Float):Float {
+ var bpmChange = bpmChangeMap[index];
+ if (bpmChange.continuous && stepTime > bpmChange.stepTime && index > 0) {
+ var prevBPM = bpmChangeMap[index - 1].bpm;
+ var time = bpmChange.songTime + (stepTime - bpmChange.stepTime) / (bpm - prevBPM) * Math.log(bpm / prevBPM) * 15000;
+ if (time > bpmChange.endSongTime)
+ return (15000 * (stepTime - bpmChange.stepTime) - ((bpmChange.endSongTime - bpmChange.songTime) * (bpm - prevBPM))
+ / Math.log(bpm / prevBPM)) / bpm + bpmChange.endSongTime;
+ else
+ return time;
+ }
+ else {
+ return bpmChange.songTime + (stepTime - bpmChange.stepTime) * (15000 / bpm);
+ }
+ }
+
+ public static function getMeasuresInTime(measureTime:Float, from:Int = 0):Float {
+ var index = getMeasuresInChangeIndex(measureTime, from);
+ if (index == -1) return measureTime * (60000 / 100 / 4);
+ else if (index == 0) return measureTime * (15000 / bpmChangeMap[index].bpm) * bpmChangeMap[index].stepsPerBeat * bpmChangeMap[index].beatsPerMeasure;
+ else {
+ var change = bpmChangeMap[index];
+ var stepTime = change.stepTime + (measureTime - change.measureTime) * change.stepsPerBeat * change.beatsPerMeasure;
+ return getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
+ }
+ }
+
+ public static function getBeatsInTime(beatTime:Float, from:Int = 0):Float {
+ var index = getBeatsInChangeIndex(beatTime, from);
+ if (index == -1) return beatTime * (60000 / 100);
+ else if (index == 0) return beatTime * (15000 / bpmChangeMap[index].bpm) * bpmChangeMap[index].stepsPerBeat;
+ else {
+ var change = bpmChangeMap[index];
+ var stepTime = change.stepTime + (beatTime - change.beatTime) * change.stepsPerBeat;
+ return getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
+ }
+ }
+
+ public static function getStepsInTime(stepTime:Float, from:Int = 0):Float {
+ var index = getStepsInChangeIndex(stepTime, from);
+ return index < 1 ? stepTime * (15000 / bpmChangeMap[index].bpm) : getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
+ }
+
+ @:noCompletion
+ @:haxe.warning("-WDeprecated")
+ public static inline function getTimeForStep(steps:Float):Float return getStepsInTime(steps);
+
public static inline function getMeasureLength()
return stepsPerBeat * beatsPerMeasure;
public static inline function getMeasuresLength() {
if (FlxG.sound.music == null) return 0.0;
- return getStepForTime(FlxG.sound.music.length) / getMeasureLength();
+ var length = FlxG.sound.music.length;
+ var index = getTimeInChangeIndex(length, bpmChangeMap.length - 1);
+ var change = bpmChangeMap[index];
+ return change.measureTime + (getTimeInBeats(length, index) - change.beatTime) / change.beatsPerMeasure;
}
-}
+}
\ No newline at end of file
diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx
index f1fc14d87..ba40f4201 100644
--- a/source/funkin/backend/utils/CoolUtil.hx
+++ b/source/funkin/backend/utils/CoolUtil.hx
@@ -394,7 +394,7 @@ class CoolUtil
var timeSignParsed:Array> = musicInfo["TimeSignature"] == null ? [] : [for(s in musicInfo["TimeSignature"].split("/")) Std.parseFloat(s)];
var beatsPerMeasure:Float = 4;
- var stepsPerBeat:Float = 4;
+ var stepsPerBeat:Int = 4;
// Check later, i dont think timeSignParsed can contain null, only nan
if (timeSignParsed.length == 2 && !timeSignParsed.contains(null)) {
@@ -403,7 +403,7 @@ class CoolUtil
}
var bpm:Null = Std.parseFloat(musicInfo["BPM"]).getDefault(DefaultBPM);
- Conductor.changeBPM(bpm, beatsPerMeasure, stepsPerBeat);
+ Conductor.changeBPM(bpm, beatsPerMeasure, floorInt(stepsPerBeat));
} else
Conductor.changeBPM(DefaultBPM);
}
@@ -740,6 +740,15 @@ class CoolUtil
@:noUsing public static inline function maxInt(p1:Int, p2:Int)
return p1 < p2 ? p2 : p1;
+ /**
+ * Equivalent of `Math.min`, except doesn't require a Int -> Float -> Int conversion.
+ * @param p1
+ * @param p2
+ * @return return p1 > p2 ? p2 : p1
+ */
+ @:noUsing public static inline function minInt(p1:Int, p2:Int)
+ return p1 > p2 ? p2 : p1;
+
/**
* Equivalent of `Math.floor`, except doesn't require a Int -> Float -> Int conversion.
* @param e Value to get the floor of.
@@ -781,6 +790,36 @@ class CoolUtil
return file.file;
}
+ public static inline function bound(Value:Float, Min:Float, Max:Float):Float {
+ #if cpp
+ var _hx_tmp1:Float = Value;
+ var _hx_tmp2:Float = Min;
+ var _hx_tmp3:Float = Max;
+ return untyped __cpp__("((({0}) < ({1})) ? ({1}) : (({0}) > ({2})) ? ({2}) : ({0}))", _hx_tmp1, _hx_tmp2, _hx_tmp3);
+ #else
+ return (Value < Min) ? Min : (Value > Max) ? Max : Value;
+ #end
+ }
+
+ public static inline function boundInt(Value:Int, Min:Int, Max:Int):Int {
+ #if cpp
+ var _hx_tmp1:Int = Value;
+ var _hx_tmp2:Int = Min;
+ var _hx_tmp3:Int = Max;
+ return untyped __cpp__("((({0}) < ({1})) ? ({1}) : (({0}) > ({2})) ? ({2}) : ({0}))", _hx_tmp1, _hx_tmp2, _hx_tmp3);
+ #else
+ return (Value < Min) ? Min : (Value > Max) ? Max : Value;
+ #end
+ }
+
+ public static inline function boolToInt(b:Bool):Int {
+ #if cpp
+ return untyped __cpp__("(({0}) ? 1 : 0)", b);
+ #else
+ return b ? 1 : 0;
+ #end
+ }
+
/**
* Converts a string of "1..3,5,7..9,8..5" into an array of numbers like [1,2,3,5,7,8,9,8,7,6,5]
* @param input String to parse
diff --git a/source/funkin/backend/utils/MathUtil.hx b/source/funkin/backend/utils/MathUtil.hx
new file mode 100644
index 000000000..833f7328e
--- /dev/null
+++ b/source/funkin/backend/utils/MathUtil.hx
@@ -0,0 +1,28 @@
+package funkin.backend.utils;
+
+final class MathUtil {
+ //Remind me to add the descriptions for the Wiki later - sen
+ public static inline function lessThan(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
+ return aVal < bVal - diff;
+ }
+
+ public static inline function lessThanEqual(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
+ return aVal <= bVal + diff;
+ }
+
+ public static inline function greaterThan(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
+ return aVal > bVal + diff;
+ }
+
+ public static inline function greaterThanEqual(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
+ return aVal >= bVal - diff;
+ }
+
+ public static inline function equal(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
+ return Math.abs(aVal - bVal) <= diff;
+ }
+
+ public static inline function notEqual(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
+ return Math.abs(aVal - bVal) > diff;
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx
index daf7aad2d..96855473f 100644
--- a/source/funkin/editors/charter/Charter.hx
+++ b/source/funkin/editors/charter/Charter.hx
@@ -16,6 +16,7 @@ import flixel.input.keyboard.FlxKey;
import flixel.sound.FlxSound;
import flixel.math.FlxPoint;
import funkin.editors.charter.CharterBackdropGroup.CharterBackdropDummy;
+import funkin.editors.charter.CharterBackdropGroup.CharterGridSeperatorBase;
import funkin.backend.system.Conductor;
import funkin.backend.chart.*;
import funkin.backend.chart.ChartData;
@@ -622,6 +623,8 @@ class Charter extends UIState {
gridBackdrops.bottomLimitY = __endStep * 40;
eventsBackdrop.bottomSeparator.y = gridBackdrops.bottomLimitY-2;
+ CharterGridSeperatorBase.lastConductorSprY = Math.NEGATIVE_INFINITY;
+
updateWaveforms();
}
@@ -840,6 +843,7 @@ class Charter extends UIState {
if (s is CharterNote) cast(s, CharterNote).snappedToStrumline = true;
if (s is UISprite) cast(s, UISprite).cursor = BUTTON;
}
+ checkSelectionForBPMUpdates();
if (!(verticalChange == 0 && horizontalChange == 0)) {
notesGroup.sortNotes(); eventsGroup.sortEvents();
undos.addToUndo(CSelectionDrag(undoDrags));
@@ -1028,11 +1032,7 @@ class Charter extends UIState {
notesGroup.sortNotes();
notesGroup.autoSort = true;
- for (s in selection)
- if (s is CharterEvent) {
- Charter.instance.updateBPMEvents();
- break;
- }
+ checkSelectionForBPMUpdates();
if (addToUndo)
undos.addToUndo(CCreateSelection(selection));
@@ -1056,11 +1056,7 @@ class Charter extends UIState {
notesGroup.sortNotes();
notesGroup.autoSort = true;
- for (s in selection)
- if (s is CharterEvent) {
- Charter.instance.updateBPMEvents();
- break;
- }
+ checkSelectionForBPMUpdates();
if (addToUndo)
undos.addToUndo(CDeleteSelection(selection));
@@ -1251,7 +1247,7 @@ class Charter extends UIState {
+ '\nStep: ${curStep}'
+ '\nBeat: ${curBeat}'
+ '\nMeasure: ${curMeasure}'
- + '\nBPM: ${Conductor.bpm}'
+ + '\nBPM: ${Math.floor(Conductor.bpm*1000)/1000}'
+ '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.stepsPerBeat}';
if (charterCamera.zoom != (charterCamera.zoom = lerp(charterCamera.zoom, __camZoom, __firstFrame ? 1 : 0.125)))
@@ -1429,6 +1425,8 @@ class Charter extends UIState {
selection = sObjects;
_edit_copy(_); // to fix stupid bugs
+ checkSelectionForBPMUpdates();
+
undos.addToUndo(CCreateSelection(sObjects.copy()));
}
@@ -1474,6 +1472,7 @@ class Charter extends UIState {
if (s.selectable.draggable) s.selectable.handleDrag(s.change * -1);
selection = [for (s in selectionDrags) s.selectable];
+ Charter.instance.updateBPMEvents();
case CEditSustains(changes):
for(n in changes)
n.note.updatePos(n.note.step, n.note.id, n.before, n.note.type);
@@ -1519,6 +1518,7 @@ class Charter extends UIState {
for (s in selectionDrags)
if (s.selectable.draggable) s.selectable.handleDrag(s.change);
//this.selection = selection;
+ Charter.instance.updateBPMEvents();
case CEditSustains(changes):
for(n in changes)
n.note.updatePos(n.note.step, n.note.id, n.after, n.note.type);
@@ -1631,11 +1631,9 @@ class Charter extends UIState {
}
function _view_showeventSecSeparator(t) {
t.icon = (Options.charterShowSections = !Options.charterShowSections) ? 1 : 0;
- eventsBackdrop.eventSecSeparator.visible = gridBackdrops.sectionsVisible = Options.charterShowSections;
}
function _view_showeventBeatSeparator(t) {
t.icon = (Options.charterShowBeats = !Options.charterShowBeats) ? 1 : 0;
- eventsBackdrop.eventBeatSeparator.visible = gridBackdrops.beatsVisible = Options.charterShowBeats;
}
function _view_switchWaveformDetail(t) {
t.icon = (Options.charterLowDetailWaveforms = !Options.charterLowDetailWaveforms) ? 1 : 0;
@@ -1835,14 +1833,30 @@ class Charter extends UIState {
}
public function updateBPMEvents() {
+ eventsGroup.sortEvents();
+ Conductor.mapCharterBPMChanges(PlayState.SONG);
buildEvents();
- Conductor.mapBPMChanges(PlayState.SONG);
- Conductor.changeBPM(PlayState.SONG.meta.bpm, cast PlayState.SONG.meta.beatsPerMeasure.getDefault(4), cast PlayState.SONG.meta.stepsPerBeat.getDefault(4));
-
+ for(e in eventsGroup.members) {
+ for(event in e.events) {
+ if (event.name == "BPM Change" || event.name == "Time Signature Change" || event.name == "Continuous BPM Change") {
+ e.refreshEventIcons();
+ break;
+ }
+ }
+ }
+
refreshBPMSensitive();
}
+ public inline function checkSelectionForBPMUpdates() {
+ for (s in selection)
+ if (s is CharterEvent) {
+ Charter.instance.updateBPMEvents();
+ break;
+ }
+ }
+
public inline function hitsoundsEnabled(id:Int)
return strumLines.members[id] != null && strumLines.members[id].hitsounds;
diff --git a/source/funkin/editors/charter/CharterBackdropGroup.hx b/source/funkin/editors/charter/CharterBackdropGroup.hx
index 21eeb90fb..484b03802 100644
--- a/source/funkin/editors/charter/CharterBackdropGroup.hx
+++ b/source/funkin/editors/charter/CharterBackdropGroup.hx
@@ -12,8 +12,6 @@ class CharterBackdropGroup extends FlxTypedGroup {
public var conductorSprY:Float = 0;
public var bottomLimitY:Float = 0;
- public var sectionsVisible:Bool = true;
- public var beatsVisible:Bool = true;
// Just here so you can update display sprites all dat and above
public var strumlinesAmount:Int = 0;
@@ -61,8 +59,6 @@ class CharterBackdropGroup extends FlxTypedGroup {
grid.conductorFollowerSpr.y = conductorSprY;
grid.bottomSeparator.y = (grid.bottomLimit.y = bottomLimitY)-2;
- grid.sectionSeparator.visible = sectionsVisible;
- grid.beatSeparator.visible = beatsVisible;
grid.waveformSprite.shader = strumLine.waveformShader;
@@ -119,8 +115,7 @@ class CharterBackdrop extends FlxTypedGroup {
public var waveformSprite:FlxSprite;
public var conductorFollowerSpr:FlxSprite;
- public var beatSeparator:FlxBackdrop;
- public var sectionSeparator:FlxBackdrop;
+ public var beatSeparator:CharterGridSeperator;
public var notesGroup:FlxTypedGroup = new FlxTypedGroup();
public var strumLine:CharterStrumline;
@@ -136,23 +131,14 @@ class CharterBackdrop extends FlxTypedGroup {
waveformSprite.updateHitbox();
add(waveformSprite);
- sectionSeparator = new FlxBackdrop(null, Y, 0, 0);
- sectionSeparator.y = -2;
- sectionSeparator.visible = Options.charterShowSections;
-
- beatSeparator = new FlxBackdrop(null, Y, 0, 0);
- beatSeparator.y = -1;
- beatSeparator.visible = Options.charterShowBeats;
-
- for(sep in [sectionSeparator, beatSeparator]) {
- sep.makeSolid(1, 1, -1);
- sep.alpha = 0.5;
- sep.scrollFactor.set(1, 1);
- sep.scale.set((4 * 40), sep == sectionSeparator ? 4 : 2);
- sep.updateHitbox();
- }
+ beatSeparator = new CharterGridSeperator();
+ beatSeparator.makeSolid(1, 1, -1);
+ beatSeparator.alpha = 0.5;
+ beatSeparator.scrollFactor.set(1, 1);
+ beatSeparator.scale.set((4 * 40), 2);
+ beatSeparator.updateHitbox();
add(beatSeparator);
- add(sectionSeparator);
+
add(notesGroup);
bottomSeparator = new FlxSprite(0,-2);
@@ -200,15 +186,12 @@ class CharterBackdrop extends FlxTypedGroup {
alpha = strumLine.strumLine.visible ? 0.9 : 0.4;
} else alpha = 0.9;
- for (spr in [gridBackDrop, sectionSeparator, beatSeparator, topLimit, bottomLimit,
+ for (spr in [gridBackDrop, beatSeparator, topLimit, bottomLimit,
topSeparator, bottomSeparator, conductorFollowerSpr, waveformSprite]) {
spr.x = x; if (spr != waveformSprite) spr.alpha = alpha;
spr.cameras = this.cameras;
}
- sectionSeparator.spacing.y = (10 * Conductor.beatsPerMeasure * Conductor.stepsPerBeat) - 1;
- beatSeparator.spacing.y = (20 * Conductor.stepsPerBeat) - 1;
-
topLimit.scale.set(4 * 40, Math.ceil(FlxG.height / cameras[0].zoom));
topLimit.updateHitbox();
topLimit.y = -topLimit.height;
@@ -237,6 +220,159 @@ class CharterBackdrop extends FlxTypedGroup {
}
}
+class CharterGridSeperatorBase extends FlxSprite {
+
+ private static var minStep:Float = 0;
+ private static var maxStep:Float = 0;
+
+ private static var minBeat:Float = 0;
+ private static var maxBeat:Float = 0;
+
+ private static var minMeasure:Float = 0;
+ private static var maxMeasure:Float = 0;
+
+ private static var lastMinBeat:Float = -1;
+ private static var lastMaxBeat:Float = -1;
+
+ private static var lastMinMeasure:Float = -1;
+ private static var lastMaxMeasure:Float = -1;
+
+ public static var lastConductorSprY:Float = Math.NEGATIVE_INFINITY;
+
+ private static var beatStepTimes:Array = [];
+ private static var measureStepTimes:Array = [];
+ private static var timeSignatureChangeGaps:Array = [];
+
+ private function recalculateBeats() {
+ var conductorSprY = Charter.instance.gridBackdrops.conductorSprY;
+ if (conductorSprY == lastConductorSprY) return;
+
+ var zoomOffset = ((FlxG.height * (1/cameras[0].zoom)) * 0.5);
+
+ minStep = (conductorSprY - zoomOffset)/40;
+ maxStep = (conductorSprY + zoomOffset)/40;
+
+ var minTime:Float = Conductor.getStepsInTime(minStep);
+ var maxTime:Float = Conductor.getStepsInTime(maxStep);
+
+ var minBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(minTime)];
+ var maxBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(maxTime)];
+
+ minBeat = Conductor.getTimeInBeats(minTime);
+ maxBeat = Conductor.getTimeInBeats(maxTime);
+
+ minMeasure = minBpmChange.measureTime + (minBeat - minBpmChange.beatTime) / minBpmChange.beatsPerMeasure;
+ maxMeasure = maxBpmChange.measureTime + (maxBeat - maxBpmChange.beatTime) / maxBpmChange.beatsPerMeasure;
+
+ //cap out the beats/measures at the end of the song
+ var endTime = Conductor.getStepsInTime(Charter.instance.__endStep);
+ var endBeat = Conductor.getTimeInBeats(endTime);
+ var endBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(endTime)];
+ var endMeasure = endBpmChange.measureTime + (endBeat - endBpmChange.beatTime) / endBpmChange.beatsPerMeasure;
+
+ if (maxBeat > endBeat) maxBeat = endBeat;
+ if (maxMeasure > endMeasure) maxMeasure = endMeasure;
+ if (minMeasure < 0) minMeasure = 0;
+ if (minBeat < 0) minBeat = 0;
+
+ //only calculate if needed
+ if ((minBeat != lastMinBeat) || (maxBeat != lastMaxBeat) || (minMeasure != lastMinMeasure) || (maxMeasure != lastMaxMeasure) || lastConductorSprY == Math.NEGATIVE_INFINITY) {
+ calculateTimeSignatureGaps();
+ calculateStepTimes();
+ lastMinBeat = minBeat;
+ lastMaxBeat = maxBeat;
+ lastMinMeasure = minMeasure;
+ lastMaxMeasure = maxMeasure;
+ }
+
+ lastConductorSprY = conductorSprY;
+ }
+
+ private inline function calculateTimeSignatureGaps() {
+ //for time signatures that start mid step
+ timeSignatureChangeGaps.splice(0, timeSignatureChangeGaps.length);
+ for (i => change in Conductor.bpmChangeMap) {
+ if (change.stepTime >= minStep && change.stepTime <= maxStep) {
+ //get step while ignoring the current change
+ var index:Int = CoolUtil.boundInt(i-1, 0, Conductor.bpmChangeMap.length - 1);
+ var step:Float = Conductor.getTimeWithBPMInSteps(change.songTime, index, Conductor.getTimeWithIndexInBPM(change.songTime, index));
+
+ if (Math.ceil(step) - step > 0 && (step - Math.floor(step)) > FlxMath.EPSILON) { //mid step change
+ timeSignatureChangeGaps.push(step);
+ }
+ }
+ }
+ }
+
+ private inline function calculateStepTimes() {
+ beatStepTimes.splice(0, beatStepTimes.length);
+ for (i in Math.floor(minBeat)...Math.ceil(maxBeat)) {
+ beatStepTimes.push(Conductor.getTimeInSteps(Conductor.getBeatsInTime(i)));
+ }
+ measureStepTimes.splice(0, measureStepTimes.length);
+ for (i in Math.floor(minMeasure)...Math.ceil(maxMeasure)) {
+ measureStepTimes.push(Conductor.getTimeInSteps(Conductor.getMeasuresInTime(i)));
+ }
+ }
+
+ override public function draw() {
+
+ //should only need to recalculate once per frame and will be shared across each instance
+ recalculateBeats();
+
+ drawTimeSignatureChangeGaps();
+
+ if (Options.charterShowBeats) drawBeats();
+ if (Options.charterShowSections) drawMeasures();
+ }
+
+ private function drawBeats(offset:Float = 0.0) {
+ for (i in beatStepTimes) {
+ y = (i*40)+offset;
+ super.draw();
+ }
+ }
+ private function drawMeasures(offset:Float = 0.0) {
+ for (i in measureStepTimes) {
+ y = (i*40)+offset;
+ super.draw();
+ }
+ }
+ private function drawTimeSignatureChangeGaps() {
+ if (timeSignatureChangeGaps.length == 0) return;
+ var prevColor = color;
+ var prevBlend = blend;
+
+ color = 0xFF888888;
+ blend = MULTIPLY;
+
+ for (step in timeSignatureChangeGaps) {
+ y = step*40;
+ var diff = Math.ceil(step) - step;
+ scale.y = diff*40;
+ updateHitbox();
+
+ super.draw();
+ }
+
+ color = prevColor;
+ blend = prevBlend;
+ }
+}
+
+class CharterGridSeperator extends CharterGridSeperatorBase {
+ override private function drawBeats(offset:Float = 0.0) {
+ scale.y = 2;
+ updateHitbox();
+ super.drawBeats(-2);
+ }
+ override private function drawMeasures(offset:Float = 0.0) {
+ scale.y = 4;
+ updateHitbox();
+ super.drawMeasures(-3);
+ }
+}
+
class CharterBackdropDummy extends UISprite {
var parent:CharterBackdropGroup;
public function new(parent:CharterBackdropGroup) {
@@ -258,8 +394,7 @@ class CharterBackdropDummy extends UISprite {
}
class EventBackdrop extends FlxBackdrop {
- public var eventBeatSeparator:FlxBackdrop;
- public var eventSecSeparator:FlxBackdrop;
+ public var eventBeatSeparator:CharterEventGridSeperator;
public var topSeparator:FlxSprite;
public var bottomSeparator:FlxSprite;
@@ -269,25 +404,10 @@ class EventBackdrop extends FlxBackdrop {
alpha = 0.9;
// Separators
- eventSecSeparator = new FlxBackdrop(null, Y, 0, 0);
- eventSecSeparator.y = -2;
- eventSecSeparator.visible = Options.charterShowSections;
-
- eventBeatSeparator = new FlxBackdrop(null, Y, 0, 0);
- eventBeatSeparator.y = -1;
- eventBeatSeparator.visible = Options.charterShowBeats;
-
- for(sep in [eventSecSeparator, eventBeatSeparator]) {
- sep.makeSolid(1, 1, -1);
- sep.alpha = 0.5;
- sep.scrollFactor.set(1, 1);
- }
-
- eventSecSeparator.scale.set(20, 4);
- eventSecSeparator.updateHitbox();
-
- eventBeatSeparator.scale.set(10, 2);
- eventBeatSeparator.updateHitbox();
+ eventBeatSeparator = new CharterEventGridSeperator();
+ eventBeatSeparator.makeSolid(1, 1, -1);
+ eventBeatSeparator.alpha = 0.5;
+ eventBeatSeparator.scrollFactor.set(1, 1);
bottomSeparator = new FlxSprite(0,-2);
bottomSeparator.makeSolid(1, 1, -1);
@@ -308,23 +428,32 @@ class EventBackdrop extends FlxBackdrop {
public override function draw() {
super.draw();
- eventSecSeparator.spacing.y = (10 * Conductor.beatsPerMeasure * Conductor.stepsPerBeat) - 1;
- eventBeatSeparator.spacing.y = (20 * Conductor.stepsPerBeat) - 1;
-
- eventSecSeparator.cameras = cameras;
- eventSecSeparator.x = (x+width) - 20;
- if (eventSecSeparator.visible) eventSecSeparator.draw();
-
eventBeatSeparator.cameras = cameras;
- eventBeatSeparator.x = (x+width) - 10;
- if (eventBeatSeparator.visible) eventBeatSeparator.draw();
+ eventBeatSeparator.xPos = x+width;
+ eventBeatSeparator.draw();
topSeparator.x = (x+width) - 20;
topSeparator.cameras = this.cameras;
- if (!eventSecSeparator.visible) topSeparator.draw();
+ if (!Options.charterShowSections) topSeparator.draw();
bottomSeparator.x = (x+width) - 20;
bottomSeparator.cameras = this.cameras;
bottomSeparator.draw();
}
+}
+class CharterEventGridSeperator extends CharterGridSeperatorBase {
+ public var xPos:Float = 0.0;
+ override private function drawBeats(offset:Float = 0.0) {
+ scale.set(10, 2);
+ updateHitbox();
+ x = xPos-10;
+ super.drawBeats(-2);
+ }
+ override private function drawMeasures(offset:Float = 0.0) {
+ scale.set(20, 4);
+ updateHitbox();
+ x = xPos-20;
+ super.drawMeasures(-3);
+ }
+ override private function drawTimeSignatureChangeGaps() {}
}
\ No newline at end of file
diff --git a/source/funkin/editors/charter/CharterEvent.hx b/source/funkin/editors/charter/CharterEvent.hx
index 8bb7fc75b..9dcee4b1e 100644
--- a/source/funkin/editors/charter/CharterEvent.hx
+++ b/source/funkin/editors/charter/CharterEvent.hx
@@ -1,12 +1,18 @@
package funkin.editors.charter;
+import funkin.backend.shaders.CustomShader;
+import funkin.backend.system.Conductor;
+import flixel.group.FlxSpriteGroup;
+import flixel.math.FlxPoint;
import flixel.system.FlxAssets.FlxGraphicAsset;
+import funkin.backend.chart.ChartData.ChartEvent;
+import funkin.backend.scripting.DummyScript;
+import funkin.backend.scripting.Script;
import funkin.editors.charter.Charter.ICharterSelectable;
-import flixel.math.FlxPoint;
+import funkin.editors.charter.CharterBackdropGroup.EventBackdrop;
import funkin.game.Character;
import funkin.game.HealthIcon;
-import funkin.editors.charter.CharterBackdropGroup.EventBackdrop;
-import funkin.backend.chart.ChartData.ChartEvent;
+import openfl.display.BitmapData;
class CharterEvent extends UISliceSprite implements ICharterSelectable {
public var events:Array;
@@ -45,19 +51,78 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
sprite.colorTransform = colorTransform;
}
+ /**
+ * Pack data is a list of 4 strings separated by `________PACKSEP________`
+ * [0] Event Name
+ * [1] Event Script
+ * [2] Event JSON Info
+ * [3] Event Icon
+ * [4] Event UI Script / Icon Script
+ **/
+ @:dox(hide) public static function getPackData(name:String):Array {
+ var packFile = Paths.pack('events/${name}');
+ if (Assets.exists(packFile)) {
+ return Assets.getText(packFile).split('________PACKSEP________');
+ }
+ return null;
+ }
+
+ @:dox(hide) public static function getUIScript(event:ChartEvent, caller:String):Script {
+ var uiScript = Paths.script('data/events/${event.name}.ui');
+ var script:Script = null;
+ if(Assets.exists(uiScript)) {
+ script = Script.create(uiScript);
+ } else {
+ var packData = getPackData(event.name);
+ if(packData != null) {
+ var scriptFile = packData[4];
+ if(scriptFile != null) {
+ script = Script.fromString(scriptFile, uiScript);
+ }
+ }
+ }
+
+ if(script != null && !(script is DummyScript)) {
+ // classes and functions
+ script.set("EventIconGroup", EventIconGroup); // automatically imported
+ script.set("EventNumber", EventNumber); // automatically imported
+ script.set("getIconFromStrumline", getIconFromStrumline);
+ script.set("getIconFromCharName", getIconFromCharName);
+ script.set("generateDefaultIcon", generateDefaultIcon);
+ script.set("generateEventIconDurationArrow", generateEventIconDurationArrow);
+ script.set("generateEventIconNumbers", generateEventIconNumbers);
+ script.set("generateEventIconWarning", generateEventIconWarning);
+ script.set("getPackData", getPackData);
+ script.set("getEventComponent", getEventComponent);
+ // data
+ script.set("event", event);
+ script.set("caller", caller);
+
+ script.load();
+ }
+
+ return script;
+ }
+
+ /**
+ * Generates the default event icon for the wanted event
+ * @param name The name of the event
+ * @return The icon
+ **/
private static function generateDefaultIcon(name:String) {
var isBase64:Bool = false;
var path:String = Paths.image('editors/charter/event-icons/$name');
var defaultPath = Paths.image('editors/charter/event-icons/Unknown');
- if (!Assets.exists(path)) path = defaultPath;
-
- var packPath = Paths.pack('events/$name');
- if (Assets.exists(packPath)) {
- var packText = Assets.getText(packPath).split('________PACKSEP________');
- var packImg = packText[3];
- if(packImg != null && packImg.length > 0) {
- isBase64 = !packImg.startsWith("assets/");
- path = packImg;
+ if(!Assets.exists(path)) {
+ path = defaultPath;
+
+ var packData = getPackData(name);
+ if(packData != null) {
+ var packImg = packData[3];
+ if(packImg != null && packImg.length > 0) {
+ isBase64 = !packImg.startsWith("assets/");
+ path = packImg;
+ }
}
}
path = path.trim();
@@ -78,24 +143,174 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
return new FlxSprite().loadGraphic(graphic);
}
- public static function generateEventIcon(event:ChartEvent) {
- return switch(event.name) {
- default:
- generateDefaultIcon(event.name);
+ /**
+ * Gets a component sprite from the editors/charter/event-icons/components folder
+ * If you wanna use a number, please use the EventNumber class instead
+ * @param type The type of component to get
+ * @param x The x position of the sprite (optional)
+ * @param y The y position of the sprite (optional)
+ * @return The component sprite
+ **/
+ public static function getEventComponent(type:String, x:Float = 0.0, y:Float = 0.0) {
+ var componentPath = Paths.image("editors/charter/event-icons/components/" + type);
+ if(Assets.exists(componentPath))
+ return new FlxSprite(x, y).loadGraphic(componentPath);
+
+ Logs.trace('Could not find component $type', WARNING);
+ return null;
+ }
+
+ /**
+ * Expected to be called from inside of a ui script,
+ * calling this elsewhere might cause unexpected results or crashes
+ **/
+ public static function getIconFromStrumline(index:Null) {
+ var state = cast(FlxG.state, Charter);
+ if (index != null && index >= 0 && index < state.strumLines.length) {
+ return getIconFromCharName(state.strumLines.members[index].strumLine.characters[0]);
+ }
+ return null;
+ }
+
+ public static function getIconFromCharName(name:String) {
+ var icon = Character.getIconFromCharName(name);
+ var healthIcon = new HealthIcon(icon);
+ CoolUtil.setUnstretchedGraphicSize(healthIcon, 32, 32, false);
+ healthIcon.scrollFactor.set(1, 1);
+ healthIcon.active = false;
+ return healthIcon;
+ }
+
+ public static function generateEventIcon(event:ChartEvent, inMenu:Bool = true):FlxSprite {
+ var script = getUIScript(event, "event-icon");
+ if(script != null && !(script is DummyScript)) {
+ script.set("inMenu", inMenu);
+ if(script.get("generateIcon") != null) {
+ var res:FlxSprite = script.call("generateIcon");
+ if(res != null)
+ return res;
+ }
+ }
+
+ switch(event.name) {
+ case "Time Signature Change":
+ if(event.params != null && (event.params[0] >= 0 || event.params[1] >= 0)) {
+ var group = new EventIconGroup();
+ group.add(generateDefaultIcon(event.name));
+ group.add({ // top
+ var num = new EventNumber(9, -1, event.params[0], EventNumber.ALIGN_CENTER);
+ num.active = false;
+ num;
+ });
+ group.add({ // bottom
+ var num = new EventNumber(9, 10, event.params[1], EventNumber.ALIGN_CENTER);
+ num.active = false;
+ num;
+ });
+ if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
+ return group;
+ }
+ case "Continuous BPM Change":
+ if(event.params != null && event.params[1] != null) {
+ var group = new EventIconGroup();
+ group.add(generateDefaultIcon("BPM Change Start"));
+ if (!inMenu) {
+ generateEventIconDurationArrow(group, event.params[1]);
+ group.members[0].y -= 2;
+ generateEventIconNumbers(group, event.params[0], 3);
+ }
+ if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
+ return group;
+ } else {
+ return generateDefaultIcon("BPM Change Start");
+ }
+ case "BPM Change":
+ if(event.params != null && event.params[0] != null) {
+ var group = new EventIconGroup();
+ group.add(generateDefaultIcon(event.name));
+ if (!inMenu) {
+ group.members[0].y -= 2;
+ generateEventIconNumbers(group, event.params[0], 3);
+ }
+ if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
+ return group;
+ }
+
+ case "Scroll Speed Change":
+ if(event.params != null && !inMenu) {
+ var group = new EventIconGroup();
+ group.add(generateDefaultIcon(event.name));
+ if (event.params[0]) generateEventIconDurationArrow(group, event.params[2]);
+ group.members[0].y -= 2;
+ generateEventIconNumbers(group, event.params[1]);
+ return group;
+ }
+
case "Camera Movement":
- // custom icon for camera movement
- var state = cast(FlxG.state, Charter);
- if (event.params != null && event.params[0] != null && event.params[0] >= 0 && event.params[0] < state.strumLines.length) {
- // camera movement, use health icon
- var icon = Character.getIconFromCharName(state.strumLines.members[event.params[0]].strumLine.characters[0]);
- var healthIcon = new HealthIcon(icon);
- healthIcon.setUnstretchedGraphicSize(32, 32, false);
- healthIcon.scrollFactor.set(1, 1);
- healthIcon.active = false;
- healthIcon;
- } else
- generateDefaultIcon(event.name);
+ // camera movement, use health icon
+ if(event.params != null) {
+ var icon = getIconFromStrumline(event.params[0]);
+ if(icon != null) return icon;
+ }
}
+ return generateDefaultIcon(event.name);
+ }
+
+ private static function generateEventIconNumbers(group:EventIconGroup, number:Float, x:Float = 4, y:Float = 15, spacing:Float = 5, precision:Int = 3) {
+ group.add({
+ var num = new EventNumber(x, y, number, EventNumber.ALIGN_CENTER, spacing, precision);
+ if (num.numWidth > 20) {
+ num.scale.x = num.scale.y = 20 / num.numWidth;
+ }
+ num.active = false;
+ num;
+ });
+ }
+
+ private static function generateEventIconDurationArrow(group:EventIconGroup, stepDuration:Float) {
+ //var group = new EventIconGroup();
+ //group.add(generateDefaultIcon(startIcon));
+
+ var xOffset = 4;
+ var yGap = 24;
+ var endGap = 2;
+
+ if (stepDuration >= 0.55) { //min time for showing arrow
+ var tail = new FlxSprite(xOffset, yGap);
+ var arrow = new FlxSprite(xOffset, (stepDuration * 40) + endGap);
+ var arrowSegment = new FlxSprite(xOffset, yGap);
+ tail.frames = arrow.frames = arrowSegment.frames = Paths.getSparrowAtlas("editors/charter/event-icons/components/arrow-down");
+
+ group.add({
+ tail.animation.addByPrefix("tail", "tail");
+ tail.animation.play("tail");
+ tail;
+ });
+
+ group.add({
+ arrowSegment.animation.addByPrefix("segment", "segment");
+ arrowSegment.animation.play("segment");
+ arrowSegment.scale.y = endGap + (stepDuration * 40) - (tail.height + yGap);
+ arrowSegment.updateHitbox();
+ arrowSegment.y += tail.height;
+ arrowSegment;
+ });
+
+ group.add({
+ arrow.animation.addByPrefix("arrow", "arrow");
+ arrow.animation.play("arrow");
+ arrow;
+ });
+ }
+ }
+
+ private static function generateEventIconWarning(group:EventIconGroup) {
+ for (spr in group) {
+ spr.colorTransform.redMultiplier = spr.colorTransform.greenMultiplier = spr.colorTransform.blueMultiplier = 0.5;
+ spr.colorTransform.redOffset = 100;
+ }
+ group.add(getEventComponent("warning", 16, -8));
+ group.copyColorTransformToChildren = false;
}
public override function onHovered() {
@@ -123,18 +338,152 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
}
for(event in events) {
- var spr = generateEventIcon(event);
+ var spr = generateEventIcon(event, false);
icons.push(spr);
members.push(spr);
}
draggable = true;
- for (event in events)
- if (event.name == "BPM Change") {
- draggable = false;
- break;
- }
x = (snappedToGrid && eventsBackdrop != null ? eventsBackdrop.x : 0) - (bWidth = 37 + (icons.length * 22));
}
+}
+
+class EventIconGroup extends FlxSpriteGroup {
+ public var forceWidth:Float = 16;
+ public var forceHeight:Float = 16;
+ public var dontTransformChildren:Bool = true;
+ public var copyColorTransformToChildren:Bool = true;
+
+ public function new() {
+ super();
+ scrollFactor.set(1, 1);
+ }
+
+ override function preAdd(sprite:FlxSprite):Void
+ {
+ super.preAdd(sprite);
+ sprite.scrollFactor.set(1, 1);
+ }
+
+ public override function transformChildren(Function:FlxSprite->V->Void, Value:V):Void
+ {
+ if (dontTransformChildren)
+ return;
+
+ super.transformChildren(Function, Value);
+ }
+
+ override function set_x(Value:Float):Float
+ {
+ if (exists && x != Value)
+ transformChildren(xTransform, Value - x); // offset
+ return x = Value;
+ }
+
+ override function set_y(Value:Float):Float
+ {
+ if (exists && y != Value)
+ transformChildren(yTransform, Value - y); // offset
+ return y = Value;
+ }
+
+ override function get_width() {
+ return forceWidth;
+ }
+ override function get_height() {
+ return forceHeight;
+ }
+
+ override public function draw() {
+ @:privateAccess
+ if (copyColorTransformToChildren && colorTransform != null) for (child in members) child.colorTransform.__copyFrom(colorTransform);
+ super.draw();
+ }
+
+ override function update(elapsed:Float) {
+ super.update(elapsed);
+ }
+}
+
+class EventNumber extends FlxSprite {
+ public static inline final ALIGN_NORMAL:Int = 0;
+ public static inline final ALIGN_CENTER:Int = 1;
+
+ public var digits:Array = [];
+ public static inline final FRAME_POINT:Int = 10;
+ public static inline final FRAME_NEGATIVE:Int = 11;
+
+ public var align:Int = ALIGN_NORMAL;
+ public var spacing:Float = 6;
+
+ public function new(x:Float, y:Float, number:Float, ?align:Int = ALIGN_NORMAL, spacing:Float = 6, precision:Int = 3) {
+ super(x, y);
+ this.digits = [];
+ this.align = align;
+ this.spacing = spacing;
+
+
+
+ if (number == 0) {
+ this.digits.insert(0, 0);
+ } else {
+
+ var decimals:Float = FlxMath.roundDecimal(Math.abs(number % 1), precision);
+ if (decimals > 0) this.digits.insert(0, FRAME_POINT);
+ while(decimals > 0) {
+ this.digits.push(Math.floor(decimals * 10));
+ decimals = FlxMath.roundDecimal((decimals * 10) % 1, precision);
+ }
+
+ var ints = Std.int(Math.abs(number));
+ if (ints == 0) this.digits.insert(0, 0);
+ while (ints > 0) {
+ this.digits.insert(0, ints % 10);
+ ints = Std.int(ints / 10);
+ }
+
+ if (number < 0) {
+ this.digits.insert(0, FRAME_NEGATIVE);
+ }
+ }
+
+ loadGraphic(Paths.image('editors/charter/event-icons/components/eventNums'), true, 6, 7);
+ }
+
+ override function update(elapsed:Float) {
+ super.update(elapsed);
+ }
+
+ override function draw() {
+ var baseX = x;
+ var offsetX = 0.0;
+ if(align == ALIGN_CENTER) offsetX = -(digits.length - 1) * spacing * Math.abs(scale.x) / 2;
+
+ x = baseX + offsetX;
+ for (i in 0...digits.length) {
+ frame = frames.frames[digits[i]];
+ super.draw();
+ x += spacing * Math.abs(scale.x);
+ }
+ x = baseX;
+ }
+
+ public var numWidth(get, never):Float;
+ private function get_numWidth():Float {
+ return Math.abs(scale.x) * spacing * digits.length;
+ }
+ public var numHeight(get, never):Float;
+ private function get_numHeight():Float {
+ return Math.abs(scale.y) * frameHeight;
+ }
+
+ public override function updateHitbox():Void {
+ var numWidth = this.numWidth;
+ var numHeight = this.numHeight;
+ width = numWidth;
+ height = numHeight;
+ offset.set(-0.5 * (numWidth - spacing * digits.length), -0.5 * (numHeight - frameHeight));
+ centerOrigin();
+ }
}
\ No newline at end of file
diff --git a/source/funkin/editors/charter/CharterEventScreen.hx b/source/funkin/editors/charter/CharterEventScreen.hx
index 7c2c03c19..b2feccca9 100644
--- a/source/funkin/editors/charter/CharterEventScreen.hx
+++ b/source/funkin/editors/charter/CharterEventScreen.hx
@@ -1,10 +1,13 @@
package funkin.editors.charter;
-import funkin.backend.chart.ChartData.ChartEvent;
-import funkin.backend.system.Conductor;
import flixel.group.FlxGroup;
-import funkin.backend.chart.EventsData;
import flixel.util.FlxColor;
+import funkin.backend.chart.ChartData.ChartEvent;
+import funkin.backend.chart.EventsData;
+import funkin.backend.scripting.DummyScript;
+import funkin.backend.scripting.Script;
+import funkin.backend.system.Conductor;
+import funkin.editors.charter.CharterEvent.EventNumber;
using StringTools;
@@ -55,7 +58,7 @@ class CharterEventScreen extends UISubstateWindow {
});
eventsList.add(new EventButton(events[events.length-1], CharterEvent.generateEventIcon(events[events.length-1]), events.length-1, this, eventsList));
changeTab(events.length-1);
- }));
+ }, chartEvent.step));
for (k=>i in events)
eventsList.add(new EventButton(i, CharterEvent.generateEventIcon(i), k, this, eventsList));
add(eventsList);
@@ -80,12 +83,12 @@ class CharterEventScreen extends UISubstateWindow {
else {
chartEvent.events = [for (i in eventsList.buttons.members) i.event];
chartEvent.refreshEventIcons();
- Charter.instance.updateBPMEvents();
Charter.undos.addToUndo(CEditEvent(chartEvent, oldEvents, [for (event in events) Reflect.copy(event)]));
}
}
+ Charter.instance.updateBPMEvents();
close();
});
saveButton.x -= saveButton.bWidth;
@@ -110,83 +113,115 @@ class CharterEventScreen extends UISubstateWindow {
// destroy old elements
paramsFields = [];
- for(e in paramsPanel) {
- e.destroy();
- paramsPanel.remove(e);
+ while(paramsPanel.members.length > 0) {
+ var e = paramsPanel.members.pop();
+ if(e != null) {
+ e.destroy();
+ }
}
if (id >= 0 && id < events.length) {
curEvent = id;
var curEvent = events[curEvent];
eventName.text = curEvent.name;
- // add new elements
- var y:Float = eventName.y + eventName.height + 10;
- for(k=>param in EventsData.getEventParams(curEvent.name)) {
- function addLabel() {
- var label:UIText = new UIText(eventName.x, y, 0, param.name);
- y += label.height + 4;
- paramsPanel.add(label);
- };
-
- var value:Dynamic = CoolUtil.getDefault(curEvent.params[k], param.defValue);
- var lastAdded = switch(param.type) {
- case TString:
- addLabel();
- var textBox:UITextBox = new UITextBox(eventName.x, y, cast value);
- paramsPanel.add(textBox); paramsFields.push(textBox);
- textBox;
- case TBool:
- var checkbox = new UICheckbox(eventName.x, y, param.name, cast value);
- paramsPanel.add(checkbox); paramsFields.push(checkbox);
- checkbox;
- case TInt(min, max, step):
- addLabel();
- var numericStepper = new UINumericStepper(eventName.x, y, cast value, step.getDefault(1), 0, min, max);
- paramsPanel.add(numericStepper); paramsFields.push(numericStepper);
- numericStepper;
- case TFloat(min, max, step, precision):
- addLabel();
- var numericStepper = new UINumericStepper(eventName.x, y, cast value, step.getDefault(1), precision, min, max);
- paramsPanel.add(numericStepper); paramsFields.push(numericStepper);
- numericStepper;
- case TStrumLine:
- addLabel();
- var dropdown = new UIDropDown(eventName.x, y, 320, 32, [for(k=>s in cast(FlxG.state, Charter).strumLines.members) 'Strumline #${k+1} (${s.strumLine.characters[0]})'], cast value);
- paramsPanel.add(dropdown); paramsFields.push(dropdown);
- dropdown;
- case TColorWheel:
- addLabel();
- var colorWheel = new UIColorwheel(eventName.x, y, value is String ? FlxColor.fromString(value) : Std.int(value));
- paramsPanel.add(colorWheel); paramsFields.push(colorWheel);
- colorWheel;
- case TDropDown(options):
- addLabel();
- var dropdown = new UIDropDown(eventName.x, y, 320, 32, options, Std.int(Math.abs(options.indexOf(cast value))));
- paramsPanel.add(dropdown); paramsFields.push(dropdown);
- dropdown;
- default:
- paramsFields.push(null);
- null;
- }
- if (lastAdded is UISliceSprite)
- y += cast(lastAdded, UISliceSprite).bHeight + 4;
- else if (lastAdded is FlxSprite)
- y += cast(lastAdded, FlxSprite).height + 6;
- }
+
+ generateEventUI(curEvent);
} else {
eventName.text = "No event";
curEvent = -1;
}
}
+ function generateEventUI(event:ChartEvent):Void {
+ var script = CharterEvent.getUIScript(event, "event-ui");
+ if(script != null && !(script is DummyScript)) {
+ script.set("paramsPanel", paramsPanel);
+ script.set("paramsFields", paramsFields);
+ if(script.get("generateUI") != null) {
+ if(script.call("generateUI") == false)
+ return;
+ }
+ }
+
+ // add new elements
+ var _y:Float = eventName.y + eventName.height + 10;
+ var params = EventsData.getEventParams(event.name);
+ for(k=>param in params) {
+ var x = eventName.x + (param.x == null ? 0 : param.x);
+ var y = (param.y == null ? _y : param.y);
+
+ function addLabel() {
+ var label:UIText = new UIText(x, y, 0, param.name);
+ _y += label.height + 4;
+ y += label.height + 4;
+ paramsPanel.add(label);
+ };
+
+ var value:Dynamic = CoolUtil.getDefault(event.params[k], param.defValue);
+ var lastAdded = switch(param.type) {
+ case TString:
+ addLabel();
+ var textBox:UITextBox = new UITextBox(x, y, cast value);
+ paramsPanel.add(textBox); paramsFields.push(textBox);
+ textBox;
+ case TBool:
+ var checkbox = new UICheckbox(x, y, param.name, cast value);
+ paramsPanel.add(checkbox); paramsFields.push(checkbox);
+ checkbox;
+ case TInt(min, max, step):
+ addLabel();
+ var numericStepper = new UINumericStepper(x, y, cast value, CoolUtil.getDefault(step, 1), 0, min, max);
+ paramsPanel.add(numericStepper); paramsFields.push(numericStepper);
+ numericStepper;
+ case TFloat(min, max, step, precision):
+ addLabel();
+ var numericStepper = new UINumericStepper(x, y, cast value, CoolUtil.getDefault(step, 1), precision, min, max);
+ paramsPanel.add(numericStepper); paramsFields.push(numericStepper);
+ numericStepper;
+ case TStrumLine:
+ addLabel();
+ var dropdown = new UIDropDown(x, y, 320, 32, [
+ for(k=>s in cast(FlxG.state, Charter).strumLines.members)
+ 'Strumline #${k+1} (${s.strumLine.characters[0]})'
+ ], cast value);
+ paramsPanel.add(dropdown); paramsFields.push(dropdown);
+ dropdown;
+ case TColorWheel:
+ addLabel();
+ var colorWheel = new UIColorwheel(x, y, CoolUtil.getColorFromDynamic(value));
+ paramsPanel.add(colorWheel); paramsFields.push(colorWheel);
+ colorWheel;
+ case TDropDown(options):
+ addLabel();
+ var dropdown = new UIDropDown(x, y, 320, 32, options, Std.int(Math.abs(options.indexOf(cast value))));
+ paramsPanel.add(dropdown); paramsFields.push(dropdown);
+ dropdown;
+ default:
+ paramsFields.push(null);
+ null;
+ }
+ if (lastAdded is UISliceSprite)
+ _y += cast(lastAdded, UISliceSprite).bHeight + 4;
+ else if (lastAdded is FlxSprite)
+ _y += cast(lastAdded, FlxSprite).height + 6;
+ }
+
+ if(script != null && !(script is DummyScript)) {
+ if(script.get("postGenerateUI") != null) {
+ script.set("params", params);
+ script.call("postGenerateUI");
+ }
+ }
+ }
+
public function saveCurTab() {
if (curEvent < 0) return;
+ var dataParams = EventsData.getEventParams(events[curEvent].name);
events[curEvent].params = [
- for(p in paramsFields) {
+ for(i=>p in paramsFields) {
if (p is UIDropDown) {
- var dataParams = EventsData.getEventParams(events[curEvent].name);
- if (dataParams[paramsFields.indexOf(p)].type == TStrumLine) cast(p, UIDropDown).index;
+ if (dataParams[i].type == TStrumLine) cast(p, UIDropDown).index;
else cast(p, UIDropDown).label.text;
}
else if (p is UINumericStepper) {
diff --git a/source/funkin/editors/charter/CharterEventTypeSelection.hx b/source/funkin/editors/charter/CharterEventTypeSelection.hx
index bfa6cfe78..1b1754c66 100644
--- a/source/funkin/editors/charter/CharterEventTypeSelection.hx
+++ b/source/funkin/editors/charter/CharterEventTypeSelection.hx
@@ -1,9 +1,11 @@
package funkin.editors.charter;
+import funkin.backend.system.Conductor;
import funkin.backend.chart.EventsData;
class CharterEventTypeSelection extends UISubstateWindow {
var callback:String->Void;
+ var eventStepTime:Float;
var buttons:Array = [];
@@ -13,9 +15,10 @@ class CharterEventTypeSelection extends UISubstateWindow {
var upIndicator:UIText;
var downIndicator:UIText;
- public function new(callback:String->Void) {
+ public function new(callback:String->Void, eventStepTime:Float) {
super();
this.callback = callback;
+ this.eventStepTime = eventStepTime;
}
public override function create() {
@@ -32,6 +35,15 @@ class CharterEventTypeSelection extends UISubstateWindow {
buttonsBG.frames = Paths.getFrames('editors/ui/inputbox');
add(buttonsBG);
+ var disableConductorEvents:Bool = false;
+ var disableOnlyContinuousChanges:Bool = false;
+ for (change in Conductor.bpmChangeMap) {
+ if (change.continuous && MathUtil.greaterThanEqual(eventStepTime, change.stepTime) && MathUtil.lessThan(eventStepTime, change.endStepTime)) {
+ disableOnlyContinuousChanges = MathUtil.equal(eventStepTime, change.stepTime); //allow time sig and instant bpm changes on the same event
+ disableConductorEvents = !disableOnlyContinuousChanges;
+ }
+ }
+
for(k=>eventName in EventsData.eventsList) {
var button = new UIButton(0, (32 * k), eventName, function() {
close();
@@ -52,6 +64,11 @@ class CharterEventTypeSelection extends UISubstateWindow {
icon.x = button.x + 8;
icon.y = button.y + Math.abs(button.bHeight - icon.height) / 2;
add(icon);
+
+ if (disableConductorEvents && (eventName == "Time Signature Change" || eventName == "Continuous BPM Change" || eventName == "BPM Change") || (disableOnlyContinuousChanges && eventName == "Continuous BPM Change")) {
+ button.selectable = button.shouldPress = false;
+ button.autoAlpha = true;
+ }
}
windowSpr.bHeight = 61 + (32 * (17));
@@ -79,7 +96,7 @@ class CharterEventTypeSelection extends UISubstateWindow {
sinner += elapsed;
for (button in buttons)
- button.selectable = buttonsBG.hovered;
+ if (button.shouldPress) button.selectable = buttonsBG.hovered;
buttonCameras.zoom = subCam.zoom;
diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx
index bff696ced..1744c3acf 100644
--- a/source/funkin/game/PlayState.hx
+++ b/source/funkin/game/PlayState.hx
@@ -232,6 +232,15 @@ class PlayState extends MusicBeatState
* How strong the cam zooms should be (defaults to 1)
*/
public var camZoomingStrength:Float = 1;
+ /**
+ * Number of Beats to offset camZooming by.
+ * Will automatically be set when a Time Signature Change Occurs.
+ */
+ public var camZoomingOffset:Float = 0;
+ /**
+ * The curBeat position of the last Time Signature Change that occured
+ */
+ public var lastTimeSigBeat:Float = 0;
/**
* Maximum amount of zoom for the camera.
*/
@@ -1026,8 +1035,6 @@ class PlayState extends MusicBeatState
camZoomingInterval = cast songData.meta.beatsPerMeasure.getDefault(4);
- Conductor.changeBPM(songData.meta.bpm, cast songData.meta.beatsPerMeasure.getDefault(4), cast songData.meta.stepsPerBeat.getDefault(4));
-
curSong = songData.meta.name.toLowerCase();
inst = FlxG.sound.load(Paths.inst(SONG.meta.name, difficulty));
@@ -1463,6 +1470,9 @@ class PlayState extends MusicBeatState
if (strumLines.members[event.params[0]] != null && strumLines.members[event.params[0]].characters != null)
for (char in strumLines.members[event.params[0]].characters)
if (char != null) char.playAnim(event.params[1], event.params[2], null);
+ case "Time Signature Change":
+ lastTimeSigBeat = Conductor.getTimeInBeats(event.time);
+ // the rest is automatically handled by conductor
case "Unknown": // nothing
}
}
@@ -1875,10 +1885,10 @@ class PlayState extends MusicBeatState
override function beatHit(curBeat:Int)
{
super.beatHit(curBeat);
+
+ camZoomingOffset = curBeat - lastTimeSigBeat;
- if (camZoomingInterval < 1) camZoomingInterval = 1;
- if (Options.camZoomOnBeat && camZooming && FlxG.camera.zoom < maxCamZoom && curBeat % camZoomingInterval == 0)
- {
+ if (Options.camZoomOnBeat && camZooming && FlxG.camera.zoom < maxCamZoom && camZoomingOffset % camZoomingInterval == 0) {
FlxG.camera.zoom += 0.015 * camZoomingStrength;
camHUD.zoom += 0.03 * camZoomingStrength;
}