Difficulty selector and player previews

This commit is contained in:
EliteMasterEric 2022-12-17 15:19:42 -05:00
parent a2d803cc83
commit b3b7fb49c2
27 changed files with 1539 additions and 713 deletions

54
docs/style-guide.md Normal file
View file

@ -0,0 +1,54 @@
# Funkin' Repo Code Style Guide
This short document is designed to give a run-down on how code should be formatted to maintain a consistent style throughout, making the repo easier to maintain.
## Notes on IDEs and Extensions
The Visual Studio Code IDE is highly recommended, as this repo contains various configuration that works with VSCode extensions to automatically style certain things for you.
VSCode is also the only IDE that has any good tools for Haxe so yeah.
## Whitespace and Indentation
The Haxe extension of VSCode will use the repo's `hxformat.json` to tell VSCode how to format the code files of the repo. Enabling VSCode's "Format on Save" feature is recommended.
## Variable and Function Names
It is recommended to name variables and functions with descriptive titles, in lowerCamelCase. There is no penalty for giving a variable a longer name as long as it isn't excessive.
## Code Comments
The CodeDox extension for VSCode provides extensive support for JavaDoc-style code comments, and these should be used for public functions wherever possible to ensure the usage of each function is clear.
Example:
```
/**
* Finds the largest deviation from the desired time inside this VoicesGroup.
*
* @param targetTime The time to check against.
* If none is provided, it checks the time of all members against the first member of this VoicesGroup.
* @return The largest deviation from the target time found.
*/
public function checkSyncError(?targetTime:Float):Float
```
## License Headers
Do not include headers specifying code license on individual files in the repo, since the main `LICENSE.md` file covers all of them.
## Imports
Imports should be placed in a single group, in alphabetical order, at the top of the code file. The exception is conditional imports (using compile defines), which should be placed at the end of the list (and sorted alphabetically where possible).
Example:
```
import haxe.format.JsonParser;
import openfl.Assets;
import openfl.geom.Matrix;
import openfl.geom.Matrix3D;
#if sys
import funkin.io.FileUtil;
import sys.io.File;
#end
```

View file

@ -42,14 +42,14 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "fc8d656b",
"ref": "82fde3a",
"url": "https://github.com/haxeui/haxeui-core/"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "80941a7",
"ref": "f7b403a",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{

View file

@ -116,7 +116,20 @@ class Conductor
public static var offset:Float = 0;
// TODO: Add code to update this.
public static var beatsPerMeasure:Int = 4;
public static var beatsPerMeasure(get, null):Int;
static function get_beatsPerMeasure():Int
{
return timeSignatureNumerator;
}
public static var stepsPerMeasure(get, null):Int;
static function get_stepsPerMeasure():Int
{
// Is this always x4?
return timeSignatureNumerator * 4;
}
private function new()
{
@ -151,7 +164,10 @@ class Conductor
*/
public static function forceBPM(?bpm:Float = null)
{
trace('[CONDUCTOR] Forcing BPM to ' + bpm);
if (bpm != null)
trace('[CONDUCTOR] Forcing BPM to ' + bpm);
else
trace('[CONDUCTOR] Resetting BPM to default');
Conductor.bpmOverride = bpm;
}

View file

@ -351,5 +351,5 @@ class MultiCallback
return fired.copy();
public function getUnfired()
return [for (id in unfired.keys()) id];
return unfired.array();
}

View file

@ -60,6 +60,7 @@ class MusicBeatState extends FlxUIState
{
super.update(elapsed);
// Emergency exit button.
if (FlxG.keys.justPressed.F4)
FlxG.switchState(new MainMenuState());
@ -67,7 +68,7 @@ class MusicBeatState extends FlxUIState
if (FlxG.keys.justPressed.F5)
debug_refreshModules();
// ` / ~
// ` / ~ to open the debug menu.
if (FlxG.keys.justPressed.GRAVEACCENT)
{
// TODO: Does this break anything?
@ -76,7 +77,9 @@ class MusicBeatState extends FlxUIState
FlxG.state.openSubState(new DebugMenuSubState());
}
// Display Conductor info in the watch window.
FlxG.watch.addQuick("songPos", Conductor.songPosition);
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
FlxG.watch.addQuick("bpm", Conductor.bpm);
dispatchEvent(new UpdateScriptEvent(elapsed));

View file

@ -3,4 +3,11 @@
import funkin.Paths;
import flixel.FlxG; // This one in particular causes a compile error if you're using macros.
// These are great.
using Lambda;
using StringTools;
using funkin.util.tools.IteratorTools;
using funkin.util.tools.StringTools;
#end

View file

@ -6,8 +6,6 @@ import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.module.Module;
import funkin.modding.module.ScriptedModule;
using funkin.util.IteratorTools;
/**
* Utility functions for loading and manipulating active modules.
*/

View file

@ -96,8 +96,9 @@ class BaseCharacter extends Bopper
if (animOffsets == value)
return value;
var xDiff = animOffsets[0] - value[0];
var yDiff = animOffsets[1] - value[1];
// Make sure animOffets are halved when scale is 0.5.
var xDiff = (animOffsets[0] * this.scale.x) - value[0];
var yDiff = (animOffsets[1] * this.scale.y) - value[1];
// Call the super function so that camera focus point is not affected.
super.set_x(this.x + xDiff);

View file

@ -225,7 +225,7 @@ class CharacterDataParser
public static function listCharacterIds():Array<String>
{
return [for (x in characterCache.keys()) x];
return characterCache.keys().array();
}
static function clearCharacterCache():Void

View file

@ -127,8 +127,11 @@ class Song // implements IPlayStateScriptedClass
/**
* 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)
diffId = difficulties.keys().array()[0];
return difficulties.get(diffId);
}
@ -219,7 +222,7 @@ class SongDifficulty
public function getPlayableChars():Array<String>
{
return [for (i in chars.keys()) i];
return chars.keys().array();
}
public function getEvents():Array<SongEvent>

View file

@ -116,7 +116,7 @@ class SongDataParser
public static function listSongIds():Array<String>
{
return [for (x in songCache.keys()) x];
return songCache.keys().array();
}
public static function parseSongMetadata(songId:String):Array<SongMetadata>
@ -261,9 +261,24 @@ abstract SongMetadata(RawSongMetadata)
noteSkin: 'Normal'
},
generatedBy: SongValidator.DEFAULT_GENERATEDBY,
// Variation ID.
variation: variation
};
}
public function clone(?newVariation:String = null):SongMetadata {
var result = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
result.version = this.version;
result.timeFormat = this.timeFormat;
result.divisions = this.divisions;
result.timeChanges = this.timeChanges;
result.loop = this.loop;
result.playData = this.playData;
result.generatedBy = this.generatedBy;
return result;
}
}
typedef SongPlayData =

View file

@ -135,7 +135,7 @@ class SongDataUtils
trace('Read ' + notesString.length + ' characters from clipboard.');
var notes:Array<SongNoteData> = SerializerUtil.fromJSON(notesString);
var notes:Array<SongNoteData> = notesString.parseJSON();
if (notes == null)
{

View file

@ -25,7 +25,7 @@ class SongSerializer
if (fileData == null)
return null;
var songChartData:SongChartData = SerializerUtil.fromJSON(fileData);
var songChartData:SongChartData = fileData.parseJSON();
return songChartData;
}
@ -41,7 +41,7 @@ class SongSerializer
if (fileData == null)
return null;
var songMetadata:SongMetadata = SerializerUtil.fromJSON(fileData);
var songMetadata:SongMetadata = fileData.parseJSON();
return songMetadata;
}
@ -59,7 +59,7 @@ class SongSerializer
if (data == null)
return;
var songChartData:SongChartData = SerializerUtil.fromJSON(data);
var songChartData:SongChartData = data.parseJSON();
if (songChartData != null)
callback(songChartData);
@ -79,7 +79,7 @@ class SongSerializer
if (data == null)
return;
var songMetadata:SongMetadata = SerializerUtil.fromJSON(data);
var songMetadata:SongMetadata = data.parseJSON();
if (songMetadata != null)
callback(songMetadata);

View file

@ -6,6 +6,9 @@ import flixel.util.FlxTimer;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.events.ScriptEvent;
typedef AnimationFrameCallback = String->Int->Int->Void;
typedef AnimationFinishedCallback = String->Void;
/**
* A Bopper is a stage prop which plays a dance animation.
* Y'know, a thingie that bops. A bopper.
@ -68,8 +71,8 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
this.x += xDiff;
this.y += yDiff;
return animOffsets = value;
}
private var animOffsets(default, set):Array<Float> = [0, 0];
@ -113,6 +116,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
*/
function onAnimationFinished(name:String)
{
// TODO: Can we make a system of like, animation priority or something?
if (!canPlayOtherAnims)
{
canPlayOtherAnims = true;
@ -131,10 +135,10 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
function onAnimationFrame(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1)
{
// 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()`.
// Try not to do anything expensive here, it runs many times a second.
// Sometimes this gets called with empty values? IDK why but adding defaults keeps it from crashing.
}
/**

View file

@ -143,7 +143,7 @@ class StageDataParser
public static function listStageIds():Array<String>
{
return [for (x in stageCache.keys()) x];
return stageCache.keys().array();
}
static function loadStageFile(stagePath:String):String

View file

@ -1,17 +1,17 @@
package funkin.ui.debug.charting;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.character.BaseCharacter;
import haxe.ui.components.Label;
import haxe.ui.events.MouseEvent;
import funkin.play.song.SongData.SongPlayableChar;
import flixel.FlxSprite;
import flixel.util.FlxTimer;
import funkin.input.Cursor;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.SongData.SongDataParser;
import funkin.play.song.SongData.SongPlayableChar;
import funkin.play.song.SongData.SongTimeChange;
import haxe.ui.components.Button;
import haxe.ui.components.DropDown;
import haxe.ui.components.Image;
import haxe.ui.components.Label;
import haxe.ui.components.Link;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.TextField;
@ -22,6 +22,7 @@ import haxe.ui.containers.properties.Property;
import haxe.ui.containers.properties.PropertyGrid;
import haxe.ui.containers.properties.PropertyGroup;
import haxe.ui.containers.VBox;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
using Lambda;
@ -110,6 +111,8 @@ class ChartEditorDialogHandler
}
// TODO: Get the list of songs and insert them as links into the "Create From Song" section.
/*
var linkTemplateDadBattle:Link = dialog.findComponent('splashTemplateDadBattle', Link);
linkTemplateDadBattle.onClick = (_event) ->
{
@ -126,9 +129,33 @@ class ChartEditorDialogHandler
// Load song from template
state.loadSongAsTemplate('bopeebo');
}
*/
var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox);
var songList:Array<String> = SongDataParser.listSongIds();
for (targetSongId in songList) {
var songData = SongDataParser.fetchSong(targetSongId);
if (songData == null)
continue;
var songName = songData.getDifficulty().songName;
var linkTemplateSong:Link = new Link();
linkTemplateSong.text = songName;
linkTemplateSong.onClick = (_event) ->
{
dialog.hideDialog(DialogButton.CANCEL);
// Load song from template
state.loadSongAsTemplate(targetSongId);
}
splashTemplateContainer.addComponent(linkTemplateSong);
}
return dialog;
}

View file

@ -1,12 +1,8 @@
package funkin.ui.debug.charting;
import funkin.play.song.SongData.SongDataParser;
import funkin.play.song.Song;
import lime.media.AudioBuffer;
import funkin.input.Cursor;
import flixel.FlxSprite;
import flixel.addons.display.FlxSliceSprite;
import flixel.addons.display.FlxTiledSprite;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
@ -15,8 +11,14 @@ import flixel.util.FlxColor;
import flixel.util.FlxSort;
import flixel.util.FlxTimer;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.VocalGroup;
import funkin.input.Cursor;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.HealthIcon;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongChartData;
import funkin.play.song.SongData.SongDataParser;
import funkin.play.song.SongData.SongEventData;
import funkin.play.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongNoteData;
@ -25,24 +27,28 @@ import funkin.play.song.SongSerializer;
import funkin.ui.debug.charting.ChartEditorCommand;
import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme;
import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode;
import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.ui.haxeui.HaxeUIState;
import funkin.util.Constants;
import funkin.util.FileUtil;
import funkin.util.SerializerUtil;
import haxe.ui.components.Button;
import haxe.ui.components.CheckBox;
import haxe.ui.components.Label;
import haxe.ui.components.Slider;
import haxe.ui.containers.SideBar;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
import haxe.ui.containers.dialogs.Dialog;
import haxe.ui.containers.dialogs.MessageBox;
import haxe.ui.containers.menus.MenuCheckBox;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.containers.SideBar;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
import haxe.ui.core.Component;
import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import lime.media.AudioBuffer;
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
@ -77,7 +83,11 @@ class ChartEditorState extends HaxeUIState
static final CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT = Paths.ui('chart-editor/toolbox/tools');
static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT = Paths.ui('chart-editor/toolbox/notedata');
static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT = Paths.ui('chart-editor/toolbox/eventdata');
static final CHART_EDITOR_TOOLBOX_SONGDATA_LAYOUT = Paths.ui('chart-editor/toolbox/songdata');
static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT = Paths.ui('chart-editor/toolbox/metadata');
static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT = Paths.ui('chart-editor/toolbox/difficulty');
static final CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT = Paths.ui('chart-editor/toolbox/characters');
static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/player-preview');
static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/opponent-preview');
// The base grid size for the chart editor.
public static final GRID_SIZE:Int = 40;
@ -214,32 +224,47 @@ class ChartEditorState extends HaxeUIState
/**
* songLength, converted to steps.
* TODO: Handle BPM changes.
*/
var songLengthInSteps(get, null):Float;
var songLengthInSteps(get, set):Float;
function get_songLengthInSteps():Float
{
return songLengthInPixels / GRID_SIZE;
}
function set_songLengthInSteps(value:Float):Float
{
songLengthInPixels = Std.int(value * GRID_SIZE);
return value;
}
/**
* songLength, converted to milliseconds.
* TODO: Handle BPM changes.
*/
var songLengthInMs(get, null):Float;
var songLengthInMs(get, set):Float;
function get_songLengthInMs():Float
{
return songLengthInSteps * Conductor.stepCrochet;
}
function set_songLengthInMs(value:Float):Float
{
songLengthInSteps = Conductor.getTimeInSteps(audioInstTrack.length);
return value;
}
var currentTheme(default, set):ChartEditorTheme = null;
function set_currentTheme(value:ChartEditorTheme):ChartEditorTheme
{
if (value == null || value == currentTheme)
return currentTheme;
currentTheme = value;
ChartEditorThemeHandler.updateTheme(this);
return value;
}
@ -262,7 +287,6 @@ class ChartEditorState extends HaxeUIState
/**
* The note kind to use for notes being placed in the chart. Defaults to `''`.
* Use the input in the sidebar to change this.
*/
var selectedNoteKind:String = '';
@ -276,6 +300,16 @@ class ChartEditorState extends HaxeUIState
*/
var currentToolMode:ChartEditorToolMode = ChartEditorToolMode.Select;
/**
* The character sprite in the Player Preview window.
*/
var currentPlayerCharacterPlayer:CharacterPlayer = null;
/**
* The character sprite in the Opponent Preview window.
*/
var currentOpponentCharacterPlayer:CharacterPlayer = null;
/**
* Whether the current view is in downscroll mode.
*/
@ -402,11 +436,17 @@ class ChartEditorState extends HaxeUIState
var notePreviewDirty:Bool = true;
/**
* Whether the difficulty tree view in the sidebar has been modified and needs to be updated.
* Whether the difficulty tree view in the toolbox has been modified and needs to be updated.
* This happens when we add/remove difficulties.
*/
var difficultySelectDirty:Bool = true;
/**
* Whether the character select view in the toolbox has been modified and needs to be updated.
* This happens when we add/remove characters.
*/
var characterSelectDirty:Bool = true;
var isInPlaytestMode:Bool = false;
/**
@ -468,9 +508,17 @@ class ChartEditorState extends HaxeUIState
/**
* The audio track for the vocals.
* TODO: Replace with a VocalSoundGroup.
*/
var audioVocalTrackGroup:VoicesGroup;
var audioVocalTrackGroup:VocalGroup;
/**
* A map of the audio tracks for each character's vocals.
* - Keys are the character IDs.
* - Values are the FlxSound objects to play that character's vocals.
*
* When switching characters, the elements of the VocalGroup will be swapped to match the new character.
*/
var audioVocalTracks:Map<String, FlxSound> = new Map<String, FlxSound>();
/**
* CHART DATA
@ -661,6 +709,13 @@ class ChartEditorState extends HaxeUIState
return currentSongMetadata.songName = value;
}
var currentSongId(get, null):String;
function get_currentSongId():String
{
return currentSongName.toLowerKebabCase();
}
var currentSongArtist(get, set):String;
function get_currentSongArtist():String
@ -811,7 +866,7 @@ class ChartEditorState extends HaxeUIState
// Initialize the song chart data.
songChartData = new Map<String, SongChartData>();
audioVocalTrackGroup = new VoicesGroup();
audioVocalTrackGroup = new VocalGroup();
}
/**
@ -839,7 +894,7 @@ class ChartEditorState extends HaxeUIState
add(gridTiledSprite);
gridGhostNote = new ChartEditorNoteSprite(this);
gridGhostNote.alpha = 0.8;
gridGhostNote.alpha = 0.6;
gridGhostNote.noteData = new SongNoteData(-1, -1, 0, "");
gridGhostNote.visible = false;
add(gridGhostNote);
@ -947,17 +1002,19 @@ class ChartEditorState extends HaxeUIState
*/
}
var playbarHeadLayout:Component;
function buildAdditionalUI():Void
{
notifBar = cast buildComponent(CHART_EDITOR_NOTIFBAR_LAYOUT);
add(notifBar);
var playbarHeadLayout:Component = buildComponent(CHART_EDITOR_PLAYBARHEAD_LAYOUT);
playbarHeadLayout = buildComponent(CHART_EDITOR_PLAYBARHEAD_LAYOUT);
playbarHeadLayout.width = FlxG.width;
playbarHeadLayout.width = FlxG.width - 8;
playbarHeadLayout.height = 10;
playbarHeadLayout.x = 0;
playbarHeadLayout.x = 4;
playbarHeadLayout.y = FlxG.height - 48 - 8;
playbarHead = playbarHeadLayout.findComponent('playbarHead', Slider);
@ -1018,6 +1075,7 @@ class ChartEditorState extends HaxeUIState
// Add functionality to the menu items.
addUIClickListener('menubarItemNewChart', (event:MouseEvent) -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
addUIClickListener('menubarItemSaveChartAs', (event:MouseEvent) -> exportAllSongData());
addUIClickListener('menubarItemLoadInst', (event:MouseEvent) -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
addUIClickListener('menubarItemUndo', (event:MouseEvent) -> undoLastCommand());
@ -1075,37 +1133,43 @@ class ChartEditorState extends HaxeUIState
addUIClickListener('menubarItemUserGuide', (event:MouseEvent) -> ChartEditorDialogHandler.openUserGuideDialog(this));
addUIChangeListener('menubarItemToggleSidebar', (event:UIEvent) ->
{
var sidebar:Component = findComponent('sidebar', Component);
sidebar.visible = event.value;
});
setUISelected('menubarItemToggleSidebar', true);
addUIChangeListener('menubarItemDownscroll', (event:UIEvent) ->
{
isViewDownscroll = event.value;
});
setUISelected('menubarItemDownscroll', isViewDownscroll);
setUICheckboxSelected('menubarItemDownscroll', isViewDownscroll);
addUIChangeListener('menuBarItemThemeLight', (event:UIEvent) ->
{
if (event.target.value)
currentTheme = ChartEditorTheme.Light;
});
setUICheckboxSelected('menuBarItemThemeLight', currentTheme == ChartEditorTheme.Light);
addUIChangeListener('menuBarItemThemeDark', (event:UIEvent) ->
{
if (event.target.value)
currentTheme = ChartEditorTheme.Dark;
});
setUICheckboxSelected('menuBarItemThemeDark', currentTheme == ChartEditorTheme.Dark);
addUIChangeListener('menubarItemMetronomeEnabled', (event:UIEvent) ->
{
shouldPlayMetronome = event.value;
});
setUISelected('menubarItemMetronomeEnabled', shouldPlayMetronome);
setUICheckboxSelected('menubarItemMetronomeEnabled', shouldPlayMetronome);
addUIChangeListener('menubarItemPlayerHitsounds', (event:UIEvent) ->
{
hitsoundsEnabledPlayer = event.value;
});
setUISelected('menubarItemPlayerHitsounds', hitsoundsEnabledPlayer);
setUICheckboxSelected('menubarItemPlayerHitsounds', hitsoundsEnabledPlayer);
addUIChangeListener('menubarItemOpponentHitsounds', (event:UIEvent) ->
{
hitsoundsEnabledOpponent = event.value;
});
setUISelected('menubarItemOpponentHitsounds', hitsoundsEnabledOpponent);
setUICheckboxSelected('menubarItemOpponentHitsounds', hitsoundsEnabledOpponent);
var instVolumeLabel:Label = findComponent('menubarLabelVolumeInstrumental', Label);
addUIChangeListener('menubarItemVolumeInstrumental', (event:UIEvent) ->
@ -1142,8 +1206,7 @@ class ChartEditorState extends HaxeUIState
{
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT, event.value);
});
setUISelected('menubarItemToggleToolboxTools', true);
// setUICheckboxSelected('menubarItemToggleToolboxTools', true);
addUIChangeListener('menubarItemToggleToolboxNotes', (event:UIEvent) ->
{
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value);
@ -1152,66 +1215,34 @@ class ChartEditorState extends HaxeUIState
{
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxSong', (event:UIEvent) ->
addUIChangeListener('menubarItemToggleToolboxDifficulty', (event:UIEvent) ->
{
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_SONGDATA_LAYOUT, event.value);
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxMetadata', (event:UIEvent) ->
{
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxCharacters', (event:UIEvent) ->
{
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxPlayerPreview', (event:UIEvent) ->
{
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxOpponentPreview', (event:UIEvent) ->
{
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value);
});
addUIClickListener('sidebarSaveMetadata', (event:MouseEvent) ->
{
// Save metadata for current variation.
SongSerializer.exportSongMetadata(currentSongMetadata);
});
addUIClickListener('sidebarSaveChart', (event:MouseEvent) ->
{
// Save chart data for current variation.
SongSerializer.exportSongChartData(currentSongChartData);
});
addUIClickListener('sidebarLoadMetadata', (event:MouseEvent) ->
{
// Replace metadata for current variation.
SongSerializer.importSongMetadataAsync(function(songMetadata:SongMetadata)
{
currentSongMetadata = songMetadata;
});
});
addUIClickListener('sidebarLoadChart', (event:MouseEvent) ->
{
// Replace chart data for current variation.
SongSerializer.importSongChartDataAsync(function(songChartData:SongChartData)
{
currentSongChartData = songChartData;
noteDisplayDirty = true;
});
});
addUIChangeListener('sidebarSongName', (event:UIEvent) ->
{
// Set song name (for current variation)
currentSongName = event.value;
});
setUIValue('sidebarSongName', currentSongName);
addUIChangeListener('sidebarSongArtist', (event:UIEvent) ->
{
currentSongArtist = event.value;
});
setUIValue('sidebarSongArtist', currentSongArtist);
addUIChangeListener('sidebarStage', (event:UIEvent) ->
{
currentSongStage = event.value;
});
setUIValue('sidebarStage', currentSongStage);
addUIChangeListener('sidebarNoteSkin', (event:UIEvent) ->
{
currentSongNoteSkin = event.value;
});
setUIValue('sidebarNoteSkin', currentSongNoteSkin);
// TODO: Pass specific HaxeUI components to add context menus to them.
registerContextMenu(null, Paths.ui('chart-editor/context/test'));
}
public override function update(elapsed:Float)
{
// dispatchEvent gets called here.
super.update(elapsed);
FlxG.mouse.visible = true;
@ -1222,12 +1253,12 @@ class ChartEditorState extends HaxeUIState
// These ones only happen if the modal dialog is not open.
handleScrollKeybinds();
handleZoom();
handleSnap();
// handleZoom();
// handleSnap();
handleCursor();
handleMenubar();
handleSidebar();
handleToolboxes();
handlePlaybar();
handlePlayhead();
@ -1247,6 +1278,16 @@ class ChartEditorState extends HaxeUIState
ChartEditorDialogHandler.openWelcomeDialog(this, true);
}
if (FlxG.keys.justPressed.W)
{
difficultySelectDirty = true;
}
if (FlxG.keys.justPressed.E)
{
currentSongMetadata.timeChanges[0].timeSignatureNum = (currentSongMetadata.timeChanges[0].timeSignatureNum == 4 ? 3 : 4);
}
// Right align the BF health icon.
// Base X position to the right of the grid.
@ -1261,6 +1302,7 @@ class ChartEditorState extends HaxeUIState
*/
override function beatHit():Bool
{
// dispatchEvent gets called here.
if (!super.beatHit())
return false;
@ -1277,6 +1319,7 @@ class ChartEditorState extends HaxeUIState
*/
override function stepHit():Bool
{
// dispatchEvent gets called here.
if (!super.stepHit())
return false;
@ -1757,7 +1800,7 @@ class ChartEditorState extends HaxeUIState
if (cursorColumn == eventColumn)
{
// Create an event and place it in the chart.
// TODO: Allow configuring the event to place from the sidebar.
// TODO: Allow configuring the event to place.
var newEventData:SongEventData = new SongEventData(cursorMs, "test", {});
performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL));
@ -1810,16 +1853,12 @@ class ChartEditorState extends HaxeUIState
// Indicate that we can pla
gridGhostNote.visible = (cursorColumn != eventColumn);
if (cursorColumn != gridGhostNote.noteData.data || selectedNoteKind != gridGhostNote.noteData.kind) {
if (cursorColumn != gridGhostNote.noteData.data || selectedNoteKind != gridGhostNote.noteData.kind)
{
gridGhostNote.noteData.kind = selectedNoteKind;
gridGhostNote.noteData.data = cursorColumn;
gridGhostNote.playNoteAnimation();
}
FlxG.watch.addQuick("cursorY", cursorY);
FlxG.watch.addQuick("cursorFractionalStep", cursorFractionalStep);
FlxG.watch.addQuick("cursorStep", cursorStep);
FlxG.watch.addQuick("cursorMs", cursorMs);
gridGhostNote.noteData.time = cursorMs;
gridGhostNote.updateNotePosition(renderedNotes);
@ -2004,6 +2043,10 @@ class ChartEditorState extends HaxeUIState
*/
function handlePlaybar()
{
// Make sure the playbar is never nudged out of the correct spot.
playbarHeadLayout.x = 4;
playbarHeadLayout.y = FlxG.height - 48 - 8;
var songPos = Conductor.songPosition;
var songRemaining = songLengthInMs - songPos;
@ -2140,9 +2183,6 @@ class ChartEditorState extends HaxeUIState
*/
function handleViewKeybinds()
{
// B = Toggle Sidebar
if (FlxG.keys.justPressed.B)
toggleSidebar();
}
/**
@ -2155,68 +2195,129 @@ class ChartEditorState extends HaxeUIState
ChartEditorDialogHandler.openUserGuideDialog(this);
}
function handleSidebar()
function handleToolboxes()
{
handleDifficultyToolbox();
handlePlayerPreviewToolbox();
handleOpponentPreviewToolbox();
}
function handleDifficultyToolbox()
{
if (difficultySelectDirty)
{
difficultySelectDirty = false;
// Manage the Select Difficulty tree view.
var treeView:TreeView = findComponent('sidebarDifficulties');
var difficultyToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
if (difficultyToolbox == null)
return;
if (treeView != null)
var treeView:TreeView = difficultyToolbox.findComponent('difficultyToolboxTree');
if (treeView == null)
return;
// Clear the tree view so we can rebuild it.
treeView.clearNodes();
var treeSong = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: "haxeui-core/styles/default/haxeui_tiny.png"});
treeSong.expanded = true;
for (curVariation in availableVariations)
{
// Clear the tree view so we can rebuild it.
treeView.clearNodes();
var variationMetadata:SongMetadata = songMetadata.get(curVariation);
var treeSong = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: "haxeui-core/styles/default/haxeui_tiny.png"});
treeSong.expanded = true;
var treeVariationDefault = treeSong.addNode({
id: 'stv_variation_default',
text: "V: Default",
var treeVariation = treeSong.addNode({
id: 'stv_variation_$curVariation',
text: 'V: ${curVariation.toTitleCase()}',
// icon: "haxeui-core/styles/default/haxeui_tiny.png"
});
treeVariationDefault.expanded = true;
treeVariation.expanded = true;
var treeDifficultyEasy = treeVariationDefault.addNode({
id: 'stv_difficulty_default_easy',
text: "D: Easy",
// icon: "haxeui-core/styles/default/haxeui_tiny.png"
});
var treeDifficultyNormal = treeVariationDefault.addNode({
id: 'stv_difficulty_default_normal',
text: "D: Normal",
// icon: "haxeui-core/styles/default/haxeui_tiny.png"
});
var treeDifficultyHard = treeVariationDefault.addNode({
id: 'stv_difficulty_default_hard',
text: "D: Hard",
// icon: "haxeui-core/styles/default/haxeui_tiny.png"
});
var difficultyList = variationMetadata.playData.difficulties;
var treeVariationErect = treeSong.addNode({
id: 'stv_variation_erect',
text: "V: Erect",
// icon: "haxeui-core/styles/default/haxeui_tiny.png"
});
treeVariationErect.expanded = true;
for (difficulty in difficultyList)
{
var treeDifficulty = treeVariation.addNode({
id: 'stv_difficulty_${curVariation}_$difficulty',
text: 'D: ${difficulty.toTitleCase()}',
// icon: "haxeui-core/styles/default/haxeui_tiny.png"
});
}
}
var treeDifficultyErect = treeVariationErect.addNode({
id: 'stv_difficulty_erect_erect',
text: "D: Erect",
// icon: "haxeui-core/styles/default/haxeui_tiny.png"
});
treeView.onChange = onChangeTreeDifficulty;
treeView.selectedNode = getCurrentTreeDifficultyNode();
}
}
treeView.onChange = onChangeTreeDifficulty;
treeView.selectedNode = getCurrentTreeDifficultyNode();
function handlePlayerPreviewToolbox()
{
// Manage the Select Difficulty tree view.
var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
if (charPreviewToolbox == null)
return;
var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer');
if (charPlayer == null)
return;
currentPlayerCharacterPlayer = charPlayer;
}
function handleOpponentPreviewToolbox()
{
// Manage the Select Difficulty tree view.
var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
if (charPreviewToolbox == null)
return;
var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer');
if (charPlayer == null)
return;
currentOpponentCharacterPlayer = charPlayer;
}
override function dispatchEvent(event:ScriptEvent)
{
super.dispatchEvent(event);
// We can't use the ScriptedEventDispatcher with currentCharPlayer because we can't use the IScriptedClass interface on it.
if (currentPlayerCharacterPlayer != null)
{
switch (event.type)
{
case ScriptEvent.UPDATE:
currentPlayerCharacterPlayer.onUpdate(cast event);
case ScriptEvent.SONG_BEAT_HIT:
currentPlayerCharacterPlayer.onBeatHit(cast event);
case ScriptEvent.SONG_STEP_HIT:
currentPlayerCharacterPlayer.onStepHit(cast event);
case ScriptEvent.NOTE_HIT:
currentPlayerCharacterPlayer.onNoteHit(cast event);
}
}
if (currentOpponentCharacterPlayer != null)
{
switch (event.type)
{
case ScriptEvent.UPDATE:
currentOpponentCharacterPlayer.onUpdate(cast event);
case ScriptEvent.SONG_BEAT_HIT:
currentOpponentCharacterPlayer.onBeatHit(cast event);
case ScriptEvent.SONG_STEP_HIT:
currentOpponentCharacterPlayer.onStepHit(cast event);
case ScriptEvent.NOTE_HIT:
currentOpponentCharacterPlayer.onNoteHit(cast event);
}
}
}
function getCurrentTreeDifficultyNode():TreeViewNode
{
var treeView:TreeView = findComponent('sidebarDifficulties');
var treeView:TreeView = findComponent('difficultyToolboxTree');
if (treeView == null)
return null;
@ -2264,6 +2365,18 @@ class ChartEditorState extends HaxeUIState
}
}
function addDifficulty(variation:String)
{
}
function addVariation(variationId:String)
{
// Create a new variation with the specified ID.
songMetadata.set(variationId, currentSongMetadata.clone(variationId));
// Switch to the new variation.
selectedVariation = variationId;
}
/**
* Handle the player preview/gameplay test area on the left side.
*/
@ -2432,6 +2545,23 @@ class ChartEditorState extends HaxeUIState
return;
// Note was just hit.
// Character preview.
// Why does NOTESCRIPTEVENT TAKE A SPRITE AAAAA
var tempNote:Note = new Note(noteData.time, noteData.data, null, false, NORMAL);
tempNote.mustPress = noteData.getMustHitNote();
tempNote.data.sustainLength = noteData.length;
tempNote.data.noteKind = noteData.kind;
tempNote.scrollFactor.set(0, 0);
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, tempNote, 1, true);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled)
continue;
// Hitsounds.
switch (noteData.getStrumlineIndex())
{
case 0: // Player
@ -2575,20 +2705,6 @@ class ChartEditorState extends HaxeUIState
return this.playheadPositionInPixels;
}
/**
* Show the sidebar if it's hidden, or hide it if it's shown.
*/
function toggleSidebar()
{
var sidebar:Component = findComponent('sidebar', Component);
// Set visibility while syncing the checkbox.
if (sidebar != null)
{
sidebar.visible = setUISelected('menubarItemToggleSidebar', !sidebar.visible);
}
}
/**
* Loads an instrumental from an absolute file path, replacing the current instrumental.
*/
@ -2622,8 +2738,8 @@ class ChartEditorState extends HaxeUIState
public function loadInstrumentalFromAsset(path:String):Void
{
var vocalTrack = FlxG.sound.load(path, 1.0, false);
audioInstTrack = vocalTrack;
var instTrack = FlxG.sound.load(path, 1.0, false);
audioInstTrack = instTrack;
postLoadInstrumental();
}
@ -2639,7 +2755,7 @@ class ChartEditorState extends HaxeUIState
audioVocalTrackGroup.pause();
};
songLengthInPixels = Std.int(Conductor.getTimeInSteps(audioInstTrack.length) * GRID_SIZE);
songLengthInMs = audioInstTrack.length;
gridTiledSprite.height = songLengthInPixels;
if (gridSpectrogram != null)
@ -2668,7 +2784,7 @@ class ChartEditorState extends HaxeUIState
public function loadVocalsFromAsset(path:String):Void
{
var vocalTrack = FlxG.sound.load(path, 1.0, false);
var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
audioVocalTrackGroup.add(vocalTrack);
}
@ -2679,7 +2795,7 @@ class ChartEditorState extends HaxeUIState
{
var openflSound = new openfl.media.Sound();
openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
var vocalTrack = FlxG.sound.load(openflSound, 1.0, false);
var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
audioVocalTrackGroup.add(vocalTrack);
// Tell the user the load was successful.
@ -2747,85 +2863,9 @@ class ChartEditorState extends HaxeUIState
noteDisplayDirty = true;
}
/**
* Add an onClick listener to a HaxeUI menu bar item.
**/
function addUIClickListener(key:String, callback:MouseEvent->Void)
{
var target:Component = findComponent(key);
if (target == null)
{
// Gracefully handle the case where the item can't be located.
trace('WARN: Could not locate menu item: $key');
}
else
{
target.onClick = callback;
}
}
/**
* Add an onChange listener to a HaxeUI menu bar item such as a slider.
*/
function addUIChangeListener(key:String, callback:UIEvent->Void)
{
var target:Component = findComponent(key);
if (target == null)
{
// Gracefully handle the case where the item can't be located.
trace('WARN: Could not locate menu item: $key');
}
else
{
target.onChange = callback;
}
}
/**
* Set the value of a HaxeUI component.
* Usually modifies the text of a label.
*/
function setUIValue<T>(key:String, value:T):T
{
var target:Component = findComponent(key);
if (target == null)
{
// Gracefully handle the case where the item can't be located.
trace('WARN: Could not locate menu item: $key');
return value;
}
else
{
return target.value = value;
}
}
/**
* Set the value of a HaxeUI checkbox,
* since that's on 'selected' instead of 'value'.
*/
function setUISelected<T>(key:String, value:Bool):Bool
{
var targetA:CheckBox = findComponent(key, CheckBox);
if (targetA != null)
{
return targetA.selected = value;
}
var targetB:MenuCheckBox = findComponent(key, MenuCheckBox);
if (targetB != null)
{
return targetB.selected = value;
}
// Gracefully handle the case where the item can't be located.
trace('WARN: Could not locate check box: $key');
return value;
}
/**
* Perform (or redo) a command, then add it to the undo stack.
*
* @param command The command to perform.
* @param purgeRedoStack If true, the redo stack will be cleared.
*/
@ -2958,39 +2998,49 @@ class ChartEditorState extends HaxeUIState
notifBar.hide();
}
/**
* Displays a prompt to the user, to save their changes made to this chart,
* or to discard them.
*
* @param onComplete Function to call after the user clicks Save or Don't Save.
* If Save was clicked, we save before calling this.
* @param onCancel Function to call if the user clicks Cancel.
*/
function promptSaveChanges(onComplete:Void->Void, ?onCancel:Void->Void):Void
public function exportAllSongData():Void
{
var messageBox:MessageBox = new MessageBox();
var zipEntries = [];
messageBox.title = 'Save Changes?';
messageBox.message = 'Do you want to save the changes you made to $currentSongName?\n\nYour changes will be lost if you don\'t save them.';
messageBox.type = 'question';
messageBox.modal = true;
messageBox.buttons = DialogButton.SAVE | "Don't Save" | "Cancel";
messageBox.registerEvent(DialogEvent.DIALOG_CLOSED, function(e:DialogEvent):Void
for (variation in availableVariations)
{
trace('Pressed: ${e.button}');
switch (e.button)
var variationId = variation;
if (variation == '' || variation == 'default' || variation == 'normal')
{
case 'Save':
// TODO: Make sure to actually save.
// saveChart();
onComplete();
case "Don't Save":
onComplete();
case 'Cancel':
if (onCancel != null)
onCancel();
variationId = '';
}
});
if (variationId == '')
{
var variationMetadata = songMetadata.get(variation);
zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata.json', SerializerUtil.toJSON(variationMetadata)));
var variationChart = songChartData.get(variation);
zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart.json', SerializerUtil.toJSON(variationChart)));
}
else
{
var variationMetadata = songMetadata.get(variation);
zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata-$variationId.json', SerializerUtil.toJSON(variationMetadata)));
var variationChart = songChartData.get(variation);
zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart-$variationId.json', SerializerUtil.toJSON(variationChart)));
}
}
// TODO: Add audio files to the ZIP.
trace('Exporting ${zipEntries.length} files to ZIP...');
// Prompt and save.
var onSave:Array<String>->Void = (paths:Array<String>) ->
{
trace('Successfully exported files.');
};
var onCancel:Void->Void = () ->
{
trace('Export cancelled.');
};
FileUtil.saveFilesAsZIP(zipEntries, onSave, onCancel, '$currentSongId-chart.zip');
}
}

View file

@ -62,10 +62,6 @@ class ChartEditorThemeHandler
static final SELECTION_SQUARE_FILL_COLOR_LIGHT:FlxColor = 0x4033FF33;
static final SELECTION_SQUARE_FILL_COLOR_DARK:FlxColor = 0x4033FF33;
// TODO: Un-hardcode these to be based on time signature.
static final STEPS_PER_BEAT:Int = 4;
static final BEATS_PER_MEASURE:Int = 4;
static final PLAYHEAD_BLOCK_BORDER_WIDTH:Int = 2;
static final PLAYHEAD_BLOCK_BORDER_COLOR:FlxColor = 0xFF9D0011;
static final PLAYHEAD_BLOCK_FILL_COLOR:FlxColor = 0xFFBD0231;
@ -112,7 +108,7 @@ class ChartEditorThemeHandler
// 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall.
// This gets reused to fill the screen.
var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2 + 1));
var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * (STEPS_PER_BEAT * BEATS_PER_MEASURE));
var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * (Conductor.stepsPerMeasure));
state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1,
gridColor2);
@ -130,7 +126,7 @@ class ChartEditorThemeHandler
selectionBorderColor);
// Selection borders in the middle.
for (i in 1...(STEPS_PER_BEAT * BEATS_PER_MEASURE))
for (i in 1...(Conductor.stepsPerMeasure))
{
state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2),
state.gridBitmap.width, ChartEditorState.GRID_SELECTION_BORDER_WIDTH),

View file

@ -1,5 +1,14 @@
package funkin.ui.debug.charting;
import funkin.play.song.SongData.SongTimeChange;
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.ui.haxeui.components.CharacterPlayer;
import funkin.play.song.SongSerializer;
import haxe.ui.components.Button;
import haxe.ui.components.DropDown;
import haxe.ui.containers.Group;
import haxe.ui.containers.dialogs.Dialog;
@ -77,33 +86,58 @@ class ChartEditorToolboxHandler
toolbox = buildToolboxNoteDataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
toolbox = buildToolboxEventDataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_SONGDATA_LAYOUT:
toolbox = buildToolboxSongDataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
toolbox = buildToolboxDifficultyLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
toolbox = buildToolboxMetadataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
toolbox = buildToolboxCharactersLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
toolbox = buildToolboxPlayerPreviewLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
toolbox = buildToolboxOpponentPreviewLayout(state);
default:
trace('ChartEditorToolboxHandler.initToolbox() - Unknown toolbox ID: $id');
toolbox = null;
}
// Make sure we can reuse the toolbox later.
toolbox.destroyOnClose = false;
state.activeToolboxes.set(id, toolbox);
return toolbox;
}
public static function getToolbox(state:ChartEditorState, id:String):Dialog
{
var toolbox:Dialog = state.activeToolboxes.get(id);
// Initialize the toolbox without showing it.
if (toolbox == null)
toolbox = initToolbox(state, id);
return toolbox;
}
static function buildToolboxToolsLayout(state:ChartEditorState):Dialog
{
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT);
if (toolbox == null) return null;
// Starting position.
toolbox.x = 50;
toolbox.y = 50;
toolbox.onDialogClosed = (event:DialogEvent) ->
{
state.setUISelected('menubarItemToggleToolboxTools', false);
state.setUICheckboxSelected('menubarItemToggleToolboxTools', false);
}
var toolsGroup:Group = toolbox.findComponent("toolboxToolsGroup", Group);
if (toolsGroup == null) return null;
toolsGroup.onChange = (event:UIEvent) ->
{
switch (event.target.id)
@ -124,13 +158,15 @@ class ChartEditorToolboxHandler
{
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
if (toolbox == null) return null;
// Starting position.
toolbox.x = 75;
toolbox.y = 100;
toolbox.onDialogClosed = (event:DialogEvent) ->
{
state.setUISelected('menubarItemToggleToolboxNotes', false);
state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false);
}
var toolboxNotesNoteKind:DropDown = toolbox.findComponent("toolboxNotesNoteKind", DropDown);
@ -147,38 +183,251 @@ class ChartEditorToolboxHandler
{
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
if (toolbox == null) return null;
// Starting position.
toolbox.x = 100;
toolbox.y = 150;
toolbox.onDialogClosed = (event:DialogEvent) ->
{
state.setUISelected('menubarItemToggleToolboxEvents', false);
state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false);
}
return toolbox;
}
static function buildToolboxSongDataLayout(state:ChartEditorState):Dialog
static function buildToolboxDifficultyLayout(state:ChartEditorState):Dialog
{
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_SONGDATA_LAYOUT);
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
if (toolbox == null) return null;
// Starting position.
toolbox.x = 950;
toolbox.y = 50;
toolbox.x = 125;
toolbox.y = 200;
toolbox.onDialogClosed = (event:DialogEvent) ->
{
state.setUISelected('menubarItemToggleToolboxSong', false);
state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false);
}
var difficultyToolboxSaveMetadata:Button = toolbox.findComponent("difficultyToolboxSaveMetadata", Button);
var difficultyToolboxSaveChart:Button = toolbox.findComponent("difficultyToolboxSaveChart", Button);
var difficultyToolboxSaveAll:Button = toolbox.findComponent("difficultyToolboxSaveAll", Button);
var difficultyToolboxLoadMetadata:Button = toolbox.findComponent("difficultyToolboxLoadMetadata", Button);
var difficultyToolboxLoadChart:Button = toolbox.findComponent("difficultyToolboxLoadChart", Button);
difficultyToolboxSaveMetadata.onClick = (event:UIEvent) ->
{
SongSerializer.exportSongMetadata(state.currentSongMetadata);
};
difficultyToolboxSaveChart.onClick = (event:UIEvent) ->
{
SongSerializer.exportSongChartData(state.currentSongChartData);
};
difficultyToolboxSaveAll.onClick = (event:UIEvent) ->
{
state.exportAllSongData();
};
difficultyToolboxLoadMetadata.onClick = (event:UIEvent) ->
{
// Replace metadata for current variation.
SongSerializer.importSongMetadataAsync(function(songMetadata)
{
state.currentSongMetadata = songMetadata;
});
};
difficultyToolboxLoadChart.onClick = (event:UIEvent) ->
{
// Replace chart data for current variation.
SongSerializer.importSongChartDataAsync(function(songChartData)
{
state.currentSongChartData = songChartData;
state.noteDisplayDirty = true;
});
};
state.difficultySelectDirty = true;
return toolbox;
}
static function buildToolboxMetadataLayout(state:ChartEditorState):Dialog
{
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
if (toolbox == null) return null;
// Starting position.
toolbox.x = 150;
toolbox.y = 250;
toolbox.onDialogClosed = (event:DialogEvent) ->
{
state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false);
}
var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField);
inputSongName.onChange = (event:UIEvent) ->
{
var valid = event.target.text != null && event.target.text != "";
if (valid)
{
inputSongName.removeClass('invalid-value');
state.currentSongMetadata.songName = event.target.text;
}
else
{
state.currentSongMetadata.songName = null;
}
};
var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField);
inputSongArtist.onChange = (event:UIEvent) ->
{
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 buildDialog(state:ChartEditorState, id:String):Dialog
static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Dialog
{
var dialog:Dialog = cast state.buildComponent(id);
dialog.destroyOnClose = false;
return 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

@ -1,5 +1,10 @@
package funkin.ui.haxeui;
import haxe.ui.containers.menus.MenuCheckBox;
import haxe.ui.components.CheckBox;
import haxe.ui.events.DragEvent;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.RuntimeComponentBuilder;
import haxe.ui.core.Component;
import haxe.ui.core.Screen;
@ -93,6 +98,83 @@ class HaxeUIState extends MusicBeatState
}
}
/**
* Add an onClick listener to a HaxeUI menu bar item.
*/
function addUIClickListener(key:String, callback:MouseEvent->Void)
{
var target:Component = findComponent(key);
if (target == null)
{
// Gracefully handle the case where the item can't be located.
trace('WARN: Could not locate menu item: $key');
}
else
{
target.onClick = callback;
}
}
/**
* Add an onChange listener to a HaxeUI input component such as a slider or text field.
*/
function addUIChangeListener(key:String, callback:UIEvent->Void)
{
var target:Component = findComponent(key);
if (target == null)
{
// Gracefully handle the case where the item can't be located.
trace('WARN: Could not locate menu item: $key');
}
else
{
target.onChange = callback;
}
}
/**
* Set the value of a HaxeUI component.
* Usually modifies the text of a label or value of a text field.
*/
function setUIValue<T>(key:String, value:T):T
{
var target:Component = findComponent(key);
if (target == null)
{
// Gracefully handle the case where the item can't be located.
trace('WARN: Could not locate menu item: $key');
return value;
}
else
{
return target.value = value;
}
}
/**
* Set the value of a HaxeUI checkbox,
* since that's on 'selected' instead of 'value'.
*/
public function setUICheckboxSelected<T>(key:String, value:Bool):Bool
{
var targetA:CheckBox = findComponent(key, CheckBox);
if (targetA != null)
{
return targetA.selected = value;
}
var targetB:MenuCheckBox = findComponent(key, MenuCheckBox);
if (targetB != null)
{
return targetB.selected = value;
}
// Gracefully handle the case where the item can't be located.
trace('WARN: Could not locate check box: $key');
return value;
}
public function findComponent<T:Component>(criteria:String = null, type:Class<T> = null, recursive:Null<Bool> = null, searchType:String = "id"):Null<T>
{
if (component == null)

View file

@ -0,0 +1,276 @@
package funkin.ui.haxeui.components;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.math.FlxRect;
import funkin.modding.events.ScriptEvent;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser;
import haxe.ui.containers.Box;
import haxe.ui.core.Component;
import haxe.ui.core.IDataComponent;
import haxe.ui.data.DataSource;
import haxe.ui.events.AnimationEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.geom.Size;
import haxe.ui.layouts.DefaultLayout;
import haxe.ui.styles.Style;
import openfl.Assets;
private typedef AnimationInfo =
{
var name:String;
var prefix:String;
var frameRate:Null<Int>; // default 30
var looped:Null<Bool>; // default true
var flipX:Null<Bool>; // default false
var flipY:Null<Bool>; // default false
}
@:composite(Layout)
class CharacterPlayer extends Box
{
private var character:BaseCharacter;
public function new(?defaultToBf:Bool = true)
{
super();
this._overrideSkipTransformChildren = false;
if (defaultToBf)
{
loadCharacter('bf');
}
}
private var _charId:String;
public var charId(get, set):String;
private function get_charId():String
{
return _charId;
}
private function set_charId(value:String):String
{
_charId = value;
loadCharacter(_charId);
return value;
}
private var _redispatchLoaded:Bool = false; // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... needs thinking about, is it smart to "collect and redispatch"? Not sure
private var _redispatchStart:Bool = false; // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... needs thinking about, is it smart to "collect and redispatch"? Not sure
public override function onReady()
{
super.onReady();
invalidateComponentLayout();
if (_redispatchLoaded)
{
_redispatchLoaded = false;
dispatch(new AnimationEvent(AnimationEvent.LOADED));
}
if (_redispatchStart)
{
_redispatchStart = false;
dispatch(new AnimationEvent(AnimationEvent.START));
}
parentComponent._overrideSkipTransformChildren = false;
}
public function loadCharacter(id:String)
{
if (id == null)
{
return;
}
if (character != null)
{
remove(character);
character.destroy();
character = null;
}
var newCharacter:BaseCharacter = CharacterDataParser.fetchCharacter(id);
if (newCharacter == null)
{
return;
}
character = newCharacter;
if (_characterType != null)
{
character.characterType = _characterType;
}
if (flip)
{
character.flipX = !character.flipX;
}
character.scale.x *= _scale;
character.scale.y *= _scale;
character.animation.callback = function(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1)
{
@:privateAccess
character.onAnimationFrame(name, frameNumber, frameIndex);
dispatch(new AnimationEvent(AnimationEvent.FRAME));
};
character.animation.finishCallback = function(name:String = "")
{
@:privateAccess
character.onAnimationFinished(name);
dispatch(new AnimationEvent(AnimationEvent.END));
};
add(character);
invalidateComponentLayout();
if (hasEvent(AnimationEvent.LOADED))
{
dispatch(new AnimationEvent(AnimationEvent.LOADED));
}
else
{
_redispatchLoaded = true;
}
}
private override function repositionChildren()
{
super.repositionChildren();
@:privateAccess
var animOffsets = character.animOffsets;
character.x = this.screenX + ((this.width / 2) - (character.frameWidth / 2));
character.x -= animOffsets[0];
character.y = this.screenY + ((this.height / 2) - (character.frameHeight / 2));
character.y -= animOffsets[1];
}
private var _characterType:CharacterType;
public function setCharacterType(value:CharacterType)
{
_characterType = value;
if (character != null)
{
character.characterType = value;
}
}
public var flip(default, set):Bool;
private function set_flip(value:Bool):Bool
{
if (value == flip)
return value;
if (character != null)
{
character.flipX = !character.flipX;
}
return flip = value;
}
var _scale:Float = 1.0;
public function setScale(value)
{
_scale = value;
if (character != null)
{
character.scale.x *= _scale;
character.scale.y *= _scale;
}
}
public function onUpdate(event:UpdateScriptEvent)
{
if (character != null)
character.onUpdate(event);
}
public function onBeatHit(event:SongTimeScriptEvent):Void
{
if (character != null)
character.onBeatHit(event);
this.repositionChildren();
}
public function onStepHit(event:SongTimeScriptEvent):Void
{
if (character != null)
character.onStepHit(event);
}
public function onNoteHit(event:NoteScriptEvent):Void
{
if (character != null)
character.onNoteHit(event);
this.repositionChildren();
}
public function onNoteMiss(event:NoteScriptEvent):Void
{
if (character != null)
character.onNoteMiss(event);
this.repositionChildren();
}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void
{
if (character != null)
character.onNoteGhostMiss(event);
this.repositionChildren();
}
}
@:access(funkin.ui.haxeui.components.CharacterPlayer)
private class Layout extends DefaultLayout
{
public override function repositionChildren()
{
var player = cast(_component, CharacterPlayer);
var sprite:BaseCharacter = player.character;
if (sprite == null)
{
return super.repositionChildren();
}
@:privateAccess
var animOffsets = sprite.animOffsets;
sprite.x = _component.screenLeft + ((_component.width / 2) - (sprite.frameWidth / 2));
sprite.x += animOffsets[0];
sprite.y = _component.screenTop + ((_component.height / 2) - (sprite.frameHeight / 2));
sprite.y += animOffsets[1];
}
public override function calcAutoSize(exclusions:Array<Component> = null):Size
{
var player = cast(_component, CharacterPlayer);
var sprite = player.character;
if (sprite == null)
{
return super.calcAutoSize(exclusions);
}
var size = new Size();
size.width = sprite.frameWidth + paddingLeft + paddingRight;
size.height = sprite.frameHeight + paddingTop + paddingBottom;
return size;
}
}

View file

@ -1,8 +0,0 @@
package funkin.ui.haxeui.components;
import haxe.ui.components.Image;
class SparrowImage extends Image
{
//
}

View file

@ -0,0 +1,381 @@
package funkin.util;
import haxe.zip.Entry;
import lime.utils.Bytes;
import lime.ui.FileDialog;
import openfl.net.FileFilter;
import haxe.io.Path;
import lime.utils.Resource;
/**
* Utilities for reading and writing files on various platforms.
*/
class FileUtil
{
/**
* Browses for a single file, then calls `onSelect(path)` when a path chosen.
* Note that on HTML5 this will immediately fail, you should call `openFile(onOpen:Resource->Void)` instead.
*
* @param typeFilter Filters what kinds of files can be selected.
* @return Whether the file dialog was opened successfully.
*/
public static function browseForFile(?typeFilter:Array<FileFilter>, ?onSelect:String->Void, ?onCancel:Void->Void, ?defaultPath:String,
?dialogTitle:String):Bool
{
#if desktop
var filter = convertTypeFilter(typeFilter);
var fileDialog = new FileDialog();
if (onSelect != null)
fileDialog.onSelect.add(onSelect);
if (onCancel != null)
fileDialog.onCancel.add(onCancel);
fileDialog.browse(OPEN, filter, defaultPath, dialogTitle);
return true;
#elseif html5
onCancel();
return false;
#else
onCancel();
return false;
#end
}
/**
* Browses for a directory, then calls `onSelect(path)` when a path chosen.
* Note that on HTML5 this will immediately fail.
*
* @param typeFilter TODO What does this do?
* @return Whether the file dialog was opened successfully.
*/
public static function browseForDirectory(?typeFilter:Array<FileFilter>, ?onSelect:String->Void, ?onCancel:Void->Void, ?defaultPath:String,
?dialogTitle:String):Bool
{
#if desktop
var filter = convertTypeFilter(typeFilter);
var fileDialog = new FileDialog();
if (onSelect != null)
fileDialog.onSelect.add(onSelect);
if (onCancel != null)
fileDialog.onCancel.add(onCancel);
fileDialog.browse(OPEN_DIRECTORY, filter, defaultPath, dialogTitle);
return true;
#elseif html5
onCancel();
return false;
#else
onCancel();
return false;
#end
}
/**
* Browses for multiple file, then calls `onSelect(paths)` when a path chosen.
* Note that on HTML5 this will immediately fail.
*
* @return Whether the file dialog was opened successfully.
*/
public static function browseForMultipleFiles(?typeFilter:Array<FileFilter>, ?onSelect:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String,
?dialogTitle:String):Bool
{
#if desktop
var filter = convertTypeFilter(typeFilter);
var fileDialog = new FileDialog();
if (onSelect != null)
fileDialog.onSelectMultiple.add(onSelect);
if (onCancel != null)
fileDialog.onCancel.add(onCancel);
fileDialog.browse(OPEN_MULTIPLE, filter, defaultPath, dialogTitle);
return true;
#elseif html5
onCancel();
return false;
#else
onCancel();
return false;
#end
}
/**
* Browses for a file location to save to, then calls `onSelect(path)` when a path chosen.
* Note that on HTML5 you can't do much with this, you should call `saveFile(resource:haxe.io.Bytes)` instead.
*
* @param typeFilter TODO What does this do?
* @return Whether the file dialog was opened successfully.
*/
public static function browseForSaveFile(?typeFilter:Array<FileFilter>, ?onSelect:String->Void, ?onCancel:Void->Void, ?defaultPath:String,
?dialogTitle:String):Bool
{
#if desktop
var filter = convertTypeFilter(typeFilter);
var fileDialog = new FileDialog();
if (onSelect != null)
fileDialog.onSelect.add(onSelect);
if (onCancel != null)
fileDialog.onCancel.add(onCancel);
fileDialog.browse(SAVE, filter, defaultPath, dialogTitle);
return true;
#elseif html5
onCancel();
return false;
#else
onCancel();
return false;
#end
}
/**
* Browses for a single file location, then reads it and passes it to `onOpen(resource:haxe.io.Bytes)`.
* Works great on desktop and HTML5.
*
* @param typeFilter TODO What does this do?
* @return Whether the file dialog was opened successfully.
*/
public static function openFile(?typeFilter:Array<FileFilter>, ?onOpen:Bytes->Void, ?onCancel:Void->Void, ?defaultPath:String, ?dialogTitle:String):Bool
{
#if desktop
var filter = convertTypeFilter(typeFilter);
var fileDialog = new FileDialog();
if (onOpen != null)
fileDialog.onOpen.add(onOpen);
if (onCancel != null)
fileDialog.onCancel.add(onCancel);
fileDialog.open(filter, defaultPath, dialogTitle);
return true;
#elseif html5
var filter = convertTypeFilter(typeFilter);
var onFileLoaded = function(event)
{
var loadedFileRef:FileReference = event.target;
trace('Loaded file: ' + loadedFileRef.name);
onOpen(loadedFileRef.data);
}
var onFileSelected = function(event)
{
var selectedFileRef:FileReference = event.target;
trace('Selected file: ' + selectedFileRef.name);
selectedFileRef.addEventListener(Event.COMPLETE, onFileLoaded);
selectedFileRef.load();
}
var fileRef = new FileReference();
file.addEventListener(Event.SELECT, onFileSelected);
file.open(filter, defaultPath, dialogTitle);
#else
onCancel();
return false;
#end
}
/**
* Browses for a single file location, then writes the provided `haxe.io.Bytes` data and calls `onSave(path)` when done.
* Works great on desktop and HTML5.
*
* @param typeFilter TODO What does this do?
* @return Whether the file dialog was opened successfully.
*/
public static function saveFile(data:Bytes, ?onSave:String->Void, ?onCancel:Void->Void, ?defaultFileName:String, ?dialogTitle:String):Bool
{
#if desktop
var filter = defaultFileName != null ? Path.extension(defaultFileName) : null;
var fileDialog = new FileDialog();
if (onSave != null)
fileDialog.onSelect.add(onSave);
if (onCancel != null)
fileDialog.onCancel.add(onCancel);
fileDialog.save(data, filter, defaultFileName, dialogTitle);
return true;
#elseif html5
var filter = defaultFileName != null ? Path.extension(defaultFileName) : null;
var fileDialog = new FileDialog();
if (onSave != null)
fileDialog.onSave.add(onSave);
if (onCancel != null)
fileDialog.onCancel.add(onCancel);
fileDialog.save(data, filter, defaultFileName, dialogTitle);
#else
onCancel();
return false;
#end
}
/**
* Prompts the user to save multiple files.
* On desktop, this will prompt the user for a directory, then write all of the files to there.
* On HTML5, this will zip the files up and prompt the user to save that.
*
* @param typeFilter TODO What does this do?
* @return Whether the file dialog was opened successfully.
*/
public static function saveMultipleFiles(resources:Array<Entry>, ?onSaveAll:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String, ?force:Bool = false):Bool
{
#if desktop
// Prompt the user for a directory, then write all of the files to there.
var onSelectDir = function(targetPath:String)
{
var paths:Array<String> = [];
for (resource in resources)
{
var filePath = haxe.io.Path.join([targetPath, resource.fileName]);
try {
if (resource.data == null) {
trace('WARNING: File $filePath has no data or content. Skipping.');
continue;
} else {
writeBytesToPath(filePath, resource.data, force);
}
} catch (e:Dynamic) {
trace('Failed to write file (probably already exists): $filePath' + filePath);
continue;
}
paths.push(filePath);
}
onSaveAll(paths);
}
browseForDirectory(null, onSelectDir, onCancel, defaultPath, "Choose directory to save all files to...");
return true;
#elseif html5
saveFilesAsZIP(resources, onSaveAll, onCancel, defaultPath, force);
return true;
#else
onCancel();
return false;
#end
}
/**
* Takes an array of file entries and prompts the user to save them as a ZIP file.
*/
public static function saveFilesAsZIP(resources:Array<Entry>, ?onSave:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String, ?force:Bool = false):Bool {
// Create a ZIP file.
var zipBytes = createZIPFromEntries(resources);
var onSave = function(path:String)
{
onSave([path]);
};
// Prompt the user to save the ZIP file.
saveFile(zipBytes, onSave, onCancel, defaultPath, "Save files as ZIP...");
return true;
}
/**
* Write string file contents directly to a given path.
* Only works on desktop.
*/
public static function writeStringToPath(path:String, data:String, force:Bool = false)
{
if (force || !sys.FileSystem.exists(path))
{
sys.io.File.saveContent(path, data);
}
else
{
throw 'File already exists: $path';
}
}
/**
* Write byte file contents directly to a given path.
* Only works on desktop.
*/
public static function writeBytesToPath(path:String, data:Bytes, force:Bool = false)
{
if (force || !sys.FileSystem.exists(path))
{
sys.io.File.saveBytes(path, data);
}
else
{
throw 'File already exists: $path';
}
}
/**
* Write string file contents directly to the end of a file at the given path.
* Only works on desktop.
*/
public static function appendStringToPath(path:String, data:String)
{
sys.io.File.append(path, false).writeString(data);
}
/**
* Create a Bytes object containing a ZIP file, containing the provided entries.
*
* @param entries The entries to add to the ZIP file.
* @return The ZIP file as a Bytes object.
*/
public static function createZIPFromEntries(entries:Array<Entry>):Bytes
{
var o = new haxe.io.BytesOutput();
var zipWriter = new haxe.zip.Writer(o);
zipWriter.write(entries.list());
return o.getBytes();
}
/**
* Create a ZIP file entry from a file name and its string contents.
*
* @param name The name of the file. You can use slashes to create subdirectories.
* @param content The string contents of the file.
* @return The resulting entry.
*/
public static function makeZIPEntry(name:String, content:String):Entry
{
var data = haxe.io.Bytes.ofString(content, UTF8);
return {
fileName : name,
fileSize : data.length,
data : data,
dataSize : data.length,
compressed : false,
fileTime : Date.now(),
crc32 : null,
extraFields : null,
};
}
static function convertTypeFilter(typeFilter:Array<FileFilter>):String
{
var filter = null;
if (typeFilter != null)
{
var filters = [];
for (type in typeFilter)
{
filters.push(StringTools.replace(StringTools.replace(type.extension, "*.", ""), ";", ","));
}
filter = filters.join(";");
}
return filter;
}
}

View file

@ -0,0 +1,2 @@
package funkin.util;

View file

@ -1,389 +0,0 @@
package funkin.util;
import haxe.io.Path;
import haxe.io.Bytes;
import haxe.io.BytesOutput;
import haxe.io.Eof;
import haxe.zip.Entry;
import haxe.zip.Writer;
import haxe.Json;
import haxe.Template;
import sys.io.File;
import sys.io.Process;
import sys.FileSystem;
class SystemUtil
{
public static var hostArchitecture(get, never):HostArchitecture;
public static var hostPlatform(get, never):HostPlatform;
public static var processorCores(get, never):Int;
private static var _hostArchitecture:HostArchitecture;
private static var _hostPlatform:HostPlatform;
private static var _processorCores:Int = 0;
private static function get_hostPlatform():HostPlatform
{
if (_hostPlatform == null)
{
if (new EReg("window", "i").match(Sys.systemName()))
{
_hostPlatform = WINDOWS;
}
else if (new EReg("linux", "i").match(Sys.systemName()))
{
_hostPlatform = LINUX;
}
else if (new EReg("mac", "i").match(Sys.systemName()))
{
_hostPlatform = MAC;
}
trace("", " - \x1b[1mDetected host platform:\x1b[0m " + Std.string(_hostPlatform).toUpperCase());
}
return _hostPlatform;
}
private static function get_hostArchitecture():HostArchitecture
{
if (_hostArchitecture == null)
{
switch (hostPlatform)
{
case WINDOWS:
var architecture = Sys.getEnv("PROCESSOR_ARCHITECTURE");
var wow64Architecture = Sys.getEnv("PROCESSOR_ARCHITEW6432");
if (architecture.indexOf("64") > -1 || wow64Architecture != null && wow64Architecture.indexOf("64") > -1)
{
_hostArchitecture = X64;
}
else
{
_hostArchitecture = X86;
}
case LINUX, MAC:
#if nodejs
switch (js.Node.process.arch)
{
case "arm":
_hostArchitecture = ARMV7;
case "x64":
_hostArchitecture = X64;
default:
_hostArchitecture = X86;
}
#else
var process = new Process("uname", ["-m"]);
var output = process.stdout.readAll().toString();
var error = process.stderr.readAll().toString();
process.exitCode();
process.close();
if (output.indexOf("armv6") > -1)
{
_hostArchitecture = ARMV6;
}
else if (output.indexOf("armv7") > -1)
{
_hostArchitecture = ARMV7;
}
else if (output.indexOf("64") > -1)
{
_hostArchitecture = X64;
}
else
{
_hostArchitecture = X86;
}
#end
default:
_hostArchitecture = ARMV6;
}
trace("", " - \x1b[1mDetected host architecture:\x1b[0m " + Std.string(_hostArchitecture).toUpperCase());
}
return _hostArchitecture;
}
private static function get_processorCores():Int
{
if (_processorCores < 1)
{
var result = null;
if (hostPlatform == WINDOWS)
{
var env = Sys.getEnv("NUMBER_OF_PROCESSORS");
if (env != null)
{
result = env;
}
}
else if (hostPlatform == LINUX)
{
result = runProcess("", "nproc", null, true, true, true);
if (result == null)
{
var cpuinfo = runProcess("", "cat", ["/proc/cpuinfo"], true, true, true);
if (cpuinfo != null)
{
var split = cpuinfo.split("processor");
result = Std.string(split.length - 1);
}
}
}
else if (hostPlatform == MAC)
{
var cores = ~/Total Number of Cores: (\d+)/;
var output = runProcess("", "/usr/sbin/system_profiler", ["-detailLevel", "full", "SPHardwareDataType"]);
if (cores.match(output))
{
result = cores.matched(1);
}
}
if (result == null || Std.parseInt(result) < 1)
{
_processorCores = 1;
}
else
{
_processorCores = Std.parseInt(result);
}
}
return _processorCores;
}
public static function runProcess(path:String, command:String, args:Array<String> = null, waitForOutput:Bool = true, safeExecute:Bool = true,
ignoreErrors:Bool = false, print:Bool = false, returnErrorValue:Bool = false):String
{
if (print)
{
var message = command;
if (args != null)
{
for (arg in args)
{
if (arg.indexOf(" ") > -1)
{
message += " \"" + arg + "\"";
}
else
{
message += " " + arg;
}
}
}
Sys.println(message);
}
#if (haxe_ver < "3.3.0")
command = Path.escape(command);
#end
if (safeExecute)
{
try
{
if (path != null
&& path != ""
&& !FileSystem.exists(FileSystem.fullPath(path))
&& !FileSystem.exists(FileSystem.fullPath(new Path(path).dir)))
{
trace("The specified target path \"" + path + "\" does not exist");
}
return _runProcess(path, command, args, waitForOutput, safeExecute, ignoreErrors, returnErrorValue);
}
catch (e:Dynamic)
{
if (!ignoreErrors)
{
trace("", e);
}
return null;
}
}
else
{
return _runProcess(path, command, args, waitForOutput, safeExecute, ignoreErrors, returnErrorValue);
}
}
private static function _runProcess(path:String, command:String, args:Null<Array<String>>, waitForOutput:Bool, safeExecute:Bool, ignoreErrors:Bool,
returnErrorValue:Bool):String
{
var oldPath:String = "";
if (path != null && path != "")
{
trace("", " - \x1b[1mChanging directory:\x1b[0m " + path + "");
oldPath = Sys.getCwd();
Sys.setCwd(path);
}
var argString = "";
if (args != null)
{
for (arg in args)
{
if (arg.indexOf(" ") > -1)
{
argString += " \"" + arg + "\"";
}
else
{
argString += " " + arg;
}
}
}
trace("", " - \x1b[1mRunning process:\x1b[0m " + command + argString);
var output = "";
var result = 0;
var process:Process;
if (args != null && args.length > 0)
{
process = new Process(command, args);
}
else
{
process = new Process(command);
}
if (waitForOutput)
{
var buffer = new BytesOutput();
var waiting = true;
while (waiting)
{
try
{
var current = process.stdout.readAll(1024);
buffer.write(current);
if (current.length == 0)
{
waiting = false;
}
}
catch (e:Eof)
{
waiting = false;
}
}
result = process.exitCode();
output = buffer.getBytes().toString();
if (output == "")
{
var error = process.stderr.readAll().toString();
process.close();
if (result != 0 || error != "")
{
if (ignoreErrors)
{
output = error;
}
else if (!safeExecute)
{
throw error;
}
else
{
trace(error);
}
if (returnErrorValue)
{
return output;
}
else
{
return null;
}
}
/*if (error != "") {
trace (error);
}*/
}
else
{
process.close();
}
}
if (oldPath != "")
{
Sys.setCwd(oldPath);
}
return output;
}
public static function getTempDirectory(extension:String = ""):String
{
#if (flash || html5)
return null;
#else
var path = "";
if (hostPlatform == WINDOWS)
{
path = Sys.getEnv("TEMP");
}
else
{
path = Sys.getEnv("TMPDIR");
if (path == null)
{
path = "/tmp";
}
}
path = Path.join([path, "Funkin"]);
return path;
#end
}
}
enum HostArchitecture
{
ARMV6;
ARMV7;
X86;
X64;
}
@:enum abstract HostPlatform(String) from String to String
{
public var WINDOWS = "windows";
public var MAC = "mac";
public var LINUX = "linux";
}

View file

@ -1,4 +1,4 @@
package funkin.util;
package funkin.util.tools;
/**
* A static extension which provides utility functions for Iterators.

View file

@ -0,0 +1,59 @@
package funkin.util.tools;
/**
* A static extension which provides utility functions for Strings.
*/
class StringTools
{
/**
* Converts a string to title case. For example, "hello world" becomes "Hello World".
*
* @param value The string to convert.
* @return The converted string.
*/
public static function toTitleCase(value:String):String
{
var words:Array<String> = value.split(" ");
var result:String = "";
for (i in 0...words.length)
{
var word:String = words[i];
result += word.charAt(0).toUpperCase() + word.substr(1).toLowerCase();
if (i < words.length - 1)
{
result += " ";
}
}
return result;
}
/**
* Converts a string to lower kebab case. For example, "Hello World" becomes "hello-world".
*
* @param value The string to convert.
* @return The converted string.
*/
public static function toLowerKebabCase(value:String):String {
return value.toLowerCase().replace(' ', "-");
}
/**
* Converts a string to upper kebab case, aka screaming kebab case. For example, "Hello World" becomes "HELLO-WORLD".
*
* @param value The string to convert.
* @return The converted string.
*/
public static function toUpperKebabCase(value:String):String {
return value.toUpperCase().replace(' ', "-");
}
/**
* Parses the string data as JSON and returns the resulting object.
*
* @return The parsed object.
*/
public static function parseJSON(value:String):Dynamic
{
return SerializerUtil.fromJSON(value);
}
}