Merge branch 'feature/chart-editor-note-kind' into feature/2hot-death-animation

This commit is contained in:
EliteMasterEric 2024-02-16 22:46:06 -05:00
commit de8fe2e271
25 changed files with 784 additions and 186 deletions

45
.vscode/settings.json vendored
View file

@ -95,16 +95,31 @@
"target": "windows",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug",
"target": "hl",
"args": ["-debug"]
},
{
"label": "Windows / Debug (FlxAnimate Test)",
"target": "windows",
"args": ["-debug", "-DANIMATE", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (FlxAnimate Test)",
"target": "hl",
"args": ["-debug", "-DANIMATE"]
},
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
"args": ["-debug", "-DFREEPLAY", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Freeplay)",
"target": "hl",
"args": ["-debug", "-DFREEPLAY"]
},
{
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
"target": "windows",
@ -114,31 +129,61 @@
"-DFORCE_DEBUG_VERSION"
]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
"target": "hl",
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
},
{
"label": "Windows / Debug (Conversation Test)",
"target": "windows",
"args": ["-debug", "-DDIALOGUE", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Conversation Test)",
"target": "hl",
"args": ["-debug", "-DDIALOGUE"]
},
{
"label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows",
"args": ["-debug", "-DCHARTING", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Chart Editor)",
"target": "hl",
"args": ["-debug", "-DCHARTING"]
},
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",
"args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
"target": "hl",
"args": ["-debug", "-DANIMDEBUG"]
},
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
"args": ["-debug", "-DLATENCY", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Latency Test)",
"target": "hl",
"args": ["-debug", "-DLATENCY"]
},
{
"label": "Windows / Debug (Waveform Test)",
"target": "windows",
"args": ["-debug", "-DWAVEFORM", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Waveform Test)",
"target": "hl",
"args": ["-debug", "-DWAVEFORM"]
},
{
"label": "HTML5 / Debug",
"target": "html5",

View file

@ -113,7 +113,6 @@
<haxelib name="json2object" /> <!-- JSON parsing -->
<haxelib name="thx.semver" /> <!-- Version string handling -->
<haxelib name="hmm" /> <!-- Read library version data at compile time so it can be baked into logs -->
<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
<!--Disable the Flixel core focus lost screen-->

View file

@ -7,7 +7,8 @@
- If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way.
2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`)
3. Install all haxelibs of the current branch by running `hmm install`
4. Platform setup
4. Setup lime: `haxelib run lime setup`
5. Platform setup
- For Windows, download the [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe)
- When prompted, select "Individual Components" and make sure to download the following:
- MSVC v143 VS 2022 C++ x64/x86 build tools
@ -15,5 +16,5 @@
- Mac: [`lime setup mac` Documentation](https://lime.openfl.org/docs/advanced-setup/macos/)
- Linux: [`lime setup linux` Documentation](https://lime.openfl.org/docs/advanced-setup/linux/)
- HTML5: Compiles without any extra setup
5. If you are targeting for native, you likely need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
6. `lime test PLATFORM` !
6. If you are targeting for native, you likely need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
7. `lime test PLATFORM` !

View file

@ -64,11 +64,6 @@
"ref": "e9f880522e27134b29df4067f82df7d7e5237b70",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
"name": "hmm",
"type": "haxelib",
"version": "3.1.0"
},
{
"name": "hscript",
"type": "haxelib",
@ -151,7 +146,7 @@
"name": "polymod",
"type": "git",
"dir": null,
"ref": "0b53e478bc375ec51b760b650201ac7a965d2ef4",
"ref": "d5a3b8995f64d20b95f844454e8c3b38c3d3a9fa",
"url": "https://github.com/larsiusprime/polymod"
},
{

View file

@ -50,11 +50,13 @@ class InitState extends FlxState
*/
public override function create():Void
{
// Setup a bunch of important Flixel stuff.
setupShit();
// loadSaveData(); // Moved to Main.hx
// Load player options from save data.
// Flixel has already loaded the save data, so we can just use it.
Preferences.init();
// Load controls from save data.
PlayerSettings.init();
@ -198,8 +200,13 @@ class InitState extends FlxState
//
// FLIXEL PLUGINS
//
// Plugins provide a useful interface for globally active Flixel objects,
// that receive update events regardless of the current state.
// TODO: Move Module behavior to a Flixel plugin.
funkin.util.plugins.EvacuateDebugPlugin.initialize();
funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
funkin.util.plugins.ScreenshotPlugin.initialize();
funkin.util.plugins.VolumePlugin.initialize();
funkin.util.plugins.WatchPlugin.initialize();
//

View file

@ -20,7 +20,10 @@ class Preferences
static function set_naughtyness(value:Bool):Bool
{
return Save.get().options.naughtyness = value;
var save = Save.get();
save.options.naughtyness = value;
save.flush();
return value;
}
/**
@ -36,7 +39,10 @@ class Preferences
static function set_downscroll(value:Bool):Bool
{
return Save.get().options.downscroll = value;
var save = Save.get();
save.options.downscroll = value;
save.flush();
return value;
}
/**
@ -52,7 +58,10 @@ class Preferences
static function set_flashingLights(value:Bool):Bool
{
return Save.get().options.flashingLights = value;
var save = Save.get();
save.options.flashingLights = value;
save.flush();
return value;
}
/**
@ -68,7 +77,10 @@ class Preferences
static function set_zoomCamera(value:Bool):Bool
{
return Save.get().options.zoomCamera = value;
var save = Save.get();
save.options.zoomCamera = value;
save.flush();
return value;
}
/**
@ -89,7 +101,10 @@ class Preferences
toggleDebugDisplay(value);
}
return Save.get().options.debugDisplay = value;
var save = Save.get();
save.options.debugDisplay = value;
save.flush();
return value;
}
/**
@ -107,7 +122,10 @@ class Preferences
{
if (value != Save.get().options.autoPause) FlxG.autoPause = value;
return Save.get().options.autoPause = value;
var save = Save.get();
save.options.autoPause = value;
save.flush();
return value;
}
public static function init():Void

View file

@ -898,20 +898,26 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
/**
* The kind of the note.
* This can allow the note to include information used for custom behavior.
* Defaults to blank or `Constants.DEFAULT_DIFFICULTY`.
* Defaults to `null` for no kind.
*/
@:alias("k")
@:default("normal")
@:optional
public var kind(get, default):String = '';
@:isVar
public var kind(get, set):Null<String> = null;
function get_kind():String
function get_kind():Null<String>
{
if (this.kind == null || this.kind == '') return 'normal';
if (this.kind == null || this.kind == '') return null;
return this.kind;
}
function set_kind(value:Null<String>):Null<String>
{
if (value == '') value = null;
return this.kind = value;
}
public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
{
this.time = time;
@ -1067,13 +1073,13 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
if (this == null) return other == null;
if (other == null) return false;
if (this.kind == '')
if (this.kind == null || this.kind == '')
{
if (other.kind != '' && other.kind != 'normal') return false;
if (other.kind != '' && this.kind != null) return false;
}
else
{
if (other.kind == '' || other.kind != this.kind) return false;
if (other.kind == '' || this.kind == null) return false;
}
return this.time == other.time && this.data == other.data && this.length == other.length;
@ -1088,11 +1094,11 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
if (this.kind == '')
{
if (other.kind != '' && other.kind != 'normal') return true;
if (other.kind != '') return true;
}
else
{
if (other.kind == '' || other.kind != this.kind) return true;
if (other.kind == '') return true;
}
return this.time != other.time || this.data != other.data || this.length != other.length;

View file

@ -210,14 +210,13 @@ class SongDataUtils
*/
public static function writeItemsToClipboard(data:SongClipboardItems):Void
{
var writer = new json2object.JsonWriter<SongClipboardItems>();
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongClipboardItems>(ignoreNullOptionals);
var dataString:String = writer.write(data, ' ');
ClipboardUtil.setClipboard(dataString);
trace('Wrote ' + data.notes.length + ' notes and ' + data.events.length + ' events to clipboard.');
trace(dataString);
}
/**

View file

@ -63,12 +63,10 @@ class Controls extends FlxActionSet
var _debug_menu = new FlxActionDigital(Action.DEBUG_MENU);
var _debug_chart = new FlxActionDigital(Action.DEBUG_CHART);
var _debug_stage = new FlxActionDigital(Action.DEBUG_STAGE);
var _screenshot = new FlxActionDigital(Action.SCREENSHOT);
var _volume_up = new FlxActionDigital(Action.VOLUME_UP);
var _volume_down = new FlxActionDigital(Action.VOLUME_DOWN);
var _volume_mute = new FlxActionDigital(Action.VOLUME_MUTE);
#if CAN_CHEAT
var _cheat = new FlxActionDigital(Action.CHEAT);
#end
var byName:Map<String, FlxActionDigital> = new Map<String, FlxActionDigital>();
@ -235,6 +233,11 @@ class Controls extends FlxActionSet
inline function get_DEBUG_STAGE()
return _debug_stage.check();
public var SCREENSHOT(get, never):Bool;
inline function get_SCREENSHOT()
return _screenshot.check();
public var VOLUME_UP(get, never):Bool;
inline function get_VOLUME_UP()
@ -255,13 +258,6 @@ class Controls extends FlxActionSet
inline function get_RESET()
return _reset.check();
#if CAN_CHEAT
public var CHEAT(get, never):Bool;
inline function get_CHEAT()
return _cheat.check();
#end
public function new(name, scheme:KeyboardScheme = null)
{
super(name);
@ -295,13 +291,14 @@ class Controls extends FlxActionSet
add(_pause);
add(_cutscene_advance);
add(_cutscene_skip);
add(_debug_menu);
add(_debug_chart);
add(_debug_stage);
add(_screenshot);
add(_volume_up);
add(_volume_down);
add(_volume_mute);
add(_reset);
#if CAN_CHEAT
add(_cheat);
#end
for (action in digitalActions)
byName[action.name] = action;
@ -391,12 +388,10 @@ class Controls extends FlxActionSet
case DEBUG_MENU: _debug_menu;
case DEBUG_CHART: _debug_chart;
case DEBUG_STAGE: _debug_stage;
case SCREENSHOT: _screenshot;
case VOLUME_UP: _volume_up;
case VOLUME_DOWN: _volume_down;
case VOLUME_MUTE: _volume_mute;
#if CAN_CHEAT
case CHEAT: _cheat;
#end
}
}
@ -464,6 +459,8 @@ class Controls extends FlxActionSet
func(_debug_chart, JUST_PRESSED);
case DEBUG_STAGE:
func(_debug_stage, JUST_PRESSED);
case SCREENSHOT:
func(_screenshot, JUST_PRESSED);
case VOLUME_UP:
func(_volume_up, JUST_PRESSED);
case VOLUME_DOWN:
@ -472,10 +469,6 @@ class Controls extends FlxActionSet
func(_volume_mute, JUST_PRESSED);
case RESET:
func(_reset, JUST_PRESSED);
#if CAN_CHEAT
case CHEAT:
func(_cheat, JUST_PRESSED);
#end
}
}
@ -666,6 +659,8 @@ class Controls extends FlxActionSet
bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU));
bindKeys(Control.DEBUG_CHART, getDefaultKeybinds(scheme, Control.DEBUG_CHART));
bindKeys(Control.DEBUG_STAGE, getDefaultKeybinds(scheme, Control.DEBUG_STAGE));
bindKeys(Control.RESET, getDefaultKeybinds(scheme, Control.RESET));
bindKeys(Control.SCREENSHOT, getDefaultKeybinds(scheme, Control.SCREENSHOT));
bindKeys(Control.VOLUME_UP, getDefaultKeybinds(scheme, Control.VOLUME_UP));
bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN));
bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE));
@ -693,6 +688,7 @@ class Controls extends FlxActionSet
case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return [];
case Control.DEBUG_STAGE: return [];
case Control.SCREENSHOT: return [F3]; // TODO: Change this back to PrintScreen
case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS];
case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO];
@ -716,6 +712,7 @@ class Controls extends FlxActionSet
case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return [];
case Control.DEBUG_STAGE: return [];
case Control.SCREENSHOT: return [PRINTSCREEN];
case Control.VOLUME_UP: return [PLUS];
case Control.VOLUME_DOWN: return [MINUS];
case Control.VOLUME_MUTE: return [ZERO];
@ -739,6 +736,7 @@ class Controls extends FlxActionSet
case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return [];
case Control.DEBUG_STAGE: return [];
case Control.SCREENSHOT: return [PRINTSCREEN];
case Control.VOLUME_UP: return [NUMPADPLUS];
case Control.VOLUME_DOWN: return [NUMPADMINUS];
case Control.VOLUME_MUTE: return [NUMPADZERO];
@ -845,6 +843,7 @@ class Controls extends FlxActionSet
Control.NOTE_LEFT => getDefaultGamepadBinds(Control.NOTE_LEFT),
Control.NOTE_RIGHT => getDefaultGamepadBinds(Control.NOTE_RIGHT),
Control.PAUSE => getDefaultGamepadBinds(Control.PAUSE),
// Control.SCREENSHOT => [],
// Control.VOLUME_UP => [RIGHT_SHOULDER],
// Control.VOLUME_DOWN => [LEFT_SHOULDER],
// Control.VOLUME_MUTE => [RIGHT_TRIGGER],
@ -852,8 +851,7 @@ class Controls extends FlxActionSet
Control.CUTSCENE_SKIP => getDefaultGamepadBinds(Control.CUTSCENE_SKIP),
// Control.DEBUG_MENU
// Control.DEBUG_CHART
Control.RESET => getDefaultGamepadBinds(Control.RESET),
#if CAN_CHEAT, Control.CHEAT => getDefaultGamepadBinds(Control.CHEAT) #end
Control.RESET => getDefaultGamepadBinds(Control.RESET)
]);
}
@ -870,6 +868,7 @@ class Controls extends FlxActionSet
case Control.NOTE_LEFT: return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT];
case Control.NOTE_RIGHT: return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT];
case Control.PAUSE: return [START];
case Control.SCREENSHOT: return [];
case Control.VOLUME_UP: return [];
case Control.VOLUME_DOWN: return [];
case Control.VOLUME_MUTE: return [];
@ -878,7 +877,6 @@ class Controls extends FlxActionSet
case Control.DEBUG_MENU: return [];
case Control.DEBUG_CHART: return [];
case Control.RESET: return [RIGHT_SHOULDER];
#if CAN_CHEAT, Control.CHEAT: return [X]; #end
default:
// Fallthrough.
}
@ -1236,6 +1234,8 @@ enum Control
// CUTSCENE
CUTSCENE_ADVANCE;
CUTSCENE_SKIP;
// SCREENSHOT
SCREENSHOT;
// VOLUME
VOLUME_UP;
VOLUME_DOWN;
@ -1244,9 +1244,6 @@ enum Control
DEBUG_MENU;
DEBUG_CHART;
DEBUG_STAGE;
#if CAN_CHEAT
CHEAT;
#end
}
enum
@ -1289,13 +1286,12 @@ abstract Action(String) to String from String
var VOLUME_UP = "volume_up";
var VOLUME_DOWN = "volume_down";
var VOLUME_MUTE = "volume_mute";
// SCREENSHOT
var SCREENSHOT = "screenshot";
// DEBUG
var DEBUG_MENU = "debug_menu";
var DEBUG_CHART = "debug_chart";
var DEBUG_STAGE = "debug_stage";
#if CAN_CHEAT
var CHEAT = "cheat";
#end
}
enum Device

View file

@ -354,6 +354,11 @@ class PlayState extends MusicBeatSubState
*/
var startingSong:Bool = false;
/**
* False if `FlxG.sound.music`
*/
var musicPausedBySubState:Bool = false;
/**
* False until `create()` has completed.
*/
@ -697,7 +702,7 @@ class PlayState extends MusicBeatSubState
function assertChartExists():Bool
{
// Returns null if the song failed to load or doesn't have the selected difficulty.
if (currentSong == null || currentChart == null)
if (currentSong == null || currentChart == null || currentChart.notes == null)
{
// We have encountered a critical error. Prevent Flixel from trying to run any gameplay logic.
criticalFailure = true;
@ -716,6 +721,10 @@ class PlayState extends MusicBeatSubState
{
message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
}
else if (currentChart.notes == null)
{
message = 'The was a critical error retrieving note data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
}
// Display a popup. This blocks the application until the user clicks OK.
lime.app.Application.current.window.alert(message, 'Error loading PlayState');
@ -1043,6 +1052,7 @@ class PlayState extends MusicBeatSubState
// Pause the music.
if (FlxG.sound.music != null)
{
musicPausedBySubState = FlxG.sound.music.playing;
FlxG.sound.music.pause();
if (vocals != null) vocals.pause();
}
@ -1050,7 +1060,6 @@ class PlayState extends MusicBeatSubState
// Pause the countdown.
Countdown.pauseCountdown();
}
else {}
super.openSubState(subState);
}
@ -1070,7 +1079,10 @@ class PlayState extends MusicBeatSubState
if (event.eventCanceled) return;
// Resume
FlxG.sound.music.play(FlxG.sound.music.time);
if (musicPausedBySubState)
{
FlxG.sound.music.play(FlxG.sound.music.time);
}
if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals();

View file

@ -113,6 +113,9 @@ abstract Save(RawSaveData)
};
}
/**
* NOTE: Modifications will not be saved without calling `Save.flush()`!
*/
public var options(get, never):SaveDataOptions;
function get_options():SaveDataOptions
@ -120,6 +123,9 @@ abstract Save(RawSaveData)
return this.options;
}
/**
* NOTE: Modifications will not be saved without calling `Save.flush()`!
*/
public var modOptions(get, never):Map<String, Dynamic>;
function get_modOptions():Map<String, Dynamic>

View file

@ -58,19 +58,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
Conductor.stepHit.remove(this.stepHit);
}
function handleControls():Void
{
var isHaxeUIFocused:Bool = haxe.ui.focus.FocusManager.instance?.focus != null;
if (!isHaxeUIFocused)
{
// Rebindable volume keys.
if (controls.VOLUME_MUTE) FlxG.sound.toggleMuted();
else if (controls.VOLUME_UP) FlxG.sound.changeVolume(0.1);
else if (controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1);
}
}
function handleFunctionControls():Void
{
// Emergency exit button.
@ -84,8 +71,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
{
super.update(elapsed);
handleControls();
dispatchEvent(new UpdateScriptEvent(elapsed));
}

View file

@ -53,11 +53,6 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
{
super.update(elapsed);
// Rebindable volume keys.
if (controls.VOLUME_MUTE) FlxG.sound.toggleMuted();
else if (controls.VOLUME_UP) FlxG.sound.changeVolume(0.1);
else if (controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1);
// Emergency exit button.
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());

View file

@ -9,6 +9,7 @@ import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.MusicBeatSubState;
import funkin.util.logging.CrashHandler;
import flixel.addons.transition.FlxTransitionableState;
import funkin.util.FileUtil;
class DebugMenuSubState extends MusicBeatSubState
{
@ -110,16 +111,7 @@ class DebugMenuSubState extends MusicBeatSubState
#if sys
function openLogFolder()
{
#if windows
Sys.command('explorer', [CrashHandler.LOG_FOLDER]);
#elseif mac
// mac could be fuckie with where the log folder is relative to the game file...
// if this comment is still here... it means it has NOT been verified on mac yet!
Sys.command('open', [CrashHandler.LOG_FOLDER]);
#end
// TODO: implement linux
// some shit with xdg-open :thinking: emoji...
FileUtil.openFolder(CrashHandler.LOG_FOLDER);
}
#end

View file

@ -162,8 +162,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
public static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/opponent-preview');
public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata');
public static final CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT:String = Paths.ui('chart-editor/toolbox/offsets');
public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata');
public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata');
public static final CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/note-data');
public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/event-data');
public static final CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT:String = Paths.ui('chart-editor/toolbox/freeplay');
public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties');
@ -538,9 +538,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Tools Status
/**
* The note kind to use for notes being placed in the chart. Defaults to `''`.
* The note kind to use for notes being placed in the chart. Defaults to `null`.
*/
var noteKindToPlace:String = '';
var noteKindToPlace:Null<String> = null;
/**
* The event type to use for events being placed in the chart. Defaults to `''`.
@ -2458,11 +2458,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
override public function draw():Void
{
if (selectionBoxStartPos != null)
{
trace('selectionBoxSprite: ${selectionBoxSprite.visible} ${selectionBoxSprite.exists} ${this.members.contains(selectionBoxSprite)}');
}
super.draw();
}
@ -2968,7 +2963,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value);
menubarItemToggleToolboxMetadata.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value);
menubarItemToggleToolboxOffsets.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT, event.value);
menubarItemToggleToolboxNotes.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value);
menubarItemToggleToolboxNoteData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT, event.value);
menubarItemToggleToolboxEventData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT, event.value);
menubarItemToggleToolboxFreeplay.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT, event.value);
menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value);
@ -5286,6 +5281,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0);
FlxG.watch.addQuick('noteKindToPlace', noteKindToPlace);
FlxG.watch.addQuick('eventKindToPlace', eventKindToPlace);
FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels);
FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels);

View file

@ -59,6 +59,17 @@ class SelectItemsCommand implements ChartEditorCommand
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
}
// If we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note.
if (this.events.length == 0 && this.notes.length >= 1)
{
var noteSelected = this.notes[0];
state.noteKindToPlace = noteSelected.kind;
// This code is here to parse note data that's not built as a struct for some reason.
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT);
}
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
}

View file

@ -56,6 +56,16 @@ class SetItemSelectionCommand implements ChartEditorCommand
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
}
// IF we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note.
if (this.events.length == 0 && this.notes.length >= 1)
{
var noteSelected = this.notes[0];
state.noteKindToPlace = noteSelected.kind;
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT);
}
state.noteDisplayDirty = true;
}

View file

@ -38,6 +38,7 @@ import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorFreeplayToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorNoteDataToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox;
import haxe.ui.containers.Frame;
import haxe.ui.containers.Grid;
@ -79,17 +80,16 @@ class ChartEditorToolboxHandler
switch (id)
{
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
onShowToolboxNoteData(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT:
cast(toolbox, ChartEditorBaseToolbox).refresh();
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
// TODO: Fix this.
// TODO: Make these better.
cast(toolbox, ChartEditorBaseToolbox).refresh();
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
onShowToolboxPlaytestProperties(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
cast(toolbox, ChartEditorBaseToolbox).refresh();
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
// TODO: Fix this.
cast(toolbox, ChartEditorBaseToolbox).refresh();
case ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT:
cast(toolbox, ChartEditorBaseToolbox).refresh();
@ -124,10 +124,6 @@ class ChartEditorToolboxHandler
switch (id)
{
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
onHideToolboxNoteData(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
onHideToolboxEventData(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
onHideToolboxPlaytestProperties(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
@ -196,7 +192,7 @@ class ChartEditorToolboxHandler
var toolbox:Null<CollapsibleDialog> = null;
switch (id)
{
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT:
toolbox = buildToolboxNoteDataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
toolbox = buildToolboxEventDataLayout(state);
@ -262,58 +258,13 @@ class ChartEditorToolboxHandler
static function buildToolboxNoteDataLayout(state:ChartEditorState):Null<CollapsibleDialog>
{
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
var toolbox:ChartEditorBaseToolbox = ChartEditorNoteDataToolbox.build(state);
if (toolbox == null) return null;
// Starting position.
toolbox.x = 75;
toolbox.y = 100;
toolbox.onDialogClosed = function(event:DialogEvent) {
state.menubarItemToggleToolboxNotes.selected = false;
}
var toolboxNotesNoteKind:Null<DropDown> = toolbox.findComponent('toolboxNotesNoteKind', DropDown);
if (toolboxNotesNoteKind == null) throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesNoteKind component.';
var toolboxNotesCustomKindLabel:Null<Label> = toolbox.findComponent('toolboxNotesCustomKindLabel', Label);
if (toolboxNotesCustomKindLabel == null)
throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesCustomKindLabel component.';
var toolboxNotesCustomKind:Null<TextField> = toolbox.findComponent('toolboxNotesCustomKind', TextField);
if (toolboxNotesCustomKind == null) throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesCustomKind component.';
toolboxNotesNoteKind.onChange = function(event:UIEvent) {
var isCustom:Bool = (event.data.id == '~CUSTOM~');
if (isCustom)
{
toolboxNotesCustomKindLabel.hidden = false;
toolboxNotesCustomKind.hidden = false;
state.noteKindToPlace = toolboxNotesCustomKind.text;
}
else
{
toolboxNotesCustomKindLabel.hidden = true;
toolboxNotesCustomKind.hidden = true;
state.noteKindToPlace = event.data.id;
}
}
toolboxNotesCustomKind.onChange = function(event:UIEvent) {
state.noteKindToPlace = toolboxNotesCustomKind.text;
}
return toolbox;
}
static function onShowToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}

View file

@ -0,0 +1,115 @@
package funkin.ui.debug.charting.toolboxes;
import haxe.ui.components.DropDown;
import haxe.ui.components.TextField;
import haxe.ui.events.UIEvent;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
/**
* The toolbox which allows modifying information like Note Kind.
*/
@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/note-data.xml"))
class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
{
var toolboxNotesNoteKind:DropDown;
var toolboxNotesCustomKind:TextField;
var _initializing:Bool = true;
public function new(chartEditorState2:ChartEditorState)
{
super(chartEditorState2);
initialize();
this.onDialogClosed = onClose;
this._initializing = false;
}
function onClose(event:UIEvent)
{
chartEditorState.menubarItemToggleToolboxNoteData.selected = false;
}
function initialize():Void
{
toolboxNotesNoteKind.onChange = function(event:UIEvent) {
var noteKind:Null<String> = event?.data?.id ?? null;
if (noteKind == '') noteKind = null;
trace('ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Note kind changed: $noteKind');
// Edit the note data to place.
if (noteKind == '~CUSTOM~')
{
showCustom();
toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
}
else
{
hideCustom();
chartEditorState.noteKindToPlace = noteKind;
toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
}
if (!_initializing && chartEditorState.currentNoteSelection.length > 0)
{
// Edit the note data of any selected notes.
for (note in chartEditorState.currentNoteSelection)
{
note.kind = chartEditorState.noteKindToPlace;
}
chartEditorState.saveDataDirty = true;
chartEditorState.noteDisplayDirty = true;
chartEditorState.notePreviewDirty = true;
}
};
var startingValueNoteKind = ChartEditorDropdowns.populateDropdownWithNoteKinds(toolboxNotesNoteKind, '');
toolboxNotesNoteKind.value = startingValueNoteKind;
toolboxNotesCustomKind.onChange = function(event:UIEvent) {
var customKind:Null<String> = event?.target?.text;
chartEditorState.noteKindToPlace = customKind;
if (chartEditorState.currentEventSelection.length > 0)
{
// Edit the note data of any selected notes.
for (note in chartEditorState.currentNoteSelection)
{
note.kind = chartEditorState.noteKindToPlace;
}
chartEditorState.saveDataDirty = true;
chartEditorState.noteDisplayDirty = true;
chartEditorState.notePreviewDirty = true;
}
};
toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
}
public override function refresh():Void
{
super.refresh();
toolboxNotesNoteKind.value = ChartEditorDropdowns.lookupNoteKind(chartEditorState.noteKindToPlace);
toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
}
function showCustom():Void
{
toolboxNotesCustomKindLabel.hidden = false;
toolboxNotesCustomKind.hidden = false;
}
function hideCustom():Void
{
toolboxNotesCustomKindLabel.hidden = true;
toolboxNotesCustomKind.hidden = true;
}
public static function build(chartEditorState:ChartEditorState):ChartEditorNoteDataToolbox
{
return new ChartEditorNoteDataToolbox(chartEditorState);
}
}

View file

@ -108,6 +108,55 @@ class ChartEditorDropdowns
return returnValue;
}
static final NOTE_KINDS:Map<String, String> = [
// Base
"" => "Default",
"~CUSTOM~" => "Custom",
// Weeks 1-7
"mom" => "Mom Sings (Week 5)",
"ugh" => "Ugh (Week 7)",
"hehPrettyGood" => "Heh, Pretty Good (Week 7)",
// Weekend 1
"weekend-1-lightcan" => "Light Can (2hot)",
"weekend-1-kickcan" => "Kick Can (2hot)",
"weekend-1-kneecan" => "Knee Can (2hot)",
"weekend-1-cockgun" => "Cock Gun (2hot)",
"weekend-1-firegun" => "Fire Gun (2hot)",
"weekend-1-punchlow" => "Punch Low (Blazin)",
"weekend-1-punchhigh" => "Punch High (Blazin)",
"weekend-1-punchlowblocked" => "Punch Low Blocked (Blazin)",
"weekend-1-punchhighblocked" => "Punch High Blocked (Blazin)",
"weekend-1-dodgelow" => "Dodge Low (Blazin)",
"weekend-1-blockhigh" => "Block High (Blazin)",
"weekend-1-fakeout" => "Fakeout (Blazin)",
];
public static function populateDropdownWithNoteKinds(dropDown:DropDown, startingKindId:String):DropDownEntry
{
dropDown.dataSource.clear();
var returnValue:DropDownEntry = lookupNoteKind('~CUSTOM');
for (noteKindId in NOTE_KINDS.keys())
{
var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Default';
var value:DropDownEntry = {id: noteKindId, text: noteKind};
if (startingKindId == noteKindId) returnValue = value;
dropDown.dataSource.add(value);
}
return returnValue;
}
public static function lookupNoteKind(noteKindId:Null<String>):DropDownEntry
{
if (noteKindId == null) return lookupNoteKind('');
if (!NOTE_KINDS.exists(noteKindId)) return {id: '~CUSTOM~', text: 'Custom'};
return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Default'};
}
/**
* Populate a dropdown with a list of song variations.
*/

View file

@ -20,6 +20,7 @@ class FileUtil
{
public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc");
public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip");
public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png");
public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo =
{
@ -31,6 +32,11 @@ class FileUtil
extension: 'zip',
label: 'ZIP Archive',
};
public static final FILE_EXTENSION_INFO_PNG:FileDialogExtensionInfo =
{
extension: 'png',
label: 'PNG Image',
};
/**
* Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected.
@ -639,6 +645,23 @@ class FileUtil
};
}
public static function openFolder(pathFolder:String)
{
#if windows
Sys.command('explorer', [pathFolder]);
#elseif mac
// mac could be fuckie with where the log folder is relative to the game file...
// if this comment is still here... it means it has NOT been verified on mac yet!
//
// FileUtil.hx note: this was originally used to open the logs specifically!
// thats why the above comment is there!
Sys.command('open', [pathFolder]);
#end
// TODO: implement linux
// some shit with xdg-open :thinking: emoji...
}
static function convertTypeFilter(typeFilter:Array<FileFilter>):String
{
var filter:String = null;

View file

@ -5,6 +5,12 @@ package funkin.util;
*/
class MathUtil
{
/**
* Euler's constant and the base of the natural logarithm.
* Math.E is not a constant in Haxe, so we'll just define it ourselves.
*/
public static final E:Float = 2.71828182845904523536;
/**
* Perform linear interpolation between the base and the target, based on the current framerate.
*/
@ -24,8 +30,44 @@ class MathUtil
* @param value The value to get the logarithm of.
* @return `log_base(value)`
*/
public static function logBase(base:Float, value:Float):Float
public static function logBase(base:Float, value:Float)
{
return Math.log(value) / Math.log(base);
}
/**
* @returns `2^x`
*/
public static function exp2(x:Float)
{
return Math.pow(2, x);
}
/**
* Linearly interpolate between two values.
* @param base The starting value, when `progress <= 0`.
* @param target The ending value, when `progress >= 1`.
* @param progress Value used to interpolate between `base` and `target`.
*/
public static function lerp(base:Float, target:Float, progress:Float)
{
return base + progress * (target - base);
}
/**
* Perform a framerate-independent linear interpolation between the base value and the target.
* @param current The current value.
* @param target The target value.
* @param elapsed The time elapsed since the last frame.
* @param duration The total duration of the interpolation. Nominal duration until remaining distance is less than `precision`.
* @param precision The target precision of the interpolation. Defaults to 1% of distance remaining.
* @see https://twitter.com/FreyaHolmer/status/1757918211679650262
*/
public static function smoothLerp(current:Float, target:Float, elapsed:Float, duration:Float, precision:Float = 1 / 100):Float
{
// var halfLife:Float = -duration / logBase(2, precision);
// lerp(current, target, 1 - exp2(-elapsed / halfLife));
return lerp(current, target, 1 - Math.pow(precision, elapsed / duration));
}
}

View file

@ -7,7 +7,7 @@ class HaxelibVersions
public static macro function getLibraryVersions():haxe.macro.Expr.ExprOf<Array<String>>
{
#if !display
return macro $v{formatHmmData(readHmmData())};
return macro $v{formatHmmData()};
#else
// `#if display` is used for code completion. In this case returning an
// empty string is good enough; We don't want to call functions on every hint.
@ -17,44 +17,33 @@ class HaxelibVersions
}
#if (macro)
static function readHmmData():hmm.HmmConfig
{
return hmm.HmmConfig.HmmConfigs.readHmmJsonOrThrow();
}
static function formatHmmData(hmmData:hmm.HmmConfig):Array<String>
@SuppressWarnings('checkstyle:Dynamic')
static function formatHmmData():Array<String>
{
var result:Array<String> = [];
for (library in hmmData.dependencies)
var hmmData:Dynamic = haxe.Json.parse(sys.io.File.getContent('hmm.json'));
var dependencies:Array<Dynamic> = cast hmmData.dependencies;
for (library in dependencies)
{
switch (library)
switch (library.type)
{
case Haxelib(name, version):
result.push('${name} haxelib(${o(version)})');
case Git(name, url, ref, dir):
result.push('${name} git(${url}/${o(dir, '')}:${o(ref)})');
case Mercurial(name, url, ref, dir):
result.push('${name} mercurial(${url}/${o(dir, '')}:${o(ref)})');
case Dev(name, path):
result.push('${name} dev(${path})');
case 'haxelib':
result.push('${library.name} haxelib(${library.version ?? 'None'})');
case 'git':
result.push('${library.name} git(${library.url}/${library.dir ?? ''}:${library.ref ?? 'None'}');
case 'mercurial':
result.push('${library.name} mercurial(${library.url}/${library.dir ?? ''}:${library.ref ?? 'None'})');
case 'dev':
result.push('${library.name} dev(${library.path})');
case ty:
throw 'Unhandled hmm library type ${ty}';
}
}
return result;
}
static function o(option:haxe.ds.Option<String>, defaultValue:String = 'None'):String
{
switch (option)
{
case Some(value):
return value;
case None:
return defaultValue;
}
}
static function readLibraryCurrentVersion(libraryName:String):String
{
var path = Path.join([Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.current']);

View file

@ -0,0 +1,320 @@
package funkin.util.plugins;
import flixel.FlxBasic;
import flixel.FlxCamera;
import flixel.FlxG;
import flixel.FlxState;
import flixel.graphics.FlxGraphic;
import flixel.input.keyboard.FlxKey;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxSignal;
import flixel.util.FlxTimer;
import funkin.graphics.FunkinSprite;
import funkin.input.Cursor;
import openfl.display.Bitmap;
import openfl.display.Sprite;
import openfl.display.BitmapData;
import openfl.display.PNGEncoderOptions;
import openfl.geom.Matrix;
import openfl.geom.Rectangle;
import openfl.utils.ByteArray;
import openfl.events.MouseEvent;
typedef ScreenshotPluginParams =
{
hotkeys:Array<FlxKey>,
?region:Rectangle,
shouldHideMouse:Bool,
flashColor:Null<FlxColor>,
fancyPreview:Bool,
};
/**
* What if `flixel.addons.plugin.screengrab.FlxScreenGrab` but it's better?
* TODO: Contribute this upstream.
*/
class ScreenshotPlugin extends FlxBasic
{
public static final SCREENSHOT_FOLDER = 'screenshots';
var _hotkeys:Array<FlxKey>;
var _region:Null<Rectangle>;
var _shouldHideMouse:Bool;
var _flashColor:Null<FlxColor>;
var _fancyPreview:Bool;
/**
* A signal fired before the screenshot is taken.
*/
public var onPreScreenshot(default, null):FlxTypedSignal<Void->Void>;
/**
* A signal fired after the screenshot is taken.
* @param bitmap The bitmap that was captured.
*/
public var onPostScreenshot(default, null):FlxTypedSignal<Bitmap->Void>;
public function new(params:ScreenshotPluginParams)
{
super();
_hotkeys = params.hotkeys;
_region = params.region ?? null;
_shouldHideMouse = params.shouldHideMouse;
_flashColor = params.flashColor;
_fancyPreview = params.fancyPreview;
onPreScreenshot = new FlxTypedSignal<Void->Void>();
onPostScreenshot = new FlxTypedSignal<Bitmap->Void>();
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (hasPressedScreenshot())
{
capture();
}
}
/**
* Initialize the screenshot plugin.
*/
public static function initialize():Void
{
FlxG.plugins.addPlugin(new ScreenshotPlugin(
{
flashColor: Preferences.flashingLights ? FlxColor.WHITE : null, // Was originally a black flash.
// TODO: Add a way to configure screenshots from the options menu.
hotkeys: [FlxKey.F3],
shouldHideMouse: false,
fancyPreview: true,
}));
}
public function hasPressedScreenshot():Bool
{
return PlayerSettings.player1.controls.SCREENSHOT;
}
public function updatePreferences():Void
{
_flashColor = Preferences.flashingLights ? FlxColor.WHITE : null;
}
/**
* Defines the region of the screen that should be captured.
* You don't need to call this method if you want to capture the entire screen, that's the default behavior.
*/
public function defineCaptureRegion(x:Int, y:Int, width:Int, height:Int):Void
{
_region = new Rectangle(x, y, width, height);
}
/**
* Capture the game screen as a bitmap.
*/
public function capture():Void
{
onPreScreenshot.dispatch();
var captureRegion = _region != null ? _region : new Rectangle(0, 0, FlxG.stage.stageWidth, FlxG.stage.stageHeight);
var wasMouseHidden = false;
if (_shouldHideMouse && FlxG.mouse.visible)
{
wasMouseHidden = true;
Cursor.hide();
}
// The actual work.
// var bitmap = new Bitmap(new BitmapData(Math.floor(captureRegion.width), Math.floor(captureRegion.height), true, 0x00000000)); // Create a transparent empty bitmap.
// var drawMatrix = new Matrix(1, 0, 0, 1, -captureRegion.x, -captureRegion.y); // Modifying this will scale or skew the bitmap.
// bitmap.bitmapData.draw(FlxG.stage, drawMatrix);
var bitmap = new Bitmap(BitmapData.fromImage(FlxG.stage.window.readPixels()));
if (wasMouseHidden)
{
Cursor.show();
}
// Save the bitmap to a file.
saveScreenshot(bitmap);
// Show some feedback.
showCaptureFeedback();
if (_fancyPreview)
{
showFancyPreview(bitmap);
}
onPostScreenshot.dispatch(bitmap);
}
final CAMERA_FLASH_DURATION = 0.25;
/**
* Visual (and audio?) feedback when a screenshot is taken.
*/
function showCaptureFeedback():Void
{
var flashBitmap = new Bitmap(new BitmapData(Std.int(FlxG.stage.width), Std.int(FlxG.stage.height), false, 0xFFFFFFFF));
var flashSpr = new Sprite();
flashSpr.addChild(flashBitmap);
FlxG.stage.addChild(flashSpr);
FlxTween.tween(flashSpr, {alpha: 0}, 0.15, {ease: FlxEase.quadOut, onComplete: _ -> FlxG.stage.removeChild(flashSpr)});
}
static final PREVIEW_INITIAL_DELAY = 0.25; // How long before the preview starts fading in.
static final PREVIEW_FADE_IN_DURATION = 0.3; // How long the preview takes to fade in.
static final PREVIEW_FADE_OUT_DELAY = 1.25; // How long the preview stays on screen.
static final PREVIEW_FADE_OUT_DURATION = 0.3; // How long the preview takes to fade out.
function showFancyPreview(bitmap:Bitmap):Void
{
// ermmm stealing this??
var wasMouseHidden = false;
if (!FlxG.mouse.visible)
{
wasMouseHidden = true;
Cursor.show();
}
// so that it doesnt change the alpha when tweening in/out
var changingAlpha:Bool = false;
// fuck it, cursed locally scoped functions, purely because im lazy
// (and so we can check changingAlpha, which is locally scoped.... because I'm lazy...)
var onHover = function(e:MouseEvent) {
if (!changingAlpha) e.target.alpha = 0.6;
};
var onHoverOut = function(e:MouseEvent) {
if (!changingAlpha) e.target.alpha = 1;
}
var scale:Float = 0.25;
var w:Int = Std.int(bitmap.bitmapData.width * scale);
var h:Int = Std.int(bitmap.bitmapData.height * scale);
var preview:BitmapData = new BitmapData(w, h, true);
var matrix:openfl.geom.Matrix = new openfl.geom.Matrix();
matrix.scale(scale, scale);
preview.draw(bitmap.bitmapData, matrix);
// used for movement + button stuff
var previewSprite = new Sprite();
previewSprite.buttonMode = true;
previewSprite.addEventListener(MouseEvent.MOUSE_DOWN, openScreenshotsFolder);
previewSprite.addEventListener(MouseEvent.MOUSE_OVER, onHover);
previewSprite.addEventListener(MouseEvent.MOUSE_OUT, onHoverOut);
FlxG.stage.addChild(previewSprite);
previewSprite.alpha = 0.0;
previewSprite.y -= 10;
var previewBitmap = new Bitmap(preview);
previewSprite.addChild(previewBitmap);
// Wait to fade in.
new FlxTimer().start(PREVIEW_INITIAL_DELAY, function(_) {
// Fade in.
changingAlpha = true;
FlxTween.tween(previewSprite, {alpha: 1.0, y: 0}, PREVIEW_FADE_IN_DURATION,
{
ease: FlxEase.quartOut,
onComplete: function(_) {
changingAlpha = false;
// Wait to fade out.
new FlxTimer().start(PREVIEW_FADE_OUT_DELAY, function(_) {
changingAlpha = true;
// Fade out.
FlxTween.tween(previewSprite, {alpha: 0.0, y: 10}, PREVIEW_FADE_OUT_DURATION,
{
ease: FlxEase.quartInOut,
onComplete: function(_) {
if (wasMouseHidden)
{
Cursor.hide();
}
previewSprite.removeEventListener(MouseEvent.MOUSE_DOWN, openScreenshotsFolder);
previewSprite.removeEventListener(MouseEvent.MOUSE_OVER, onHover);
previewSprite.removeEventListener(MouseEvent.MOUSE_OUT, onHoverOut);
FlxG.stage.removeChild(previewSprite);
}
});
});
}
});
});
}
function openScreenshotsFolder(e:MouseEvent):Void
{
FileUtil.openFolder(SCREENSHOT_FOLDER);
}
static function getCurrentState():FlxState
{
var state = FlxG.state;
while (state.subState != null)
{
state = state.subState;
}
return state;
}
static function getScreenshotPath():String
{
return '$SCREENSHOT_FOLDER/screenshot-${DateUtil.generateTimestamp()}.png';
}
static function makeScreenshotPath():Void
{
FileUtil.createDirIfNotExists(SCREENSHOT_FOLDER);
}
/**
* Convert a Bitmap to a PNG ByteArray to save to a file.
*/
static function encodePNG(bitmap:Bitmap):ByteArray
{
return bitmap.bitmapData.encode(bitmap.bitmapData.rect, new PNGEncoderOptions());
}
/**
* Save the generated bitmap to a file.
* @param bitmap The bitmap to save.
*/
static function saveScreenshot(bitmap:Bitmap)
{
makeScreenshotPath();
var targetPath:String = getScreenshotPath();
var pngData = encodePNG(bitmap);
if (pngData == null)
{
trace('[WARN] Failed to encode PNG data.');
return;
}
else
{
trace('Saving screenshot to: ' + targetPath);
// TODO: Make this work on browser.
FileUtil.writeBytesToPath(targetPath, pngData);
}
}
}

View file

@ -0,0 +1,34 @@
package funkin.util.plugins;
import flixel.FlxBasic;
/**
* Handles volume control in a way that is compatible with alternate control schemes.
*/
class VolumePlugin extends FlxBasic
{
public function new()
{
super();
}
public static function initialize()
{
FlxG.plugins.addPlugin(new VolumePlugin());
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
var isHaxeUIFocused:Bool = haxe.ui.focus.FocusManager.instance?.focus != null;
if (!isHaxeUIFocused)
{
// Rebindable volume keys.
if (PlayerSettings.player1.controls.VOLUME_MUTE) FlxG.sound.toggleMuted();
else if (PlayerSettings.player1.controls.VOLUME_UP) FlxG.sound.changeVolume(0.1);
else if (PlayerSettings.player1.controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1);
}
}
}