1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-21 09:29:41 +00:00

Merge branch 'feature/chart-editor-events'

This commit is contained in:
EliteMasterEric 2023-01-22 20:03:08 -05:00
commit e922c026a9
44 changed files with 13825 additions and 12167 deletions

View file

@ -151,16 +151,23 @@
<!-- HScript relies heavily on Reflection, which means we can't use DCE. --> <!-- HScript relies heavily on Reflection, which means we can't use DCE. -->
<haxeflag name="-dce no" /> <haxeflag name="-dce no" />
<!-- Ensure all Funkin' classes are available at runtime. -->
<haxeflag name="--macro" value="include('funkin')" /> <haxeflag name="--macro" value="include('funkin')" />
<!-- Ensure all UI components are available at runtime. --> <!-- Ensure all UI components are available at runtime. -->
<haxeflag name="--macro" value="include('haxe.ui.components')" /> <haxeflag name="--macro" value="include('haxe.ui.components')" />
<haxeflag name="--macro" value="include('haxe.ui.containers')" /> <haxeflag name="--macro" value="include('haxe.ui.containers')" />
<!-- Ensure all UI components are available at runtime. -->
<haxeflag name="--macro" value="include('haxe.ui.components')" />
<haxeflag name="--macro" value="include('haxe.ui.containers')" />
<haxeflag name="--macro" value="include('haxe.ui.containers.menus')" /> <haxeflag name="--macro" value="include('haxe.ui.containers.menus')" />
<!--
Ensure additional class packages are available at runtime (some only really used by scripts).
Ignore packages we can't include.
-->
<haxeflag name="--macro" value="include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*' ])" />
<!-- Necessary to provide stack traces for HScript. --> <!-- Necessary to provide stack traces for HScript. -->
<haxedef name="hscriptPos" /> <haxedef name="hscriptPos" />
<haxedef name="HXCPP_CHECK_POINTER" /> <haxedef name="HXCPP_CHECK_POINTER" />

View file

@ -2,11 +2,14 @@
"lineEnds": { "lineEnds": {
"leftCurly": "both", "leftCurly": "both",
"rightCurly": "both", "rightCurly": "both",
"emptyCurly": "break", "emptyCurly": "noBreak",
"objectLiteralCurly": { "objectLiteralCurly": {
"leftCurly": "after" "leftCurly": "after"
} }
}, },
"indentation": {
"character": " "
},
"sameLine": { "sameLine": {
"ifElse": "next", "ifElse": "next",
"doWhile": "next", "doWhile": "next",

View file

@ -7,297 +7,295 @@ import funkin.play.song.SongData.SongTimeChange;
typedef BPMChangeEvent = typedef BPMChangeEvent =
{ {
var stepTime:Int; var stepTime:Int;
var songTime:Float; var songTime:Float;
var bpm:Float; var bpm:Float;
} }
class Conductor class Conductor
{ {
/** /**
* The list of time changes in the song. * The list of time changes in the song.
* There should be at least one time change (at the beginning of the song) to define the BPM. * There should be at least one time change (at the beginning of the song) to define the BPM.
*/ */
private static var timeChanges:Array<SongTimeChange> = []; private static var timeChanges:Array<SongTimeChange> = [];
/** /**
* The current time change. * The current time change.
*/ */
private static var currentTimeChange:SongTimeChange; private static var currentTimeChange:SongTimeChange;
/** /**
* The current position in the song in milliseconds. * The current position in the song in milliseconds.
* Updated every frame based on the audio position. * Updated every frame based on the audio position.
*/ */
public static var songPosition:Float; public static var songPosition:Float;
/** /**
* Beats per minute of the current song at the current time. * Beats per minute of the current song at the current time.
*/ */
public static var bpm(get, null):Float; public static var bpm(get, null):Float;
static function get_bpm():Float static function get_bpm():Float
{ {
if (bpmOverride != null) if (bpmOverride != null)
return bpmOverride; return bpmOverride;
if (currentTimeChange == null) if (currentTimeChange == null)
return 100; return 100;
return currentTimeChange.bpm; return currentTimeChange.bpm;
} }
static var bpmOverride:Null<Float> = null; static var bpmOverride:Null<Float> = null;
// OLD, replaced with timeChanges. // OLD, replaced with timeChanges.
public static var bpmChangeMap:Array<BPMChangeEvent> = []; public static var bpmChangeMap:Array<BPMChangeEvent> = [];
/** /**
* Duration of a beat in millisecond. Calculated based on bpm. * Duration of a beat in millisecond. Calculated based on bpm.
*/ */
public static var crochet(get, null):Float; public static var crochet(get, null):Float;
static function get_crochet():Float static function get_crochet():Float
{ {
return ((60 / bpm) * 1000); return ((60 / bpm) * 1000);
} }
/** /**
* Duration of a step (quarter) in milliseconds. Calculated based on bpm. * Duration of a step (quarter) in milliseconds. Calculated based on bpm.
*/ */
public static var stepCrochet(get, null):Float; public static var stepCrochet(get, null):Float;
static function get_stepCrochet():Float static function get_stepCrochet():Float
{ {
return crochet / timeSignatureNumerator; return crochet / timeSignatureNumerator;
} }
public static var timeSignatureNumerator(get, null):Int; public static var timeSignatureNumerator(get, null):Int;
static function get_timeSignatureNumerator():Int static function get_timeSignatureNumerator():Int
{ {
if (currentTimeChange == null) if (currentTimeChange == null)
return 4; return 4;
return currentTimeChange.timeSignatureNum; return currentTimeChange.timeSignatureNum;
} }
public static var timeSignatureDenominator(get, null):Int; public static var timeSignatureDenominator(get, null):Int;
static function get_timeSignatureDenominator():Int static function get_timeSignatureDenominator():Int
{ {
if (currentTimeChange == null) if (currentTimeChange == null)
return 4; return 4;
return currentTimeChange.timeSignatureDen; return currentTimeChange.timeSignatureDen;
} }
/** /**
* Current position in the song, in beats. * Current position in the song, in beats.
**/ **/
public static var currentBeat(default, null):Int; public static var currentBeat(default, null):Int;
/** /**
* Current position in the song, in steps. * Current position in the song, in steps.
*/ */
public static var currentStep(default, null):Int; public static var currentStep(default, null):Int;
/** /**
* Current position in the song, in steps and fractions of a step. * Current position in the song, in steps and fractions of a step.
*/ */
public static var currentStepTime(default, null):Float; public static var currentStepTime(default, null):Float;
public static var beatHit(default, null):FlxSignal = new FlxSignal(); public static var beatHit(default, null):FlxSignal = new FlxSignal();
public static var stepHit(default, null):FlxSignal = new FlxSignal(); public static var stepHit(default, null):FlxSignal = new FlxSignal();
public static var lastSongPos:Float; public static var lastSongPos:Float;
public static var visualOffset:Float = 0; public static var visualOffset:Float = 0;
public static var audioOffset:Float = 0; public static var audioOffset:Float = 0;
public static var offset:Float = 0; public static var offset:Float = 0;
// TODO: Add code to update this. // TODO: Add code to update this.
public static var beatsPerMeasure(get, null):Int; public static var beatsPerMeasure(get, null):Int;
static function get_beatsPerMeasure():Int static function get_beatsPerMeasure():Int
{ {
return timeSignatureNumerator; return timeSignatureNumerator;
} }
public static var stepsPerMeasure(get, null):Int; public static var stepsPerMeasure(get, null):Int;
static function get_stepsPerMeasure():Int static function get_stepsPerMeasure():Int
{ {
// Is this always x4? // Is this always x4?
return timeSignatureNumerator * 4; return timeSignatureNumerator * 4;
} }
private function new() private function new() {}
{
}
public static function getLastBPMChange() public static function getLastBPMChange()
{ {
var lastChange:BPMChangeEvent = { var lastChange:BPMChangeEvent = {
stepTime: 0, stepTime: 0,
songTime: 0, songTime: 0,
bpm: 0 bpm: 0
} }
for (i in 0...Conductor.bpmChangeMap.length) for (i in 0...Conductor.bpmChangeMap.length)
{ {
if (Conductor.songPosition >= Conductor.bpmChangeMap[i].songTime) if (Conductor.songPosition >= Conductor.bpmChangeMap[i].songTime)
lastChange = Conductor.bpmChangeMap[i]; lastChange = Conductor.bpmChangeMap[i];
if (Conductor.songPosition < Conductor.bpmChangeMap[i].songTime) if (Conductor.songPosition < Conductor.bpmChangeMap[i].songTime)
break; break;
} }
return lastChange; return lastChange;
} }
/** /**
* Forcibly defines the current BPM of the song. * Forcibly defines the current BPM of the song.
* Useful for things like the chart editor that need to manipulate BPM in real time. * Useful for things like the chart editor that need to manipulate BPM in real time.
* *
* Set to null to reset to the BPM defined by the timeChanges. * Set to null to reset to the BPM defined by the timeChanges.
* *
* WARNING: Avoid this for things like setting the BPM of the title screen music, * WARNING: Avoid this for things like setting the BPM of the title screen music,
* you should have a metadata file for it instead. * you should have a metadata file for it instead.
*/ */
public static function forceBPM(?bpm:Float = null) public static function forceBPM(?bpm:Float = null)
{ {
if (bpm != null) if (bpm != null)
trace('[CONDUCTOR] Forcing BPM to ' + bpm); trace('[CONDUCTOR] Forcing BPM to ' + bpm);
else else
trace('[CONDUCTOR] Resetting BPM to default'); trace('[CONDUCTOR] Resetting BPM to default');
Conductor.bpmOverride = bpm; Conductor.bpmOverride = bpm;
} }
/** /**
* Update the conductor with the current song position. * Update the conductor with the current song position.
* BPM, current step, etc. will be re-calculated based on the song position. * BPM, current step, etc. will be re-calculated based on the song position.
* *
* @param songPosition The current position in the song in milliseconds. * @param songPosition The current position in the song in milliseconds.
* Leave blank to use the FlxG.sound.music position. * Leave blank to use the FlxG.sound.music position.
*/ */
public static function update(songPosition:Float = null) public static function update(songPosition:Float = null)
{ {
if (songPosition == null) if (songPosition == null)
songPosition = (FlxG.sound.music != null) ? (FlxG.sound.music.time + Conductor.offset) : 0; songPosition = (FlxG.sound.music != null) ? FlxG.sound.music.time + Conductor.offset : 0.0;
var oldBeat = currentBeat; var oldBeat = currentBeat;
var oldStep = currentStep; var oldStep = currentStep;
Conductor.songPosition = songPosition; Conductor.songPosition = songPosition;
// Conductor.bpm = Conductor.getLastBPMChange().bpm; // Conductor.bpm = Conductor.getLastBPMChange().bpm;
currentTimeChange = timeChanges[0]; currentTimeChange = timeChanges[0];
for (i in 0...timeChanges.length) for (i in 0...timeChanges.length)
{ {
if (songPosition >= timeChanges[i].timeStamp) if (songPosition >= timeChanges[i].timeStamp)
currentTimeChange = timeChanges[i]; currentTimeChange = timeChanges[i];
if (songPosition < timeChanges[i].timeStamp) if (songPosition < timeChanges[i].timeStamp)
break; break;
} }
if (currentTimeChange == null && bpmOverride == null) if (currentTimeChange == null && bpmOverride == null && FlxG.sound.music != null)
{ {
trace('WARNING: Conductor is broken, timeChanges is empty.'); trace('WARNING: Conductor is broken, timeChanges is empty.');
} }
else if (currentTimeChange != null) else if (currentTimeChange != null)
{ {
currentStepTime = (currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepCrochet; currentStepTime = (currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepCrochet;
currentStep = Math.floor(currentStepTime); currentStep = Math.floor(currentStepTime);
currentBeat = Math.floor(currentStep / 4); currentBeat = Math.floor(currentStep / 4);
} }
else else
{ {
// Assume a constant BPM equal to the forced value. // Assume a constant BPM equal to the forced value.
currentStepTime = (songPosition / stepCrochet); currentStepTime = (songPosition / stepCrochet);
currentStep = Math.floor(currentStepTime); currentStep = Math.floor(currentStepTime);
currentBeat = Math.floor(currentStep / 4); currentBeat = Math.floor(currentStep / 4);
} }
// FlxSignals are really cool. // FlxSignals are really cool.
if (currentStep != oldStep) if (currentStep != oldStep)
stepHit.dispatch(); stepHit.dispatch();
if (currentBeat != oldBeat) if (currentBeat != oldBeat)
beatHit.dispatch(); beatHit.dispatch();
} }
@:deprecated // Switch to TimeChanges instead. @:deprecated // Switch to TimeChanges instead.
public static function mapBPMChanges(song:SwagSong) public static function mapBPMChanges(song:SwagSong)
{ {
bpmChangeMap = []; bpmChangeMap = [];
var curBPM:Float = song.bpm; var curBPM:Float = song.bpm;
var totalSteps:Int = 0; var totalSteps:Int = 0;
var totalPos:Float = 0; var totalPos:Float = 0;
for (i in 0...SongLoad.getSong().length) for (i in 0...SongLoad.getSong().length)
{ {
if (SongLoad.getSong()[i].changeBPM && SongLoad.getSong()[i].bpm != curBPM) if (SongLoad.getSong()[i].changeBPM && SongLoad.getSong()[i].bpm != curBPM)
{ {
curBPM = SongLoad.getSong()[i].bpm; curBPM = SongLoad.getSong()[i].bpm;
var event:BPMChangeEvent = { var event:BPMChangeEvent = {
stepTime: totalSteps, stepTime: totalSteps,
songTime: totalPos, songTime: totalPos,
bpm: curBPM bpm: curBPM
}; };
bpmChangeMap.push(event); bpmChangeMap.push(event);
} }
var deltaSteps:Int = SongLoad.getSong()[i].lengthInSteps; var deltaSteps:Int = SongLoad.getSong()[i].lengthInSteps;
totalSteps += deltaSteps; totalSteps += deltaSteps;
totalPos += ((60 / curBPM) * 1000 / 4) * deltaSteps; totalPos += ((60 / curBPM) * 1000 / 4) * deltaSteps;
} }
} }
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>) public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
{ {
timeChanges = []; timeChanges = [];
for (currentTimeChange in songTimeChanges) for (currentTimeChange in songTimeChanges)
{ {
timeChanges.push(currentTimeChange); timeChanges.push(currentTimeChange);
} }
trace('Done mapping time changes: ' + timeChanges); trace('Done mapping time changes: ' + timeChanges);
// Done. // Done.
} }
/** /**
* Given a time in milliseconds, return a time in steps. * Given a time in milliseconds, return a time in steps.
*/ */
public static function getTimeInSteps(ms:Float):Int public static function getTimeInSteps(ms:Float):Int
{ {
if (timeChanges.length == 0) if (timeChanges.length == 0)
{ {
// Assume a constant BPM equal to the forced value. // Assume a constant BPM equal to the forced value.
return Math.floor(ms / stepCrochet); return Math.floor(ms / stepCrochet);
} }
else else
{ {
var resultStep:Int = 0; var resultStep:Int = 0;
var lastTimeChange:SongTimeChange = timeChanges[0]; var lastTimeChange:SongTimeChange = timeChanges[0];
for (timeChange in timeChanges) for (timeChange in timeChanges)
{ {
if (ms >= timeChange.timeStamp) if (ms >= timeChange.timeStamp)
{ {
lastTimeChange = timeChange; lastTimeChange = timeChange;
resultStep = lastTimeChange.beatTime * 4; resultStep = lastTimeChange.beatTime * 4;
} }
else else
{ {
// This time change is after the requested time. // This time change is after the requested time.
break; break;
} }
} }
resultStep += Math.floor((ms - lastTimeChange.timeStamp) / stepCrochet); resultStep += Math.floor((ms - lastTimeChange.timeStamp) / stepCrochet);
return resultStep; return resultStep;
} }
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@ import flixel.util.FlxColor;
import funkin.modding.module.ModuleHandler; import funkin.modding.module.ModuleHandler;
import funkin.play.PlayState; import funkin.play.PlayState;
import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.event.SongEvent.SongEventHandler; import funkin.play.event.SongEvent.SongEventParser;
import funkin.play.song.SongData.SongDataParser; import funkin.play.song.SongData.SongDataParser;
import funkin.play.stage.StageData; import funkin.play.stage.StageData;
import funkin.ui.PreferencesMenu; import funkin.ui.PreferencesMenu;
@ -32,232 +32,235 @@ import Discord.DiscordClient;
*/ */
class InitState extends FlxTransitionableState class InitState extends FlxTransitionableState
{ {
override public function create():Void override public function create():Void
{ {
trace('This is a debug build, loading InitState...'); trace('This is a debug build, loading InitState...');
#if android #if android
FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK]; FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
#end #end
#if newgrounds #if newgrounds
NGio.init(); NGio.init();
#end #end
#if discord_rpc #if discord_rpc
DiscordClient.initialize(); DiscordClient.initialize();
Application.current.onExit.add(function(exitCode) Application.current.onExit.add(function(exitCode)
{ {
DiscordClient.shutdown(); DiscordClient.shutdown();
}); });
#end #end
// ==== flixel shit ==== // // ==== flixel shit ==== //
// This big obnoxious white button is for MOBILE, so that you can press it // This big obnoxious white button is for MOBILE, so that you can press it
// easily with your finger when debug bullshit pops up during testing lol! // easily with your finger when debug bullshit pops up during testing lol!
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function()
{ {
FlxG.debugger.visible = false; FlxG.debugger.visible = false;
}); });
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function() FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function()
{ {
if (FlxG.vcr.paused) if (FlxG.vcr.paused)
{ {
FlxG.vcr.resume(); FlxG.vcr.resume();
for (snd in FlxG.sound.list) for (snd in FlxG.sound.list)
snd.resume(); snd.resume();
FlxG.sound.music.resume(); FlxG.sound.music.resume();
} }
else else
{ {
FlxG.vcr.pause(); FlxG.vcr.pause();
for (snd in FlxG.sound.list) for (snd in FlxG.sound.list)
snd.pause(); snd.pause();
FlxG.sound.music.pause(); FlxG.sound.music.pause();
} }
}); });
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function()
{ {
FlxG.game.debugger.vcr.onStep(); FlxG.game.debugger.vcr.onStep();
for (snd in FlxG.sound.list) for (snd in FlxG.sound.list)
{ {
snd.pause(); snd.pause();
snd.time += FlxG.elapsed * 1000; snd.time += FlxG.elapsed * 1000;
} }
FlxG.sound.music.pause(); FlxG.sound.music.pause();
FlxG.sound.music.time += FlxG.elapsed * 1000; FlxG.sound.music.time += FlxG.elapsed * 1000;
}); });
FlxG.sound.muteKeys = [ZERO]; FlxG.sound.muteKeys = [ZERO];
FlxG.game.focusLostFramerate = 60; FlxG.game.focusLostFramerate = 60;
// FlxG.stage.window.borderless = true; // FlxG.stage.window.borderless = true;
// FlxG.stage.window.mouseLock = true; // FlxG.stage.window.mouseLock = true;
var diamond:FlxGraphic = FlxGraphic.fromClass(GraphicTransTileDiamond); var diamond:FlxGraphic = FlxGraphic.fromClass(GraphicTransTileDiamond);
diamond.persist = true; diamond.persist = true;
diamond.destroyOnNoUse = false; diamond.destroyOnNoUse = false;
FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), {asset: diamond, width: 32, height: 32}, FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), {asset: diamond, width: 32, height: 32},
new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), {asset: diamond, width: 32, height: 32}, FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), {asset: diamond, width: 32, height: 32},
new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
// ===== save shit ===== // // ===== save shit ===== //
FlxG.save.bind('funkin', 'ninjamuffin99'); FlxG.save.bind('funkin', 'ninjamuffin99');
// https://github.com/HaxeFlixel/flixel/pull/2396 // https://github.com/HaxeFlixel/flixel/pull/2396
// IF/WHEN MY PR GOES THRU AND IT GETS INTO MAIN FLIXEL, DELETE THIS CHUNKOF CODE, AND THEN UNCOMMENT THE LINE BELOW // IF/WHEN MY PR GOES THRU AND IT GETS INTO MAIN FLIXEL, DELETE THIS CHUNKOF CODE, AND THEN UNCOMMENT THE LINE BELOW
// FlxG.sound.loadSavedPrefs(); // FlxG.sound.loadSavedPrefs();
if (FlxG.save.data.volume != null) if (FlxG.save.data.volume != null)
FlxG.sound.volume = FlxG.save.data.volume; FlxG.sound.volume = FlxG.save.data.volume;
if (FlxG.save.data.mute != null) if (FlxG.save.data.mute != null)
FlxG.sound.muted = FlxG.save.data.mute; FlxG.sound.muted = FlxG.save.data.mute;
// Make errors and warnings less annoying. // Make errors and warnings less annoying.
LogStyle.ERROR.openConsole = false; LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null; LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false; LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null; LogStyle.WARNING.errorSound = null;
// FlxG.save.close(); // FlxG.save.close();
// FlxG.sound.loadSavedPrefs(); // FlxG.sound.loadSavedPrefs();
WindowUtil.initWindowEvents(); WindowUtil.initWindowEvents();
WindowUtil.disableCrashHandler();
PreferencesMenu.initPrefs(); PreferencesMenu.initPrefs();
PlayerSettings.init(); PlayerSettings.init();
Highscore.load(); Highscore.load();
if (FlxG.save.data.weekUnlocked != null) if (FlxG.save.data.weekUnlocked != null)
{ {
// FIX LATER!!! // FIX LATER!!!
// WEEK UNLOCK PROGRESSION!! // WEEK UNLOCK PROGRESSION!!
// StoryMenuState.weekUnlocked = FlxG.save.data.weekUnlocked; // StoryMenuState.weekUnlocked = FlxG.save.data.weekUnlocked;
if (StoryMenuState.weekUnlocked.length < 4) if (StoryMenuState.weekUnlocked.length < 4)
StoryMenuState.weekUnlocked.insert(0, true); StoryMenuState.weekUnlocked.insert(0, true);
// QUICK PATCH OOPS! // QUICK PATCH OOPS!
if (!StoryMenuState.weekUnlocked[0]) if (!StoryMenuState.weekUnlocked[0])
StoryMenuState.weekUnlocked[0] = true; StoryMenuState.weekUnlocked[0] = true;
} }
if (FlxG.save.data.seenVideo != null) if (FlxG.save.data.seenVideo != null)
VideoState.seenVideo = FlxG.save.data.seenVideo; VideoState.seenVideo = FlxG.save.data.seenVideo;
// ===== fuck outta here ===== // // ===== fuck outta here ===== //
// FlxTransitionableState.skipNextTransOut = true; // FlxTransitionableState.skipNextTransOut = true;
FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransIn = true;
SongEventHandler.registerBaseEventCallbacks(); // TODO: Register custom event callbacks here
// TODO: Register custom event callbacks here
SongDataParser.loadSongCache(); SongEventParser.loadEventCache();
StageDataParser.loadStageCache(); SongDataParser.loadSongCache();
CharacterDataParser.loadCharacterCache(); StageDataParser.loadStageCache();
ModuleHandler.buildModuleCallbacks(); CharacterDataParser.loadCharacterCache();
ModuleHandler.loadModuleCache(); ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
FlxG.debugger.toggleKeys = [F2]; FlxG.debugger.toggleKeys = [F2];
#if song ModuleHandler.callOnCreate();
var song = getSong();
var weeks = [ #if song
['bopeebo', 'fresh', 'dadbattle'], var song = getSong();
['spookeez', 'south', 'monster'],
['spooky', 'spooky', 'monster'],
['pico', 'philly', 'blammed'],
['satin-panties', 'high', 'milf'],
['cocoa', 'eggnog', 'winter-horrorland'],
['senpai', 'roses', 'thorns'],
['ugh', 'guns', 'stress']
];
var week = 0; var weeks = [
for (i in 0...weeks.length) ['bopeebo', 'fresh', 'dadbattle'],
{ ['spookeez', 'south', 'monster'],
if (weeks[i].contains(song)) ['spooky', 'spooky', 'monster'],
{ ['pico', 'philly', 'blammed'],
week = i + 1; ['satin-panties', 'high', 'milf'],
break; ['cocoa', 'eggnog', 'winter-horrorland'],
} ['senpai', 'roses', 'thorns'],
} ['ugh', 'guns', 'stress']
];
if (week == 0) var week = 0;
throw 'Invalid -D song=$song'; for (i in 0...weeks.length)
{
if (weeks[i].contains(song))
{
week = i + 1;
break;
}
}
startSong(week, song, false); if (week == 0)
#elseif week throw 'Invalid -D song=$song';
var week = getWeek();
var songs = [ startSong(week, song, false);
'bopeebo', 'spookeez', 'spooky', 'pico', #elseif week
'satin-panties', 'cocoa', 'senpai', 'ugh' var week = getWeek();
];
if (week <= 0 || week >= songs.length) var songs = [
throw "invalid -D week=" + week; 'bopeebo', 'spookeez', 'spooky', 'pico',
'satin-panties', 'cocoa', 'senpai', 'ugh'
];
startSong(week, songs[week - 1], true); if (week <= 0 || week >= songs.length)
#elseif FREEPLAY throw "invalid -D week=" + week;
FlxG.switchState(new FreeplayState());
#elseif ANIMATE
FlxG.switchState(new funkin.animate.dotstuff.DotStuffTestStage());
#elseif CHARTING
FlxG.switchState(new ChartingState());
#elseif STAGEBUILD
FlxG.switchState(new StageBuilderState());
#elseif FIGHT
FlxG.switchState(new PicoFight());
#elseif ANIMDEBUG
FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
#elseif LATENCY
FlxG.switchState(new LatencyState());
#elseif NETTEST
FlxG.switchState(new netTest.NetTest());
#else
FlxG.sound.cache(Paths.music('freakyMenu'));
FlxG.switchState(new TitleState());
#end
}
function startSong(week, song, isStoryMode) startSong(week, songs[week - 1], true);
{ #elseif FREEPLAY
var dif = getDif(); FlxG.switchState(new FreeplayState());
#elseif ANIMATE
FlxG.switchState(new funkin.animate.dotstuff.DotStuffTestStage());
#elseif CHARTING
FlxG.switchState(new ChartingState());
#elseif STAGEBUILD
FlxG.switchState(new StageBuilderState());
#elseif FIGHT
FlxG.switchState(new PicoFight());
#elseif ANIMDEBUG
FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
#elseif LATENCY
FlxG.switchState(new LatencyState());
#elseif NETTEST
FlxG.switchState(new netTest.NetTest());
#else
FlxG.sound.cache(Paths.music('freakyMenu'));
FlxG.switchState(new TitleState());
#end
}
PlayState.currentSong = SongLoad.loadFromJson(song, song); function startSong(week, song, isStoryMode)
PlayState.currentSong_NEW = SongDataParser.fetchSong(song); {
PlayState.isStoryMode = isStoryMode; var dif = getDif();
PlayState.storyDifficulty = dif;
PlayState.storyDifficulty_NEW = switch (dif) PlayState.currentSong = SongLoad.loadFromJson(song, song);
{ PlayState.currentSong_NEW = SongDataParser.fetchSong(song);
case 0: 'easy'; PlayState.isStoryMode = isStoryMode;
case 1: 'normal'; PlayState.storyDifficulty = dif;
case 2: 'hard'; PlayState.storyDifficulty_NEW = switch (dif)
default: 'normal'; {
}; case 0: 'easy';
SongLoad.curDiff = PlayState.storyDifficulty_NEW; case 1: 'normal';
PlayState.storyWeek = week; case 2: 'hard';
LoadingState.loadAndSwitchState(new PlayState()); default: 'normal';
} };
SongLoad.curDiff = PlayState.storyDifficulty_NEW;
PlayState.storyWeek = week;
LoadingState.loadAndSwitchState(new PlayState());
}
} }
function getWeek() function getWeek()
return Std.parseInt(MacroUtil.getDefine("week")); return Std.parseInt(MacroUtil.getDefine("week"));
function getSong() function getSong()
return MacroUtil.getDefine("song"); return MacroUtil.getDefine("song");
function getDif() function getDif()
return Std.parseInt(MacroUtil.getDefine("dif", "1")); return Std.parseInt(MacroUtil.getDefine("dif", "1"));

View file

@ -6,178 +6,178 @@ import funkin.util.assets.FlxAnimationUtil;
class DJBoyfriend extends FlxSprite class DJBoyfriend extends FlxSprite
{ {
// Represents the sprite's current status. // Represents the sprite's current status.
// Without state machines I would have driven myself crazy years ago. // Without state machines I would have driven myself crazy years ago.
public var currentState:DJBoyfriendState = Intro; public var currentState:DJBoyfriendState = Intro;
// A callback activated when the intro animation finishes. // A callback activated when the intro animation finishes.
public var onIntroDone:FlxSignal = new FlxSignal(); public var onIntroDone:FlxSignal = new FlxSignal();
// A callback activated when Boyfriend gets spooked. // A callback activated when Boyfriend gets spooked.
public var onSpook:FlxSignal = new FlxSignal(); public var onSpook:FlxSignal = new FlxSignal();
// playAnim stolen from Character.hx, cuz im lazy lol! // playAnim stolen from Character.hx, cuz im lazy lol!
// TODO: Switch this class to use SwagSprite instead. // TODO: Switch this class to use SwagSprite instead.
public var animOffsets:Map<String, Array<Dynamic>>; public var animOffsets:Map<String, Array<Dynamic>>;
static final SPOOK_PERIOD:Float = 180.0; static final SPOOK_PERIOD:Float = 180.0;
// Time since dad last SPOOKED you. // Time since dad last SPOOKED you.
var timeSinceSpook:Float = 0; var timeSinceSpook:Float = 0;
public function new(x:Float, y:Float) public function new(x:Float, y:Float)
{ {
super(x, y); super(x, y);
animOffsets = new Map<String, Array<Dynamic>>(); animOffsets = new Map<String, Array<Dynamic>>();
setupAnimations(); setupAnimations();
animation.finishCallback = onFinishAnim; animation.finishCallback = onFinishAnim;
} }
public override function update(elapsed:Float):Void public override function update(elapsed:Float):Void
{ {
super.update(elapsed); super.update(elapsed);
if (FlxG.keys.justPressed.LEFT) if (FlxG.keys.justPressed.LEFT)
{ {
animOffsets["confirm"] = [animOffsets["confirm"][0] + 1, animOffsets["confirm"][1]]; animOffsets["confirm"] = [animOffsets["confirm"][0] + 1, animOffsets["confirm"][1]];
applyAnimOffset(); applyAnimOffset();
} }
else if (FlxG.keys.justPressed.RIGHT) else if (FlxG.keys.justPressed.RIGHT)
{ {
animOffsets["confirm"] = [animOffsets["confirm"][0] - 1, animOffsets["confirm"][1]]; animOffsets["confirm"] = [animOffsets["confirm"][0] - 1, animOffsets["confirm"][1]];
applyAnimOffset(); applyAnimOffset();
} }
else if (FlxG.keys.justPressed.UP) else if (FlxG.keys.justPressed.UP)
{ {
animOffsets["confirm"] = [animOffsets["confirm"][0], animOffsets["confirm"][1] + 1]; animOffsets["confirm"] = [animOffsets["confirm"][0], animOffsets["confirm"][1] + 1];
applyAnimOffset(); applyAnimOffset();
} }
else if (FlxG.keys.justPressed.DOWN) else if (FlxG.keys.justPressed.DOWN)
{ {
animOffsets["confirm"] = [animOffsets["confirm"][0], animOffsets["confirm"][1] - 1]; animOffsets["confirm"] = [animOffsets["confirm"][0], animOffsets["confirm"][1] - 1];
applyAnimOffset(); applyAnimOffset();
} }
switch (currentState) switch (currentState)
{ {
case Intro: case Intro:
// Play the intro animation then leave this state immediately. // Play the intro animation then leave this state immediately.
if (getCurrentAnimation() != 'intro') if (getCurrentAnimation() != 'intro')
playAnimation('intro', true); playAnimation('intro', true);
timeSinceSpook = 0; timeSinceSpook = 0;
case Idle: case Idle:
// We are in this state the majority of the time. // We are in this state the majority of the time.
if (getCurrentAnimation() != 'idle' || animation.finished) if (getCurrentAnimation() != 'idle' || animation.finished)
{ {
if (timeSinceSpook > SPOOK_PERIOD) if (timeSinceSpook > SPOOK_PERIOD)
{ {
currentState = Spook; currentState = Spook;
} }
else else
{ {
playAnimation('idle', false); playAnimation('idle', false);
} }
} }
timeSinceSpook += elapsed; timeSinceSpook += elapsed;
case Confirm: case Confirm:
if (getCurrentAnimation() != 'confirm') if (getCurrentAnimation() != 'confirm')
playAnimation('confirm', false); playAnimation('confirm', false);
timeSinceSpook = 0; timeSinceSpook = 0;
case Spook: case Spook:
if (getCurrentAnimation() != 'spook') if (getCurrentAnimation() != 'spook')
{ {
onSpook.dispatch(); onSpook.dispatch();
playAnimation('spook', false); playAnimation('spook', false);
} }
timeSinceSpook = 0; timeSinceSpook = 0;
default: default:
// I shit myself. // I shit myself.
} }
} }
function onFinishAnim(name:String):Void function onFinishAnim(name:String):Void
{ {
switch (name) switch (name)
{ {
case "intro": case "intro":
trace('Finished intro'); // trace('Finished intro');
currentState = Idle; currentState = Idle;
onIntroDone.dispatch(); onIntroDone.dispatch();
case "idle": case "idle":
trace('Finished idle'); // trace('Finished idle');
case "spook": case "spook":
trace('Finished spook'); // trace('Finished spook');
currentState = Idle; currentState = Idle;
case "confirm": case "confirm":
trace('Finished confirm'); // trace('Finished confirm');
} }
} }
public function resetAFKTimer():Void public function resetAFKTimer():Void
{ {
timeSinceSpook = 0; timeSinceSpook = 0;
} }
function setupAnimations():Void function setupAnimations():Void
{ {
frames = FlxAnimationUtil.combineFramesCollections(Paths.getSparrowAtlas('freeplay/bfFreeplay'), Paths.getSparrowAtlas('freeplay/bf-freeplay-afk')); frames = FlxAnimationUtil.combineFramesCollections(Paths.getSparrowAtlas('freeplay/bfFreeplay'), Paths.getSparrowAtlas('freeplay/bf-freeplay-afk'));
animation.addByPrefix('intro', "boyfriend dj intro", 24, false); animation.addByPrefix('intro', "boyfriend dj intro", 24, false);
addOffset('intro', 0, 0); addOffset('intro', 0, 0);
animation.addByPrefix('idle', "Boyfriend DJ0", 24, false); animation.addByPrefix('idle', "Boyfriend DJ0", 24, false);
addOffset('idle', -4, -426); addOffset('idle', -4, -426);
animation.addByPrefix('confirm', "Boyfriend DJ confirm", 24, false); animation.addByPrefix('confirm', "Boyfriend DJ confirm", 24, false);
addOffset('confirm', 40, -451); addOffset('confirm', 40, -451);
animation.addByPrefix('spook', "bf dj afk0", 24, false); animation.addByPrefix('spook', "bf dj afk0", 24, false);
addOffset('spook', -3, -272); addOffset('spook', -3, -272);
} }
public function confirm():Void public function confirm():Void
{ {
currentState = Confirm; currentState = Confirm;
} }
public inline function addOffset(name:String, x:Float = 0, y:Float = 0) public inline function addOffset(name:String, x:Float = 0, y:Float = 0)
{ {
animOffsets[name] = [x, y]; animOffsets[name] = [x, y];
} }
public function getCurrentAnimation():String public function getCurrentAnimation():String
{ {
if (this.animation == null || this.animation.curAnim == null) if (this.animation == null || this.animation.curAnim == null)
return ""; return "";
return this.animation.curAnim.name; return this.animation.curAnim.name;
} }
public function playAnimation(AnimName:String, Force:Bool = false, Reversed:Bool = false, Frame:Int = 0):Void public function playAnimation(AnimName:String, Force:Bool = false, Reversed:Bool = false, Frame:Int = 0):Void
{ {
animation.play(AnimName, Force, Reversed, Frame); animation.play(AnimName, Force, Reversed, Frame);
applyAnimOffset(); applyAnimOffset();
} }
function applyAnimOffset() function applyAnimOffset()
{ {
var AnimName = getCurrentAnimation(); var AnimName = getCurrentAnimation();
var daOffset = animOffsets.get(AnimName); var daOffset = animOffsets.get(AnimName);
if (animOffsets.exists(AnimName)) if (animOffsets.exists(AnimName))
{ {
offset.set(daOffset[0], daOffset[1]); offset.set(daOffset[0], daOffset[1]);
} }
else else
offset.set(0, 0); offset.set(0, 0);
} }
} }
enum DJBoyfriendState enum DJBoyfriendState
{ {
Intro; Intro;
Idle; Idle;
Confirm; Confirm;
Spook; Spook;
} }

View file

@ -6,8 +6,7 @@ import flixel.FlxG; // This one in particular causes a compile error if you're u
// These are great. // These are great.
using Lambda; using Lambda;
using StringTools; using StringTools;
using funkin.util.tools.MapTools;
using funkin.util.tools.IteratorTools; using funkin.util.tools.IteratorTools;
using funkin.util.tools.StringTools; using funkin.util.tools.StringTools;
#end #end

View file

@ -60,6 +60,7 @@ interface IPlayStateScriptedClass extends IScriptedClass
* and can be cancelled by scripts. * and can be cancelled by scripts.
*/ */
public function onPause(event:PauseScriptEvent):Void; public function onPause(event:PauseScriptEvent):Void;
/** /**
* Called when the game is unpaused. * Called when the game is unpaused.
*/ */
@ -70,18 +71,22 @@ interface IPlayStateScriptedClass extends IScriptedClass
* Use this to mutate the chart. * Use this to mutate the chart.
*/ */
public function onSongLoaded(event:SongLoadScriptEvent):Void; public function onSongLoaded(event:SongLoadScriptEvent):Void;
/** /**
* Called when the song starts (conductor time is 0 seconds). * Called when the song starts (conductor time is 0 seconds).
*/ */
public function onSongStart(event:ScriptEvent):Void; public function onSongStart(event:ScriptEvent):Void;
/** /**
* Called when the song ends and the song is about to be unloaded. * Called when the song ends and the song is about to be unloaded.
*/ */
public function onSongEnd(event:ScriptEvent):Void; public function onSongEnd(event:ScriptEvent):Void;
/** /**
* Called as the player runs out of health just before the game over substate is entered. * Called as the player runs out of health just before the game over substate is entered.
*/ */
public function onGameOver(event:ScriptEvent):Void; public function onGameOver(event:ScriptEvent):Void;
/** /**
* Called when the player restarts the song, either via pause menu or restarting after a game over. * Called when the player restarts the song, either via pause menu or restarting after a game over.
*/ */
@ -92,19 +97,27 @@ interface IPlayStateScriptedClass extends IScriptedClass
* Query the note attached to the event to determine if it was hit by the player or CPU. * Query the note attached to the event to determine if it was hit by the player or CPU.
*/ */
public function onNoteHit(event:NoteScriptEvent):Void; public function onNoteHit(event:NoteScriptEvent):Void;
/** /**
* Called when EITHER player (usually the player) misses a note. * Called when EITHER player (usually the player) misses a note.
*/ */
public function onNoteMiss(event:NoteScriptEvent):Void; public function onNoteMiss(event:NoteScriptEvent):Void;
/** /**
* Called when the player presses a key when no note is on the strumline. * Called when the player presses a key when no note is on the strumline.
*/ */
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void; public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void;
/**
* Called when the song reaches an event.
*/
public function onSongEvent(event:SongEventScriptEvent):Void;
/** /**
* Called once every step of the song. * Called once every step of the song.
*/ */
public function onStepHit(event:SongTimeScriptEvent):Void; public function onStepHit(event:SongTimeScriptEvent):Void;
/** /**
* Called once every beat of the song. * Called once every beat of the song.
*/ */
@ -114,10 +127,12 @@ interface IPlayStateScriptedClass extends IScriptedClass
* Called when the countdown of the song starts. * Called when the countdown of the song starts.
*/ */
public function onCountdownStart(event:CountdownScriptEvent):Void; public function onCountdownStart(event:CountdownScriptEvent):Void;
/** /**
* Called when the a part of the countdown happens. * Called when the a part of the countdown happens.
*/ */
public function onCountdownStep(event:CountdownScriptEvent):Void; public function onCountdownStep(event:CountdownScriptEvent):Void;
/** /**
* Called when the countdown of the song ends. * Called when the countdown of the song ends.
*/ */

View file

@ -4,68 +4,88 @@ import polymod.Polymod;
class PolymodErrorHandler class PolymodErrorHandler
{ {
/** /**
* Show a popup with the given text. * Show a popup with the given text.
* This displays a system popup, it WILL interrupt the game. * This displays a system popup, it WILL interrupt the game.
* Make sure to only use this when it's important, like when there's a script error. * Make sure to only use this when it's important, like when there's a script error.
* *
* @param name The name at the top of the popup. * @param name The name at the top of the popup.
* @param desc The body text of the popup. * @param desc The body text of the popup.
*/ */
public static function showAlert(name:String, desc:String):Void public static function showAlert(name:String, desc:String):Void
{ {
lime.app.Application.current.window.alert(desc, name); lime.app.Application.current.window.alert(desc, name);
} }
public static function onPolymodError(error:PolymodError):Void public static function onPolymodError(error:PolymodError):Void
{ {
// Perform an action based on the error code. // Perform an action based on the error code.
switch (error.code) switch (error.code)
{ {
case MOD_LOAD_PREPARE: case FRAMEWORK_INIT, FRAMEWORK_AUTODETECT, SCRIPT_PARSING:
logInfo('[POLYMOD]: ${error.message}'); // Unimportant.
case MOD_LOAD_DONE: return;
logInfo('[POLYMOD]: ${error.message}');
case MISSING_ICON:
logWarn('[POLYMOD]: A mod is missing an icon. Please add one.');
case SCRIPT_PARSE_ERROR:
// A syntax error when parsing a script.
logError('[POLYMOD]: ${error.message}');
showAlert('Polymod Script Parsing Error', error.message);
case SCRIPT_EXCEPTION:
// A runtime error when running a script.
logError('[POLYMOD]: ${error.message}');
showAlert('Polymod Script Execution Error', error.message);
case SCRIPT_CLASS_NOT_FOUND:
// A scripted class tried to reference an unknown superclass.
logError('[POLYMOD]: ${error.message}');
showAlert('Polymod Script Parsing Error', error.message);
default:
// Log the message based on its severity.
switch (error.severity)
{
case NOTICE:
logInfo('[POLYMOD]: ${error.message}');
case WARNING:
logWarn('[POLYMOD]: ${error.message}');
case ERROR:
logError('[POLYMOD]: ${error.message}');
}
}
}
static function logInfo(message:String):Void case MOD_LOAD_PREPARE, MOD_LOAD_DONE:
{ logInfo('LOADING MOD - ${error.message}');
trace('[INFO ] ${message}');
}
static function logError(message:String):Void case MISSING_ICON:
{ logWarn('A mod is missing an icon. Please add one.');
trace('[ERROR] ${message}');
}
static function logWarn(message:String):Void case SCRIPT_PARSE_ERROR:
{ // A syntax error when parsing a script.
trace('[WARN ] ${message}'); logError(error.message);
} // Notify the user via popup.
showAlert('Polymod Script Parsing Error', error.message);
case SCRIPT_RUNTIME_EXCEPTION:
// A runtime error when running a script.
logError(error.message);
// Notify the user via popup.
showAlert('Polymod Script Exception', error.message);
case SCRIPT_CLASS_MODULE_NOT_FOUND:
// A scripted class tried to reference an unknown class or module.
logError(error.message);
// Last word is the class name.
var className:String = error.message.split(' ').pop();
var msg:String = 'Import error in ${error.origin}';
msg += '\nCould not import unknown class ${className}';
msg += '\nCheck to ensure the class exists and is spelled correctly.';
// Notify the user via popup.
showAlert('Polymod Script Import Error', msg);
case SCRIPT_CLASS_MODULE_BLACKLISTED:
// A scripted class tried to reference a blacklisted class or module.
logError(error.message);
// Notify the user via popup.
showAlert('Polymod Script Blacklist Violation', error.message);
default:
// Log the message based on its severity.
switch (error.severity)
{
case NOTICE:
logInfo(error.message);
case WARNING:
logWarn(error.message);
case ERROR:
logError(error.message);
}
}
}
static function logInfo(message:String):Void
{
trace('[INFO-] ${message}');
}
static function logError(message:String):Void
{
trace('[ERROR] ${message}');
}
static function logWarn(message:String):Void
{
trace('[WARN-] ${message}');
}
} }

View file

@ -1,5 +1,6 @@
package funkin.modding; package funkin.modding;
import funkin.util.macro.ClassMacro;
import funkin.modding.module.ModuleHandler; import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.SongData; import funkin.play.song.SongData;
@ -11,251 +12,271 @@ import funkin.util.FileUtil;
class PolymodHandler class PolymodHandler
{ {
/** /**
* The API version that mods should comply with. * The API version that mods should comply with.
* Format this with Semantic Versioning; <MAJOR>.<MINOR>.<PATCH>. * Format this with Semantic Versioning; <MAJOR>.<MINOR>.<PATCH>.
* Bug fixes increment the patch version, new features increment the minor version. * Bug fixes increment the patch version, new features increment the minor version.
* Changes that break old mods increment the major version. * Changes that break old mods increment the major version.
*/ */
static final API_VERSION = "0.1.0"; static final API_VERSION = "0.1.0";
/** /**
* Where relative to the executable that mods are located. * Where relative to the executable that mods are located.
*/ */
static final MOD_FOLDER = "mods"; static final MOD_FOLDER = "mods";
public static function createModRoot() public static function createModRoot()
{ {
FileUtil.createDirIfNotExists(MOD_FOLDER); FileUtil.createDirIfNotExists(MOD_FOLDER);
} }
/** /**
* Loads the game with ALL mods enabled with Polymod. * Loads the game with ALL mods enabled with Polymod.
*/ */
public static function loadAllMods() public static function loadAllMods()
{ {
// Create the mod root if it doesn't exist. // Create the mod root if it doesn't exist.
createModRoot(); createModRoot();
trace("Initializing Polymod (using all mods)..."); trace("Initializing Polymod (using all mods)...");
loadModsById(getAllModIds()); loadModsById(getAllModIds());
} }
/** /**
* Loads the game with configured mods enabled with Polymod. * Loads the game with configured mods enabled with Polymod.
*/ */
public static function loadEnabledMods() public static function loadEnabledMods()
{ {
// Create the mod root if it doesn't exist. // Create the mod root if it doesn't exist.
createModRoot(); createModRoot();
trace("Initializing Polymod (using configured mods)..."); trace("Initializing Polymod (using configured mods)...");
loadModsById(getEnabledModIds()); loadModsById(getEnabledModIds());
} }
/** /**
* Loads the game without any mods enabled with Polymod. * Loads the game without any mods enabled with Polymod.
*/ */
public static function loadNoMods() public static function loadNoMods()
{ {
// Create the mod root if it doesn't exist. // Create the mod root if it doesn't exist.
createModRoot(); createModRoot();
// We still need to configure the debug print calls etc. // We still need to configure the debug print calls etc.
trace("Initializing Polymod (using no mods)..."); trace("Initializing Polymod (using no mods)...");
loadModsById([]); loadModsById([]);
} }
public static function loadModsById(ids:Array<String>) public static function loadModsById(ids:Array<String>)
{ {
if (ids.length == 0) if (ids.length == 0)
{ {
trace('You attempted to load zero mods.'); trace('You attempted to load zero mods.');
} }
else else
{ {
trace('Attempting to load ${ids.length} mods...'); trace('Attempting to load ${ids.length} mods...');
} }
var loadedModList = polymod.Polymod.init({
// Root directory for all mods.
modRoot: MOD_FOLDER,
// The directories for one or more mods to load.
dirs: ids,
// Framework being used to load assets.
framework: OPENFL,
// The current version of our API.
apiVersionRule: API_VERSION,
// Call this function any time an error occurs.
errorCallback: PolymodErrorHandler.onPolymodError,
// Enforce semantic version patterns for each mod.
// modVersions: null,
// A map telling Polymod what the asset type is for unfamiliar file extensions.
// extensionMap: [],
frameworkParams: buildFrameworkParams(), buildImports();
// List of filenames to ignore in mods. Use the default list to ignore the metadata file, etc. var loadedModList = polymod.Polymod.init({
ignoredFiles: Polymod.getDefaultIgnoreList(), // Root directory for all mods.
modRoot: MOD_FOLDER,
// The directories for one or more mods to load.
dirs: ids,
// Framework being used to load assets.
framework: OPENFL,
// The current version of our API.
apiVersionRule: API_VERSION,
// Call this function any time an error occurs.
errorCallback: PolymodErrorHandler.onPolymodError,
// Enforce semantic version patterns for each mod.
// modVersions: null,
// A map telling Polymod what the asset type is for unfamiliar file extensions.
// extensionMap: [],
// Parsing rules for various data formats. frameworkParams: buildFrameworkParams(),
parseRules: buildParseRules(),
// Parse hxc files and register the scripted classes in them. // List of filenames to ignore in mods. Use the default list to ignore the metadata file, etc.
useScriptedClasses: true, ignoredFiles: Polymod.getDefaultIgnoreList(),
});
if (loadedModList == null) // Parsing rules for various data formats.
{ parseRules: buildParseRules(),
trace('[POLYMOD] An error occurred! Failed when loading mods!');
}
else
{
if (loadedModList.length == 0)
{
trace('[POLYMOD] Mod loading complete. We loaded no mods / ${ids.length} mods.');
}
else
{
trace('[POLYMOD] Mod loading complete. We loaded ${loadedModList.length} / ${ids.length} mods.');
}
}
for (mod in loadedModList) // Parse hxc files and register the scripted classes in them.
{ useScriptedClasses: true,
trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]'); });
}
#if debug if (loadedModList == null)
var fileList = Polymod.listModFiles(PolymodAssetType.IMAGE); {
trace('[POLYMOD] Installed mods have replaced ${fileList.length} images.'); trace('An error occurred! Failed when loading mods!');
for (item in fileList) }
trace(' * $item'); else
{
if (loadedModList.length == 0)
{
trace('Mod loading complete. We loaded no mods / ${ids.length} mods.');
}
else
{
trace('Mod loading complete. We loaded ${loadedModList.length} / ${ids.length} mods.');
}
}
fileList = Polymod.listModFiles(PolymodAssetType.TEXT); for (mod in loadedModList)
trace('[POLYMOD] Installed mods have replaced ${fileList.length} text files.'); {
for (item in fileList) trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]');
trace(' * $item'); }
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_MUSIC); #if debug
trace('[POLYMOD] Installed mods have replaced ${fileList.length} music files.'); var fileList = Polymod.listModFiles(PolymodAssetType.IMAGE);
for (item in fileList) trace('Installed mods have replaced ${fileList.length} images.');
trace(' * $item'); for (item in fileList)
trace(' * $item');
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_SOUND); fileList = Polymod.listModFiles(PolymodAssetType.TEXT);
trace('[POLYMOD] Installed mods have replaced ${fileList.length} sound files.'); trace('Installed mods have added/replaced ${fileList.length} text files.');
for (item in fileList) for (item in fileList)
trace(' * $item'); trace(' * $item');
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_GENERIC); fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_MUSIC);
trace('[POLYMOD] Installed mods have replaced ${fileList.length} generic audio files.'); trace('Installed mods have replaced ${fileList.length} music files.');
for (item in fileList) for (item in fileList)
trace(' * $item'); trace(' * $item');
#end
}
static function buildParseRules():polymod.format.ParseRules fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_SOUND);
{ trace('Installed mods have replaced ${fileList.length} sound files.');
var output = polymod.format.ParseRules.getDefault(); for (item in fileList)
// Ensure TXT files have merge support. trace(' * $item');
output.addType("txt", TextFileFormat.LINES);
// Ensure script files have merge support.
output.addType("hscript", TextFileFormat.PLAINTEXT);
output.addType("hxs", TextFileFormat.PLAINTEXT);
output.addType("hxc", TextFileFormat.PLAINTEXT);
output.addType("hx", TextFileFormat.PLAINTEXT);
// You can specify the format of a specific file, with file extension. fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_GENERIC);
// output.addFile("data/introText.txt", TextFileFormat.LINES) trace('Installed mods have replaced ${fileList.length} generic audio files.');
return output; for (item in fileList)
} trace(' * $item');
#end
}
static inline function buildFrameworkParams():polymod.Polymod.FrameworkParams static function buildImports():Void
{ {
return { // Add default imports for common classes.
assetLibraryPaths: [
"songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2",
"week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
]
}
}
public static function getAllMods():Array<ModMetadata> // Add import aliases for certain classes.
{ // NOTE: Scripted classes are automatically aliased to their parent class.
trace('Scanning the mods folder...'); Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
var modMetadata = Polymod.scan({
modRoot: MOD_FOLDER,
apiVersionRule: API_VERSION,
errorCallback: PolymodErrorHandler.onPolymodError
});
trace('Found ${modMetadata.length} mods when scanning.');
return modMetadata;
}
public static function getAllModIds():Array<String> // Add blacklisting for prohibited classes and packages.
{ // `polymod.*`
var modIds = [for (i in getAllMods()) i.id]; for (cls in ClassMacro.listClassesInPackage('polymod'))
return modIds; {
} var className = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
}
public static function setEnabledMods(newModList:Array<String>):Void static function buildParseRules():polymod.format.ParseRules
{ {
FlxG.save.data.enabledMods = newModList; var output = polymod.format.ParseRules.getDefault();
// Make sure to COMMIT the changes. // Ensure TXT files have merge support.
FlxG.save.flush(); output.addType("txt", TextFileFormat.LINES);
} // Ensure script files have merge support.
output.addType("hscript", TextFileFormat.PLAINTEXT);
output.addType("hxs", TextFileFormat.PLAINTEXT);
output.addType("hxc", TextFileFormat.PLAINTEXT);
output.addType("hx", TextFileFormat.PLAINTEXT);
/** // You can specify the format of a specific file, with file extension.
* Returns the list of enabled mods. // output.addFile("data/introText.txt", TextFileFormat.LINES)
* @return Array<String> return output;
*/ }
public static function getEnabledModIds():Array<String>
{
if (FlxG.save.data.enabledMods == null)
{
// NOTE: If the value is null, the enabled mod list is unconfigured.
// Currently, we default to disabling newly installed mods.
// If we want to auto-enable new mods, but otherwise leave the configured list in place,
// we will need some custom logic.
FlxG.save.data.enabledMods = [];
}
return FlxG.save.data.enabledMods;
}
public static function getEnabledMods():Array<ModMetadata> static inline function buildFrameworkParams():polymod.Polymod.FrameworkParams
{ {
var modIds = getEnabledModIds(); return {
var modMetadata = getAllMods(); assetLibraryPaths: [
var enabledMods = []; "songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2",
for (item in modMetadata) "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
{ ]
if (modIds.indexOf(item.id) != -1) }
{ }
enabledMods.push(item);
}
}
return enabledMods;
}
public static function forceReloadAssets() public static function getAllMods():Array<ModMetadata>
{ {
// Forcibly clear scripts so that scripts can be edited. trace('Scanning the mods folder...');
ModuleHandler.clearModuleCache(); var modMetadata = Polymod.scan({
Polymod.clearScripts(); modRoot: MOD_FOLDER,
apiVersionRule: API_VERSION,
errorCallback: PolymodErrorHandler.onPolymodError
});
trace('Found ${modMetadata.length} mods when scanning.');
return modMetadata;
}
// Forcibly reload Polymod so it finds any new files. public static function getAllModIds():Array<String>
// TODO: Replace this with loadEnabledMods(). {
funkin.modding.PolymodHandler.loadAllMods(); var modIds = [for (i in getAllMods()) i.id];
return modIds;
}
// Reload scripted classes so stages and modules will update. public static function setEnabledMods(newModList:Array<String>):Void
Polymod.registerAllScriptClasses(); {
FlxG.save.data.enabledMods = newModList;
// Make sure to COMMIT the changes.
FlxG.save.flush();
}
// Reload everything that is cached. /**
// Currently this freezes the game for a second but I guess that's tolerable? * Returns the list of enabled mods.
* @return Array<String>
*/
public static function getEnabledModIds():Array<String>
{
if (FlxG.save.data.enabledMods == null)
{
// NOTE: If the value is null, the enabled mod list is unconfigured.
// Currently, we default to disabling newly installed mods.
// If we want to auto-enable new mods, but otherwise leave the configured list in place,
// we will need some custom logic.
FlxG.save.data.enabledMods = [];
}
return FlxG.save.data.enabledMods;
}
// TODO: Reload event callbacks public static function getEnabledMods():Array<ModMetadata>
{
var modIds = getEnabledModIds();
var modMetadata = getAllMods();
var enabledMods = [];
for (item in modMetadata)
{
if (modIds.indexOf(item.id) != -1)
{
enabledMods.push(item);
}
}
return enabledMods;
}
SongDataParser.loadSongCache(); public static function forceReloadAssets()
StageDataParser.loadStageCache(); {
CharacterDataParser.loadCharacterCache(); // Forcibly clear scripts so that scripts can be edited.
ModuleHandler.loadModuleCache(); ModuleHandler.clearModuleCache();
} Polymod.clearScripts();
// Forcibly reload Polymod so it finds any new files.
// TODO: Replace this with loadEnabledMods().
funkin.modding.PolymodHandler.loadAllMods();
// Reload scripted classes so stages and modules will update.
Polymod.registerAllScriptClasses();
// Reload everything that is cached.
// Currently this freezes the game for a second but I guess that's tolerable?
// TODO: Reload event callbacks
SongDataParser.loadSongCache();
StageDataParser.loadStageCache();
CharacterDataParser.loadCharacterCache();
ModuleHandler.loadModuleCache();
}
} }

View file

@ -15,269 +15,277 @@ typedef ScriptEventType = EventType<ScriptEvent>;
*/ */
class ScriptEvent class ScriptEvent
{ {
/** /**
* Called when the relevant object is created. * Called when the relevant object is created.
* Keep in mind that the constructor may be called before the object is needed, * Keep in mind that the constructor may be called before the object is needed,
* for the purposes of caching data or otherwise. * for the purposes of caching data or otherwise.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final CREATE:ScriptEventType = "CREATE"; public static inline final CREATE:ScriptEventType = "CREATE";
/** /**
* Called when the relevant object is destroyed. * Called when the relevant object is destroyed.
* This should perform relevant cleanup to ensure good performance. * This should perform relevant cleanup to ensure good performance.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final DESTROY:ScriptEventType = "DESTROY"; public static inline final DESTROY:ScriptEventType = "DESTROY";
/** /**
* Called during the update function. * Called during the update function.
* This is called every frame, so be careful! * This is called every frame, so be careful!
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final UPDATE:ScriptEventType = "UPDATE"; public static inline final UPDATE:ScriptEventType = "UPDATE";
/** /**
* Called when the player moves to pause the game. * Called when the player moves to pause the game.
* *
* This event IS cancelable! Canceling the event will prevent the game from pausing. * This event IS cancelable! Canceling the event will prevent the game from pausing.
*/ */
public static inline final PAUSE:ScriptEventType = "PAUSE"; public static inline final PAUSE:ScriptEventType = "PAUSE";
/** /**
* Called when the player moves to unpause the game while paused. * Called when the player moves to unpause the game while paused.
* *
* This event IS cancelable! Canceling the event will prevent the game from resuming. * This event IS cancelable! Canceling the event will prevent the game from resuming.
*/ */
public static inline final RESUME:ScriptEventType = "RESUME"; public static inline final RESUME:ScriptEventType = "RESUME";
/** /**
* Called once per step in the song. This happens 4 times per measure. * Called once per step in the song. This happens 4 times per measure.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final SONG_BEAT_HIT:ScriptEventType = "BEAT_HIT"; public static inline final SONG_BEAT_HIT:ScriptEventType = "BEAT_HIT";
/** /**
* Called once per step in the song. This happens 16 times per measure. * Called once per step in the song. This happens 16 times per measure.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final SONG_STEP_HIT:ScriptEventType = "STEP_HIT"; public static inline final SONG_STEP_HIT:ScriptEventType = "STEP_HIT";
/** /**
* Called when a character hits a note. * Called when a character hits a note.
* Important information such as judgement/timing, note data, player/opponent, etc. are all provided. * Important information such as judgement/timing, note data, player/opponent, etc. are all provided.
* *
* This event IS cancelable! Canceling this event prevents the note from being hit, * This event IS cancelable! Canceling this event prevents the note from being hit,
* and will likely result in a miss later. * and will likely result in a miss later.
*/ */
public static inline final NOTE_HIT:ScriptEventType = "NOTE_HIT"; public static inline final NOTE_HIT:ScriptEventType = "NOTE_HIT";
/** /**
* Called when a character misses a note. * Called when a character misses a note.
* Important information such as note data, player/opponent, etc. are all provided. * Important information such as note data, player/opponent, etc. are all provided.
* *
* This event IS cancelable! Canceling this event prevents the note from being considered missed, * This event IS cancelable! Canceling this event prevents the note from being considered missed,
* avoiding a combo break and lost health. * avoiding a combo break and lost health.
*/ */
public static inline final NOTE_MISS:ScriptEventType = "NOTE_MISS"; public static inline final NOTE_MISS:ScriptEventType = "NOTE_MISS";
/** /**
* Called when a character presses a note when there was none there, causing them to lose health. * Called when a character presses a note when there was none there, causing them to lose health.
* Important information such as direction pressed, etc. are all provided. * Important information such as direction pressed, etc. are all provided.
* *
* This event IS cancelable! Canceling this event prevents the note from being considered missed, * This event IS cancelable! Canceling this event prevents the note from being considered missed,
* avoiding lost health/score and preventing the miss animation. * avoiding lost health/score and preventing the miss animation.
*/ */
public static inline final NOTE_GHOST_MISS:ScriptEventType = "NOTE_GHOST_MISS"; public static inline final NOTE_GHOST_MISS:ScriptEventType = "NOTE_GHOST_MISS";
/** /**
* Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin. * Called when a song event is reached in the chart.
* *
* This event is not cancelable. * This event IS cancelable! Cancelling this event prevents the event from being triggered,
*/ * thus blocking its normal functionality.
public static inline final SONG_START:ScriptEventType = "SONG_START"; */
public static inline final SONG_EVENT:ScriptEventType = "SONG_EVENT";
/** /**
* Called when the song ends. This happens as the instrumental and vocals end. * Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final SONG_END:ScriptEventType = "SONG_END"; public static inline final SONG_START:ScriptEventType = "SONG_START";
/** /**
* Called when the countdown begins. This occurs before the song starts. * Called when the song ends. This happens as the instrumental and vocals end.
* *
* This event IS cancelable! Canceling this event will prevent the countdown from starting. * This event is not cancelable.
* - The song will not start until you call Countdown.performCountdown() later. */
* - Note that calling performCountdown() will trigger this event again, so be sure to add logic to ignore it. public static inline final SONG_END:ScriptEventType = "SONG_END";
*/
public static inline final COUNTDOWN_START:ScriptEventType = "COUNTDOWN_START";
/** /**
* Called when a step of the countdown happens. * Called when the countdown begins. This occurs before the song starts.
* Includes information about what step of the countdown was hit. *
* * This event IS cancelable! Canceling this event will prevent the countdown from starting.
* This event IS cancelable! Canceling this event will pause the countdown. * - The song will not start until you call Countdown.performCountdown() later.
* - The countdown will not resume until you call PlayState.resumeCountdown(). * - Note that calling performCountdown() will trigger this event again, so be sure to add logic to ignore it.
*/ */
public static inline final COUNTDOWN_STEP:ScriptEventType = "COUNTDOWN_STEP"; public static inline final COUNTDOWN_START:ScriptEventType = "COUNTDOWN_START";
/** /**
* Called when the countdown is done but just before the song starts. * Called when a step of the countdown happens.
* * Includes information about what step of the countdown was hit.
* This event is not cancelable. *
*/ * This event IS cancelable! Canceling this event will pause the countdown.
public static inline final COUNTDOWN_END:ScriptEventType = "COUNTDOWN_END"; * - The countdown will not resume until you call PlayState.resumeCountdown().
*/
public static inline final COUNTDOWN_STEP:ScriptEventType = "COUNTDOWN_STEP";
/** /**
* Called before the game over screen triggers and the death animation plays. * Called when the countdown is done but just before the song starts.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final GAME_OVER:ScriptEventType = "GAME_OVER"; public static inline final COUNTDOWN_END:ScriptEventType = "COUNTDOWN_END";
/** /**
* Called after the player presses a key to restart the game. * Called before the game over screen triggers and the death animation plays.
* This can happen from the pause menu or the game over screen. *
* * This event is not cancelable.
* This event IS cancelable! Canceling this event will prevent the game from restarting. */
*/ public static inline final GAME_OVER:ScriptEventType = "GAME_OVER";
public static inline final SONG_RETRY:ScriptEventType = "SONG_RETRY";
/** /**
* Called when the player pushes down any key on the keyboard. * Called after the player presses a key to restart the game.
* * This can happen from the pause menu or the game over screen.
* This event is not cancelable. *
*/ * This event IS cancelable! Canceling this event will prevent the game from restarting.
public static inline final KEY_DOWN:ScriptEventType = "KEY_DOWN"; */
public static inline final SONG_RETRY:ScriptEventType = "SONG_RETRY";
/** /**
* Called when the player releases a key on the keyboard. * Called when the player pushes down any key on the keyboard.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final KEY_UP:ScriptEventType = "KEY_UP"; public static inline final KEY_DOWN:ScriptEventType = "KEY_DOWN";
/** /**
* Called when the game has finished loading the notes from JSON. * Called when the player releases a key on the keyboard.
* This allows modders to mutate the notes before they are used in the song. *
* * This event is not cancelable.
* This event is not cancelable. */
*/ public static inline final KEY_UP:ScriptEventType = "KEY_UP";
public static inline final SONG_LOADED:ScriptEventType = "SONG_LOADED";
/** /**
* Called when the game is about to switch the current FlxState. * Called when the game has finished loading the notes from JSON.
* * This allows modders to mutate the notes before they are used in the song.
* This event is not cancelable. *
*/ * This event is not cancelable.
public static inline final STATE_CHANGE_BEGIN:ScriptEventType = "STATE_CHANGE_BEGIN"; */
public static inline final SONG_LOADED:ScriptEventType = "SONG_LOADED";
/** /**
* Called when the game has finished switching the current FlxState. * Called when the game is about to switch the current FlxState.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final STATE_CHANGE_END:ScriptEventType = "STATE_CHANGE_END"; public static inline final STATE_CHANGE_BEGIN:ScriptEventType = "STATE_CHANGE_BEGIN";
/** /**
* Called when the game is about to open a new FlxSubState. * Called when the game has finished switching the current FlxState.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final SUBSTATE_OPEN_BEGIN:ScriptEventType = "SUBSTATE_OPEN_BEGIN"; public static inline final STATE_CHANGE_END:ScriptEventType = "STATE_CHANGE_END";
/** /**
* Called when the game has finished opening a new FlxSubState. * Called when the game is about to open a new FlxSubState.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final SUBSTATE_OPEN_END:ScriptEventType = "SUBSTATE_OPEN_END"; public static inline final SUBSTATE_OPEN_BEGIN:ScriptEventType = "SUBSTATE_OPEN_BEGIN";
/** /**
* Called when the game is about to close the current FlxSubState. * Called when the game has finished opening a new FlxSubState.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final SUBSTATE_CLOSE_BEGIN:ScriptEventType = "SUBSTATE_CLOSE_BEGIN"; public static inline final SUBSTATE_OPEN_END:ScriptEventType = "SUBSTATE_OPEN_END";
/** /**
* Called when the game has finished closing the current FlxSubState. * Called when the game is about to close the current FlxSubState.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
public static inline final SUBSTATE_CLOSE_END:ScriptEventType = "SUBSTATE_CLOSE_END"; public static inline final SUBSTATE_CLOSE_BEGIN:ScriptEventType = "SUBSTATE_CLOSE_BEGIN";
/** /**
* Called when the game is exiting the current FlxState. * Called when the game has finished closing the current FlxSubState.
* *
* This event is not cancelable. * This event is not cancelable.
*/ */
/** public static inline final SUBSTATE_CLOSE_END:ScriptEventType = "SUBSTATE_CLOSE_END";
* If true, the behavior associated with this event can be prevented.
* For example, cancelling COUNTDOWN_START should prevent the countdown from starting,
* until another script restarts it, or cancelling NOTE_HIT should cause the note to be missed.
*/
public var cancelable(default, null):Bool;
/** /**
* The type associated with the event. * Called when the game is exiting the current FlxState.
*/ *
public var type(default, null):ScriptEventType; * This event is not cancelable.
*/
/**
* If true, the behavior associated with this event can be prevented.
* For example, cancelling COUNTDOWN_START should prevent the countdown from starting,
* until another script restarts it, or cancelling NOTE_HIT should cause the note to be missed.
*/
public var cancelable(default, null):Bool;
/** /**
* Whether the event should continue to be triggered on additional targets. * The type associated with the event.
*/ */
public var shouldPropagate(default, null):Bool; public var type(default, null):ScriptEventType;
/** /**
* Whether the event has been canceled by one of the scripts that received it. * Whether the event should continue to be triggered on additional targets.
*/ */
public var eventCanceled(default, null):Bool; public var shouldPropagate(default, null):Bool;
public function new(type:ScriptEventType, cancelable:Bool = false):Void /**
{ * Whether the event has been canceled by one of the scripts that received it.
this.type = type; */
this.cancelable = cancelable; public var eventCanceled(default, null):Bool;
this.eventCanceled = false;
this.shouldPropagate = true;
}
/** public function new(type:ScriptEventType, cancelable:Bool = false):Void
* Call this function on a cancelable event to cancel the associated behavior. {
* For example, cancelling COUNTDOWN_START will prevent the countdown from starting. this.type = type;
*/ this.cancelable = cancelable;
public function cancelEvent():Void this.eventCanceled = false;
{ this.shouldPropagate = true;
if (cancelable) }
{
eventCanceled = true;
}
}
public function cancel():Void /**
{ * Call this function on a cancelable event to cancel the associated behavior.
// This typo happens enough that I just added this. * For example, cancelling COUNTDOWN_START will prevent the countdown from starting.
cancelEvent(); */
} public function cancelEvent():Void
{
if (cancelable)
{
eventCanceled = true;
}
}
/** public function cancel():Void
* Call this function to stop any other Scripteds from receiving the event. {
*/ // This typo happens enough that I just added this.
public function stopPropagation():Void cancelEvent();
{ }
shouldPropagate = false;
}
public function toString():String /**
{ * Call this function to stop any other Scripteds from receiving the event.
return 'ScriptEvent(type=$type, cancelable=$cancelable)'; */
} public function stopPropagation():Void
{
shouldPropagate = false;
}
public function toString():String
{
return 'ScriptEvent(type=$type, cancelable=$cancelable)';
}
} }
/** /**
@ -288,29 +296,29 @@ class ScriptEvent
*/ */
class NoteScriptEvent extends ScriptEvent class NoteScriptEvent extends ScriptEvent
{ {
/** /**
* The note associated with this event. * The note associated with this event.
* You cannot replace it, but you can edit it. * You cannot replace it, but you can edit it.
*/ */
public var note(default, null):Note; public var note(default, null):Note;
/** /**
* The combo count as it is with this event. * The combo count as it is with this event.
* Will be (combo) on miss events and (combo + 1) on hit events (the stored combo count won't update if the event is cancelled). * Will be (combo) on miss events and (combo + 1) on hit events (the stored combo count won't update if the event is cancelled).
*/ */
public var comboCount(default, null):Int; public var comboCount(default, null):Int;
public function new(type:ScriptEventType, note:Note, comboCount:Int = 0, cancelable:Bool = false):Void public function new(type:ScriptEventType, note:Note, comboCount:Int = 0, cancelable:Bool = false):Void
{ {
super(type, cancelable); super(type, cancelable);
this.note = note; this.note = note;
this.comboCount = comboCount; this.comboCount = comboCount;
} }
public override function toString():String public override function toString():String
{ {
return 'NoteScriptEvent(type=' + type + ', cancelable=' + cancelable + ', note=' + note + ', comboCount=' + comboCount + ')'; return 'NoteScriptEvent(type=' + type + ', cancelable=' + cancelable + ', note=' + note + ', comboCount=' + comboCount + ')';
} }
} }
/** /**
@ -318,52 +326,81 @@ class NoteScriptEvent extends ScriptEvent
*/ */
class GhostMissNoteScriptEvent extends ScriptEvent class GhostMissNoteScriptEvent extends ScriptEvent
{ {
/** /**
* The direction that was mistakenly pressed. * The direction that was mistakenly pressed.
*/ */
public var dir(default, null):NoteDir; public var dir(default, null):NoteDir;
/** /**
* Whether there was a note within judgement range when this ghost note was pressed. * Whether there was a note within judgement range when this ghost note was pressed.
*/ */
public var hasPossibleNotes(default, null):Bool; public var hasPossibleNotes(default, null):Bool;
/** /**
* How much health should be lost when this ghost note is pressed. * How much health should be lost when this ghost note is pressed.
* Remember that max health is 2.00. * Remember that max health is 2.00.
*/ */
public var healthChange(default, default):Float; public var healthChange(default, default):Float;
/** /**
* How much score should be lost when this ghost note is pressed. * How much score should be lost when this ghost note is pressed.
*/ */
public var scoreChange(default, default):Int; public var scoreChange(default, default):Int;
/** /**
* Whether to play the record scratch sound. * Whether to play the record scratch sound.
*/ */
public var playSound(default, default):Bool; public var playSound(default, default):Bool;
/** /**
* Whether to play the miss animation on the player. * Whether to play the miss animation on the player.
*/ */
public var playAnim(default, default):Bool; public var playAnim(default, default):Bool;
public function new(dir:NoteDir, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void public function new(dir:NoteDir, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
{ {
super(ScriptEvent.NOTE_GHOST_MISS, true); super(ScriptEvent.NOTE_GHOST_MISS, true);
this.dir = dir; this.dir = dir;
this.hasPossibleNotes = hasPossibleNotes; this.hasPossibleNotes = hasPossibleNotes;
this.healthChange = healthChange; this.healthChange = healthChange;
this.scoreChange = scoreChange; this.scoreChange = scoreChange;
this.playSound = true; this.playSound = true;
this.playAnim = true; this.playAnim = true;
} }
public override function toString():String public override function toString():String
{ {
return 'GhostMissNoteScriptEvent(dir=' + dir + ', hasPossibleNotes=' + hasPossibleNotes + ')'; return 'GhostMissNoteScriptEvent(dir=' + dir + ', hasPossibleNotes=' + hasPossibleNotes + ')';
} }
}
/**
* An event that is fired when the song reaches an event.
*/
class SongEventScriptEvent extends ScriptEvent
{
/**
* The note associated with this event.
* You cannot replace it, but you can edit it.
*/
public var event(default, null):funkin.play.song.SongData.SongEventData;
/**
* The combo count as it is with this event.
* Will be (combo) on miss events and (combo + 1) on hit events (the stored combo count won't update if the event is cancelled).
*/
public var comboCount(default, null):Int;
public function new(event:funkin.play.song.SongData.SongEventData):Void
{
super(ScriptEvent.SONG_EVENT, true);
this.event = event;
}
public override function toString():String
{
return 'SongEventScriptEvent(event=' + event + ')';
}
} }
/** /**
@ -371,22 +408,22 @@ class GhostMissNoteScriptEvent extends ScriptEvent
*/ */
class UpdateScriptEvent extends ScriptEvent class UpdateScriptEvent extends ScriptEvent
{ {
/** /**
* The note associated with this event. * The note associated with this event.
* You cannot replace it, but you can edit it. * You cannot replace it, but you can edit it.
*/ */
public var elapsed(default, null):Float; public var elapsed(default, null):Float;
public function new(elapsed:Float):Void public function new(elapsed:Float):Void
{ {
super(ScriptEvent.UPDATE, false); super(ScriptEvent.UPDATE, false);
this.elapsed = elapsed; this.elapsed = elapsed;
} }
public override function toString():String public override function toString():String
{ {
return 'UpdateScriptEvent(elapsed=$elapsed)'; return 'UpdateScriptEvent(elapsed=$elapsed)';
} }
} }
/** /**
@ -395,27 +432,27 @@ class UpdateScriptEvent extends ScriptEvent
*/ */
class SongTimeScriptEvent extends ScriptEvent class SongTimeScriptEvent extends ScriptEvent
{ {
/** /**
* The current beat of the song. * The current beat of the song.
*/ */
public var beat(default, null):Int; public var beat(default, null):Int;
/** /**
* The current step of the song. * The current step of the song.
*/ */
public var step(default, null):Int; public var step(default, null):Int;
public function new(type:ScriptEventType, beat:Int, step:Int):Void public function new(type:ScriptEventType, beat:Int, step:Int):Void
{ {
super(type, true); super(type, true);
this.beat = beat; this.beat = beat;
this.step = step; this.step = step;
} }
public override function toString():String public override function toString():String
{ {
return 'SongTimeScriptEvent(type=' + type + ', beat=' + beat + ', step=' + step + ')'; return 'SongTimeScriptEvent(type=' + type + ', beat=' + beat + ', step=' + step + ')';
} }
} }
/** /**
@ -424,21 +461,21 @@ class SongTimeScriptEvent extends ScriptEvent
*/ */
class CountdownScriptEvent extends ScriptEvent class CountdownScriptEvent extends ScriptEvent
{ {
/** /**
* The current step of the countdown. * The current step of the countdown.
*/ */
public var step(default, null):CountdownStep; public var step(default, null):CountdownStep;
public function new(type:ScriptEventType, step:CountdownStep, cancelable = true):Void public function new(type:ScriptEventType, step:CountdownStep, cancelable = true):Void
{ {
super(type, cancelable); super(type, cancelable);
this.step = step; this.step = step;
} }
public override function toString():String public override function toString():String
{ {
return 'CountdownScriptEvent(type=' + type + ', step=' + step + ')'; return 'CountdownScriptEvent(type=' + type + ', step=' + step + ')';
} }
} }
/** /**
@ -446,21 +483,21 @@ class CountdownScriptEvent extends ScriptEvent
*/ */
class KeyboardInputScriptEvent extends ScriptEvent class KeyboardInputScriptEvent extends ScriptEvent
{ {
/** /**
* The associated keyboard event. * The associated keyboard event.
*/ */
public var event(default, null):KeyboardEvent; public var event(default, null):KeyboardEvent;
public function new(type:ScriptEventType, event:KeyboardEvent):Void public function new(type:ScriptEventType, event:KeyboardEvent):Void
{ {
super(type, false); super(type, false);
this.event = event; this.event = event;
} }
public override function toString():String public override function toString():String
{ {
return 'KeyboardInputScriptEvent(type=' + type + ', event=' + event + ')'; return 'KeyboardInputScriptEvent(type=' + type + ', event=' + event + ')';
} }
} }
/** /**
@ -468,35 +505,35 @@ class KeyboardInputScriptEvent extends ScriptEvent
*/ */
class SongLoadScriptEvent extends ScriptEvent class SongLoadScriptEvent extends ScriptEvent
{ {
/** /**
* The note associated with this event. * The note associated with this event.
* You cannot replace it, but you can edit it. * You cannot replace it, but you can edit it.
*/ */
public var notes(default, set):Array<Note>; public var notes(default, set):Array<Note>;
public var id(default, null):String; public var id(default, null):String;
public var difficulty(default, null):String; public var difficulty(default, null):String;
function set_notes(notes:Array<Note>):Array<Note> function set_notes(notes:Array<Note>):Array<Note>
{ {
this.notes = notes; this.notes = notes;
return this.notes; return this.notes;
} }
public function new(id:String, difficulty:String, notes:Array<Note>):Void public function new(id:String, difficulty:String, notes:Array<Note>):Void
{ {
super(ScriptEvent.SONG_LOADED, false); super(ScriptEvent.SONG_LOADED, false);
this.id = id; this.id = id;
this.difficulty = difficulty; this.difficulty = difficulty;
this.notes = notes; this.notes = notes;
} }
public override function toString():String public override function toString():String
{ {
var noteStr = notes == null ? 'null' : 'Array(' + notes.length + ')'; var noteStr = notes == null ? 'null' : 'Array(' + notes.length + ')';
return 'SongLoadScriptEvent(notes=$noteStr, id=$id, difficulty=$difficulty)'; return 'SongLoadScriptEvent(notes=$noteStr, id=$id, difficulty=$difficulty)';
} }
} }
/** /**
@ -504,21 +541,21 @@ class SongLoadScriptEvent extends ScriptEvent
*/ */
class StateChangeScriptEvent extends ScriptEvent class StateChangeScriptEvent extends ScriptEvent
{ {
/** /**
* The state the game is moving into. * The state the game is moving into.
*/ */
public var targetState(default, null):FlxState; public var targetState(default, null):FlxState;
public function new(type:ScriptEventType, targetState:FlxState, cancelable:Bool = false):Void public function new(type:ScriptEventType, targetState:FlxState, cancelable:Bool = false):Void
{ {
super(type, cancelable); super(type, cancelable);
this.targetState = targetState; this.targetState = targetState;
} }
public override function toString():String public override function toString():String
{ {
return 'StateChangeScriptEvent(type=' + type + ', targetState=' + targetState + ')'; return 'StateChangeScriptEvent(type=' + type + ', targetState=' + targetState + ')';
} }
} }
/** /**
@ -526,21 +563,21 @@ class StateChangeScriptEvent extends ScriptEvent
*/ */
class SubStateScriptEvent extends ScriptEvent class SubStateScriptEvent extends ScriptEvent
{ {
/** /**
* The state the game is moving into. * The state the game is moving into.
*/ */
public var targetState(default, null):FlxSubState; public var targetState(default, null):FlxSubState;
public function new(type:ScriptEventType, targetState:FlxSubState, cancelable:Bool = false):Void public function new(type:ScriptEventType, targetState:FlxSubState, cancelable:Bool = false):Void
{ {
super(type, cancelable); super(type, cancelable);
this.targetState = targetState; this.targetState = targetState;
} }
public override function toString():String public override function toString():String
{ {
return 'SubStateScriptEvent(type=' + type + ', targetState=' + targetState + ')'; return 'SubStateScriptEvent(type=' + type + ', targetState=' + targetState + ')';
} }
} }
/** /**
@ -548,14 +585,14 @@ class SubStateScriptEvent extends ScriptEvent
*/ */
class PauseScriptEvent extends ScriptEvent class PauseScriptEvent extends ScriptEvent
{ {
/** /**
* Whether to use the Gitaroo Man pause. * Whether to use the Gitaroo Man pause.
*/ */
public var gitaroo(default, default):Bool; public var gitaroo(default, default):Bool;
public function new(gitaroo:Bool):Void public function new(gitaroo:Bool):Void
{ {
super(ScriptEvent.PAUSE, true); super(ScriptEvent.PAUSE, true);
this.gitaroo = gitaroo; this.gitaroo = gitaroo;
} }
} }

View file

@ -10,109 +10,111 @@ import funkin.modding.events.ScriptEvent;
*/ */
class Module implements IPlayStateScriptedClass implements IStateChangingScriptedClass class Module implements IPlayStateScriptedClass implements IStateChangingScriptedClass
{ {
/** /**
* Whether the module is currently active. * Whether the module is currently active.
*/ */
public var active(default, set):Bool = true; public var active(default, set):Bool = true;
function set_active(value:Bool):Bool function set_active(value:Bool):Bool
{ {
this.active = value; this.active = value;
return value; return value;
} }
public var moduleId(default, null):String = 'UNKNOWN'; public var moduleId(default, null):String = 'UNKNOWN';
/** /**
* Determines the order in which modules receive events. * Determines the order in which modules receive events.
* You can modify this to change the order in which a given module receives events. * You can modify this to change the order in which a given module receives events.
* *
* Priority 1 is processed before Priority 1000, etc. * Priority 1 is processed before Priority 1000, etc.
*/ */
public var priority(default, set):Int; public var priority(default, set):Int;
function set_priority(value:Int):Int function set_priority(value:Int):Int
{ {
this.priority = value; this.priority = value;
@:privateAccess @:privateAccess
ModuleHandler.reorderModuleCache(); ModuleHandler.reorderModuleCache();
return value; return value;
} }
/** /**
* Called when the module is initialized. * Called when the module is initialized.
* It may not be safe to reference other modules here since they may not be loaded yet. * It may not be safe to reference other modules here since they may not be loaded yet.
* *
* NOTE: To make the module start inactive, call `this.active = false` in the constructor. * NOTE: To make the module start inactive, call `this.active = false` in the constructor.
*/ */
public function new(moduleId:String, priority:Int = 1000):Void public function new(moduleId:String, priority:Int = 1000):Void
{ {
this.moduleId = moduleId; this.moduleId = moduleId;
this.priority = priority; this.priority = priority;
} }
public function toString() public function toString()
{ {
return 'Module(' + this.moduleId + ')'; return 'Module(' + this.moduleId + ')';
} }
// TODO: Half of these aren't actually being called!!!!!!! // TODO: Half of these aren't actually being called!!!!!!!
public function onScriptEvent(event:ScriptEvent) {} public function onScriptEvent(event:ScriptEvent) {}
/** /**
* Called when the module is first created. * Called when the module is first created.
* This happens before the title screen appears! * This happens before the title screen appears!
*/ */
public function onCreate(event:ScriptEvent) {} public function onCreate(event:ScriptEvent) {}
/** /**
* Called when a module is destroyed. * Called when a module is destroyed.
* This currently only happens when reloading modules with F5. * This currently only happens when reloading modules with F5.
*/ */
public function onDestroy(event:ScriptEvent) {} public function onDestroy(event:ScriptEvent) {}
public function onUpdate(event:UpdateScriptEvent) {} public function onUpdate(event:UpdateScriptEvent) {}
public function onPause(event:PauseScriptEvent) {} public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {} public function onResume(event:ScriptEvent) {}
public function onSongStart(event:ScriptEvent) {} public function onSongStart(event:ScriptEvent) {}
public function onSongEnd(event:ScriptEvent) {} public function onSongEnd(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent) {} public function onGameOver(event:ScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent) {} public function onNoteHit(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {} public function onNoteMiss(event:NoteScriptEvent) {}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {} public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
public function onStepHit(event:SongTimeScriptEvent) {} public function onStepHit(event:SongTimeScriptEvent) {}
public function onBeatHit(event:SongTimeScriptEvent) {} public function onBeatHit(event:SongTimeScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent) {} public function onSongEvent(event:SongEventScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent) {} public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownEnd(event:CountdownScriptEvent) {} public function onCountdownStep(event:CountdownScriptEvent) {}
public function onSongLoaded(event:SongLoadScriptEvent) {} public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onStateChangeBegin(event:StateChangeScriptEvent) {} public function onSongLoaded(event:SongLoadScriptEvent) {}
public function onStateChangeEnd(event:StateChangeScriptEvent) {} public function onStateChangeBegin(event:StateChangeScriptEvent) {}
public function onSubstateOpenBegin(event:SubStateScriptEvent) {} public function onStateChangeEnd(event:StateChangeScriptEvent) {}
public function onSubstateOpenEnd(event:SubStateScriptEvent) {} public function onSubstateOpenBegin(event:SubStateScriptEvent) {}
public function onSubstateCloseBegin(event:SubStateScriptEvent) {} public function onSubstateOpenEnd(event:SubStateScriptEvent) {}
public function onSubstateCloseEnd(event:SubStateScriptEvent) {} public function onSubstateCloseBegin(event:SubStateScriptEvent) {}
public function onSongRetry(event:ScriptEvent) {} public function onSubstateCloseEnd(event:SubStateScriptEvent) {}
public function onSongRetry(event:ScriptEvent) {}
} }

View file

@ -11,132 +11,137 @@ import funkin.modding.module.ScriptedModule;
*/ */
class ModuleHandler class ModuleHandler
{ {
static final moduleCache:Map<String, Module> = new Map<String, Module>(); static final moduleCache:Map<String, Module> = new Map<String, Module>();
static var modulePriorityOrder:Array<String> = []; static var modulePriorityOrder:Array<String> = [];
/** /**
* Parses and preloads the game's stage data and scripts when the game starts. * Parses and preloads the game's stage data and scripts when the game starts.
* *
* If you want to force stages to be reloaded, you can just call this function again. * If you want to force stages to be reloaded, you can just call this function again.
*/ */
public static function loadModuleCache():Void public static function loadModuleCache():Void
{ {
// Clear any stages that are cached if there were any. // Clear any stages that are cached if there were any.
clearModuleCache(); clearModuleCache();
trace("[MODULEHANDLER] Loading module cache..."); trace("[MODULEHANDLER] Loading module cache...");
var scriptedModuleClassNames:Array<String> = ScriptedModule.listScriptClasses(); var scriptedModuleClassNames:Array<String> = ScriptedModule.listScriptClasses();
trace(' Instantiating ${scriptedModuleClassNames.length} modules...'); trace(' Instantiating ${scriptedModuleClassNames.length} modules...');
for (moduleCls in scriptedModuleClassNames) for (moduleCls in scriptedModuleClassNames)
{ {
var module:Module = ScriptedModule.init(moduleCls, moduleCls); var module:Module = ScriptedModule.init(moduleCls, moduleCls);
if (module != null) if (module != null)
{ {
trace(' Loaded module: ${moduleCls}'); trace(' Loaded module: ${moduleCls}');
// Then store it. // Then store it.
addToModuleCache(module); addToModuleCache(module);
} }
else else
{ {
trace(' Failed to instantiate module: ${moduleCls}'); trace(' Failed to instantiate module: ${moduleCls}');
} }
} }
reorderModuleCache(); reorderModuleCache();
trace("[MODULEHANDLER] Module cache loaded."); trace("[MODULEHANDLER] Module cache loaded.");
} }
public static function buildModuleCallbacks():Void public static function buildModuleCallbacks():Void
{ {
FlxG.signals.postStateSwitch.add(onStateSwitchComplete); FlxG.signals.postStateSwitch.add(onStateSwitchComplete);
} }
static function onStateSwitchComplete():Void static function onStateSwitchComplete():Void
{ {
callEvent(new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_END, FlxG.state, true)); callEvent(new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_END, FlxG.state, true));
} }
static function addToModuleCache(module:Module):Void static function addToModuleCache(module:Module):Void
{ {
moduleCache.set(module.moduleId, module); moduleCache.set(module.moduleId, module);
} }
static function reorderModuleCache():Void static function reorderModuleCache():Void
{ {
modulePriorityOrder = moduleCache.keys().array(); modulePriorityOrder = moduleCache.keys().array();
modulePriorityOrder.sort(function(a:String, b:String):Int modulePriorityOrder.sort(function(a:String, b:String):Int
{ {
var aModule:Module = moduleCache.get(a); var aModule:Module = moduleCache.get(a);
var bModule:Module = moduleCache.get(b); var bModule:Module = moduleCache.get(b);
if (aModule.priority != bModule.priority) if (aModule.priority != bModule.priority)
{ {
return aModule.priority - bModule.priority; return aModule.priority - bModule.priority;
} }
else else
{ {
// Sort alphabetically. Yes that's how this works. // Sort alphabetically. Yes that's how this works.
return a > b ? 1 : -1; return a > b ? 1 : -1;
} }
}); });
} }
public static function getModule(moduleId:String):Module public static function getModule(moduleId:String):Module
{ {
return moduleCache.get(moduleId); return moduleCache.get(moduleId);
} }
public static function activateModule(moduleId:String):Void public static function activateModule(moduleId:String):Void
{ {
var module:Module = getModule(moduleId); var module:Module = getModule(moduleId);
if (module != null) if (module != null)
{ {
module.active = true; module.active = true;
} }
} }
public static function deactivateModule(moduleId:String):Void public static function deactivateModule(moduleId:String):Void
{ {
var module:Module = getModule(moduleId); var module:Module = getModule(moduleId);
if (module != null) if (module != null)
{ {
module.active = false; module.active = false;
} }
} }
/** /**
* Clear the module cache, forcing all modules to call shutdown events. * Clear the module cache, forcing all modules to call shutdown events.
*/ */
public static function clearModuleCache():Void public static function clearModuleCache():Void
{ {
if (moduleCache != null) if (moduleCache != null)
{ {
var event = new ScriptEvent(ScriptEvent.DESTROY, false); var event = new ScriptEvent(ScriptEvent.DESTROY, false);
// Note: Ignore stopPropagation() // Note: Ignore stopPropagation()
for (key => value in moduleCache) for (key => value in moduleCache)
{ {
ScriptEventDispatcher.callEvent(value, event); ScriptEventDispatcher.callEvent(value, event);
moduleCache.remove(key); moduleCache.remove(key);
} }
moduleCache.clear(); moduleCache.clear();
modulePriorityOrder = []; modulePriorityOrder = [];
} }
} }
public static function callEvent(event:ScriptEvent):Void public static function callEvent(event:ScriptEvent):Void
{ {
for (moduleId in modulePriorityOrder) for (moduleId in modulePriorityOrder)
{ {
var module:Module = moduleCache.get(moduleId); var module:Module = moduleCache.get(moduleId);
// The module needs to be active to receive events. // The module needs to be active to receive events.
if (module != null && module.active) if (module != null && module.active)
{ {
ScriptEventDispatcher.callEvent(module, event); ScriptEventDispatcher.callEvent(module, event);
} }
} }
} }
public static inline function callOnCreate():Void
{
callEvent(new ScriptEvent(ScriptEvent.CREATE, false));
}
} }

View file

@ -13,87 +13,87 @@ import openfl.Assets;
*/ */
class NoteUtil class NoteUtil
{ {
/** /**
* IDK THING FOR BOTH LOL! DIS SHIT HACK-Y * IDK THING FOR BOTH LOL! DIS SHIT HACK-Y
* @param jsonPath * @param jsonPath
* @return Map<Int, Array<SongEventInfo>> * @return Map<Int, Array<SongEventInfo>>
*/ */
public static function loadSongEvents(jsonPath:String):Map<Int, Array<SongEventInfo>> public static function loadSongEvents(jsonPath:String):Map<Int, Array<SongEventInfo>>
{ {
return parseSongEvents(loadSongEventFromJson(jsonPath)); return parseSongEvents(loadSongEventFromJson(jsonPath));
} }
public static function loadSongEventFromJson(jsonPath:String):Array<SongEvent> public static function loadSongEventFromJson(jsonPath:String):Array<SongEvent>
{ {
var daEvents:Array<SongEvent>; var daEvents:Array<SongEvent>;
daEvents = cast Json.parse(Assets.getText(jsonPath)).events; // DUMB LIL DETAIL HERE: MAKE SURE THAT .events IS THERE?? daEvents = cast Json.parse(Assets.getText(jsonPath)).events; // DUMB LIL DETAIL HERE: MAKE SURE THAT .events IS THERE??
trace('GET JSON SONG EVENTS:'); trace('GET JSON SONG EVENTS:');
trace(daEvents); trace(daEvents);
return daEvents; return daEvents;
} }
/** /**
* Parses song event json stuff into a neater lil map grouping? * Parses song event json stuff into a neater lil map grouping?
* @param songEvents * @param songEvents
*/ */
public static function parseSongEvents(songEvents:Array<SongEvent>):Map<Int, Array<SongEventInfo>> public static function parseSongEvents(songEvents:Array<SongEvent>):Map<Int, Array<SongEventInfo>>
{ {
var songData:Map<Int, Array<SongEventInfo>> = new Map(); var songData:Map<Int, Array<SongEventInfo>> = new Map();
for (songEvent in songEvents) for (songEvent in songEvents)
{ {
trace(songEvent); trace(songEvent);
if (songData[songEvent.t] == null) if (songData[songEvent.t] == null)
songData[songEvent.t] = []; songData[songEvent.t] = [];
songData[songEvent.t].push({songEventType: songEvent.e, value: songEvent.v, activated: false}); songData[songEvent.t].push({songEventType: songEvent.e, value: songEvent.v, activated: false});
} }
trace("FINISH SONG EVENTS!"); trace("FINISH SONG EVENTS!");
trace(songData); trace(songData);
return songData; return songData;
} }
public static function checkSongEvents(songData:Map<Int, Array<SongEventInfo>>, time:Float) public static function checkSongEvents(songData:Map<Int, Array<SongEventInfo>>, time:Float)
{ {
for (eventGrp in songData.keys()) for (eventGrp in songData.keys())
{ {
if (time >= eventGrp) if (time >= eventGrp)
{ {
for (events in songData[eventGrp]) for (events in songData[eventGrp])
{ {
if (!events.activated) if (!events.activated)
{ {
// TURN TO NICER SWITCH STATEMENT CHECKER OF EVENT TYPES!! // TURN TO NICER SWITCH STATEMENT CHECKER OF EVENT TYPES!!
trace(events.value); trace(events.value);
trace(eventGrp); trace(eventGrp);
trace(Conductor.songPosition); trace(Conductor.songPosition);
events.activated = true; events.activated = true;
} }
} }
} }
} }
} }
} }
typedef SongEventInfo = typedef SongEventInfo =
{ {
var songEventType:SongEventType; var songEventType:SongEventType;
var value:Dynamic; var value:Dynamic;
var activated:Bool; var activated:Bool;
} }
typedef SongEvent = typedef SongEvent =
{ {
var t:Int; var t:Int;
var e:SongEventType; var e:SongEventType;
var v:Dynamic; var v:Dynamic;
} }
enum abstract SongEventType(String) enum abstract SongEventType(String)
{ {
var FocusCamera; var FocusCamera;
var PlayCharAnim; var PlayCharAnim;
var Trace; var Trace;
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -20,199 +20,199 @@ import funkin.util.assets.FlxAnimationUtil;
*/ */
class MultiSparrowCharacter extends BaseCharacter class MultiSparrowCharacter extends BaseCharacter
{ {
/** /**
* The actual group which holds all spritesheets this character uses. * The actual group which holds all spritesheets this character uses.
*/ */
private var members:Map<String, FlxFramesCollection> = new Map<String, FlxFramesCollection>(); private var members:Map<String, FlxFramesCollection> = new Map<String, FlxFramesCollection>();
/** /**
* A map between animation names and what frame collection the animation should use. * A map between animation names and what frame collection the animation should use.
*/ */
private var animAssetPath:Map<String, String> = new Map<String, String>(); private var animAssetPath:Map<String, String> = new Map<String, String>();
/** /**
* The current frame collection being used. * The current frame collection being used.
*/ */
private var activeMember:String; private var activeMember:String;
public function new(id:String) public function new(id:String)
{ {
super(id); super(id);
} }
override function onCreate(event:ScriptEvent):Void override function onCreate(event:ScriptEvent):Void
{ {
trace('Creating MULTI SPARROW CHARACTER: ' + this.characterId); trace('Creating Multi-Sparrow character: ' + this.characterId);
buildSprites(); buildSprites();
super.onCreate(event); super.onCreate(event);
} }
function buildSprites() function buildSprites()
{ {
buildSpritesheets(); buildSpritesheets();
buildAnimations(); buildAnimations();
if (_data.isPixel) if (_data.isPixel)
{ {
this.antialiasing = false; this.antialiasing = false;
} }
else else
{ {
this.antialiasing = true; this.antialiasing = true;
} }
} }
function buildSpritesheets() function buildSpritesheets()
{ {
// Build the list of asset paths to use. // Build the list of asset paths to use.
// Ignore nulls and duplicates. // Ignore nulls and duplicates.
var assetList = [_data.assetPath]; var assetList = [_data.assetPath];
for (anim in _data.animations) for (anim in _data.animations)
{ {
if (anim.assetPath != null && !assetList.contains(anim.assetPath)) if (anim.assetPath != null && !assetList.contains(anim.assetPath))
{ {
assetList.push(anim.assetPath); assetList.push(anim.assetPath);
} }
animAssetPath.set(anim.name, anim.assetPath); animAssetPath.set(anim.name, anim.assetPath);
} }
// Load the Sparrow atlas for each path and store them in the members map. // Load the Sparrow atlas for each path and store them in the members map.
for (asset in assetList) for (asset in assetList)
{ {
var texture:FlxFramesCollection = Paths.getSparrowAtlas(asset, 'shared'); var texture:FlxFramesCollection = Paths.getSparrowAtlas(asset, 'shared');
// If we don't do this, the unused textures will be removed as soon as they're loaded. // If we don't do this, the unused textures will be removed as soon as they're loaded.
if (texture == null) if (texture == null)
{ {
trace('Multi-Sparrow atlas could not load texture: ${asset}'); trace('Multi-Sparrow atlas could not load texture: ${asset}');
} }
else else
{ {
trace('Adding multi-sparrow atlas: ${asset}'); trace('Adding multi-sparrow atlas: ${asset}');
texture.parent.destroyOnNoUse = false; texture.parent.destroyOnNoUse = false;
members.set(asset, texture); members.set(asset, texture);
} }
} }
// Use the default frame collection to start. // Use the default frame collection to start.
loadFramesByAssetPath(_data.assetPath); loadFramesByAssetPath(_data.assetPath);
} }
/** /**
* Replace this sprite's animation frames with the ones at this asset path. * Replace this sprite's animation frames with the ones at this asset path.
*/ */
function loadFramesByAssetPath(assetPath:String):Void function loadFramesByAssetPath(assetPath:String):Void
{ {
if (_data.assetPath == null) if (_data.assetPath == null)
{ {
trace('[ERROR] Multi-Sparrow character has no default asset path!'); trace('[ERROR] Multi-Sparrow character has no default asset path!');
return; return;
} }
if (assetPath == null) if (assetPath == null)
{ {
// trace('Asset path is null, falling back to default. This is normal!'); // trace('Asset path is null, falling back to default. This is normal!');
loadFramesByAssetPath(_data.assetPath); loadFramesByAssetPath(_data.assetPath);
return; return;
} }
if (this.activeMember == assetPath) if (this.activeMember == assetPath)
{ {
// trace('Already using this asset path: ${assetPath}'); // trace('Already using this asset path: ${assetPath}');
return; return;
} }
if (members.exists(assetPath)) if (members.exists(assetPath))
{ {
// Switch to a new set of sprites. // Switch to a new set of sprites.
// trace('Loading frames from asset path: ${assetPath}'); // trace('Loading frames from asset path: ${assetPath}');
this.frames = members.get(assetPath); this.frames = members.get(assetPath);
this.activeMember = assetPath; this.activeMember = assetPath;
this.setScale(_data.scale); this.setScale(_data.scale);
} }
else else
{ {
trace('[WARN] MultiSparrow character ${characterId} could not find asset path: ${assetPath}'); trace('[WARN] MultiSparrow character ${characterId} could not find asset path: ${assetPath}');
} }
} }
/** /**
* Replace this sprite's animation frames with the ones needed to play this animation. * Replace this sprite's animation frames with the ones needed to play this animation.
*/ */
function loadFramesByAnimName(animName) function loadFramesByAnimName(animName)
{ {
if (animAssetPath.exists(animName)) if (animAssetPath.exists(animName))
{ {
loadFramesByAssetPath(animAssetPath.get(animName)); loadFramesByAssetPath(animAssetPath.get(animName));
} }
else else
{ {
trace('[WARN] MultiSparrow character ${characterId} could not find animation: ${animName}'); trace('[WARN] MultiSparrow character ${characterId} could not find animation: ${animName}');
} }
} }
function buildAnimations() function buildAnimations()
{ {
trace('[MULTISPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}'); trace('[MULTISPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}');
// We need to swap to the proper frame collection before adding the animations, I think? // We need to swap to the proper frame collection before adding the animations, I think?
for (anim in _data.animations) for (anim in _data.animations)
{ {
loadFramesByAnimName(anim.name); loadFramesByAnimName(anim.name);
FlxAnimationUtil.addAtlasAnimation(this, anim); FlxAnimationUtil.addAtlasAnimation(this, anim);
if (anim.offsets == null) if (anim.offsets == null)
{ {
setAnimationOffsets(anim.name, 0, 0); setAnimationOffsets(anim.name, 0, 0);
} }
else else
{ {
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]); setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
} }
} }
var animNames = this.animation.getNameList(); var animNames = this.animation.getNameList();
trace('[MULTISPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}'); trace('[MULTISPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
} }
public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void
{ {
// Make sure we ignore other animations if we're currently playing a forced one, // Make sure we ignore other animations if we're currently playing a forced one,
// unless we're forcing a new animation. // unless we're forcing a new animation.
if (!this.canPlayOtherAnims && !ignoreOther) if (!this.canPlayOtherAnims && !ignoreOther)
return; return;
loadFramesByAnimName(name); loadFramesByAnimName(name);
super.playAnimation(name, restart, ignoreOther); super.playAnimation(name, restart, ignoreOther);
} }
override function set_frames(value:FlxFramesCollection):FlxFramesCollection override function set_frames(value:FlxFramesCollection):FlxFramesCollection
{ {
// DISABLE THIS SO WE DON'T DESTROY OUR HARD WORK // DISABLE THIS SO WE DON'T DESTROY OUR HARD WORK
// WE WILL MAKE SURE TO LOAD THE PROPER SPRITESHEET BEFORE PLAYING AN ANIM // WE WILL MAKE SURE TO LOAD THE PROPER SPRITESHEET BEFORE PLAYING AN ANIM
// if (animation != null) // if (animation != null)
// { // {
// animation.destroyAnimations(); // animation.destroyAnimations();
// } // }
if (value != null) if (value != null)
{ {
graphic = value.parent; graphic = value.parent;
this.frames = value; this.frames = value;
this.frame = value.getByIndex(0); this.frame = value.getByIndex(0);
this.numFrames = value.numFrames; this.numFrames = value.numFrames;
resetHelpers(); resetHelpers();
this.bakedRotationAngle = 0; this.bakedRotationAngle = 0;
this.animation.frameIndex = 0; this.animation.frameIndex = 0;
graphicLoaded(); graphicLoaded();
} }
else else
{ {
this.frames = null; this.frames = null;
this.frame = null; this.frame = null;
this.graphic = null; this.graphic = null;
} }
return this.frames; return this.frames;
} }
} }

View file

@ -11,65 +11,65 @@ import funkin.play.character.BaseCharacter.CharacterType;
*/ */
class PackerCharacter extends BaseCharacter class PackerCharacter extends BaseCharacter
{ {
public function new(id:String) public function new(id:String)
{ {
super(id); super(id);
} }
override function onCreate(event:ScriptEvent):Void override function onCreate(event:ScriptEvent):Void
{ {
trace('Creating PACKER CHARACTER: ' + this.characterId); trace('Creating Packer character: ' + this.characterId);
loadSpritesheet(); loadSpritesheet();
loadAnimations(); loadAnimations();
super.onCreate(event); super.onCreate(event);
} }
function loadSpritesheet() function loadSpritesheet()
{ {
trace('[PACKERCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}'); trace('[PACKERCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath, 'shared'); var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath, 'shared');
if (tex == null) if (tex == null)
{ {
trace('Could not load Packer sprite: ${_data.assetPath}'); trace('Could not load Packer sprite: ${_data.assetPath}');
return; return;
} }
this.frames = tex; this.frames = tex;
if (_data.isPixel) if (_data.isPixel)
{ {
this.antialiasing = false; this.antialiasing = false;
} }
else else
{ {
this.antialiasing = true; this.antialiasing = true;
} }
this.setScale(_data.scale); this.setScale(_data.scale);
} }
function loadAnimations() function loadAnimations()
{ {
trace('[PACKERCHAR] Loading ${_data.animations.length} animations for ${characterId}'); trace('[PACKERCHAR] Loading ${_data.animations.length} animations for ${characterId}');
FlxAnimationUtil.addAtlasAnimations(this, _data.animations); FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
for (anim in _data.animations) for (anim in _data.animations)
{ {
if (anim.offsets == null) if (anim.offsets == null)
{ {
setAnimationOffsets(anim.name, 0, 0); setAnimationOffsets(anim.name, 0, 0);
} }
else else
{ {
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]); setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
} }
} }
var animNames = this.animation.getNameList(); var animNames = this.animation.getNameList();
trace('[PACKERCHAR] Successfully loaded ${animNames.length} animations for ${characterId}'); trace('[PACKERCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
} }
} }

View file

@ -13,65 +13,65 @@ import flixel.graphics.frames.FlxFramesCollection;
*/ */
class SparrowCharacter extends BaseCharacter class SparrowCharacter extends BaseCharacter
{ {
public function new(id:String) public function new(id:String)
{ {
super(id); super(id);
} }
override function onCreate(event:ScriptEvent):Void override function onCreate(event:ScriptEvent):Void
{ {
trace('Creating SPARROW CHARACTER: ' + this.characterId); trace('Creating Sparrow character: ' + this.characterId);
loadSpritesheet(); loadSpritesheet();
loadAnimations(); loadAnimations();
super.onCreate(event); super.onCreate(event);
} }
function loadSpritesheet() function loadSpritesheet()
{ {
trace('[SPARROWCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}'); trace('[SPARROWCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath, 'shared'); var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath, 'shared');
if (tex == null) if (tex == null)
{ {
trace('Could not load Sparrow sprite: ${_data.assetPath}'); trace('Could not load Sparrow sprite: ${_data.assetPath}');
return; return;
} }
this.frames = tex; this.frames = tex;
if (_data.isPixel) if (_data.isPixel)
{ {
this.antialiasing = false; this.antialiasing = false;
} }
else else
{ {
this.antialiasing = true; this.antialiasing = true;
} }
this.setScale(_data.scale); this.setScale(_data.scale);
} }
function loadAnimations() function loadAnimations()
{ {
trace('[SPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}'); trace('[SPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}');
FlxAnimationUtil.addAtlasAnimations(this, _data.animations); FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
for (anim in _data.animations) for (anim in _data.animations)
{ {
if (anim.offsets == null) if (anim.offsets == null)
{ {
setAnimationOffsets(anim.name, 0, 0); setAnimationOffsets(anim.name, 0, 0);
} }
else else
{ {
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]); setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
} }
} }
var animNames = this.animation.getNameList(); var animNames = this.animation.getNameList();
trace('[SPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}'); trace('[SPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
} }
} }

View file

@ -0,0 +1,142 @@
package funkin.play.event;
import funkin.play.event.SongEvent;
import funkin.play.song.SongData;
/**
* This class represents a handler for a type of song event.
* It is used by the ScriptedSongEvent class to handle user-defined events.
*
* Example: Focus on Boyfriend:
* ```
* {
* "e": "FocusCamera",
* "v": {
* "char": 0,
* }
* }
* ```
*
* Example: Focus on 10px above Girlfriend:
* ```
* {
* "e": "FocusCamera",
* "v": {
* "char": 2,
* "y": -10,
* }
* }
* ```
*
* Example: Focus on (100, 100):
* ```
* {
* "e": "FocusCamera",
* "v": {
* "char": -1,
* "x": 100,
* "y": 100,
* }
* }
* ```
*/
class FocusCameraSongEvent extends SongEvent
{
public function new()
{
super('FocusCamera');
}
public override function handleEvent(data:SongEventData)
{
// Does nothing if there is no PlayState camera or stage.
if (PlayState.instance == null || PlayState.instance.currentStage == null)
return;
var posX = data.getFloat('x');
if (posX == null)
posX = 0.0;
var posY = data.getFloat('y');
if (posY == null)
posY = 0.0;
var char = data.getInt('char');
if (char == null)
char = cast data.value;
switch (char)
{
case -1: // Position
trace('Focusing camera on static position.');
var xTarget = posX;
var yTarget = posY;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
case 0: // Boyfriend
// Focus the camera on the player.
trace('Focusing camera on player.');
var xTarget = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x + posX;
var yTarget = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y + posY;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
case 1: // Dad
// Focus the camera on the dad.
trace('Focusing camera on dad.');
var xTarget = PlayState.instance.currentStage.getDad().cameraFocusPoint.x + posX;
var yTarget = PlayState.instance.currentStage.getDad().cameraFocusPoint.y + posY;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
case 2: // Girlfriend
// Focus the camera on the girlfriend.
trace('Focusing camera on girlfriend.');
var xTarget = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x + posX;
var yTarget = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y + posY;
PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
default:
trace('Unknown camera focus: ' + data);
}
}
public override function getTitle():String
{
return "Focus Camera";
}
/**
* ```
* {
* "char": ENUM, // Which character to point to
* "x": FLOAT, // Optional x offset
* "y": FLOAT, // Optional y offset
* }
* @return SongEventSchema
*/
public override function getEventSchema():SongEventSchema
{
return [
{
name: "char",
title: "Character",
defaultValue: 0,
type: SongEventFieldType.ENUM,
keys: ["Position" => -1, "Boyfriend" => 0, "Dad" => 1, "Girlfriend" => 2]
},
{
name: "x",
title: "X Position",
defaultValue: 0,
step: 10.0,
type: SongEventFieldType.FLOAT,
},
{
name: "y",
title: "Y Position",
defaultValue: 0,
step: 10.0,
type: SongEventFieldType.FLOAT,
}
];
}
}

View file

@ -0,0 +1,111 @@
package funkin.play.event;
import flixel.FlxSprite;
import funkin.play.character.BaseCharacter;
import funkin.play.event.SongEvent;
import funkin.play.song.SongData;
class PlayAnimationSongEvent extends SongEvent
{
public function new()
{
super('PlayAnimation');
}
public override function handleEvent(data:SongEventData)
{
// Does nothing if there is no PlayState camera or stage.
if (PlayState.instance == null || PlayState.instance.currentStage == null)
return;
var targetName = data.getString('target');
var anim = data.getString('anim');
var force = data.getBool('force');
if (force == null)
force = false;
var target:FlxSprite = null;
switch (targetName)
{
case 'boyfriend':
trace('Playing animation $anim on boyfriend.');
target = PlayState.instance.currentStage.getBoyfriend();
case 'bf':
trace('Playing animation $anim on boyfriend.');
target = PlayState.instance.currentStage.getBoyfriend();
case 'player':
trace('Playing animation $anim on boyfriend.');
target = PlayState.instance.currentStage.getBoyfriend();
case 'dad':
trace('Playing animation $anim on dad.');
target = PlayState.instance.currentStage.getDad();
case 'opponent':
trace('Playing animation $anim on dad.');
target = PlayState.instance.currentStage.getDad();
case 'girlfriend':
trace('Playing animation $anim on girlfriend.');
target = PlayState.instance.currentStage.getGirlfriend();
case 'gf':
trace('Playing animation $anim on girlfriend.');
target = PlayState.instance.currentStage.getGirlfriend();
default:
target = PlayState.instance.currentStage.getNamedProp(targetName);
if (target == null)
trace('Unknown animation target: $targetName');
else
trace('Fetched animation target $targetName from stage.');
}
if (target != null)
{
if (Std.isOfType(target, BaseCharacter))
{
var targetChar:BaseCharacter = cast target;
targetChar.playAnimation(anim, force, force);
}
else
{
target.animation.play(anim, force);
}
}
}
public override function getTitle():String
{
return "Play Animation";
}
/**
* ```
* {
* "target": STRING, // Name of character or prop to point to.
* "anim": STRING, // Name of animation to play.
* "force": BOOL, // Whether to force the animation to play.
* }
* @return SongEventSchema
*/
public override function getEventSchema():SongEventSchema
{
return [
{
name: 'target',
title: 'Target',
type: SongEventFieldType.STRING,
defaultValue: 'boyfriend',
},
{
name: 'anim',
title: 'Animation',
type: SongEventFieldType.STRING,
defaultValue: 'idle',
},
{
name: 'force',
title: 'Force',
type: SongEventFieldType.BOOL,
defaultValue: false
}
];
}
}

View file

@ -0,0 +1,9 @@
package funkin.play.event;
import funkin.play.song.Song;
import polymod.hscript.HScriptedClass;
@:hscriptClass
class ScriptedSongEvent extends SongEvent implements HScriptedClass
{
}

View file

@ -1,303 +1,270 @@
package funkin.play.event; package funkin.play.event;
import flixel.FlxSprite; import funkin.util.macro.ClassMacro;
import funkin.play.PlayState; import funkin.play.song.SongData.SongEventData;
import funkin.play.character.BaseCharacter;
import funkin.play.song.SongData.RawSongEventData;
import haxe.DynamicAccess;
typedef RawSongEvent = /**
* This class represents a handler for a type of song event.
* It is used by the ScriptedSongEvent class to handle user-defined events.
*/
class SongEvent
{ {
> RawSongEventData, public var id:String;
/** public function new(id:String)
* Whether the event has been activated or not. {
*/ this.id = id;
var a:Bool; }
public function handleEvent(data:SongEventData)
{
throw 'SongEvent.handleEvent() must be overridden!';
}
public function getEventSchema():SongEventSchema
{
return null;
}
public function getTitle():String
{
return this.id.toTitleCase();
}
public function toString():String
{
return 'SongEvent(${this.id})';
}
} }
@:forward class SongEventParser
abstract SongEvent(RawSongEvent)
{ {
public function new(time:Float, event:String, value:Dynamic = null) /**
{ * Every built-in event class must be added to this list.
this = { * Thankfully, with the power of `SongEventMacro`, this is done automatically.
t: time, */
e: event, private static final BUILTIN_EVENTS:List<Class<SongEvent>> = ClassMacro.listSubclassesOf(SongEvent);
v: value,
a: false
};
}
public var time(get, set):Float; /**
* Map of internal handlers for song events.
* These may be either `ScriptedSongEvents` or built-in classes extending `SongEvent`.
*/
static final eventCache:Map<String, SongEvent> = new Map<String, SongEvent>();
public function get_time():Float public static function loadEventCache():Void
{ {
return this.t; clearEventCache();
}
public function set_time(value:Float):Float //
{ // BASE GAME EVENTS
return this.t = value; //
} registerBaseEvents();
registerScriptedEvents();
}
public var event(get, set):String; static function registerBaseEvents()
{
trace('Instantiating ${BUILTIN_EVENTS.length} built-in song events...');
for (eventCls in BUILTIN_EVENTS)
{
var eventClsName:String = Type.getClassName(eventCls);
if (eventClsName == 'funkin.play.event.SongEvent' || eventClsName == 'funkin.play.event.ScriptedSongEvent')
continue;
public function get_event():String var event:SongEvent = Type.createInstance(eventCls, ["UNKNOWN"]);
{
return this.e;
}
public function set_event(value:String):String if (event != null)
{ {
return this.e = value; trace(' Loaded built-in song event: (${event.id})');
} eventCache.set(event.id, event);
}
else
{
trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}');
}
}
}
public var value(get, set):Dynamic; static function registerScriptedEvents()
{
var scriptedEventClassNames:Array<String> = ScriptedSongEvent.listScriptClasses();
if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0)
return;
public function get_value():Dynamic trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
{ for (eventCls in scriptedEventClassNames)
return this.v; {
} var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN");
public function set_value(value:Dynamic):Dynamic if (event != null)
{ {
return this.v = value; trace(' Loaded scripted song event: ${event.id}');
} eventCache.set(event.id, event);
}
else
{
trace(' Failed to instantiate scripted song event class: ${eventCls}');
}
}
}
public inline function getBool():Bool public static function listEventIds():Array<String>
{ {
return cast this.v; return eventCache.keys().array();
} }
public inline function getInt():Int public static function listEvents():Array<SongEvent>
{ {
return cast this.v; return eventCache.values();
} }
public inline function getFloat():Float public static function getEvent(id:String):SongEvent
{ {
return cast this.v; return eventCache.get(id);
} }
public inline function getString():String public static function getEventSchema(id:String):SongEventSchema
{ {
return cast this.v; var event:SongEvent = getEvent(id);
} if (event == null)
return null;
public inline function getArray():Array<Dynamic> return event.getEventSchema();
{ }
return cast this.v;
}
public inline function getMap():DynamicAccess<Dynamic> static function clearEventCache()
{ {
return cast this.v; eventCache.clear();
} }
public inline function getBoolArray():Array<Bool> public static function handleEvent(data:SongEventData):Void
{ {
return cast this.v; var eventType:String = data.event;
} var eventHandler:SongEvent = eventCache.get(eventType);
if (eventHandler != null)
{
eventHandler.handleEvent(data);
}
else
{
trace('WARNING: No event handler for event with id: ${eventType}');
}
data.activated = true;
}
public static inline function handleEvents(events:Array<SongEventData>):Void
{
for (event in events)
{
handleEvent(event);
}
}
/**
* Given a list of song events and the current timestamp,
* return a list of events that should be handled.
*/
public static function queryEvents(events:Array<SongEventData>, currentTime:Float):Array<SongEventData>
{
return events.filter(function(event:SongEventData):Bool
{
// If the event is already activated, don't activate it again.
if (event.activated)
return false;
// If the event is in the future, don't activate it.
if (event.time > currentTime)
return false;
return true;
});
}
/**
* Reset activation of all the provided events.
*/
public static function resetEvents(events:Array<SongEventData>):Void
{
for (event in events)
{
event.activated = false;
// TODO: Add an onReset() method to SongEvent?
}
}
} }
typedef SongEventCallback = SongEvent->Void; enum abstract SongEventFieldType(String) from String to String
class SongEventHandler
{ {
private static final eventCallbacks:Map<String, SongEventCallback> = new Map<String, SongEventCallback>(); /**
* The STRING type will display as a text field.
*/
var STRING = "string";
public static function registerCallback(event:String, callback:SongEventCallback):Void /**
{ * The INTEGER type will display as a text field that only accepts numbers.
eventCallbacks.set(event, callback); */
} var INTEGER = "integer";
public static function unregisterCallback(event:String):Void /**
{ * The FLOAT type will display as a text field that only accepts numbers.
eventCallbacks.remove(event); */
} var FLOAT = "float";
public static function clearCallbacks():Void /**
{ * The BOOL type will display as a checkbox.
eventCallbacks.clear(); */
} var BOOL = "bool";
/** /**
* Register each of the event callbacks provided by the base game. * The ENUM type will display as a dropdown.
*/ * Make sure to specify the `keys` field in the schema.
public static function registerBaseEventCallbacks():Void */
{ var ENUM = "enum";
// TODO: Add a system for mods to easily add their own event callbacks.
// Should be easy as creating character or stage scripts.
registerCallback('FocusCamera', VanillaEventCallbacks.focusCamera);
registerCallback('PlayAnimation', VanillaEventCallbacks.playAnimation);
}
/**
* Given a list of song events and the current timestamp,
* return a list of events that should be activated.
*/
public static function queryEvents(events:Array<SongEvent>, currentTime:Float):Array<SongEvent>
{
return events.filter(function(event:SongEvent):Bool
{
// If the event is already activated, don't activate it again.
if (event.a)
return false;
// If the event is in the future, don't activate it.
if (event.time > currentTime)
return false;
return true;
});
}
public static function activateEvents(events:Array<SongEvent>):Void
{
for (event in events)
{
activateEvent(event);
}
}
public static function activateEvent(event:SongEvent):Void
{
if (event.a)
{
trace('Event already activated: ' + event);
return;
}
// Prevent the event from being activated again.
event.a = true;
// Perform the action.
if (eventCallbacks.exists(event.event))
{
eventCallbacks.get(event.event)(event);
}
}
public static function resetEvents(events:Array<SongEvent>):Void
{
for (event in events)
{
resetEvent(event);
}
}
public static function resetEvent(event:SongEvent):Void
{
// TODO: Add a system for mods to easily add their reset callbacks.
event.a = false;
}
} }
class VanillaEventCallbacks typedef SongEventSchemaField =
{ {
/** /**
* Event Name: "FocusCamera" * The name of the property as it should be saved in the event data.
* Event Value: Int */
* 0: Focus on the player. name:String,
* 1: Focus on the opponent.
* 2: Focus on the girlfriend.
*/
public static function focusCamera(event:SongEvent):Void
{
// Does nothing if there is no PlayState camera or stage.
if (PlayState.instance == null || PlayState.instance.currentStage == null)
return;
switch (event.getInt()) /**
{ * The title of the field to display in the UI.
case 0: // Boyfriend */
// Focus the camera on the player. title:String,
trace('[EVENT] Focusing camera on player.');
PlayState.instance.cameraFollowPoint.setPosition(PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x,
PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y);
case 1: // Dad
// Focus the camera on the dad.
trace('[EVENT] Focusing camera on dad.');
PlayState.instance.cameraFollowPoint.setPosition(PlayState.instance.currentStage.getDad().cameraFocusPoint.x,
PlayState.instance.currentStage.getDad().cameraFocusPoint.y);
case 2: // Girlfriend
// Focus the camera on the girlfriend.
trace('[EVENT] Focusing camera on girlfriend.');
PlayState.instance.cameraFollowPoint.setPosition(PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x,
PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y);
default:
trace('[EVENT] Unknown camera focus: ' + event.value);
}
}
/** /**
* Event Name: "playAnimation" * The type of the field.
* Event Value: Object */
* { type:SongEventFieldType,
* target: String, // "player", "dad", "girlfriend", or <stage prop id>
* animation: String,
* force: Bool // optional
* }
*/
public static function playAnimation(event:SongEvent):Void
{
// Does nothing if there is no PlayState camera or stage.
if (PlayState.instance == null || PlayState.instance.currentStage == null)
return;
var data:Dynamic = event.value; /**
* Used for ENUM values.
var targetName:String = Reflect.field(data, 'target'); * The key is the display name and the value is the actual value.
var anim:String = Reflect.field(data, 'anim'); */
var force:Null<Bool> = Reflect.field(data, 'force'); ?keys:Map<String, Dynamic>,
if (force == null) /**
force = false; * Used for INTEGER and FLOAT values.
* The minimum value that can be entered.
var target:FlxSprite = null; */
?min:Float,
switch (targetName) /**
{ * Used for INTEGER and FLOAT values.
case 'boyfriend': * The maximum value that can be entered.
trace('[EVENT] Playing animation $anim on boyfriend.'); */
target = PlayState.instance.currentStage.getBoyfriend(); ?max:Float,
case 'bf': /**
trace('[EVENT] Playing animation $anim on boyfriend.'); * Used for INTEGER and FLOAT values.
target = PlayState.instance.currentStage.getBoyfriend(); * The step value that will be used when incrementing/decrementing the value.
case 'player': */
trace('[EVENT] Playing animation $anim on boyfriend.'); ?step:Float,
target = PlayState.instance.currentStage.getBoyfriend(); /**
case 'dad': * An optional default value for the field.
trace('[EVENT] Playing animation $anim on dad.'); */
target = PlayState.instance.currentStage.getDad(); ?defaultValue:Dynamic,
case 'opponent':
trace('[EVENT] Playing animation $anim on dad.');
target = PlayState.instance.currentStage.getDad();
case 'girlfriend':
trace('[EVENT] Playing animation $anim on girlfriend.');
target = PlayState.instance.currentStage.getGirlfriend();
case 'gf':
trace('[EVENT] Playing animation $anim on girlfriend.');
target = PlayState.instance.currentStage.getGirlfriend();
default:
target = PlayState.instance.currentStage.getNamedProp(targetName);
if (target == null)
trace('[EVENT] Unknown animation target: $targetName');
else
trace('[EVENT] Fetched animation target $targetName from stage.');
}
if (target != null)
{
if (Std.isOfType(target, BaseCharacter))
{
var targetChar:BaseCharacter = cast target;
targetChar.playAnimation(anim, force, force);
}
else
{
target.animation.play(anim, force);
}
}
}
} }
typedef SongEventSchema = Array<SongEventSchemaField>;

View file

@ -22,239 +22,239 @@ import funkin.play.song.SongData.SongTimeFormat;
*/ */
class Song // implements IPlayStateScriptedClass class Song // implements IPlayStateScriptedClass
{ {
public final songId:String; public final songId:String;
final _metadata:Array<SongMetadata>; final _metadata:Array<SongMetadata>;
final variations:Array<String>; final variations:Array<String>;
final difficulties:Map<String, SongDifficulty>; final difficulties:Map<String, SongDifficulty>;
public function new(id:String) public function new(id:String)
{ {
this.songId = id; this.songId = id;
variations = []; variations = [];
difficulties = new Map<String, SongDifficulty>(); difficulties = new Map<String, SongDifficulty>();
_metadata = SongDataParser.parseSongMetadata(songId); _metadata = SongDataParser.parseSongMetadata(songId);
if (_metadata == null || _metadata.length == 0) if (_metadata == null || _metadata.length == 0)
{ {
throw 'Could not find song data for songId: $songId'; throw 'Could not find song data for songId: $songId';
} }
populateFromMetadata(); populateFromMetadata();
} }
public function getRawMetadata():Array<SongMetadata> public function getRawMetadata():Array<SongMetadata>
{ {
return _metadata; return _metadata;
} }
/** /**
* Populate the song data from the provided metadata, * Populate the song data from the provided metadata,
* including data from individual difficulties. Does not load chart data. * including data from individual difficulties. Does not load chart data.
*/ */
function populateFromMetadata():Void function populateFromMetadata():Void
{ {
// Variations may have different artist, time format, generatedBy, etc. // Variations may have different artist, time format, generatedBy, etc.
for (metadata in _metadata) for (metadata in _metadata)
{ {
for (diffId in metadata.playData.difficulties) for (diffId in metadata.playData.difficulties)
{ {
var difficulty:SongDifficulty = new SongDifficulty(this, diffId, metadata.variation); var difficulty:SongDifficulty = new SongDifficulty(this, diffId, metadata.variation);
variations.push(metadata.variation); variations.push(metadata.variation);
difficulty.songName = metadata.songName; difficulty.songName = metadata.songName;
difficulty.songArtist = metadata.artist; difficulty.songArtist = metadata.artist;
difficulty.timeFormat = metadata.timeFormat; difficulty.timeFormat = metadata.timeFormat;
difficulty.divisions = metadata.divisions; difficulty.divisions = metadata.divisions;
difficulty.timeChanges = metadata.timeChanges; difficulty.timeChanges = metadata.timeChanges;
difficulty.loop = metadata.loop; difficulty.loop = metadata.loop;
difficulty.generatedBy = metadata.generatedBy; difficulty.generatedBy = metadata.generatedBy;
difficulty.stage = metadata.playData.stage; difficulty.stage = metadata.playData.stage;
// difficulty.noteSkin = metadata.playData.noteSkin; // difficulty.noteSkin = metadata.playData.noteSkin;
difficulty.chars = new Map<String, SongPlayableChar>(); difficulty.chars = new Map<String, SongPlayableChar>();
for (charId in metadata.playData.playableChars.keys()) for (charId in metadata.playData.playableChars.keys())
{ {
var char = metadata.playData.playableChars.get(charId); var char = metadata.playData.playableChars.get(charId);
difficulty.chars.set(charId, char); difficulty.chars.set(charId, char);
} }
difficulties.set(diffId, difficulty); difficulties.set(diffId, difficulty);
} }
} }
} }
/** /**
* Parse and cache the chart for all difficulties of this song. * Parse and cache the chart for all difficulties of this song.
*/ */
public function cacheCharts(?force:Bool = false):Void public function cacheCharts(?force:Bool = false):Void
{ {
if (force) if (force)
{ {
clearCharts(); clearCharts();
} }
trace('Caching ${variations.length} chart files for song $songId'); trace('Caching ${variations.length} chart files for song $songId');
for (variation in variations) for (variation in variations)
{ {
var chartData:SongChartData = SongDataParser.parseSongChartData(songId, variation); var chartData:SongChartData = SongDataParser.parseSongChartData(songId, variation);
var chartNotes = chartData.notes; var chartNotes = chartData.notes;
for (diffId in chartNotes.keys()) for (diffId in chartNotes.keys())
{ {
// Retrieve the cached difficulty data. // Retrieve the cached difficulty data.
var difficulty:Null<SongDifficulty> = difficulties.get(diffId); var difficulty:Null<SongDifficulty> = difficulties.get(diffId);
if (difficulty == null) if (difficulty == null)
{ {
trace('Could not find difficulty $diffId for song $songId'); trace('Could not find difficulty $diffId for song $songId');
continue; continue;
} }
// Add the chart data to the difficulty. // Add the chart data to the difficulty.
difficulty.notes = chartData.notes.get(diffId); difficulty.notes = chartData.notes.get(diffId);
difficulty.scrollSpeed = chartData.getScrollSpeed(diffId); difficulty.scrollSpeed = chartData.getScrollSpeed(diffId);
difficulty.events = chartData.events; difficulty.events = chartData.events;
} }
} }
trace('Done caching charts.'); trace('Done caching charts.');
} }
/** /**
* Retrieve the metadata for a specific difficulty, including the chart if it is loaded. * Retrieve the metadata for a specific difficulty, including the chart if it is loaded.
*/ */
public inline function getDifficulty(?diffId:String):SongDifficulty public inline function getDifficulty(?diffId:String):SongDifficulty
{ {
if (diffId == null) if (diffId == null)
diffId = difficulties.keys().array()[0]; diffId = difficulties.keys().array()[0];
return difficulties.get(diffId); return difficulties.get(diffId);
} }
/** /**
* Purge the cached chart data for each difficulty of this song. * Purge the cached chart data for each difficulty of this song.
*/ */
public function clearCharts():Void public function clearCharts():Void
{ {
for (diff in difficulties) for (diff in difficulties)
{ {
diff.clearChart(); diff.clearChart();
} }
} }
public function toString():String public function toString():String
{ {
return 'Song($songId)'; return 'Song($songId)';
} }
} }
class SongDifficulty class SongDifficulty
{ {
/** /**
* The parent song for this difficulty. * The parent song for this difficulty.
*/ */
public final song:Song; public final song:Song;
/** /**
* The difficulty ID, such as `easy` or `hard`. * The difficulty ID, such as `easy` or `hard`.
*/ */
public final difficulty:String; public final difficulty:String;
/** /**
* The metadata file that contains this difficulty. * The metadata file that contains this difficulty.
*/ */
public final variation:String; public final variation:String;
/** /**
* The note chart for this difficulty. * The note chart for this difficulty.
*/ */
public var notes:Array<SongNoteData>; public var notes:Array<SongNoteData>;
/** /**
* The event chart for this difficulty. * The event chart for this difficulty.
*/ */
public var events:Array<SongEventData>; public var events:Array<SongEventData>;
public var songName:String = SongValidator.DEFAULT_SONGNAME; public var songName:String = SongValidator.DEFAULT_SONGNAME;
public var songArtist:String = SongValidator.DEFAULT_ARTIST; public var songArtist:String = SongValidator.DEFAULT_ARTIST;
public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT; public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT;
public var divisions:Int = SongValidator.DEFAULT_DIVISIONS; public var divisions:Int = SongValidator.DEFAULT_DIVISIONS;
public var loop:Bool = SongValidator.DEFAULT_LOOP; public var loop:Bool = SongValidator.DEFAULT_LOOP;
public var generatedBy:String = SongValidator.DEFAULT_GENERATEDBY; public var generatedBy:String = SongValidator.DEFAULT_GENERATEDBY;
public var timeChanges:Array<SongTimeChange> = []; public var timeChanges:Array<SongTimeChange> = [];
public var stage:String = SongValidator.DEFAULT_STAGE; public var stage:String = SongValidator.DEFAULT_STAGE;
public var chars:Map<String, SongPlayableChar> = null; public var chars:Map<String, SongPlayableChar> = null;
public var scrollSpeed:Float = SongValidator.DEFAULT_SCROLLSPEED; public var scrollSpeed:Float = SongValidator.DEFAULT_SCROLLSPEED;
public function new(song:Song, diffId:String, variation:String) public function new(song:Song, diffId:String, variation:String)
{ {
this.song = song; this.song = song;
this.difficulty = diffId; this.difficulty = diffId;
this.variation = variation; this.variation = variation;
} }
public function clearChart():Void public function clearChart():Void
{ {
notes = null; notes = null;
} }
public function getStartingBPM():Float public function getStartingBPM():Float
{ {
if (timeChanges.length == 0) if (timeChanges.length == 0)
{ {
return 0; return 0;
} }
return timeChanges[0].bpm; return timeChanges[0].bpm;
} }
public function getPlayableChar(id:String):SongPlayableChar public function getPlayableChar(id:String):SongPlayableChar
{ {
return chars.get(id); return chars.get(id);
} }
public function getPlayableChars():Array<String> public function getPlayableChars():Array<String>
{ {
return chars.keys().array(); return chars.keys().array();
} }
public function getEvents():Array<SongEvent> public function getEvents():Array<SongEventData>
{ {
return cast events; return cast events;
} }
public inline function cacheInst() public inline function cacheInst()
{ {
FlxG.sound.cache(Paths.inst(this.song.songId)); FlxG.sound.cache(Paths.inst(this.song.songId));
} }
public inline function playInst(volume:Float = 1.0, looped:Bool = false) public inline function playInst(volume:Float = 1.0, looped:Bool = false)
{ {
FlxG.sound.playMusic(Paths.inst(this.song.songId), volume, looped); FlxG.sound.playMusic(Paths.inst(this.song.songId), volume, looped);
} }
public inline function cacheVocals() public inline function cacheVocals()
{ {
FlxG.sound.cache(Paths.voices(this.song.songId)); FlxG.sound.cache(Paths.voices(this.song.songId));
} }
public function buildVoiceList():Array<String> public function buildVoiceList():Array<String>
{ {
// TODO: Implement. // TODO: Implement.
return [""]; return [""];
} }
public function buildVocals(charId:String = "bf"):VoicesGroup public function buildVocals(charId:String = "bf"):VoicesGroup
{ {
var result:VoicesGroup = VoicesGroup.build(this.song.songId, this.buildVoiceList()); var result:VoicesGroup = VoicesGroup.build(this.song.songId, this.buildVoiceList());
return result; return result;
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -26,6 +26,22 @@ class SongDataUtils
}); });
} }
/**
* Given an array of SongEventData objects, return a new array of SongEventData objects
* whose timestamps are shifted by the given amount.
* Does not mutate the original array.
*
* @param events The events to modify.
* @param offset The time difference to apply in milliseconds.
*/
public static function offsetSongEventData(events:Array<SongEventData>, offset:Int):Array<SongEventData>
{
return events.map(function(event:SongEventData):SongEventData
{
return new SongEventData(event.time + offset, event.event, event.value);
});
}
/** /**
* Return a new array without a certain subset of notes from an array of SongNoteData objects. * Return a new array without a certain subset of notes from an array of SongNoteData objects.
* Does not mutate the original array. * Does not mutate the original array.
@ -94,11 +110,21 @@ class SongDataUtils
* *
* Offset the provided array of notes such that the first note is at 0 milliseconds. * Offset the provided array of notes such that the first note is at 0 milliseconds.
*/ */
public static function buildClipboard(notes:Array<SongNoteData>):Array<SongNoteData> public static function buildNoteClipboard(notes:Array<SongNoteData>):Array<SongNoteData>
{ {
return offsetSongNoteData(sortNotes(notes), -Std.int(notes[0].time)); return offsetSongNoteData(sortNotes(notes), -Std.int(notes[0].time));
} }
/**
* Prepare an array of events to be used as the clipboard data.
*
* Offset the provided array of events such that the first event is at 0 milliseconds.
*/
public static function buildEventClipboard(events:Array<SongEventData>):Array<SongEventData>
{
return offsetSongEventData(sortEvents(events), -Std.int(events[0].time));
}
/** /**
* Sort an array of notes by strum time. * Sort an array of notes by strum time.
*/ */
@ -113,39 +139,55 @@ class SongDataUtils
} }
/** /**
* Serialize an array of note data and write it to the clipboard. * Sort an array of events by strum time.
*/ */
public static function writeNotesToClipboard(notes:Array<SongNoteData>):Void public static function sortEvents(events:Array<SongEventData>, ?desc:Bool = false):Array<SongEventData>
{ {
var notesString = SerializerUtil.toJSON(notes); // TODO: Modifies the array in place. Is this okay?
events.sort(function(a:SongEventData, b:SongEventData):Int
{
return FlxSort.byValues(desc ? FlxSort.DESCENDING : FlxSort.ASCENDING, a.time, b.time);
});
return events;
}
ClipboardUtil.setClipboard(notesString); /**
* Serialize note and event data and write it to the clipboard.
*/
public static function writeItemsToClipboard(data:SongClipboardItems):Void
{
var dataString = SerializerUtil.toJSON(data);
trace('Wrote ' + notes.length + ' notes to clipboard.'); ClipboardUtil.setClipboard(dataString);
trace(notesString); trace('Wrote ' + data.notes.length + ' notes and ' + data.events.length + ' events to clipboard.');
trace(dataString);
} }
/** /**
* Read an array of note data from the clipboard and deserialize it. * Read an array of note data from the clipboard and deserialize it.
*/ */
public static function readNotesFromClipboard():Array<SongNoteData> public static function readItemsFromClipboard():SongClipboardItems
{ {
var notesString = ClipboardUtil.getClipboard(); var notesString = ClipboardUtil.getClipboard();
trace('Read ' + notesString.length + ' characters from clipboard.'); trace('Read ${notesString.length} characters from clipboard.');
var notes:Array<SongNoteData> = notesString.parseJSON(); var data:SongClipboardItems = notesString.parseJSON();
if (notes == null) if (data == null)
{ {
trace('Failed to parse notes from clipboard.'); trace('Failed to parse notes from clipboard.');
return []; return {
notes: [],
events: []
};
} }
else else
{ {
trace('Parsed ' + notes.length + ' notes from clipboard.'); trace('Parsed ' + data.notes.length + ' notes and ' + data.events.length + ' from clipboard.');
return notes; return data;
} }
} }
@ -160,6 +202,17 @@ class SongDataUtils
}); });
} }
/**
* Filter a list of events to only include events that are within the given time range.
*/
public static function getEventsInTimeRange(events:Array<SongEventData>, start:Float, end:Float):Array<SongEventData>
{
return events.filter(function(event:SongEventData):Bool
{
return event.time >= start && event.time <= end;
});
}
/** /**
* Filter a list of notes to only include notes whose data is within the given range. * Filter a list of notes to only include notes whose data is within the given range.
*/ */
@ -182,3 +235,9 @@ class SongDataUtils
}); });
} }
} }
typedef SongClipboardItems =
{
notes:Array<SongNoteData>,
events:Array<SongEventData>
}

View file

@ -6,74 +6,74 @@ import funkin.util.VersionUtil;
class SongMigrator class SongMigrator
{ {
/** /**
* The current latest version string for the song data format. * The current latest version string for the song data format.
* Handle breaking changes by incrementing this value * Handle breaking changes by incrementing this value
* and adding migration to the SongMigrator class. * and adding migration to the SongMigrator class.
*/ */
public static final CHART_VERSION:String = "2.0.0"; public static final CHART_VERSION:String = "2.0.0";
public static final CHART_VERSION_RULE:String = "2.0.x"; public static final CHART_VERSION_RULE:String = "2.0.x";
public static function migrateSongMetadata(jsonData:Dynamic, songId:String):SongMetadata public static function migrateSongMetadata(jsonData:Dynamic, songId:String):SongMetadata
{ {
if (jsonData.version) if (jsonData.version)
{ {
if (VersionUtil.validateVersion(jsonData.version, CHART_VERSION_RULE)) if (VersionUtil.validateVersion(jsonData.version, CHART_VERSION_RULE))
{ {
trace('[SONGDATA] Song (${songId}) metadata version (${jsonData.version}) is valid and up-to-date.'); trace('Song (${songId}) metadata version (${jsonData.version}) is valid and up-to-date.');
var songMetadata:SongMetadata = cast jsonData; var songMetadata:SongMetadata = cast jsonData;
return songMetadata; return songMetadata;
} }
else else
{ {
trace('[SONGDATA] Song (${songId}) metadata version (${jsonData.version}) is outdated.'); trace('Song (${songId}) metadata version (${jsonData.version}) is outdated.');
switch (jsonData.version) switch (jsonData.version)
{ {
// TODO: Add migration functions as cases here. // TODO: Add migration functions as cases here.
default: default:
// Unknown version. // Unknown version.
trace('[SONGDATA] Song (${songId}) unknown metadata version: ${jsonData.version}'); trace('Song (${songId}) unknown metadata version: ${jsonData.version}');
} }
} }
} }
else else
{ {
trace('[SONGDATA] Song metadata version is missing.'); trace('Song metadata version is missing.');
} }
return null; return null;
} }
public static function migrateSongChartData(jsonData:Dynamic, songId:String):SongChartData public static function migrateSongChartData(jsonData:Dynamic, songId:String):SongChartData
{ {
if (jsonData.version) if (jsonData.version)
{ {
if (VersionUtil.validateVersion(jsonData.version, CHART_VERSION_RULE)) if (VersionUtil.validateVersion(jsonData.version, CHART_VERSION_RULE))
{ {
trace('[SONGDATA] Song (${songId}) chart version (${jsonData.version}) is valid and up-to-date.'); trace('Song (${songId}) chart version (${jsonData.version}) is valid and up-to-date.');
var songChartData:SongChartData = cast jsonData; var songChartData:SongChartData = cast jsonData;
return songChartData; return songChartData;
} }
else else
{ {
trace('[SONGDATA] Song (${songId}) chart version (${jsonData.version}) is outdated.'); trace('Song (${songId}) chart version (${jsonData.version}) is outdated.');
switch (jsonData.version) switch (jsonData.version)
{ {
// TODO: Add migration functions as cases here. // TODO: Add migration functions as cases here.
default: default:
// Unknown version. // Unknown version.
trace('[SONGDATA] Song (${songId}) unknown chart version: ${jsonData.version}'); trace('Song (${songId}) unknown chart version: ${jsonData.version}');
} }
} }
} }
else else
{ {
trace('[SONGDATA] Song chart version is missing.'); trace('Song chart version is missing.');
} }
return null; return null;
} }
} }

View file

@ -15,393 +15,358 @@ typedef AnimationFinishedCallback = String->Void;
*/ */
class Bopper extends FlxSprite implements IPlayStateScriptedClass class Bopper extends FlxSprite implements IPlayStateScriptedClass
{ {
/** /**
* The bopper plays the dance animation once every `danceEvery` beats. * The bopper plays the dance animation once every `danceEvery` beats.
* Set to 0 to disable idle animation. * Set to 0 to disable idle animation.
*/ */
public var danceEvery:Int = 1; public var danceEvery:Int = 1;
/** /**
* Whether the bopper should dance left and right. * Whether the bopper should dance left and right.
* - If true, alternate playing `danceLeft` and `danceRight`. * - If true, alternate playing `danceLeft` and `danceRight`.
* - If false, play `idle` every time. * - If false, play `idle` every time.
* *
* You can manually set this value, or you can leave it as `null` to determine it automatically. * You can manually set this value, or you can leave it as `null` to determine it automatically.
*/ */
public var shouldAlternate:Null<Bool> = null; public var shouldAlternate:Null<Bool> = null;
/** /**
* Offset the character's sprite by this much when playing each animation. * Offset the character's sprite by this much when playing each animation.
*/ */
public var animationOffsets:Map<String, Array<Float>> = new Map<String, Array<Float>>(); public var animationOffsets:Map<String, Array<Float>> = new Map<String, Array<Float>>();
/** /**
* Add a suffix to the `idle` animation (or `danceLeft` and `danceRight` animations) * Add a suffix to the `idle` animation (or `danceLeft` and `danceRight` animations)
* that this bopper will play. * that this bopper will play.
*/ */
public var idleSuffix(default, set):String = ""; public var idleSuffix(default, set):String = "";
/** /**
* Whether this bopper should bop every beat. By default it's true, but when used * Whether this bopper should bop every beat. By default it's true, but when used
* for characters/players, it should be false so it doesn't cut off their animations!!!!! * for characters/players, it should be false so it doesn't cut off their animations!!!!!
*/ */
public var shouldBop:Bool = true; public var shouldBop:Bool = true;
function set_idleSuffix(value:String):String function set_idleSuffix(value:String):String
{ {
this.idleSuffix = value; this.idleSuffix = value;
this.dance(); this.dance();
return value; return value;
} }
/** /**
* The offset of the character relative to the position specified by the stage. * The offset of the character relative to the position specified by the stage.
*/ */
public var globalOffsets(default, set):Array<Float> = [0, 0]; public var globalOffsets(default, set):Array<Float> = [0, 0];
function set_globalOffsets(value:Array<Float>) function set_globalOffsets(value:Array<Float>)
{ {
if (globalOffsets == null) if (globalOffsets == null)
globalOffsets = [0, 0]; globalOffsets = [0, 0];
if (globalOffsets == value) if (globalOffsets == value)
return value; return value;
var xDiff = globalOffsets[0] - value[0]; var xDiff = globalOffsets[0] - value[0];
var yDiff = globalOffsets[1] - value[1]; var yDiff = globalOffsets[1] - value[1];
this.x += xDiff; this.x += xDiff;
this.y += yDiff; this.y += yDiff;
return animOffsets = value; return animOffsets = value;
}
}
private var animOffsets(default, set):Array<Float> = [0, 0];
private var animOffsets(default, set):Array<Float> = [0, 0];
public var originalPosition:FlxPoint = new FlxPoint(0, 0);
public var originalPosition:FlxPoint = new FlxPoint(0, 0);
function set_animOffsets(value:Array<Float>)
function set_animOffsets(value:Array<Float>) {
{ if (animOffsets == null)
if (animOffsets == null) animOffsets = [0, 0];
animOffsets = [0, 0]; if (animOffsets == value)
if (animOffsets == value) return value;
return value;
var xDiff = animOffsets[0] - value[0];
var xDiff = animOffsets[0] - value[0]; var yDiff = animOffsets[1] - value[1];
var yDiff = animOffsets[1] - value[1];
this.x += xDiff;
this.x += xDiff; this.y += yDiff;
this.y += yDiff;
return animOffsets = value;
return animOffsets = value; }
}
/**
/** * Whether to play `danceRight` next iteration.
* Whether to play `danceRight` next iteration. * Only used when `shouldAlternate` is true.
* Only used when `shouldAlternate` is true. */
*/ var hasDanced:Bool = false;
var hasDanced:Bool = false;
public function new(danceEvery:Int = 1)
public function new(danceEvery:Int = 1) {
{ super();
super(); this.danceEvery = danceEvery;
this.danceEvery = danceEvery;
this.animation.callback = this.onAnimationFrame;
this.animation.callback = this.onAnimationFrame; this.animation.finishCallback = this.onAnimationFinished;
this.animation.finishCallback = this.onAnimationFinished; }
}
/**
/** * Called when an animation finishes.
* Called when an animation finishes. * @param name The name of the animation that just finished.
* @param name The name of the animation that just finished. */
*/ function onAnimationFinished(name:String)
function onAnimationFinished(name:String) {
{ // TODO: Can we make a system of like, animation priority or something?
// TODO: Can we make a system of like, animation priority or something? if (!canPlayOtherAnims)
if (!canPlayOtherAnims) {
{ canPlayOtherAnims = true;
canPlayOtherAnims = true; }
} }
}
/**
/** * Called when the current animation's frame changes.
* Called when the current animation's frame changes. * @param name The name of the current animation.
* @param name The name of the current animation. * @param frameNumber The number of the current frame.
* @param frameNumber The number of the current frame. * @param frameIndex The index of the current frame.
* @param frameIndex The index of the current frame. *
* * For example, if an animation was defined as having the indexes [3, 0, 1, 2],
* For example, if an animation was defined as having the indexes [3, 0, 1, 2], * then the first callback would have frameNumber = 0 and frameIndex = 3.
* then the first callback would have frameNumber = 0 and frameIndex = 3. */
*/ function onAnimationFrame(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1)
function onAnimationFrame(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1) {
{ // Do nothing by default.
// Do nothing by default. // This can be overridden by, for example, scripted characters,
// This can be overridden by, for example, scripted characters, // or by calling `animationFrame.add()`.
// or by calling `animationFrame.add()`.
// Try not to do anything expensive here, it runs many times a second.
// Try not to do anything expensive here, it runs many times a second. }
}
/**
/** * If this Bopper was defined by the stage, return the prop to its original position.
* If this Bopper was defined by the stage, return the prop to its original position. */
*/ public function resetPosition()
public function resetPosition() {
{ this.x = originalPosition.x + animOffsets[0];
this.x = originalPosition.x + animOffsets[0]; this.y = originalPosition.y + animOffsets[1];
this.y = originalPosition.y + animOffsets[1]; }
}
function update_shouldAlternate():Void
function update_shouldAlternate():Void {
{ if (hasAnimation('danceLeft'))
if (hasAnimation('danceLeft')) {
{ this.shouldAlternate = true;
this.shouldAlternate = true; }
} }
}
/**
/** * Called once every beat of the song.
* Called once every beat of the song. */
*/ public function onBeatHit(event:SongTimeScriptEvent):Void
public function onBeatHit(event:SongTimeScriptEvent):Void {
{ if (danceEvery > 0 && event.beat % danceEvery == 0)
if (danceEvery > 0 && event.beat % danceEvery == 0) {
{ dance(shouldBop);
dance(shouldBop); }
} }
}
/**
/** * Called every `danceEvery` beats of the song.
* Called every `danceEvery` beats of the song. */
*/ public function dance(forceRestart:Bool = false):Void
public function dance(forceRestart:Bool = false):Void {
{ if (this.animation == null)
if (this.animation == null) {
{ return;
return; }
}
if (shouldAlternate == null)
if (shouldAlternate == null) {
{ update_shouldAlternate();
update_shouldAlternate(); }
}
if (shouldAlternate)
if (shouldAlternate) {
{ if (hasDanced)
if (hasDanced) {
{ playAnimation('danceRight$idleSuffix', forceRestart);
playAnimation('danceRight$idleSuffix', forceRestart); }
} else
else {
{ playAnimation('danceLeft$idleSuffix', forceRestart);
playAnimation('danceLeft$idleSuffix', forceRestart); }
} hasDanced = !hasDanced;
hasDanced = !hasDanced; }
} else
else {
{ playAnimation('idle$idleSuffix', forceRestart);
playAnimation('idle$idleSuffix', forceRestart); }
} }
}
public function hasAnimation(id:String):Bool
public function hasAnimation(id:String):Bool {
{ if (this.animation == null)
if (this.animation == null) return false;
return false;
return this.animation.getByName(id) != null;
return this.animation.getByName(id) != null; }
}
/**
/** * Ensure that a given animation exists before playing it.
* Ensure that a given animation exists before playing it. * Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
* Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play. * @param name
* @param name */
*/ function correctAnimationName(name:String)
function correctAnimationName(name:String) {
{ // If the animation exists, we're good.
// If the animation exists, we're good. if (hasAnimation(name))
if (hasAnimation(name)) return name;
return name;
trace('[BOPPER] Animation "$name" does not exist!');
trace('[BOPPER] Animation "$name" does not exist!');
// Attempt to strip a `-alt` suffix, if it exists.
// Attempt to strip a `-alt` suffix, if it exists. if (name.lastIndexOf('-') != -1)
if (name.lastIndexOf('-') != -1) {
{ var correctName = name.substring(0, name.lastIndexOf('-'));
var correctName = name.substring(0, name.lastIndexOf('-')); trace('[BOPPER] Attempting to fallback to "$correctName"');
trace('[BOPPER] Attempting to fallback to "$correctName"'); return correctAnimationName(correctName);
return correctAnimationName(correctName); }
} else
else {
{ if (name != 'idle')
if (name != 'idle') {
{ trace('[BOPPER] Attempting to fallback to "idle"');
trace('[BOPPER] Attempting to fallback to "idle"'); return correctAnimationName('idle');
return correctAnimationName('idle'); }
} else
else {
{ trace('[BOPPER] Failing animation playback.');
trace('[BOPPER] Failing animation playback.'); return null;
return null; }
} }
} }
}
public var canPlayOtherAnims:Bool = true;
public var canPlayOtherAnims:Bool = true;
/**
/** * @param name The name of the animation to play.
* @param name The name of the animation to play. * @param restart Whether to restart the animation if it is already playing.
* @param restart Whether to restart the animation if it is already playing. * @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
* @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing */
*/ public function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void
public function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void {
{ if (!canPlayOtherAnims && !ignoreOther)
if (!canPlayOtherAnims && !ignoreOther) return;
return;
var correctName = correctAnimationName(name);
var correctName = correctAnimationName(name); if (correctName == null)
if (correctName == null) return;
return;
this.animation.play(correctName, restart, false, 0);
this.animation.play(correctName, restart, false, 0);
if (ignoreOther)
if (ignoreOther) {
{ canPlayOtherAnims = false;
canPlayOtherAnims = false; }
}
applyAnimationOffsets(correctName);
applyAnimationOffsets(correctName); }
}
var forceAnimationTimer:FlxTimer = new FlxTimer();
var forceAnimationTimer:FlxTimer = new FlxTimer();
/**
/** * @param name The animation to play.
* @param name The animation to play. * @param duration The duration in which other (non-forced) animations will be skipped, in seconds (NOT MILLISECONDS).
* @param duration The duration in which other (non-forced) animations will be skipped, in seconds (NOT MILLISECONDS). */
*/ public function forceAnimationForDuration(name:String, duration:Float):Void
public function forceAnimationForDuration(name:String, duration:Float):Void {
{ // TODO: Might be nice to rework this function, maybe have a numbered priority system?
// TODO: Might be nice to rework this function, maybe have a numbered priority system?
if (this.animation == null)
if (this.animation == null) return;
return;
var correctName = correctAnimationName(name);
var correctName = correctAnimationName(name); if (correctName == null)
if (correctName == null) return;
return;
this.animation.play(correctName, false, false);
this.animation.play(correctName, false, false); applyAnimationOffsets(correctName);
applyAnimationOffsets(correctName);
canPlayOtherAnims = false;
canPlayOtherAnims = false; forceAnimationTimer.start(duration, (timer) ->
forceAnimationTimer.start(duration, (timer) -> {
{ canPlayOtherAnims = true;
canPlayOtherAnims = true; }, 1);
}, 1); }
}
function applyAnimationOffsets(name:String)
function applyAnimationOffsets(name:String) {
{ var offsets = animationOffsets.get(name);
var offsets = animationOffsets.get(name); if (offsets != null)
if (offsets != null) {
{ this.animOffsets = [offsets[0] + globalOffsets[0], offsets[1] + globalOffsets[1]];
this.animOffsets = [offsets[0] + globalOffsets[0], offsets[1] + globalOffsets[1]]; }
} else
else {
{ this.animOffsets = globalOffsets;
this.animOffsets = globalOffsets; }
} }
}
public function isAnimationFinished():Bool
public function isAnimationFinished():Bool {
{ return this.animation.finished;
return this.animation.finished; }
}
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void {
{ animationOffsets.set(name, [xOffset, yOffset]);
animationOffsets.set(name, [xOffset, yOffset]); }
}
/**
/** * Returns the name of the animation that is currently playing.
* Returns the name of the animation that is currently playing. * If no animation is playing (usually this means the character is BROKEN!),
* If no animation is playing (usually this means the character is BROKEN!), * returns an empty string to prevent NPEs.
* returns an empty string to prevent NPEs. */
*/ public function getCurrentAnimation():String
public function getCurrentAnimation():String {
{ if (this.animation == null || this.animation.curAnim == null)
if (this.animation == null || this.animation.curAnim == null) return "";
return ""; return this.animation.curAnim.name;
return this.animation.curAnim.name; }
}
public function onScriptEvent(event:ScriptEvent) {}
public function onScriptEvent(event:ScriptEvent)
{ public function onCreate(event:ScriptEvent) {}
}
public function onDestroy(event:ScriptEvent) {}
public function onCreate(event:ScriptEvent)
{ public function onUpdate(event:UpdateScriptEvent) {}
}
public function onPause(event:PauseScriptEvent) {}
public function onDestroy(event:ScriptEvent)
{ public function onResume(event:ScriptEvent) {}
}
public function onSongStart(event:ScriptEvent) {}
public function onUpdate(event:UpdateScriptEvent)
{ public function onSongEnd(event:ScriptEvent) {}
}
public function onGameOver(event:ScriptEvent) {}
public function onPause(event:PauseScriptEvent)
{ public function onNoteHit(event:NoteScriptEvent) {}
}
public function onNoteMiss(event:NoteScriptEvent) {}
public function onResume(event:ScriptEvent)
{ public function onSongEvent(event:SongEventScriptEvent) {}
}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
public function onSongStart(event:ScriptEvent)
{ public function onStepHit(event:SongTimeScriptEvent) {}
}
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onSongEnd(event:ScriptEvent)
{ public function onCountdownStep(event:CountdownScriptEvent) {}
}
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onGameOver(event:ScriptEvent)
{ public function onSongLoaded(event:SongLoadScriptEvent) {}
}
public function onSongRetry(event:ScriptEvent) {}
public function onNoteHit(event:NoteScriptEvent)
{
}
public function onNoteMiss(event:NoteScriptEvent)
{
}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent)
{
}
public function onStepHit(event:SongTimeScriptEvent)
{
}
public function onCountdownStart(event:CountdownScriptEvent)
{
}
public function onCountdownStep(event:CountdownScriptEvent)
{
}
public function onCountdownEnd(event:CountdownScriptEvent)
{
}
public function onSongLoaded(event:SongLoadScriptEvent)
{
}
public function onSongRetry(event:ScriptEvent)
{
}
} }

File diff suppressed because it is too large Load diff

View file

@ -13,507 +13,507 @@ import openfl.Assets;
*/ */
class StageDataParser class StageDataParser
{ {
/** /**
* The current version string for the stage data format. * The current version string for the stage data format.
* Handle breaking changes by incrementing this value * Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function. * and adding migration to the `migrateStageData()` function.
*/ */
public static final STAGE_DATA_VERSION:String = "1.0.0"; public static final STAGE_DATA_VERSION:String = "1.0.0";
/** /**
* The current version rule check for the stage data format. * The current version rule check for the stage data format.
*/ */
public static final STAGE_DATA_VERSION_RULE:String = "1.0.x"; public static final STAGE_DATA_VERSION_RULE:String = "1.0.x";
static final stageCache:Map<String, Stage> = new Map<String, Stage>(); static final stageCache:Map<String, Stage> = new Map<String, Stage>();
static final DEFAULT_STAGE_ID = 'UNKNOWN'; static final DEFAULT_STAGE_ID = 'UNKNOWN';
/** /**
* Parses and preloads the game's stage data and scripts when the game starts. * Parses and preloads the game's stage data and scripts when the game starts.
* *
* If you want to force stages to be reloaded, you can just call this function again. * If you want to force stages to be reloaded, you can just call this function again.
*/ */
public static function loadStageCache():Void public static function loadStageCache():Void
{ {
// Clear any stages that are cached if there were any. // Clear any stages that are cached if there were any.
clearStageCache(); clearStageCache();
trace("[STAGEDATA] Loading stage cache..."); trace("Loading stage cache...");
// //
// SCRIPTED STAGES // SCRIPTED STAGES
// //
var scriptedStageClassNames:Array<String> = ScriptedStage.listScriptClasses(); var scriptedStageClassNames:Array<String> = ScriptedStage.listScriptClasses();
trace(' Instantiating ${scriptedStageClassNames.length} scripted stages...'); trace(' Instantiating ${scriptedStageClassNames.length} scripted stages...');
for (stageCls in scriptedStageClassNames) for (stageCls in scriptedStageClassNames)
{ {
var stage:Stage = ScriptedStage.init(stageCls, DEFAULT_STAGE_ID); var stage:Stage = ScriptedStage.init(stageCls, DEFAULT_STAGE_ID);
if (stage != null) if (stage != null)
{ {
trace(' Loaded scripted stage: ${stage.stageName}'); trace(' Loaded scripted stage: ${stage.stageName}');
// Disable the rendering logic for stage until it's loaded. // Disable the rendering logic for stage until it's loaded.
// Note that kill() =/= destroy() // Note that kill() =/= destroy()
stage.kill(); stage.kill();
// Then store it. // Then store it.
stageCache.set(stage.stageId, stage); stageCache.set(stage.stageId, stage);
} }
else else
{ {
trace(' Failed to instantiate scripted stage class: ${stageCls}'); trace(' Failed to instantiate scripted stage class: ${stageCls}');
} }
} }
// //
// UNSCRIPTED STAGES // UNSCRIPTED STAGES
// //
var stageIdList:Array<String> = DataAssets.listDataFilesInPath('stages/'); var stageIdList:Array<String> = DataAssets.listDataFilesInPath('stages/');
var unscriptedStageIds:Array<String> = stageIdList.filter(function(stageId:String):Bool var unscriptedStageIds:Array<String> = stageIdList.filter(function(stageId:String):Bool
{ {
return !stageCache.exists(stageId); return !stageCache.exists(stageId);
}); });
trace(' Instantiating ${unscriptedStageIds.length} non-scripted stages...'); trace(' Instantiating ${unscriptedStageIds.length} non-scripted stages...');
for (stageId in unscriptedStageIds) for (stageId in unscriptedStageIds)
{ {
var stage:Stage; var stage:Stage;
try try
{ {
stage = new Stage(stageId); stage = new Stage(stageId);
if (stage != null) if (stage != null)
{ {
trace(' Loaded stage data: ${stage.stageName}'); trace(' Loaded stage data: ${stage.stageName}');
stageCache.set(stageId, stage); stageCache.set(stageId, stage);
} }
} }
catch (e) catch (e)
{ {
trace(' An error occurred while loading stage data: ${stageId}'); trace(' An error occurred while loading stage data: ${stageId}');
// Assume error was already logged. // Assume error was already logged.
continue; continue;
} }
} }
trace(' Successfully loaded ${Lambda.count(stageCache)} stages.'); trace(' Successfully loaded ${Lambda.count(stageCache)} stages.');
} }
public static function fetchStage(stageId:String):Null<Stage> public static function fetchStage(stageId:String):Null<Stage>
{ {
if (stageCache.exists(stageId)) if (stageCache.exists(stageId))
{ {
trace('[STAGEDATA] Successfully fetch stage: ${stageId}'); trace('Successfully fetch stage: ${stageId}');
var stage:Stage = stageCache.get(stageId); var stage:Stage = stageCache.get(stageId);
stage.revive(); stage.revive();
return stage; return stage;
} }
else else
{ {
trace('[STAGEDATA] Failed to fetch stage, not found in cache: ${stageId}'); trace('Failed to fetch stage, not found in cache: ${stageId}');
return null; return null;
} }
} }
static function clearStageCache():Void static function clearStageCache():Void
{ {
if (stageCache != null) if (stageCache != null)
{ {
for (stage in stageCache) for (stage in stageCache)
{ {
stage.destroy(); stage.destroy();
} }
stageCache.clear(); stageCache.clear();
} }
} }
/** /**
* Load a stage's JSON file, parse its data, and return it. * Load a stage's JSON file, parse its data, and return it.
* *
* @param stageId The stage to load. * @param stageId The stage to load.
* @return The stage data, or null if validation failed. * @return The stage data, or null if validation failed.
*/ */
public static function parseStageData(stageId:String):Null<StageData> public static function parseStageData(stageId:String):Null<StageData>
{ {
var rawJson:String = loadStageFile(stageId); var rawJson:String = loadStageFile(stageId);
var stageData:StageData = migrateStageData(rawJson, stageId); var stageData:StageData = migrateStageData(rawJson, stageId);
return validateStageData(stageId, stageData); return validateStageData(stageId, stageData);
} }
public static function listStageIds():Array<String> public static function listStageIds():Array<String>
{ {
return stageCache.keys().array(); return stageCache.keys().array();
} }
static function loadStageFile(stagePath:String):String static function loadStageFile(stagePath:String):String
{ {
var stageFilePath:String = Paths.json('stages/${stagePath}'); var stageFilePath:String = Paths.json('stages/${stagePath}');
var rawJson = Assets.getText(stageFilePath).trim(); var rawJson = Assets.getText(stageFilePath).trim();
while (!rawJson.endsWith("}")) while (!rawJson.endsWith("}"))
{ {
rawJson = rawJson.substr(0, rawJson.length - 1); rawJson = rawJson.substr(0, rawJson.length - 1);
} }
return rawJson; return rawJson;
} }
static function migrateStageData(rawJson:String, stageId:String) static function migrateStageData(rawJson:String, stageId:String)
{ {
// If you update the stage data format in a breaking way, // If you update the stage data format in a breaking way,
// handle migration here by checking the `version` value. // handle migration here by checking the `version` value.
try try
{ {
var stageData:StageData = cast Json.parse(rawJson); var stageData:StageData = cast Json.parse(rawJson);
return stageData; return stageData;
} }
catch (e) catch (e)
{ {
trace(' Error parsing data for stage: ${stageId}'); trace(' Error parsing data for stage: ${stageId}');
trace(' ${e}'); trace(' ${e}');
return null; return null;
} }
} }
static final DEFAULT_ANIMTYPE:String = "sparrow"; static final DEFAULT_ANIMTYPE:String = "sparrow";
static final DEFAULT_CAMERAZOOM:Float = 1.0; static final DEFAULT_CAMERAZOOM:Float = 1.0;
static final DEFAULT_DANCEEVERY:Int = 0; static final DEFAULT_DANCEEVERY:Int = 0;
static final DEFAULT_ISPIXEL:Bool = false; static final DEFAULT_ISPIXEL:Bool = false;
static final DEFAULT_NAME:String = "Untitled Stage"; static final DEFAULT_NAME:String = "Untitled Stage";
static final DEFAULT_OFFSETS:Array<Float> = [0, 0]; static final DEFAULT_OFFSETS:Array<Float> = [0, 0];
static final DEFAULT_CAMERA_OFFSETS_BF:Array<Float> = [-100, -100]; static final DEFAULT_CAMERA_OFFSETS_BF:Array<Float> = [-100, -100];
static final DEFAULT_CAMERA_OFFSETS_DAD:Array<Float> = [150, -100]; static final DEFAULT_CAMERA_OFFSETS_DAD:Array<Float> = [150, -100];
static final DEFAULT_POSITION:Array<Float> = [0, 0]; static final DEFAULT_POSITION:Array<Float> = [0, 0];
static final DEFAULT_SCALE:Float = 1.0; static final DEFAULT_SCALE:Float = 1.0;
static final DEFAULT_ALPHA:Float = 1.0; static final DEFAULT_ALPHA:Float = 1.0;
static final DEFAULT_SCROLL:Array<Float> = [0, 0]; static final DEFAULT_SCROLL:Array<Float> = [0, 0];
static final DEFAULT_ZINDEX:Int = 0; static final DEFAULT_ZINDEX:Int = 0;
static final DEFAULT_CHARACTER_DATA:StageDataCharacter = { static final DEFAULT_CHARACTER_DATA:StageDataCharacter = {
zIndex: DEFAULT_ZINDEX, zIndex: DEFAULT_ZINDEX,
position: DEFAULT_POSITION, position: DEFAULT_POSITION,
cameraOffsets: DEFAULT_OFFSETS, cameraOffsets: DEFAULT_OFFSETS,
} }
/** /**
* Set unspecified parameters to their defaults. * Set unspecified parameters to their defaults.
* If the parameter is mandatory, print an error message. * If the parameter is mandatory, print an error message.
* @param id * @param id
* @param input * @param input
* @return The validated stage data * @return The validated stage data
*/ */
static function validateStageData(id:String, input:StageData):Null<StageData> static function validateStageData(id:String, input:StageData):Null<StageData>
{ {
if (input == null) if (input == null)
{ {
trace('[STAGEDATA] ERROR: Could not parse stage data for "${id}".'); trace('ERROR: Could not parse stage data for "${id}".');
return null; return null;
} }
if (input.version == null) if (input.version == null)
{ {
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing version'); trace('ERROR: Could not load stage data for "$id": missing version');
return null; return null;
} }
if (!VersionUtil.validateVersion(input.version, STAGE_DATA_VERSION_RULE)) if (!VersionUtil.validateVersion(input.version, STAGE_DATA_VERSION_RULE))
{ {
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})'); trace('ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})');
return null; return null;
} }
if (input.name == null) if (input.name == null)
{ {
trace('[STAGEDATA] WARN: Stage data for "$id" missing name'); trace('WARN: Stage data for "$id" missing name');
input.name = DEFAULT_NAME; input.name = DEFAULT_NAME;
} }
if (input.cameraZoom == null) if (input.cameraZoom == null)
{ {
input.cameraZoom = DEFAULT_CAMERAZOOM; input.cameraZoom = DEFAULT_CAMERAZOOM;
} }
if (input.props == null) if (input.props == null)
{ {
input.props = []; input.props = [];
} }
for (inputProp in input.props) for (inputProp in input.props)
{ {
// It's fine for inputProp.name to be null // It's fine for inputProp.name to be null
if (inputProp.assetPath == null) if (inputProp.assetPath == null)
{ {
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing assetPath for prop "${inputProp.name}"'); trace('ERROR: Could not load stage data for "$id": missing assetPath for prop "${inputProp.name}"');
return null; return null;
} }
if (inputProp.position == null) if (inputProp.position == null)
{ {
inputProp.position = DEFAULT_POSITION; inputProp.position = DEFAULT_POSITION;
} }
if (inputProp.zIndex == null) if (inputProp.zIndex == null)
{ {
inputProp.zIndex = DEFAULT_ZINDEX; inputProp.zIndex = DEFAULT_ZINDEX;
} }
if (inputProp.isPixel == null) if (inputProp.isPixel == null)
{ {
inputProp.isPixel = DEFAULT_ISPIXEL; inputProp.isPixel = DEFAULT_ISPIXEL;
} }
if (inputProp.danceEvery == null) if (inputProp.danceEvery == null)
{ {
inputProp.danceEvery = DEFAULT_DANCEEVERY; inputProp.danceEvery = DEFAULT_DANCEEVERY;
} }
if (inputProp.scale == null) if (inputProp.scale == null)
{ {
inputProp.scale = DEFAULT_SCALE; inputProp.scale = DEFAULT_SCALE;
} }
if (inputProp.animType == null) if (inputProp.animType == null)
{ {
inputProp.animType = DEFAULT_ANIMTYPE; inputProp.animType = DEFAULT_ANIMTYPE;
} }
if (Std.isOfType(inputProp.scale, Float)) if (Std.isOfType(inputProp.scale, Float))
{ {
inputProp.scale = [inputProp.scale, inputProp.scale]; inputProp.scale = [inputProp.scale, inputProp.scale];
} }
if (inputProp.scroll == null) if (inputProp.scroll == null)
{ {
inputProp.scroll = DEFAULT_SCROLL; inputProp.scroll = DEFAULT_SCROLL;
} }
if (inputProp.alpha == null) if (inputProp.alpha == null)
{ {
inputProp.alpha = DEFAULT_ALPHA; inputProp.alpha = DEFAULT_ALPHA;
} }
if (Std.isOfType(inputProp.scroll, Float)) if (Std.isOfType(inputProp.scroll, Float))
{ {
inputProp.scroll = [inputProp.scroll, inputProp.scroll]; inputProp.scroll = [inputProp.scroll, inputProp.scroll];
} }
if (inputProp.animations == null) if (inputProp.animations == null)
{ {
inputProp.animations = []; inputProp.animations = [];
} }
if (inputProp.animations.length == 0 && inputProp.startingAnimation != null) if (inputProp.animations.length == 0 && inputProp.startingAnimation != null)
{ {
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing animations for prop "${inputProp.name}"'); trace('ERROR: Could not load stage data for "$id": missing animations for prop "${inputProp.name}"');
return null; return null;
} }
for (inputAnimation in inputProp.animations) for (inputAnimation in inputProp.animations)
{ {
if (inputAnimation.name == null) if (inputAnimation.name == null)
{ {
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing animation name for prop "${inputProp.name}"'); trace('ERROR: Could not load stage data for "$id": missing animation name for prop "${inputProp.name}"');
return null; return null;
} }
if (inputAnimation.frameRate == null) if (inputAnimation.frameRate == null)
{ {
inputAnimation.frameRate = 24; inputAnimation.frameRate = 24;
} }
if (inputAnimation.offsets == null) if (inputAnimation.offsets == null)
{ {
inputAnimation.offsets = DEFAULT_OFFSETS; inputAnimation.offsets = DEFAULT_OFFSETS;
} }
if (inputAnimation.looped == null) if (inputAnimation.looped == null)
{ {
inputAnimation.looped = true; inputAnimation.looped = true;
} }
if (inputAnimation.flipX == null) if (inputAnimation.flipX == null)
{ {
inputAnimation.flipX = false; inputAnimation.flipX = false;
} }
if (inputAnimation.flipY == null) if (inputAnimation.flipY == null)
{ {
inputAnimation.flipY = false; inputAnimation.flipY = false;
} }
} }
} }
if (input.characters == null) if (input.characters == null)
{ {
trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing characters'); trace('ERROR: Could not load stage data for "$id": missing characters');
return null; return null;
} }
if (input.characters.bf == null) if (input.characters.bf == null)
{ {
input.characters.bf = DEFAULT_CHARACTER_DATA; input.characters.bf = DEFAULT_CHARACTER_DATA;
input.characters.bf.cameraOffsets = DEFAULT_CAMERA_OFFSETS_BF; input.characters.bf.cameraOffsets = DEFAULT_CAMERA_OFFSETS_BF;
} }
if (input.characters.dad == null) if (input.characters.dad == null)
{ {
input.characters.dad = DEFAULT_CHARACTER_DATA; input.characters.dad = DEFAULT_CHARACTER_DATA;
input.characters.dad.cameraOffsets = DEFAULT_CAMERA_OFFSETS_DAD; input.characters.dad.cameraOffsets = DEFAULT_CAMERA_OFFSETS_DAD;
} }
if (input.characters.gf == null) if (input.characters.gf == null)
{ {
input.characters.gf = DEFAULT_CHARACTER_DATA; input.characters.gf = DEFAULT_CHARACTER_DATA;
} }
for (inputCharacter in [input.characters.bf, input.characters.dad, input.characters.gf]) for (inputCharacter in [input.characters.bf, input.characters.dad, input.characters.gf])
{ {
if (inputCharacter.zIndex == null) if (inputCharacter.zIndex == null)
{ {
inputCharacter.zIndex = 0; inputCharacter.zIndex = 0;
} }
if (inputCharacter.position == null || inputCharacter.position.length != 2) if (inputCharacter.position == null || inputCharacter.position.length != 2)
{ {
inputCharacter.position = [0, 0]; inputCharacter.position = [0, 0];
} }
if (inputCharacter.cameraOffsets == null || inputCharacter.cameraOffsets.length != 2) if (inputCharacter.cameraOffsets == null || inputCharacter.cameraOffsets.length != 2)
{ {
if (inputCharacter == input.characters.bf) if (inputCharacter == input.characters.bf)
inputCharacter.cameraOffsets = DEFAULT_CAMERA_OFFSETS_BF; inputCharacter.cameraOffsets = DEFAULT_CAMERA_OFFSETS_BF;
else if (inputCharacter == input.characters.dad) else if (inputCharacter == input.characters.dad)
inputCharacter.cameraOffsets = DEFAULT_CAMERA_OFFSETS_DAD; inputCharacter.cameraOffsets = DEFAULT_CAMERA_OFFSETS_DAD;
else else
{ {
inputCharacter.cameraOffsets = [0, 0]; inputCharacter.cameraOffsets = [0, 0];
} }
} }
} }
// All good! // All good!
return input; return input;
} }
} }
typedef StageData = typedef StageData =
{ {
/** /**
* The sematic version number of the stage data JSON format. * The sematic version number of the stage data JSON format.
* Supports fancy comparisons like NPM does it's neat. * Supports fancy comparisons like NPM does it's neat.
*/ */
var version:String; var version:String;
var name:String; var name:String;
var cameraZoom:Null<Float>; var cameraZoom:Null<Float>;
var props:Array<StageDataProp>; var props:Array<StageDataProp>;
var characters: var characters:
{ {
bf:StageDataCharacter, bf:StageDataCharacter,
dad:StageDataCharacter, dad:StageDataCharacter,
gf:StageDataCharacter, gf:StageDataCharacter,
}; };
}; };
typedef StageDataProp = typedef StageDataProp =
{ {
/** /**
* The name of the prop for later lookup by scripts. * The name of the prop for later lookup by scripts.
* Optional; if unspecified, the prop can't be referenced by scripts. * Optional; if unspecified, the prop can't be referenced by scripts.
*/ */
var name:String; var name:String;
/** /**
* The asset used to display the prop. * The asset used to display the prop.
*/ */
var assetPath:String; var assetPath:String;
/** /**
* The position of the prop as an [x, y] array of two floats. * The position of the prop as an [x, y] array of two floats.
*/ */
var position:Array<Float>; var position:Array<Float>;
/** /**
* A number determining the stack order of the prop, relative to other props and the characters in the stage. * A number determining the stack order of the prop, relative to other props and the characters in the stage.
* Props with lower numbers render below those with higher numbers. * Props with lower numbers render below those with higher numbers.
* This is just like CSS, it isn't hard. * This is just like CSS, it isn't hard.
* @default 0 * @default 0
*/ */
var zIndex:Null<Int>; var zIndex:Null<Int>;
/** /**
* If set to true, anti-aliasing will be forcibly disabled on the sprite. * If set to true, anti-aliasing will be forcibly disabled on the sprite.
* This prevents blurry images on pixel-art levels. * This prevents blurry images on pixel-art levels.
* @default false * @default false
*/ */
var isPixel:Null<Bool>; var isPixel:Null<Bool>;
/** /**
* Either the scale of the prop as a float, or the [w, h] scale as an array of two floats. * Either the scale of the prop as a float, or the [w, h] scale as an array of two floats.
* Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory. * Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory.
* @default 1 * @default 1
*/ */
var scale:OneOfTwo<Float, Array<Float>>; var scale:OneOfTwo<Float, Array<Float>>;
/** /**
* The alpha of the prop, as a float. * The alpha of the prop, as a float.
* @default 1.0 * @default 1.0
*/ */
var alpha:Null<Float>; var alpha:Null<Float>;
/** /**
* If not zero, this prop will play an animation every X beats of the song. * If not zero, this prop will play an animation every X beats of the song.
* This requires animations to be defined. If `danceLeft` and `danceRight` are defined, * This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
* they will alternated between, otherwise the `idle` animation will be used. * they will alternated between, otherwise the `idle` animation will be used.
* *
* @default 0 * @default 0
*/ */
var danceEvery:Null<Int>; var danceEvery:Null<Int>;
/** /**
* How much the prop scrolls relative to the camera. Used to create a parallax effect. * How much the prop scrolls relative to the camera. Used to create a parallax effect.
* Represented as a float or as an [x, y] array of two floats. * Represented as a float or as an [x, y] array of two floats.
* [1, 1] means the prop moves 1:1 with the camera. * [1, 1] means the prop moves 1:1 with the camera.
* [0.5, 0.5] means the prop half as much as the camera. * [0.5, 0.5] means the prop half as much as the camera.
* [0, 0] means the prop is not moved. * [0, 0] means the prop is not moved.
* @default [0, 0] * @default [0, 0]
*/ */
var scroll:OneOfTwo<Float, Array<Float>>; var scroll:OneOfTwo<Float, Array<Float>>;
/** /**
* An optional array of animations which the prop can play. * An optional array of animations which the prop can play.
* @default Prop has no animations. * @default Prop has no animations.
*/ */
var animations:Array<AnimationData>; var animations:Array<AnimationData>;
/** /**
* If animations are used, this is the name of the animation to play first. * If animations are used, this is the name of the animation to play first.
* @default Don't play an animation. * @default Don't play an animation.
*/ */
var startingAnimation:String; var startingAnimation:String;
/** /**
* The animation type to use. * The animation type to use.
* Options: "sparrow", "packer" * Options: "sparrow", "packer"
* @default "sparrow" * @default "sparrow"
*/ */
var animType:String; var animType:String;
}; };
typedef StageDataCharacter = typedef StageDataCharacter =
{ {
/** /**
* A number determining the stack order of the character, relative to props and other characters in the stage. * A number determining the stack order of the character, relative to props and other characters in the stage.
* Again, just like CSS. * Again, just like CSS.
* @default 0 * @default 0
*/ */
zIndex:Null<Int>, zIndex:Null<Int>,
/** /**
* The position to render the character at. * The position to render the character at.
*/ */
position:Array<Float>, position:Array<Float>,
/** /**
* The camera offsets to apply when focusing on the character on this stage. * The camera offsets to apply when focusing on the character on this stage.
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF * @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
*/ */
cameraOffsets:Array<Float>, cameraOffsets:Array<Float>,
}; };

View file

@ -55,11 +55,12 @@ class AddNotesCommand implements ChartEditorCommand
if (appendToSelection) if (appendToSelection)
{ {
state.currentSelection = state.currentSelection.concat(notes); state.currentNoteSelection = state.currentNoteSelection.concat(notes);
} }
else else
{ {
state.currentSelection = notes; state.currentNoteSelection = notes;
state.currentEventSelection = [];
} }
state.playSound(Paths.sound('funnyNoise/funnyNoise-08')); state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
@ -74,7 +75,8 @@ class AddNotesCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentSelection = []; state.currentNoteSelection = [];
state.currentEventSelection = [];
state.playSound(Paths.sound('funnyNoise/funnyNoise-01')); state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
state.saveDataDirty = true; state.saveDataDirty = true;
@ -108,7 +110,8 @@ class RemoveNotesCommand implements ChartEditorCommand
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentSelection = []; state.currentNoteSelection = [];
state.currentEventSelection = [];
state.playSound(Paths.sound('funnyNoise/funnyNoise-01')); state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
state.saveDataDirty = true; state.saveDataDirty = true;
@ -124,7 +127,8 @@ class RemoveNotesCommand implements ChartEditorCommand
{ {
state.currentSongChartNoteData.push(note); state.currentSongChartNoteData.push(note);
} }
state.currentSelection = notes; state.currentNoteSelection = notes;
state.currentEventSelection = [];
state.playSound(Paths.sound('funnyNoise/funnyNoise-08')); state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
state.saveDataDirty = true; state.saveDataDirty = true;
@ -146,6 +150,241 @@ class RemoveNotesCommand implements ChartEditorCommand
} }
} }
/**
* Appends one or more items to the selection.
*/
class SelectItemsCommand implements ChartEditorCommand
{
private var notes:Array<SongNoteData>;
private var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
for (note in this.notes)
{
state.currentNoteSelection.push(note);
}
for (event in this.events)
{
state.currentEventSelection.push(event);
}
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes);
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events);
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
var len:Int = notes.length + events.length;
if (notes.length == 0)
{
if (events.length == 1)
{
return 'Select Event';
}
else
{
return 'Select ${events.length} Events';
}
}
else if (events.length == 0)
{
if (notes.length == 1)
{
return 'Select Note';
}
else
{
return 'Select ${notes.length} Notes';
}
}
return 'Select ${len} Items';
}
}
class AddEventsCommand implements ChartEditorCommand
{
private var events:Array<SongEventData>;
private var appendToSelection:Bool;
public function new(events:Array<SongEventData>, ?appendToSelection:Bool = false)
{
this.events = events;
this.appendToSelection = appendToSelection;
}
public function execute(state:ChartEditorState):Void
{
for (event in events)
{
state.currentSongChartEventData.push(event);
}
if (appendToSelection)
{
state.currentEventSelection = state.currentEventSelection.concat(events);
}
else
{
state.currentNoteSelection = [];
state.currentEventSelection = events;
}
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var len:Int = events.length;
return 'Add $len Events';
}
}
class RemoveEventsCommand implements ChartEditorCommand
{
private var events:Array<SongEventData>;
public function new(events:Array<SongEventData>)
{
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentEventSelection = [];
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
for (event in events)
{
state.currentSongChartEventData.push(event);
}
state.currentEventSelection = events;
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
if (events.length == 1 && events[0] != null)
{
return 'Remove Event';
}
return 'Remove ${events.length} Events';
}
}
class RemoveItemsCommand implements ChartEditorCommand
{
private var notes:Array<SongNoteData>;
private var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{
this.notes = notes;
this.events = events;
}
public function execute(state:ChartEditorState):Void
{
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
for (note in notes)
{
state.currentSongChartNoteData.push(note);
}
for (event in events)
{
state.currentSongChartEventData.push(event);
}
state.currentNoteSelection = notes;
state.currentEventSelection = events;
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
return 'Remove ${notes.length + events.length} Items';
}
}
class SwitchDifficultyCommand implements ChartEditorCommand class SwitchDifficultyCommand implements ChartEditorCommand
{ {
private var prevDifficulty:String; private var prevDifficulty:String;
@ -185,61 +424,21 @@ class SwitchDifficultyCommand implements ChartEditorCommand
} }
} }
/** class DeselectItemsCommand implements ChartEditorCommand
* Adds one or more notes to the selection.
*/
class SelectNotesCommand implements ChartEditorCommand
{ {
private var notes:Array<SongNoteData>; private var notes:Array<SongNoteData>;
private var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>) public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{ {
this.notes = notes; this.notes = notes;
this.events = events;
} }
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
for (note in this.notes) state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes);
{ state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events);
state.currentSelection.push(note);
}
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function undo(state:ChartEditorState):Void
{
state.currentSelection = SongDataUtils.subtractNotes(state.currentSelection, this.notes);
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}
public function toString():String
{
if (notes.length == 1)
{
var dir:String = notes[0].getDirectionName();
return 'Select $dir Note';
}
return 'Select ${notes.length} Notes';
}
}
class DeselectNotesCommand implements ChartEditorCommand
{
private var notes:Array<SongNoteData>;
public function new(notes:Array<SongNoteData>)
{
this.notes = notes;
}
public function execute(state:ChartEditorState):Void
{
state.currentSelection = SongDataUtils.subtractNotes(state.currentSelection, this.notes);
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
@ -249,7 +448,12 @@ class DeselectNotesCommand implements ChartEditorCommand
{ {
for (note in this.notes) for (note in this.notes)
{ {
state.currentSelection.push(note); state.currentNoteSelection.push(note);
}
for (event in this.events)
{
state.currentEventSelection.push(event);
} }
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -258,13 +462,15 @@ class DeselectNotesCommand implements ChartEditorCommand
public function toString():String public function toString():String
{ {
if (notes.length == 1) var noteCount = notes.length + events.length;
if (noteCount == 1)
{ {
var dir:String = notes[0].getDirectionName(); var dir:String = notes[0].getDirectionName();
return 'Deselect $dir Note'; return 'Deselect $dir Items';
} }
return 'Deselect ${notes.length} Notes'; return 'Deselect ${noteCount} Items';
} }
} }
@ -272,20 +478,26 @@ class DeselectNotesCommand implements ChartEditorCommand
* Sets the selection rather than appends it. * Sets the selection rather than appends it.
* Deselects any notes that are not in the new selection. * Deselects any notes that are not in the new selection.
*/ */
class SetNoteSelectionCommand implements ChartEditorCommand class SetItemSelectionCommand implements ChartEditorCommand
{ {
private var notes:Array<SongNoteData>; private var notes:Array<SongNoteData>;
private var previousSelection:Array<SongNoteData>; private var events:Array<SongEventData>;
private var previousNoteSelection:Array<SongNoteData>;
private var previousEventSelection:Array<SongEventData>;
public function new(notes:Array<SongNoteData>, ?previousSelection:Array<SongNoteData>) public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, previousNoteSelection:Array<SongNoteData>,
previousEventSelection:Array<SongEventData>)
{ {
this.notes = notes; this.notes = notes;
this.previousSelection = previousSelection == null ? [] : previousSelection; this.events = events;
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
} }
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
state.currentSelection = notes; state.currentNoteSelection = notes;
state.currentEventSelection = events;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
@ -293,7 +505,8 @@ class SetNoteSelectionCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
state.currentSelection = previousSelection; state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
@ -301,29 +514,34 @@ class SetNoteSelectionCommand implements ChartEditorCommand
public function toString():String public function toString():String
{ {
return 'Select ${notes.length} Notes'; return 'Select ${notes.length} Items';
} }
} }
class SelectAllNotesCommand implements ChartEditorCommand class SelectAllItemsCommand implements ChartEditorCommand
{ {
private var previousSelection:Array<SongNoteData>; private var previousNoteSelection:Array<SongNoteData>;
private var previousEventSelection:Array<SongEventData>;
public function new(?previousSelection:Array<SongNoteData>) public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
{ {
this.previousSelection = previousSelection == null ? [] : previousSelection; this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
} }
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
state.currentSelection = state.currentSongChartNoteData; state.currentNoteSelection = state.currentSongChartNoteData;
state.currentEventSelection = state.currentSongChartEventData;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
} }
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
state.currentSelection = previousSelection; state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
@ -331,29 +549,33 @@ class SelectAllNotesCommand implements ChartEditorCommand
public function toString():String public function toString():String
{ {
return 'Select All Notes'; return 'Select All Items';
} }
} }
class InvertSelectedNotesCommand implements ChartEditorCommand class InvertSelectedItemsCommand implements ChartEditorCommand
{ {
private var previousSelection:Array<SongNoteData>; private var previousNoteSelection:Array<SongNoteData>;
private var previousEventSelection:Array<SongEventData>;
public function new(?previousSelection:Array<SongNoteData>) public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
{ {
this.previousSelection = previousSelection == null ? [] : previousSelection; this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
} }
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
state.currentSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousSelection); state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousNoteSelection);
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection);
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
} }
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
state.currentSelection = previousSelection; state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
@ -361,22 +583,25 @@ class InvertSelectedNotesCommand implements ChartEditorCommand
public function toString():String public function toString():String
{ {
return 'Invert Selected Notes'; return 'Invert Selected Items';
} }
} }
class DeselectAllNotesCommand implements ChartEditorCommand class DeselectAllItemsCommand implements ChartEditorCommand
{ {
private var previousSelection:Array<SongNoteData>; private var previousNoteSelection:Array<SongNoteData>;
private var previousEventSelection:Array<SongEventData>;
public function new(?previousSelection:Array<SongNoteData>) public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
{ {
this.previousSelection = previousSelection == null ? [] : previousSelection; this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
} }
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
state.currentSelection = []; state.currentNoteSelection = [];
state.currentEventSelection = [];
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
@ -384,7 +609,8 @@ class DeselectAllNotesCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
state.currentSelection = previousSelection; state.currentNoteSelection = previousNoteSelection;
state.currentEventSelection = previousEventSelection;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
@ -392,27 +618,35 @@ class DeselectAllNotesCommand implements ChartEditorCommand
public function toString():String public function toString():String
{ {
return 'Deselect All Notes'; return 'Deselect All Items';
} }
} }
class CutNotesCommand implements ChartEditorCommand class CutItemsCommand implements ChartEditorCommand
{ {
private var notes:Array<SongNoteData>; private var notes:Array<SongNoteData>;
private var events:Array<SongEventData>;
public function new(notes:Array<SongNoteData>) public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
{ {
this.notes = notes; this.notes = notes;
this.events = events;
} }
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
// Copy the notes. // Copy the notes.
SongDataUtils.writeNotesToClipboard(SongDataUtils.buildClipboard(notes)); SongDataUtils.writeItemsToClipboard({
notes: SongDataUtils.buildNoteClipboard(notes),
events: SongDataUtils.buildEventClipboard(events)
});
// Delete the notes. // Delete the notes.
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentSelection = []; state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
@ -422,19 +656,27 @@ class CutNotesCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes); state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes);
state.currentSelection = notes; state.currentSongChartEventData = state.currentSongChartEventData.concat(events);
state.currentNoteSelection = notes;
state.currentEventSelection = events;
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
state.notePreviewDirty = true; state.notePreviewDirty = true;
state.sortChartData(); state.sortChartData();
} }
public function toString():String public function toString():String
{ {
var len:Int = notes.length; var len:Int = notes.length + events.length;
return 'Cut $len Notes to Clipboard';
if (notes.length == 0)
return 'Cut $len Events to Clipboard';
else if (events.length == 0)
return 'Cut $len Notes to Clipboard';
else
return 'Cut $len Items to Clipboard';
} }
} }
@ -457,7 +699,8 @@ class FlipNotesCommand implements ChartEditorCommand
flippedNotes = SongDataUtils.flipNotes(notes); flippedNotes = SongDataUtils.flipNotes(notes);
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(flippedNotes); state.currentSongChartNoteData = state.currentSongChartNoteData.concat(flippedNotes);
state.currentSelection = flippedNotes; state.currentNoteSelection = flippedNotes;
state.currentEventSelection = [];
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -470,7 +713,8 @@ class FlipNotesCommand implements ChartEditorCommand
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, flippedNotes); state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, flippedNotes);
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes); state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes);
state.currentSelection = notes; state.currentNoteSelection = notes;
state.currentEventSelection = [];
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -486,11 +730,12 @@ class FlipNotesCommand implements ChartEditorCommand
} }
} }
class PasteNotesCommand implements ChartEditorCommand class PasteItemsCommand implements ChartEditorCommand
{ {
private var targetTimestamp:Float; private var targetTimestamp:Float;
// Notes we added with this command, for undo. // Notes we added with this command, for undo.
private var addedNotes:Array<SongNoteData>; private var addedNotes:Array<SongNoteData>;
private var addedEvents:Array<SongEventData>;
public function new(targetTimestamp:Float) public function new(targetTimestamp:Float)
{ {
@ -499,12 +744,15 @@ class PasteNotesCommand implements ChartEditorCommand
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
var currentClipboard:Array<SongNoteData> = SongDataUtils.readNotesFromClipboard(); var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard, Std.int(targetTimestamp)); addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp));
addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp));
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes); state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes);
state.currentSelection = addedNotes.copy(); state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents);
state.currentNoteSelection = addedNotes.copy();
state.currentEventSelection = addedEvents.copy();
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -516,7 +764,9 @@ class PasteNotesCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes); state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes);
state.currentSelection = []; state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -527,52 +777,16 @@ class PasteNotesCommand implements ChartEditorCommand
public function toString():String public function toString():String
{ {
var currentClipboard:Array<SongNoteData> = SongDataUtils.readNotesFromClipboard(); var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
return 'Paste ${currentClipboard.length} Notes from Clipboard';
}
}
class AddEventsCommand implements ChartEditorCommand var len:Int = currentClipboard.notes.length + currentClipboard.events.length;
{
private var events:Array<SongEventData>;
private var appendToSelection:Bool;
public function new(events:Array<SongEventData>, ?appendToSelection:Bool = false) if (currentClipboard.notes.length == 0)
{ return 'Paste $len Events';
this.events = events; else if (currentClipboard.events.length == 0)
this.appendToSelection = appendToSelection; return 'Paste $len Notes';
} else
return 'Paste $len Items';
public function execute(state:ChartEditorState):Void
{
state.currentSongChartEventData = state.currentSongChartEventData.concat(events);
// TODO: Allow selecting events.
// state.currentSelection = events;
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentSelection = [];
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function toString():String
{
var len:Int = events.length;
return 'Add $len Events';
} }
} }

View file

@ -0,0 +1,101 @@
package funkin.ui.debug.charting;
import openfl.display.BitmapData;
import openfl.utils.Assets;
import flixel.FlxObject;
import flixel.FlxBasic;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.graphics.frames.FlxTileFrames;
import flixel.math.FlxPoint;
import funkin.play.song.SongData.SongEventData;
/**
* A event sprite that can be used to display a song event in a chart.
* Designed to be used and reused efficiently. Has no gameplay functionality.
*/
class ChartEditorEventSprite extends FlxSprite
{
public var parentState:ChartEditorState;
/**
* The note data that this sprite represents.
* You can set this to null to kill the sprite and flag it for recycling.
*/
public var eventData(default, set):SongEventData;
/**
* The image used for all song events. Cached for performance.
*/
var eventGraphic:BitmapData;
public function new(parent:ChartEditorState)
{
super();
this.parentState = parent;
buildGraphic();
}
function buildGraphic():Void
{
if (eventGraphic == null)
{
eventGraphic = Assets.getBitmapData(Paths.image('ui/chart-editor/event'));
}
loadGraphic(eventGraphic);
setGraphicSize(ChartEditorState.GRID_SIZE);
this.updateHitbox();
}
function set_eventData(value:SongEventData):SongEventData
{
this.eventData = value;
if (this.eventData == null)
{
// Disown parent.
this.kill();
return this.eventData;
}
this.visible = true;
// Update the position to match the note data.
updateEventPosition();
return this.eventData;
}
public function updateEventPosition(?origin:FlxObject)
{
this.x = (ChartEditorState.STRUMLINE_SIZE * 2 + 1 - 1) * ChartEditorState.GRID_SIZE;
if (this.eventData.stepTime >= 0)
this.y = this.eventData.stepTime * ChartEditorState.GRID_SIZE;
if (origin != null)
{
this.x += origin.x;
this.y += origin.y;
}
}
/**
* Return whether this note (or its parent) is currently visible.
*/
public function isEventVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool
{
var outsideViewArea = (this.y + this.height < viewAreaTop || this.y > viewAreaBottom);
if (!outsideViewArea)
{
return true;
}
// TODO: Check if this note's parent or child is visible.
return false;
}
}

View file

@ -169,7 +169,8 @@ class ChartEditorNoteSprite extends FlxSprite
if (this.noteData.stepTime >= 0) if (this.noteData.stepTime >= 0)
this.y = this.noteData.stepTime * ChartEditorState.GRID_SIZE; this.y = this.noteData.stepTime * ChartEditorState.GRID_SIZE;
if (origin != null) { if (origin != null)
{
this.x += origin.x; this.x += origin.x;
this.y += origin.y; this.y += origin.y;
} }

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,25 @@
package funkin.ui.debug.charting; package funkin.ui.debug.charting;
import funkin.play.song.SongData.SongTimeChange; import haxe.ui.data.ArrayDataSource;
import haxe.ui.components.Slider;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.TextField;
import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.BaseCharacter.CharacterType;
import funkin.ui.haxeui.components.CharacterPlayer; import funkin.play.event.SongEvent;
import funkin.play.song.SongData.SongTimeChange;
import funkin.play.song.SongSerializer; import funkin.play.song.SongSerializer;
import funkin.ui.haxeui.components.CharacterPlayer;
import haxe.ui.components.Button; import haxe.ui.components.Button;
import haxe.ui.components.CheckBox;
import haxe.ui.components.DropDown; import haxe.ui.components.DropDown;
import haxe.ui.containers.Group; import haxe.ui.components.Label;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
import haxe.ui.components.TextField;
import haxe.ui.containers.dialogs.Dialog; import haxe.ui.containers.dialogs.Dialog;
import haxe.ui.containers.Box;
import haxe.ui.containers.Frame;
import haxe.ui.containers.Grid;
import haxe.ui.containers.Group;
import haxe.ui.core.Component;
import haxe.ui.events.UIEvent; import haxe.ui.events.UIEvent;
/** /**
@ -19,415 +27,556 @@ import haxe.ui.events.UIEvent;
*/ */
enum ChartEditorToolMode enum ChartEditorToolMode
{ {
Select; Select;
Place; Place;
} }
class ChartEditorToolboxHandler class ChartEditorToolboxHandler
{ {
public static function setToolboxState(state:ChartEditorState, id:String, shown:Bool):Void public static function setToolboxState(state:ChartEditorState, id:String, shown:Bool):Void
{ {
if (shown) if (shown)
showToolbox(state, id); showToolbox(state, id);
else else
hideToolbox(state, id); hideToolbox(state, id);
} }
public static function showToolbox(state:ChartEditorState, id:String) public static function showToolbox(state:ChartEditorState, id:String)
{ {
var toolbox:Dialog = state.activeToolboxes.get(id); var toolbox:Dialog = state.activeToolboxes.get(id);
if (toolbox == null) if (toolbox == null)
toolbox = initToolbox(state, id); toolbox = initToolbox(state, id);
if (toolbox != null) if (toolbox != null)
{ {
toolbox.showDialog(false); toolbox.showDialog(false);
} }
else else
{ {
trace('ChartEditorToolboxHandler.showToolbox() - Could not retrieve toolbox: $id'); trace('ChartEditorToolboxHandler.showToolbox() - Could not retrieve toolbox: $id');
} }
} }
public static function hideToolbox(state:ChartEditorState, id:String):Void public static function hideToolbox(state:ChartEditorState, id:String):Void
{ {
var toolbox:Dialog = state.activeToolboxes.get(id); var toolbox:Dialog = state.activeToolboxes.get(id);
if (toolbox == null) if (toolbox == null)
toolbox = initToolbox(state, id); toolbox = initToolbox(state, id);
if (toolbox != null) if (toolbox != null)
{ {
toolbox.hideDialog(DialogButton.CANCEL); toolbox.hideDialog(DialogButton.CANCEL);
} }
else else
{ {
trace('ChartEditorToolboxHandler.hideToolbox() - Could not retrieve toolbox: $id'); trace('ChartEditorToolboxHandler.hideToolbox() - Could not retrieve toolbox: $id');
} }
} }
public static function minimizeToolbox(state:ChartEditorState, id:String):Void public static function minimizeToolbox(state:ChartEditorState, id:String):Void {}
{
} public static function maximizeToolbox(state:ChartEditorState, id:String):Void {}
public static function maximizeToolbox(state:ChartEditorState, id:String):Void public static function initToolbox(state:ChartEditorState, id:String):Dialog
{ {
} var toolbox:Dialog = null;
switch (id)
public static function initToolbox(state:ChartEditorState, id:String):Dialog {
{ case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
var toolbox:Dialog = null; toolbox = buildToolboxToolsLayout(state);
switch (id) case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
{ toolbox = buildToolboxNoteDataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
toolbox = buildToolboxToolsLayout(state); toolbox = buildToolboxEventDataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
toolbox = buildToolboxNoteDataLayout(state); toolbox = buildToolboxDifficultyLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
toolbox = buildToolboxEventDataLayout(state); toolbox = buildToolboxMetadataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
toolbox = buildToolboxDifficultyLayout(state); toolbox = buildToolboxCharactersLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
toolbox = buildToolboxMetadataLayout(state); toolbox = buildToolboxPlayerPreviewLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
toolbox = buildToolboxCharactersLayout(state); toolbox = buildToolboxOpponentPreviewLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT: default:
toolbox = buildToolboxPlayerPreviewLayout(state); // This happens if you try to load an unknown layout.
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT: trace('ChartEditorToolboxHandler.initToolbox() - Unknown toolbox ID: $id');
toolbox = buildToolboxOpponentPreviewLayout(state); toolbox = null;
default: }
trace('ChartEditorToolboxHandler.initToolbox() - Unknown toolbox ID: $id');
toolbox = null; // This happens if the layout you try to load has a syntax error.
} if (toolbox == null)
return null;
// Make sure we can reuse the toolbox later.
toolbox.destroyOnClose = false; // Make sure we can reuse the toolbox later.
state.activeToolboxes.set(id, toolbox); toolbox.destroyOnClose = false;
state.activeToolboxes.set(id, toolbox);
return toolbox;
} return toolbox;
}
public static function getToolbox(state:ChartEditorState, id:String):Dialog
{ public static function getToolbox(state:ChartEditorState, id:String):Dialog
var toolbox:Dialog = state.activeToolboxes.get(id); {
var toolbox:Dialog = state.activeToolboxes.get(id);
// Initialize the toolbox without showing it.
if (toolbox == null) // Initialize the toolbox without showing it.
toolbox = initToolbox(state, id); if (toolbox == null)
toolbox = initToolbox(state, id);
return toolbox;
} return toolbox;
}
static function buildToolboxToolsLayout(state:ChartEditorState):Dialog
{ static function buildToolboxToolsLayout(state:ChartEditorState):Dialog
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT); {
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT);
if (toolbox == null) return null;
if (toolbox == null)
// Starting position. return null;
toolbox.x = 50;
toolbox.y = 50; // Starting position.
toolbox.x = 50;
toolbox.onDialogClosed = (event:DialogEvent) -> toolbox.y = 50;
{
state.setUICheckboxSelected('menubarItemToggleToolboxTools', false); toolbox.onDialogClosed = (event:DialogEvent) ->
} {
state.setUICheckboxSelected('menubarItemToggleToolboxTools', false);
var toolsGroup:Group = toolbox.findComponent("toolboxToolsGroup", Group); }
if (toolsGroup == null) return null; var toolsGroup:Group = toolbox.findComponent("toolboxToolsGroup", Group);
toolsGroup.onChange = (event:UIEvent) -> if (toolsGroup == null)
{ return null;
switch (event.target.id)
{ toolsGroup.onChange = (event:UIEvent) ->
case 'toolboxToolsGroupSelect': {
state.currentToolMode = ChartEditorToolMode.Select; switch (event.target.id)
case 'toolboxToolsGroupPlace': {
state.currentToolMode = ChartEditorToolMode.Place; case 'toolboxToolsGroupSelect':
default: state.currentToolMode = ChartEditorToolMode.Select;
trace('ChartEditorToolboxHandler.buildToolboxToolsLayout() - Unknown toolbox tool selected: $event.target.id'); case 'toolboxToolsGroupPlace':
} state.currentToolMode = ChartEditorToolMode.Place;
} default:
trace('ChartEditorToolboxHandler.buildToolboxToolsLayout() - Unknown toolbox tool selected: $event.target.id');
return toolbox; }
} }
static function buildToolboxNoteDataLayout(state:ChartEditorState):Dialog return toolbox;
{ }
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
static function buildToolboxNoteDataLayout(state:ChartEditorState):Dialog
if (toolbox == null) return null; {
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
// Starting position.
toolbox.x = 75; if (toolbox == null)
toolbox.y = 100; return null;
toolbox.onDialogClosed = (event:DialogEvent) -> // Starting position.
{ toolbox.x = 75;
state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false); toolbox.y = 100;
}
toolbox.onDialogClosed = (event:DialogEvent) ->
var toolboxNotesNoteKind:DropDown = toolbox.findComponent("toolboxNotesNoteKind", DropDown); {
state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false);
toolboxNotesNoteKind.onChange = (event:UIEvent) -> }
{
state.selectedNoteKind = event.data.id; var toolboxNotesNoteKind:DropDown = toolbox.findComponent("toolboxNotesNoteKind", DropDown);
} var toolboxNotesCustomKindLabel:Label = toolbox.findComponent("toolboxNotesCustomKindLabel", Label);
var toolboxNotesCustomKind:TextField = toolbox.findComponent("toolboxNotesCustomKind", TextField);
return toolbox;
} toolboxNotesNoteKind.onChange = (event:UIEvent) ->
{
static function buildToolboxEventDataLayout(state:ChartEditorState):Dialog var isCustom = (event.data.id == '~CUSTOM~');
{
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT); if (isCustom)
{
if (toolbox == null) return null; toolboxNotesCustomKindLabel.hidden = false;
toolboxNotesCustomKind.hidden = false;
// Starting position.
toolbox.x = 100; state.selectedNoteKind = toolboxNotesCustomKind.text;
toolbox.y = 150; }
else
toolbox.onDialogClosed = (event:DialogEvent) -> {
{ toolboxNotesCustomKindLabel.hidden = true;
state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false); toolboxNotesCustomKind.hidden = true;
}
state.selectedNoteKind = event.data.id;
return toolbox; }
} }
static function buildToolboxDifficultyLayout(state:ChartEditorState):Dialog toolboxNotesCustomKind.onChange = (event:UIEvent) ->
{ {
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); state.selectedNoteKind = toolboxNotesCustomKind.text;
}
if (toolbox == null) return null;
return toolbox;
// Starting position. }
toolbox.x = 125;
toolbox.y = 200; static function buildToolboxEventDataLayout(state:ChartEditorState):Dialog
{
toolbox.onDialogClosed = (event:DialogEvent) -> var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
{
state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false); if (toolbox == null)
} return null;
var difficultyToolboxSaveMetadata:Button = toolbox.findComponent("difficultyToolboxSaveMetadata", Button); // Starting position.
var difficultyToolboxSaveChart:Button = toolbox.findComponent("difficultyToolboxSaveChart", Button); toolbox.x = 100;
var difficultyToolboxSaveAll:Button = toolbox.findComponent("difficultyToolboxSaveAll", Button); toolbox.y = 150;
var difficultyToolboxLoadMetadata:Button = toolbox.findComponent("difficultyToolboxLoadMetadata", Button);
var difficultyToolboxLoadChart:Button = toolbox.findComponent("difficultyToolboxLoadChart", Button); toolbox.onDialogClosed = (event:DialogEvent) ->
{
difficultyToolboxSaveMetadata.onClick = (event:UIEvent) -> state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false);
{ }
SongSerializer.exportSongMetadata(state.currentSongMetadata);
}; var toolboxEventsEventKind:DropDown = toolbox.findComponent("toolboxEventsEventKind", DropDown);
var toolboxEventsDataGrid:Grid = toolbox.findComponent("toolboxEventsDataGrid", Grid);
difficultyToolboxSaveChart.onClick = (event:UIEvent) ->
{ toolboxEventsEventKind.dataSource = new ArrayDataSource();
SongSerializer.exportSongChartData(state.currentSongChartData);
}; var songEvents:Array<SongEvent> = SongEventParser.listEvents();
difficultyToolboxSaveAll.onClick = (event:UIEvent) -> for (event in songEvents)
{ {
state.exportAllSongData(); toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id});
}; }
difficultyToolboxLoadMetadata.onClick = (event:UIEvent) -> toolboxEventsEventKind.onChange = (event:UIEvent) ->
{ {
// Replace metadata for current variation. var eventType:String = event.data.value;
SongSerializer.importSongMetadataAsync(function(songMetadata)
{ trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType');
state.currentSongMetadata = songMetadata;
}); var schema:SongEventSchema = SongEventParser.getEventSchema(eventType);
};
if (schema == null)
difficultyToolboxLoadChart.onClick = (event:UIEvent) -> {
{ trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Unknown event kind: $eventType');
// Replace chart data for current variation. return;
SongSerializer.importSongChartDataAsync(function(songChartData) }
{
state.currentSongChartData = songChartData; buildEventDataFormFromSchema(state, toolboxEventsDataGrid, schema);
state.noteDisplayDirty = true; }
});
}; return toolbox;
}
state.difficultySelectDirty = true;
static function buildEventDataFormFromSchema(state:ChartEditorState, target:Box, schema:SongEventSchema):Void
return toolbox; {
} trace(schema);
// Clear the frame.
static function buildToolboxMetadataLayout(state:ChartEditorState):Dialog target.removeAllComponents();
{
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); state.selectedEventData = {};
if (toolbox == null) return null; for (field in schema)
{
// Starting position. // Add a label.
toolbox.x = 150; var label:Label = new Label();
toolbox.y = 250; label.text = field.title;
target.addComponent(label);
toolbox.onDialogClosed = (event:DialogEvent) ->
{ var input:Component;
state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false); switch (field.type)
} {
case INTEGER:
var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField); var numberStepper:NumberStepper = new NumberStepper();
inputSongName.onChange = (event:UIEvent) -> numberStepper.id = field.name;
{ numberStepper.step = field.step == null ? 1.0 : field.step;
var valid = event.target.text != null && event.target.text != ""; numberStepper.min = field.min;
numberStepper.max = field.max;
if (valid) numberStepper.value = field.defaultValue;
{ input = numberStepper;
inputSongName.removeClass('invalid-value'); case FLOAT:
state.currentSongMetadata.songName = event.target.text; var numberStepper:NumberStepper = new NumberStepper();
} numberStepper.id = field.name;
else numberStepper.step = field.step == null ? 0.1 : field.step;
{ numberStepper.min = field.min;
state.currentSongMetadata.songName = null; numberStepper.max = field.max;
} numberStepper.value = field.defaultValue;
}; input = numberStepper;
case BOOL:
var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField); var checkBox = new CheckBox();
inputSongArtist.onChange = (event:UIEvent) -> checkBox.id = field.name;
{ checkBox.selected = field.defaultValue == true;
var valid = event.target.text != null && event.target.text != ""; input = checkBox;
case ENUM:
if (valid) var dropDown:DropDown = new DropDown();
{ dropDown.id = field.name;
inputSongArtist.removeClass('invalid-value'); dropDown.dataSource = new ArrayDataSource();
state.currentSongMetadata.artist = event.target.text;
} // Add entries to the dropdown.
else for (optionName in field.keys.keys())
{ {
state.currentSongMetadata.artist = null; var optionValue = field.keys.get(optionName);
} trace('$optionName : $optionValue');
}; dropDown.dataSource.add({value: optionValue, text: optionName});
}
var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown);
inputStage.onChange = (event:UIEvent) -> dropDown.value = field.defaultValue;
{
var valid = event.data != null && event.data.id != null; input = dropDown;
case STRING:
if (valid) { input = new TextField();
state.currentSongMetadata.playData.stage = event.data.id; input.id = field.name;
} input.text = field.defaultValue;
}; default:
// Unknown type. Display a label so we know what it is.
var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown); input = new Label();
inputNoteSkin.onChange = (event:UIEvent) -> input.id = field.name;
{ input.text = field.type;
if (event.data.id == null) }
return;
state.currentSongMetadata.playData.noteSkin = event.data.id; target.addComponent(input);
};
input.onChange = (event:UIEvent) ->
var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper); {
inputBPM.onChange = (event:UIEvent) -> trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${event.target.value}');
{
if (event.value == null || event.value <= 0) if (event.target.value == null)
return; state.selectedEventData.remove(event.target.id);
else
var timeChanges = state.currentSongMetadata.timeChanges; state.selectedEventData.set(event.target.id, event.target.value);
if (timeChanges == null || timeChanges.length == 0) }
{ }
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])]; }
}
else static function buildToolboxDifficultyLayout(state:ChartEditorState):Dialog
{ {
timeChanges[0].bpm = event.value; var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
}
if (toolbox == null)
Conductor.forceBPM(event.value); return null;
state.currentSongMetadata.timeChanges = timeChanges; // Starting position.
}; toolbox.x = 125;
toolbox.y = 200;
var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider);
inputScrollSpeed.onChange = (event:UIEvent) -> toolbox.onDialogClosed = (event:DialogEvent) ->
{ {
var valid = event.target.value != null && event.target.value > 0; state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false);
}
if (valid)
{ var difficultyToolboxSaveMetadata:Button = toolbox.findComponent("difficultyToolboxSaveMetadata", Button);
inputScrollSpeed.removeClass('invalid-value'); var difficultyToolboxSaveChart:Button = toolbox.findComponent("difficultyToolboxSaveChart", Button);
state.currentSongChartData.scrollSpeed = event.target.value; var difficultyToolboxSaveAll:Button = toolbox.findComponent("difficultyToolboxSaveAll", Button);
} var difficultyToolboxLoadMetadata:Button = toolbox.findComponent("difficultyToolboxLoadMetadata", Button);
else var difficultyToolboxLoadChart:Button = toolbox.findComponent("difficultyToolboxLoadChart", Button);
{
state.currentSongChartData.scrollSpeed = null; difficultyToolboxSaveMetadata.onClick = (event:UIEvent) ->
} {
}; SongSerializer.exportSongMetadata(state.currentSongMetadata);
};
return toolbox; difficultyToolboxSaveChart.onClick = (event:UIEvent) ->
} {
SongSerializer.exportSongChartData(state.currentSongChartData);
static function buildToolboxCharactersLayout(state:ChartEditorState):Dialog };
{
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT); difficultyToolboxSaveAll.onClick = (event:UIEvent) ->
{
if (toolbox == null) return null; state.exportAllSongData();
};
// Starting position.
toolbox.x = 175; difficultyToolboxLoadMetadata.onClick = (event:UIEvent) ->
toolbox.y = 300; {
// Replace metadata for current variation.
toolbox.onDialogClosed = (event:DialogEvent) -> SongSerializer.importSongMetadataAsync(function(songMetadata)
{ {
state.setUICheckboxSelected('menubarItemToggleToolboxCharacters', false); state.currentSongMetadata = songMetadata;
} });
};
return toolbox;
} difficultyToolboxLoadChart.onClick = (event:UIEvent) ->
{
static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Dialog // Replace chart data for current variation.
{ SongSerializer.importSongChartDataAsync(function(songChartData)
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); {
state.currentSongChartData = songChartData;
if (toolbox == null) return null; state.noteDisplayDirty = true;
});
// Starting position. };
toolbox.x = 200;
toolbox.y = 350; state.difficultySelectDirty = true;
toolbox.onDialogClosed = (event:DialogEvent) -> return toolbox;
{ }
state.setUICheckboxSelected('menubarItemToggleToolboxPlayerPreview', false);
} static function buildToolboxMetadataLayout(state:ChartEditorState):Dialog
{
var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer'); var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
// TODO: We need to implement character swapping in ChartEditorState.
charPlayer.loadCharacter('bf'); if (toolbox == null)
//charPlayer.setScale(0.5); return null;
charPlayer.setCharacterType(CharacterType.BF);
charPlayer.flip = true; // Starting position.
toolbox.x = 150;
return toolbox; toolbox.y = 250;
}
toolbox.onDialogClosed = (event:DialogEvent) ->
static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Dialog {
{ state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false);
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); }
if (toolbox == null) return null; var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField);
inputSongName.onChange = (event:UIEvent) ->
// Starting position. {
toolbox.x = 200; var valid = event.target.text != null && event.target.text != "";
toolbox.y = 350;
if (valid)
toolbox.onDialogClosed = (event:DialogEvent) -> {
{ inputSongName.removeClass('invalid-value');
state.setUICheckboxSelected('menubarItemToggleToolboxOpponentPreview', false); state.currentSongMetadata.songName = event.target.text;
} }
else
var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer'); {
// TODO: We need to implement character swapping in ChartEditorState. state.currentSongMetadata.songName = null;
charPlayer.loadCharacter('dad'); }
// charPlayer.setScale(0.5); };
charPlayer.setCharacterType(CharacterType.DAD);
charPlayer.flip = false; var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField);
inputSongArtist.onChange = (event:UIEvent) ->
return toolbox; {
} var valid = event.target.text != null && event.target.text != "";
if (valid)
{
inputSongArtist.removeClass('invalid-value');
state.currentSongMetadata.artist = event.target.text;
}
else
{
state.currentSongMetadata.artist = null;
}
};
var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown);
inputStage.onChange = (event:UIEvent) ->
{
var valid = event.data != null && event.data.id != null;
if (valid)
{
state.currentSongMetadata.playData.stage = event.data.id;
}
};
var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown);
inputNoteSkin.onChange = (event:UIEvent) ->
{
if (event.data.id == null)
return;
state.currentSongMetadata.playData.noteSkin = event.data.id;
};
var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper);
inputBPM.onChange = (event:UIEvent) ->
{
if (event.value == null || event.value <= 0)
return;
var timeChanges = state.currentSongMetadata.timeChanges;
if (timeChanges == null || timeChanges.length == 0)
{
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])];
}
else
{
timeChanges[0].bpm = event.value;
}
Conductor.forceBPM(event.value);
state.currentSongMetadata.timeChanges = timeChanges;
};
var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider);
inputScrollSpeed.onChange = (event:UIEvent) ->
{
var valid = event.target.value != null && event.target.value > 0;
if (valid)
{
inputScrollSpeed.removeClass('invalid-value');
state.currentSongChartData.scrollSpeed = event.target.value;
}
else
{
state.currentSongChartData.scrollSpeed = null;
}
};
return toolbox;
}
static function buildToolboxCharactersLayout(state:ChartEditorState):Dialog
{
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT);
if (toolbox == null)
return null;
// Starting position.
toolbox.x = 175;
toolbox.y = 300;
toolbox.onDialogClosed = (event:DialogEvent) ->
{
state.setUICheckboxSelected('menubarItemToggleToolboxCharacters', false);
}
return toolbox;
}
static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Dialog
{
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
if (toolbox == null)
return null;
// Starting position.
toolbox.x = 200;
toolbox.y = 350;
toolbox.onDialogClosed = (event:DialogEvent) ->
{
state.setUICheckboxSelected('menubarItemToggleToolboxPlayerPreview', false);
}
var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer');
// TODO: We need to implement character swapping in ChartEditorState.
charPlayer.loadCharacter('bf');
// charPlayer.setScale(0.5);
charPlayer.setCharacterType(CharacterType.BF);
charPlayer.flip = true;
return toolbox;
}
static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Dialog
{
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
if (toolbox == null)
return null;
// Starting position.
toolbox.x = 200;
toolbox.y = 350;
toolbox.onDialogClosed = (event:DialogEvent) ->
{
state.setUICheckboxSelected('menubarItemToggleToolboxOpponentPreview', false);
}
var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer');
// TODO: We need to implement character swapping in ChartEditorState.
charPlayer.loadCharacter('dad');
// charPlayer.setScale(0.5);
charPlayer.setCharacterType(CharacterType.DAD);
charPlayer.flip = false;
return toolbox;
}
} }

View file

@ -0,0 +1,113 @@
package funkin.ui.haxeui.components;
import flixel.FlxG;
import flixel.util.FlxTimer;
import haxe.ui.RuntimeComponentBuilder;
import haxe.ui.components.Button;
import haxe.ui.components.Label;
import haxe.ui.containers.Box;
import haxe.ui.containers.SideBar;
import haxe.ui.containers.VBox;
import haxe.ui.core.Component;
class Notifbar extends SideBar
{
final NOTIFICATION_DISMISS_TIME = 5.0; // seconds
var dismissTimer:FlxTimer = null;
var outerContainer:Box = null;
var container:VBox = null;
var message:Label = null;
var action:Button = null;
var dismiss:Button = null;
public function new()
{
super();
buildSidebar();
buildChildren();
}
public function showNotification(message:String, ?actionText:String = null, ?actionCallback:Void->Void = null, ?dismissTime:Float = null)
{
if (dismissTimer != null)
dismissNotification();
if (dismissTime == null)
dismissTime = NOTIFICATION_DISMISS_TIME;
// Message text.
this.message.text = message;
// Action
if (actionText != null)
{
this.action.text = actionText;
this.action.visible = true;
this.action.disabled = false;
this.action.onClick = (_) ->
{
actionCallback();
};
}
else
{
this.action.visible = false;
this.action.disabled = false;
this.action.onClick = null;
}
this.show();
// Auto dismiss.
dismissTimer = new FlxTimer().start(dismissTime, (_:FlxTimer) -> dismissNotification());
}
public function dismissNotification()
{
if (dismissTimer != null)
{
dismissTimer.cancel();
dismissTimer = null;
}
this.hide();
}
function buildSidebar():Void
{
this.width = 256;
this.height = 80;
// border-top: 1px solid #000; border-left: 1px solid #000;
this.styleString = "border: 1px solid #000; background-color: #3d3f41; padding: 8px; border-top-left-radius: 8px;";
// float to the right
this.x = FlxG.width - this.width;
this.position = "bottom";
this.method = "float";
}
function buildChildren():Void
{
outerContainer = cast(buildComponent("assets/data/notifbar.xml"), Box);
addComponent(outerContainer);
container = outerContainer.findComponent('notifbarContainer', VBox);
message = outerContainer.findComponent('notifbarMessage', Label);
action = outerContainer.findComponent('notifbarAction', Button);
dismiss = outerContainer.findComponent('notifbarDismiss', Button);
dismiss.onClick = (_) ->
{
dismissNotification();
};
}
function buildComponent(path:String):Component
{
return RuntimeComponentBuilder.fromAsset(path);
}
}

View file

@ -13,177 +13,177 @@ import openfl.net.FileReference;
class StageOffsetSubstate extends MusicBeatSubstate class StageOffsetSubstate extends MusicBeatSubstate
{ {
var uiStuff:Component; var uiStuff:Component;
override function create() override function create()
{ {
super.create(); super.create();
FlxG.mouse.visible = true; FlxG.mouse.visible = true;
PlayState.instance.pauseMusic(); PlayState.instance.pauseMusic();
FlxG.camera.target = null; FlxG.camera.target = null;
var str = Paths.xml('ui/stage-editor-view'); var str = Paths.xml('ui/stage-editor-view');
uiStuff = RuntimeComponentBuilder.fromAsset(str); uiStuff = RuntimeComponentBuilder.fromAsset(str);
uiStuff.findComponent("lol").onClick = saveCharacterCompile; uiStuff.findComponent("lol").onClick = saveCharacterCompile;
uiStuff.findComponent('saveAs').onClick = saveStageFileRef; uiStuff.findComponent('saveAs').onClick = saveStageFileRef;
add(uiStuff); add(uiStuff);
PlayState.instance.persistentUpdate = true; PlayState.instance.persistentUpdate = true;
uiStuff.cameras = [PlayState.instance.camHUD]; uiStuff.cameras = [PlayState.instance.camHUD];
// btn.cameras = [PlayState.instance.camHUD]; // btn.cameras = [PlayState.instance.camHUD];
for (thing in PlayState.instance.currentStage) for (thing in PlayState.instance.currentStage)
{ {
FlxMouseEvent.add(thing, spr -> FlxMouseEvent.add(thing, spr ->
{ {
char = cast thing; char = cast thing;
trace("JUST PRESSED!"); trace("JUST PRESSED!");
sprOld.x = thing.x; sprOld.x = thing.x;
sprOld.y = thing.y; sprOld.y = thing.y;
mosPosOld.x = FlxG.mouse.x; mosPosOld.x = FlxG.mouse.x;
mosPosOld.y = FlxG.mouse.y; mosPosOld.y = FlxG.mouse.y;
}, null, spr -> }, null, spr ->
{ {
// ID tag is to see if currently overlapping hold basically!, a bit more reliable than checking transparency! // ID tag is to see if currently overlapping hold basically!, a bit more reliable than checking transparency!
// used for bug where you can click, and if you click on NO sprite, it snaps the thing to position! unintended! // used for bug where you can click, and if you click on NO sprite, it snaps the thing to position! unintended!
spr.ID = 1; spr.ID = 1;
spr.alpha = 0.5; spr.alpha = 0.5;
}, spr -> }, spr ->
{ {
spr.ID = 0; spr.ID = 0;
spr.alpha = 1; spr.alpha = 1;
}); });
} }
} }
var mosPosOld:FlxPoint = new FlxPoint(); var mosPosOld:FlxPoint = new FlxPoint();
var sprOld:FlxPoint = new FlxPoint(); var sprOld:FlxPoint = new FlxPoint();
var char:FlxSprite = null; var char:FlxSprite = null;
var overlappingChar:Bool = false; var overlappingChar:Bool = false;
override function update(elapsed:Float) override function update(elapsed:Float)
{ {
super.update(elapsed); super.update(elapsed);
if (char != null && char.ID == 1 && FlxG.mouse.pressed) if (char != null && char.ID == 1 && FlxG.mouse.pressed)
{ {
char.x = sprOld.x - (mosPosOld.x - FlxG.mouse.x); char.x = sprOld.x - (mosPosOld.x - FlxG.mouse.x);
char.y = sprOld.y - (mosPosOld.y - FlxG.mouse.y); char.y = sprOld.y - (mosPosOld.y - FlxG.mouse.y);
} }
FlxG.mouse.visible = true; FlxG.mouse.visible = true;
CoolUtil.mouseCamDrag(); CoolUtil.mouseCamDrag();
if (FlxG.keys.pressed.CONTROL) if (FlxG.keys.pressed.CONTROL)
CoolUtil.mouseWheelZoom(); CoolUtil.mouseWheelZoom();
if (FlxG.mouse.wheel != 0) if (FlxG.mouse.wheel != 0)
{ {
FlxG.camera.zoom += FlxG.mouse.wheel * 0.1; FlxG.camera.zoom += FlxG.mouse.wheel * 0.1;
} }
if (FlxG.keys.justPressed.Y) if (FlxG.keys.justPressed.Y)
{ {
for (thing in PlayState.instance.currentStage) for (thing in PlayState.instance.currentStage)
{ {
FlxMouseEvent.remove(thing); FlxMouseEvent.remove(thing);
thing.alpha = 1; thing.alpha = 1;
} }
if (uiStuff != null) if (uiStuff != null)
remove(uiStuff); remove(uiStuff);
uiStuff = null; uiStuff = null;
PlayState.instance.resetCamera(); PlayState.instance.resetCamera();
FlxG.mouse.visible = false; FlxG.mouse.visible = false;
close(); close();
} }
} }
var _file:FileReference; var _file:FileReference;
private function saveStageFileRef(_):Void private function saveStageFileRef(_):Void
{ {
var jsonStr = prepStageStuff(); var jsonStr = prepStageStuff();
_file = new FileReference(); _file = new FileReference();
_file.addEventListener(Event.COMPLETE, onSaveComplete); _file.addEventListener(Event.COMPLETE, onSaveComplete);
_file.addEventListener(Event.CANCEL, onSaveCancel); _file.addEventListener(Event.CANCEL, onSaveCancel);
_file.addEventListener(IOErrorEvent.IO_ERROR, onSaveError); _file.addEventListener(IOErrorEvent.IO_ERROR, onSaveError);
_file.save(jsonStr, PlayState.instance.currentStageId + ".json"); _file.save(jsonStr, PlayState.instance.currentStageId + ".json");
} }
function onSaveComplete(_) function onSaveComplete(_)
{ {
fileRemoveListens(); fileRemoveListens();
FlxG.log.notice("Successfully saved!"); FlxG.log.notice("Successfully saved!");
} }
function onSaveCancel(_) function onSaveCancel(_)
{ {
fileRemoveListens(); fileRemoveListens();
} }
function onSaveError(_) function onSaveError(_)
{ {
fileRemoveListens(); fileRemoveListens();
FlxG.log.error("Problem saving Stage file!"); FlxG.log.error("Problem saving Stage file!");
} }
function fileRemoveListens() function fileRemoveListens()
{ {
_file.removeEventListener(Event.COMPLETE, onSaveComplete); _file.removeEventListener(Event.COMPLETE, onSaveComplete);
_file.removeEventListener(Event.CANCEL, onSaveCancel); _file.removeEventListener(Event.CANCEL, onSaveCancel);
_file.removeEventListener(IOErrorEvent.IO_ERROR, onSaveError); _file.removeEventListener(IOErrorEvent.IO_ERROR, onSaveError);
_file = null; _file = null;
} }
private function saveCharacterCompile(_):Void private function saveCharacterCompile(_):Void
{ {
var outputJson:String = prepStageStuff(); var outputJson:String = prepStageStuff();
#if sys #if sys
// save "local" to the current export. // save "local" to the current export.
sys.io.File.saveContent('./assets/data/stages/' + PlayState.instance.currentStageId + '.json', outputJson); sys.io.File.saveContent('./assets/data/stages/' + PlayState.instance.currentStageId + '.json', outputJson);
// save to the dev version // save to the dev version
sys.io.File.saveContent('../../../../assets/preload/data/stages/' + PlayState.instance.currentStageId + '.json', outputJson); sys.io.File.saveContent('../../../../assets/preload/data/stages/' + PlayState.instance.currentStageId + '.json', outputJson);
#end #end
} }
private function prepStageStuff():String private function prepStageStuff():String
{ {
var stageLol:StageData = StageDataParser.parseStageData(PlayState.instance.currentStageId); var stageLol:StageData = StageDataParser.parseStageData(PlayState.instance.currentStageId);
for (prop in stageLol.props) for (prop in stageLol.props)
{ {
@:privateAccess @:privateAccess
var posStuff = PlayState.instance.currentStage.namedProps.get(prop.name); var posStuff = PlayState.instance.currentStage.namedProps.get(prop.name);
prop.position[0] = posStuff.x; prop.position[0] = posStuff.x;
prop.position[1] = posStuff.y; prop.position[1] = posStuff.y;
} }
var bfPos = PlayState.instance.currentStage.getBoyfriend().feetPosition; var bfPos = PlayState.instance.currentStage.getBoyfriend().feetPosition;
stageLol.characters.bf.position[0] = Std.int(bfPos.x); stageLol.characters.bf.position[0] = Std.int(bfPos.x);
stageLol.characters.bf.position[1] = Std.int(bfPos.y); stageLol.characters.bf.position[1] = Std.int(bfPos.y);
var dadPos = PlayState.instance.currentStage.getDad().feetPosition; var dadPos = PlayState.instance.currentStage.getDad().feetPosition;
stageLol.characters.dad.position[0] = Std.int(dadPos.x); stageLol.characters.dad.position[0] = Std.int(dadPos.x);
stageLol.characters.dad.position[1] = Std.int(dadPos.y); stageLol.characters.dad.position[1] = Std.int(dadPos.y);
var GF_FEET_SNIIIIIIIIIIIIIFFFF = PlayState.instance.currentStage.getGirlfriend().feetPosition; var GF_FEET_SNIIIIIIIIIIIIIFFFF = PlayState.instance.currentStage.getGirlfriend().feetPosition;
stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x); stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x);
stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y); stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y);
return CoolUtil.jsonStringify(stageLol); return CoolUtil.jsonStringify(stageLol);
} }
} }

View file

@ -2,35 +2,54 @@ package funkin.util;
import flixel.util.FlxSignal.FlxTypedSignal; import flixel.util.FlxSignal.FlxTypedSignal;
#if cpp
@:cppFileCode('
#include <iostream>
#include <windows.h>
#include <psapi.h>
')
#end
class WindowUtil class WindowUtil
{ {
public static function openURL(targetUrl:String) public static function openURL(targetUrl:String)
{ {
#if CAN_OPEN_LINKS #if CAN_OPEN_LINKS
#if linux #if linux
// Sys.command('/usr/bin/xdg-open', [, "&"]); // Sys.command('/usr/bin/xdg-open', [, "&"]);
Sys.command('/usr/bin/xdg-open', [targetUrl, "&"]); Sys.command('/usr/bin/xdg-open', [targetUrl, "&"]);
#else #else
FlxG.openURL(targetUrl); FlxG.openURL(targetUrl);
#end #end
#else #else
trace('Cannot open'); trace('Cannot open');
#end #end
} }
/** /**
* Dispatched when the game window is closed. * Dispatched when the game window is closed.
*/ */
public static final windowExit:FlxTypedSignal<Int->Void> = new FlxTypedSignal<Int->Void>(); public static final windowExit:FlxTypedSignal<Int->Void> = new FlxTypedSignal<Int->Void>();
public static function initWindowEvents() public static function initWindowEvents()
{ {
// onUpdate is called every frame just before rendering. // onUpdate is called every frame just before rendering.
// onExit is called when the game window is closed. // onExit is called when the game window is closed.
openfl.Lib.current.stage.application.onExit.add(function(exitCode:Int) openfl.Lib.current.stage.application.onExit.add(function(exitCode:Int)
{ {
windowExit.dispatch(exitCode); windowExit.dispatch(exitCode);
}); });
} }
/**
* Turns off that annoying "Report to Microsoft" dialog that pops up when the game crashes.
*/
public static function disableCrashHandler()
{
#if cpp
untyped __cpp__('SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);');
#else
// Do nothing.
#end
}
} }

View file

@ -4,7 +4,7 @@ class DataAssets
{ {
static function buildDataPath(path:String):String static function buildDataPath(path:String):String
{ {
return 'default:assets/data/${path}'; return 'assets/data/${path}';
} }
public static function listDataFilesInPath(path:String, ?suffix:String = '.json'):Array<String> public static function listDataFilesInPath(path:String, ?suffix:String = '.json'):Array<String>

View file

@ -0,0 +1,204 @@
package funkin.util.macro;
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;
import funkin.util.macro.MacroUtil;
/**
* Macros to generate lists of classes at compile time.
*
* This code is a bitch glad Jason figured it out.
* Based on code from CompileTime: https://github.com/jasononeil/compiletime
*/
class ClassMacro
{
/**
* Gets a list of `Class<T>` for all classes in a specified package.
*
* Example: `var list:Array<Class<Dynamic>> = listClassesInPackage("funkin", true);`
*
* @param targetPackage A String containing the package name to query.
* @param includeSubPackages Whether to include classes located in sub-packages of the target package.
* @return A list of classes matching the specified criteria.
*/
public static macro function listClassesInPackage(targetPackage:String, ?includeSubPackages:Bool = true):ExprOf<Iterable<Class<Dynamic>>>
{
if (!onGenerateCallbackRegistered)
{
onGenerateCallbackRegistered = true;
Context.onGenerate(onGenerate);
}
var request:String = 'package~${targetPackage}~${includeSubPackages ? "recursive" : "nonrecursive"}';
classListsToGenerate.push(request);
return macro funkin.util.macro.CompiledClassList.get($v{request});
}
/**
* Get a list of `Class<T>` for all classes extending a specified class.
*
* Example: `var list:Array<Class<FlxSprite>> = listSubclassesOf(FlxSprite);`
*
* @param targetClass The class to query for subclasses.
* @return A list of classes matching the specified criteria.
*/
public static macro function listSubclassesOf<T>(targetClassExpr:ExprOf<Class<T>>):ExprOf<List<Class<T>>>
{
if (!onGenerateCallbackRegistered)
{
onGenerateCallbackRegistered = true;
Context.onGenerate(onGenerate);
}
var targetClass:ClassType = MacroUtil.getClassTypeFromExpr(targetClassExpr);
var targetClassPath:String = null;
if (targetClass != null)
targetClassPath = targetClass.pack.join('.') + '.' + targetClass.name;
var request:String = 'extend~${targetClassPath}';
classListsToGenerate.push(request);
return macro funkin.util.macro.CompiledClassList.getTyped($v{request}, ${targetClassExpr});
}
#if macro
/**
* Callback executed after the typing phase but before the generation phase.
* Receives a list of `haxe.macro.Type` for all types in the program.
*
* Only metadata can be modified at this time, which makes it a BITCH to access the data at runtime.
*/
static function onGenerate(allTypes:Array<haxe.macro.Type>)
{
// Reset these, since onGenerate persists across multiple builds.
classListsRaw = [];
for (request in classListsToGenerate)
{
classListsRaw.set(request, []);
}
for (type in allTypes)
{
switch (type)
{
// Class instances
case TInst(t, _params):
var classType:ClassType = t.get();
var className:String = t.toString();
if (classType.isInterface)
{
// Ignore interfaces.
}
else
{
for (request in classListsToGenerate)
{
if (doesClassMatchRequest(classType, request))
{
classListsRaw.get(request).push(className);
}
}
}
// Other types (things like enums)
default:
continue;
}
}
compileClassLists();
}
/**
* At this stage in the program, `classListsRaw` is generated, but only accessible by macros.
* To make it accessible at runtime, we must:
* - Convert the String names to actual `Class<T>` instances, and store it as `classLists`
* - Insert the `classLists` into the metadata of the `CompiledClassList` class.
* `CompiledClassList` then extracts the metadata and stores it where it can be accessed at runtime.
*/
static function compileClassLists()
{
var compiledClassList:ClassType = MacroUtil.getClassType("funkin.util.macro.CompiledClassList");
if (compiledClassList == null)
throw "Could not find CompiledClassList class.";
// Reset outdated metadata.
if (compiledClassList.meta.has('classLists'))
compiledClassList.meta.remove('classLists');
var classLists:Array<Expr> = [];
// Generate classLists.
for (request in classListsToGenerate)
{
// Expression contains String, [Class<T>...]
var classListEntries:Array<Expr> = [macro $v{request}];
for (i in classListsRaw.get(request))
{
// TODO: Boost performance by making this an Array<Class<T>> instead of an Array<String>
// How to perform perform macro reificiation to types given a name?
classListEntries.push(macro $v{i});
}
classLists.push(macro $a{classListEntries});
}
// Insert classLists into metadata.
compiledClassList.meta.add('classLists', classLists, Context.currentPos());
}
static function doesClassMatchRequest(classType:ClassType, request:String):Bool
{
var splitRequest:Array<String> = request.split('~');
var requestType:String = splitRequest[0];
switch (requestType)
{
case 'package':
var targetPackage:String = splitRequest[1];
var recursive:Bool = splitRequest[2] == 'recursive';
var classPackage:String = classType.pack.join('.');
if (recursive)
{
return StringTools.startsWith(classPackage, targetPackage);
}
else
{
var regex:EReg = ~/^${targetPackage}(\.|$)/;
return regex.match(classPackage);
}
case 'extend':
var targetClassName:String = splitRequest[1];
var targetClassType:ClassType = MacroUtil.getClassType(targetClassName);
if (MacroUtil.implementsInterface(classType, targetClassType))
{
return true;
}
else if (MacroUtil.isSubclassOf(classType, targetClassType))
{
return true;
}
return false;
default:
throw 'Unknown request type: ${requestType}';
}
}
static var onGenerateCallbackRegistered:Bool = false;
static var classListsRaw:Map<String, Array<String>> = [];
static var classListsToGenerate:Array<String> = [];
#end
}

View file

@ -0,0 +1,69 @@
package funkin.util.macro;
import haxe.rtti.Meta;
/**
* A complement to `ClassMacro`. See `ClassMacro` for more information.
*/
class CompiledClassList
{
static var classLists:Map<String, List<Class<Dynamic>>>;
/**
* Class lists are injected into this class's metadata during the typing phase.
* This function extracts the metadata, at runtime, and stores it in `classLists`.
*/
static function init():Void
{
classLists = [];
// Meta.getType returns Dynamic<Array<Dynamic>>.
var metaData = Meta.getType(CompiledClassList);
if (metaData.classLists != null)
{
for (list in metaData.classLists)
{
var data:Array<Dynamic> = cast list;
// First element is the list ID.
var id:String = cast data[0];
// All other elements are class types.
var classes:List<Class<Dynamic>> = new List();
for (i in 1...data.length)
{
var className:String = cast data[i];
// var classType:Class<Dynamic> = cast data[i];
var classType:Class<Dynamic> = cast Type.resolveClass(className);
classes.push(classType);
}
classLists.set(id, classes);
}
}
else
{
throw "Class lists not properly generated. Try cleaning out your export folder, restarting your IDE, and rebuilding your project.";
}
}
public static function get(request:String):List<Class<Dynamic>>
{
if (classLists == null)
init();
if (!classLists.exists(request))
{
trace('[WARNING] Class list $request not properly generated. Please debug the build macro.');
classLists.set(request, new List()); // Make the error only appear once.
}
return classLists.get(request);
}
public static inline function getTyped<T>(request:String, type:Class<T>):List<Class<T>>
{
return cast get(request);
}
}

View file

@ -3,59 +3,65 @@ package funkin.util.macro;
#if debug #if debug
class GitCommit class GitCommit
{ {
public static macro function getGitCommitHash():haxe.macro.Expr.ExprOf<String> /**
{ * Get the SHA1 hash of the current Git commit.
#if !display */
// Get the current line number. public static macro function getGitCommitHash():haxe.macro.Expr.ExprOf<String>
var pos = haxe.macro.Context.currentPos(); {
#if !display
// Get the current line number.
var pos = haxe.macro.Context.currentPos();
var process = new sys.io.Process('git', ['rev-parse', 'HEAD']); var process = new sys.io.Process('git', ['rev-parse', 'HEAD']);
if (process.exitCode() != 0) if (process.exitCode() != 0)
{ {
var message = process.stderr.readAll().toString(); var message = process.stderr.readAll().toString();
haxe.macro.Context.info('[WARN] Could not determine current git commit; is this a proper Git repository?', pos); haxe.macro.Context.info('[WARN] Could not determine current git commit; is this a proper Git repository?', pos);
} }
// read the output of the process // read the output of the process
var commitHash:String = process.stdout.readLine(); var commitHash:String = process.stdout.readLine();
var commitHashSplice:String = commitHash.substr(0, 7); var commitHashSplice:String = commitHash.substr(0, 7);
trace('Git Commit ID: ${commitHashSplice}'); trace('Git Commit ID: ${commitHashSplice}');
// Generates a string expression // Generates a string expression
return macro $v{commitHashSplice}; return macro $v{commitHashSplice};
#else #else
// `#if display` is used for code completion. In this case returning an // `#if display` is used for code completion. In this case returning an
// empty string is good enough; We don't want to call git on every hint. // empty string is good enough; We don't want to call git on every hint.
var commitHash:String = ""; var commitHash:String = "";
return macro $v{commitHashSplice}; return macro $v{commitHashSplice};
#end #end
} }
public static macro function getGitBranch():haxe.macro.Expr.ExprOf<String> /**
{ * Get the branch name of the current Git commit.
#if !display */
// Get the current line number. public static macro function getGitBranch():haxe.macro.Expr.ExprOf<String>
var pos = haxe.macro.Context.currentPos(); {
var branchProcess = new sys.io.Process('git', ['rev-parse', '--abbrev-ref', 'HEAD']); #if !display
// Get the current line number.
var pos = haxe.macro.Context.currentPos();
var branchProcess = new sys.io.Process('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
if (branchProcess.exitCode() != 0) if (branchProcess.exitCode() != 0)
{ {
var message = branchProcess.stderr.readAll().toString(); var message = branchProcess.stderr.readAll().toString();
haxe.macro.Context.info('[WARN] Could not determine current git commit; is this a proper Git repository?', pos); haxe.macro.Context.info('[WARN] Could not determine current git commit; is this a proper Git repository?', pos);
} }
var branchName:String = branchProcess.stdout.readLine(); var branchName:String = branchProcess.stdout.readLine();
trace('Git Branch Name: ${branchName}'); trace('Git Branch Name: ${branchName}');
// Generates a string expression // Generates a string expression
return macro $v{branchName}; return macro $v{branchName};
#else #else
// `#if display` is used for code completion. In this case returning an // `#if display` is used for code completion. In this case returning an
// empty string is good enough; We don't want to call git on every hint. // empty string is good enough; We don't want to call git on every hint.
var branchName:String = ""; var branchName:String = "";
return macro $v{branchName}; return macro $v{branchName};
#end #end
} }
} }
#end #end

View file

@ -1,12 +1,173 @@
package funkin.util.macro; package funkin.util.macro;
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;
class MacroUtil class MacroUtil
{ {
public static macro function getDefine(key:String, defaultValue:String = null):haxe.macro.Expr /**
{ * Gets the value of a Haxe compiler define.
var value = haxe.macro.Context.definedValue(key); *
if (value == null) * @param key The name of the define to get the value of.
value = defaultValue; * @param defaultValue The value to return if the define is not set.
return macro $v{value}; * @return An expression containing the value of the define.
} */
public static macro function getDefine(key:String, defaultValue:String = null):haxe.macro.Expr
{
var value = haxe.macro.Context.definedValue(key);
if (value == null)
value = defaultValue;
return macro $v{value};
}
/**
* Gets the current date and time (at compile time).
* @return A `Date` object containing the current date and time.
*/
public static macro function getDate():ExprOf<Date>
{
var date = Date.now();
var year = toExpr(date.getFullYear());
var month = toExpr(date.getMonth());
var day = toExpr(date.getDate());
var hours = toExpr(date.getHours());
var mins = toExpr(date.getMinutes());
var secs = toExpr(date.getSeconds());
return macro new Date($year, $month, $day, $hours, $mins, $secs);
}
#if macro
//
// MACRO HELPER FUNCTIONS
//
/**
* Convert an ExprOf<Class<T>> to a ClassType.
* @see https://github.com/jasononeil/compiletime/blob/master/src/CompileTime.hx#L201
*/
public static function getClassTypeFromExpr(e:Expr):ClassType
{
var classType:ClassType = null;
var parts:Array<String> = [];
var nextSection:ExprDef = e.expr;
while (nextSection != null)
{
var section:ExprDef = nextSection;
nextSection = null;
switch (section)
{
// Expression is a class name with no packages
case EConst(c):
switch (c)
{
case CIdent(cn):
if (cn != "null") parts.unshift(cn);
default:
}
// Expression is a fully qualified package name.
// We need to traverse the expression tree to get the full package name.
case EField(exp, field):
nextSection = exp.expr;
parts.unshift(field);
// We've reached the end of the expression tree.
default:
}
}
var fullClassName:String = parts.join('.');
if (fullClassName != "")
{
var classType:Type = Context.getType(fullClassName);
// Follow typedefs to get the actual class type.
var classTypeParsed:Type = Context.follow(classType, false);
switch (classTypeParsed)
{
case TInst(t, params):
return t.get();
default:
// We couldn't parse this class type.
// This function may need to be updated to be more robust.
throw 'Class type could not be parsed: ${fullClassName}';
}
}
return null;
}
/**
* Converts a value to an equivalent macro expression.
*/
public static function toExpr(value:Dynamic):ExprOf<Dynamic>
{
return Context.makeExpr(value, Context.currentPos());
}
public static function areClassesEqual(class1:ClassType, class2:ClassType):Bool
{
return class1.pack.join('.') == class2.pack.join('.') && class1.name == class2.name;
}
/**
* Retrieve a ClassType from a string name.
*/
public static function getClassType(name:String):ClassType
{
switch (Context.getType(name))
{
case TInst(t, _params):
return t.get();
default:
throw 'Class type could not be parsed: ${name}';
}
}
/**
* Determine whether a given ClassType is a subclass of a given superclass.
* @param classType The class to check.
* @param superClass The superclass to check for.
* @return Whether the class is a subclass of the superclass.
*/
public static function isSubclassOf(classType:ClassType, superClass:ClassType):Bool
{
if (areClassesEqual(classType, superClass))
return true;
if (classType.superClass != null)
{
return isSubclassOf(classType.superClass.t.get(), superClass);
}
return false;
}
/**
* Determine whether a given ClassType implements a given interface.
* @param classType The class to check.
* @param interfaceType The interface to check for.
* @return Whether the class implements the interface.
*/
public static function implementsInterface(classType:ClassType, interfaceType:ClassType):Bool
{
for (i in classType.interfaces)
{
if (areClassesEqual(i.t.get(), interfaceType))
{
return true;
}
}
if (classType.superClass != null)
{
return implementsInterface(classType.superClass.t.get(), interfaceType);
}
return false;
}
#end
} }

View file

@ -0,0 +1,16 @@
package funkin.util.tools;
/**
* A static extension which provides utility functions for Maps.
*
* For example, add `using MapTools` then call `map.values()`.
*
* @see https://haxe.org/manual/lf-static-extension.html
*/
class MapTools
{
public static function values<K, T>(map:Map<K, T>):Array<T>
{
return [for (i in map.iterator()) i];
}
}