mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-03-21 01:19:26 +00:00
FNFC file rework (includes command line quicklaunch)
This commit is contained in:
parent
8042f6c5a8
commit
ffd0a98393
|
@ -4,10 +4,7 @@
|
|||
<app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.3.0" company="ninjamuffin99" />
|
||||
<!--Switch Export with Unique ApplicationID and Icon-->
|
||||
<set name="APP_ID" value="0x0100f6c013bbc000" />
|
||||
<!--The flixel preloader is not accurate in Chrome. You can use it regularly if you embed the swf into a html file
|
||||
or you can set the actual size of your file manually at "FlxPreloaderBase-onUpdate-bytesTotal"-->
|
||||
<!-- <app preloader="Preloader" resizable="true" /> -->
|
||||
<app preloader="Preloader" />
|
||||
<app preloader="funkin.Preloader" />
|
||||
<!--Minimum without FLX_NO_GAMEPAD: 11.8, without FLX_NO_NATIVE_CURSOR: 11.2-->
|
||||
<set name="SWF_VERSION" value="11.8" />
|
||||
<!-- ____________________________ Window Settings ___________________________ -->
|
||||
|
|
2
assets
2
assets
|
@ -1 +1 @@
|
|||
Subproject commit ef79a6cf1ae3dcbd86a5b798f8117a6c692c0156
|
||||
Subproject commit 118b622953171aaf127cb160538e21bc468620e2
|
|
@ -11,6 +11,7 @@ import openfl.display.Sprite;
|
|||
import openfl.events.Event;
|
||||
import openfl.Lib;
|
||||
import openfl.media.Video;
|
||||
import funkin.util.CLIUtil;
|
||||
import openfl.net.NetStream;
|
||||
|
||||
class Main extends Sprite
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin;
|
||||
|
||||
import funkin.ui.debug.charting.ChartEditorState;
|
||||
import flixel.FlxState;
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond;
|
||||
|
@ -26,6 +27,8 @@ import funkin.play.stage.StageData.StageDataParser;
|
|||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
import funkin.ui.title.TitleState;
|
||||
import funkin.util.CLIUtil;
|
||||
import funkin.util.CLIUtil.CLIParams;
|
||||
#if discord_rpc
|
||||
import Discord.DiscordClient;
|
||||
#end
|
||||
|
@ -247,8 +250,21 @@ class InitState extends FlxState
|
|||
*/
|
||||
function startGameNormally():Void
|
||||
{
|
||||
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
|
||||
FlxG.switchState(new TitleState());
|
||||
var params:CLIParams = CLIUtil.processArgs();
|
||||
trace('Command line args: ${params}');
|
||||
|
||||
if (params.chart.shouldLoadChart)
|
||||
{
|
||||
FlxG.switchState(new ChartEditorState(
|
||||
{
|
||||
fnfcTargetPath: params.chart.chartPath,
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
|
||||
FlxG.switchState(new TitleState());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package;
|
||||
package funkin;
|
||||
|
||||
import flash.Lib;
|
||||
import flash.display.Bitmap;
|
||||
|
@ -7,6 +7,7 @@ import flash.display.BlendMode;
|
|||
import flash.display.Sprite;
|
||||
import flixel.system.FlxBasePreloader;
|
||||
import openfl.display.Sprite;
|
||||
import funkin.util.CLIUtil;
|
||||
|
||||
@:bitmap("art/preloaderArt.png") class LogoImage extends BitmapData {}
|
||||
|
||||
|
@ -15,6 +16,8 @@ class Preloader extends FlxBasePreloader
|
|||
public function new(MinDisplayTime:Float = 0, ?AllowedURLs:Array<String>)
|
||||
{
|
||||
super(MinDisplayTime, AllowedURLs);
|
||||
|
||||
CLIUtil.resetWorkingDir(); // Bug fix for drag-and-drop.
|
||||
}
|
||||
|
||||
var logo:Sprite;
|
|
@ -104,6 +104,22 @@ class DataParse
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser which outputs a `Either<Float, Array<Float>>`.
|
||||
*/
|
||||
public static function eitherFloatOrFloats(json:Json, name:String):Null<Either<Float, Array<Float>>>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JNumber(f):
|
||||
return Either.Left(Std.parseFloat(f));
|
||||
case JArray(fields):
|
||||
return Either.Right(fields.map((field) -> cast Tools.getValue(field)));
|
||||
default:
|
||||
throw 'Expected property $name to be one or multiple floats, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser which outputs a `Either<Float, LegacyScrollSpeeds>`.
|
||||
* Used by the FNF legacy JSON importer.
|
||||
|
|
|
@ -3,11 +3,14 @@ package funkin.data;
|
|||
import funkin.util.SerializerUtil;
|
||||
import thx.semver.Version;
|
||||
import thx.semver.VersionRule;
|
||||
import haxe.ds.Either;
|
||||
|
||||
/**
|
||||
* `json2object` has an annotation `@:jcustomwrite` which allows for custom serialization of values to be written to JSON.
|
||||
*
|
||||
* Functions must be of the signature `(T) -> String`, where `T` is the type of the property.
|
||||
*
|
||||
* NOTE: Result must include quotation marks if the value is a string! json2object will not add them for you!
|
||||
*/
|
||||
class DataWrite
|
||||
{
|
||||
|
@ -23,11 +26,12 @@ class DataWrite
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* `@:jcustomwrite(funkin.data.DataWrite.semverVersion)`
|
||||
*/
|
||||
public static function semverVersion(value:Version):String
|
||||
{
|
||||
return value.toString();
|
||||
return '"${value.toString()}"';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,6 +39,22 @@ class DataWrite
|
|||
*/
|
||||
public static function semverVersionRule(value:VersionRule):String
|
||||
{
|
||||
return value.toString();
|
||||
return '"${value.toString()}"';
|
||||
}
|
||||
|
||||
/**
|
||||
* `@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)`
|
||||
*/
|
||||
public static function eitherFloatOrFloats(value:Null<Either<Float, Array<Float>>>):String
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case null:
|
||||
return '${1.0}';
|
||||
case Left(inner):
|
||||
return '$inner';
|
||||
case Right(inner):
|
||||
return dynamicValue(inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,10 @@ typedef UnnamedAnimationData =
|
|||
* The prefix for the frames of the animation as defined by the XML file.
|
||||
* This will may or may not differ from the `name` of the animation,
|
||||
* depending on how your animator organized their FLA or whatever.
|
||||
*
|
||||
* NOTE: For Sparrow animations, this is not optional, but for Packer animations it is.
|
||||
*/
|
||||
@:optional
|
||||
var prefix:String;
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,8 +15,6 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
|
|||
|
||||
public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
|
||||
|
||||
public static final DEFAULT_NOTE_STYLE_ID:String = "funkin";
|
||||
|
||||
public static final instance:NoteStyleRegistry = new NoteStyleRegistry();
|
||||
|
||||
public function new()
|
||||
|
@ -26,7 +24,7 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
|
|||
|
||||
public function fetchDefault():NoteStyle
|
||||
{
|
||||
return fetchEntry(DEFAULT_NOTE_STYLE_ID);
|
||||
return fetchEntry(Constants.DEFAULT_NOTE_STYLE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
package funkin.data.song;
|
||||
|
||||
import flixel.util.typeLimit.OneOfTwo;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import thx.semver.Version;
|
||||
|
||||
/**
|
||||
* Data containing information about a song.
|
||||
* It should contain all the data needed to display a song in the Freeplay menu, or to load the assets required to play its chart.
|
||||
* Data which is only necessary in-game should be stored in the SongChartData.
|
||||
*/
|
||||
@:nullSafety
|
||||
class SongMetadata
|
||||
{
|
||||
|
@ -35,13 +39,11 @@ class SongMetadata
|
|||
*/
|
||||
public var playData:SongPlayData;
|
||||
|
||||
// @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
|
||||
@:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
|
||||
public var generatedBy:String;
|
||||
|
||||
// @:default(funkin.data.song.SongData.SongTimeFormat.MILLISECONDS)
|
||||
public var timeFormat:SongTimeFormat;
|
||||
|
||||
// @:default(funkin.data.song.SongData.SongTimeChange.DEFAULT_SONGTIMECHANGES)
|
||||
public var timeChanges:Array<SongTimeChange>;
|
||||
|
||||
/**
|
||||
|
@ -64,7 +66,7 @@ class SongMetadata
|
|||
this.playData.difficulties = [];
|
||||
this.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
|
||||
this.playData.stage = 'mainStage';
|
||||
this.playData.noteSkin = 'funkin';
|
||||
this.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE;
|
||||
this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
|
||||
// Variation ID.
|
||||
this.variation = (variation == null) ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
@ -298,23 +300,27 @@ class SongPlayData
|
|||
|
||||
/**
|
||||
* The note style used by this song.
|
||||
* TODO: Rename to `noteStyle`? Renaming values is a breaking change to the metadata format.
|
||||
*/
|
||||
public var noteSkin:String;
|
||||
public var noteStyle:String;
|
||||
|
||||
/**
|
||||
* The difficulty rating for this song as displayed in Freeplay.
|
||||
* TODO: Adding this is a non-breaking change to the metadata format.
|
||||
* The difficulty ratings for this song as displayed in Freeplay.
|
||||
* Key is a difficulty ID or `default`.
|
||||
*/
|
||||
// public var rating:Int;
|
||||
@:default(['default' => 1])
|
||||
public var ratings:Map<String, Int>;
|
||||
|
||||
/**
|
||||
* The album ID for the album to display in Freeplay.
|
||||
* TODO: Adding this is a non-breaking change to the metadata format.
|
||||
* If `null`, display no album.
|
||||
*/
|
||||
// public var album:String;
|
||||
@:optional
|
||||
public var album:Null<String>;
|
||||
|
||||
public function new() {}
|
||||
public function new()
|
||||
{
|
||||
ratings = new Map<String, Int>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.data.song;
|
|||
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0;
|
||||
import funkin.data.song.migrator.SongData_v2_1_0.SongMetadata_v2_1_0;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.play.song.ScriptedSong;
|
||||
|
@ -18,9 +19,9 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateStageData()` function.
|
||||
*/
|
||||
public static final SONG_METADATA_VERSION:thx.semver.Version = "2.1.0";
|
||||
public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.0";
|
||||
|
||||
public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.1.x";
|
||||
public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x";
|
||||
|
||||
public static final SONG_CHART_DATA_VERSION:thx.semver.Version = "2.0.0";
|
||||
|
||||
|
@ -165,6 +166,10 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
{
|
||||
return parseEntryMetadata(id, variation);
|
||||
}
|
||||
else if (VersionUtil.validateVersion(version, "2.1.x"))
|
||||
{
|
||||
return parseEntryMetadata_v2_1_0(id, variation);
|
||||
}
|
||||
else if (VersionUtil.validateVersion(version, "2.0.x"))
|
||||
{
|
||||
return parseEntryMetadata_v2_0_0(id, variation);
|
||||
|
@ -182,6 +187,10 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
{
|
||||
return parseEntryMetadataRaw(contents, fileName);
|
||||
}
|
||||
else if (VersionUtil.validateVersion(version, "2.1.x"))
|
||||
{
|
||||
return parseEntryMetadataRaw_v2_1_0(contents, fileName);
|
||||
}
|
||||
else if (VersionUtil.validateVersion(version, "2.0.x"))
|
||||
{
|
||||
return parseEntryMetadataRaw_v2_0_0(contents, fileName);
|
||||
|
@ -192,12 +201,12 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
}
|
||||
}
|
||||
|
||||
function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null<SongMetadata>
|
||||
function parseEntryMetadata_v2_1_0(id:String, ?variation:String):Null<SongMetadata>
|
||||
{
|
||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
||||
switch (loadEntryMetadataFile(id))
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
|
||||
switch (loadEntryMetadataFile(id, variation))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
|
@ -209,6 +218,39 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
return cleanMetadata(parser.value.migrate(), variation);
|
||||
}
|
||||
|
||||
function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null<SongMetadata>
|
||||
{
|
||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
||||
switch (loadEntryMetadataFile(id, variation))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
return cleanMetadata(parser.value.migrate(), variation);
|
||||
}
|
||||
|
||||
function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value.migrate();
|
||||
}
|
||||
|
||||
|
|
84
source/funkin/data/song/importer/ChartManifestData.hx
Normal file
84
source/funkin/data/song/importer/ChartManifestData.hx
Normal file
|
@ -0,0 +1,84 @@
|
|||
package funkin.data.song.importer;
|
||||
|
||||
/**
|
||||
* A helper JSON blob found in `.fnfc` files.
|
||||
*/
|
||||
class ChartManifestData
|
||||
{
|
||||
/**
|
||||
* The current semantic version of the chart manifest data.
|
||||
*/
|
||||
public static final CHART_MANIFEST_DATA_VERSION:thx.semver.Version = "1.0.0";
|
||||
|
||||
@:default(funkin.data.song.importer.ChartManifestData.CHART_MANIFEST_DATA_VERSION)
|
||||
@:jcustomparse(funkin.data.DataParse.semverVersion)
|
||||
@:jcustomwrite(funkin.data.DataWrite.semverVersion)
|
||||
public var version:thx.semver.Version;
|
||||
|
||||
/**
|
||||
* The internal song ID for this chart.
|
||||
* The metadata and chart data file names are derived from this.
|
||||
*/
|
||||
public var songId:String;
|
||||
|
||||
public function new(songId:String)
|
||||
{
|
||||
this.version = CHART_MANIFEST_DATA_VERSION;
|
||||
this.songId = songId;
|
||||
}
|
||||
|
||||
public function getMetadataFileName(?variation:String):String
|
||||
{
|
||||
if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION;
|
||||
|
||||
return '$songId-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_DATA}';
|
||||
}
|
||||
|
||||
public function getChartDataFileName(?variation:String):String
|
||||
{
|
||||
if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION;
|
||||
|
||||
return '$songId-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_DATA}';
|
||||
}
|
||||
|
||||
public function getInstFileName(?variation:String):String
|
||||
{
|
||||
if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION;
|
||||
|
||||
return 'Inst${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_SOUND}';
|
||||
}
|
||||
|
||||
public function getVocalsFileName(charId:String, ?variation:String):String
|
||||
{
|
||||
if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION;
|
||||
|
||||
return 'Voices-$charId${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_SOUND}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize this ChartManifestData into a JSON string.
|
||||
* @return The JSON string.
|
||||
*/
|
||||
public function serialize(pretty:Bool = true):String
|
||||
{
|
||||
var writer = new json2object.JsonWriter<ChartManifestData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public static function deserialize(contents:String):Null<ChartManifestData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<ChartManifestData>();
|
||||
parser.fromJson(contents, 'manifest.json');
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
trace('[ChartManifest] Failed to parse chart file manifest');
|
||||
|
||||
for (error in parser.errors)
|
||||
DataError.printError(error);
|
||||
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0;
|
|||
import funkin.data.song.migrator.SongData_v2_0_0.SongPlayData_v2_0_0;
|
||||
import funkin.data.song.migrator.SongData_v2_0_0.SongPlayableChar_v2_0_0;
|
||||
|
||||
using funkin.data.song.migrator.SongDataMigrator; // Does this even work lol?
|
||||
|
||||
/**
|
||||
* This class contains functions to migrate older data formats to the current one.
|
||||
*
|
||||
|
@ -15,6 +17,48 @@ import funkin.data.song.migrator.SongData_v2_0_0.SongPlayableChar_v2_0_0;
|
|||
*/
|
||||
class SongDataMigrator
|
||||
{
|
||||
public static overload extern inline function migrate(input:SongData_v2_1_0.SongMetadata_v2_1_0):SongMetadata
|
||||
{
|
||||
return migrate_SongMetadata_v2_1_0(input);
|
||||
}
|
||||
|
||||
public static function migrate_SongMetadata_v2_1_0(input:SongData_v2_1_0.SongMetadata_v2_1_0):SongMetadata
|
||||
{
|
||||
var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation);
|
||||
result.version = SongRegistry.SONG_METADATA_VERSION;
|
||||
result.timeFormat = input.timeFormat;
|
||||
result.divisions = input.divisions;
|
||||
result.timeChanges = input.timeChanges;
|
||||
result.looped = input.looped;
|
||||
result.playData = input.playData.migrate();
|
||||
result.generatedBy = input.generatedBy;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static overload extern inline function migrate(input:SongData_v2_1_0.SongPlayData_v2_1_0):SongPlayData
|
||||
{
|
||||
return migrate_SongPlayData_v2_1_0(input);
|
||||
}
|
||||
|
||||
public static function migrate_SongPlayData_v2_1_0(input:SongData_v2_1_0.SongPlayData_v2_1_0):SongPlayData
|
||||
{
|
||||
var result:SongPlayData = new SongPlayData();
|
||||
result.songVariations = input.songVariations;
|
||||
result.difficulties = input.difficulties;
|
||||
result.stage = input.stage;
|
||||
result.characters = input.characters;
|
||||
|
||||
// Renamed
|
||||
result.noteStyle = input.noteSkin;
|
||||
|
||||
// Added
|
||||
result.ratings = ['default' => 1];
|
||||
result.album = null;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static overload extern inline function migrate(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata
|
||||
{
|
||||
return migrate_SongMetadata_v2_0_0(input);
|
||||
|
@ -23,12 +67,12 @@ class SongDataMigrator
|
|||
public static function migrate_SongMetadata_v2_0_0(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata
|
||||
{
|
||||
var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation);
|
||||
result.version = input.version;
|
||||
result.version = SongRegistry.SONG_METADATA_VERSION;
|
||||
result.timeFormat = input.timeFormat;
|
||||
result.divisions = input.divisions;
|
||||
result.timeChanges = input.timeChanges;
|
||||
result.looped = input.looped;
|
||||
result.playData = migrate_SongPlayData_v2_0_0(input.playData);
|
||||
result.playData = input.playData.migrate();
|
||||
result.generatedBy = input.generatedBy;
|
||||
|
||||
return result;
|
||||
|
@ -45,7 +89,13 @@ class SongDataMigrator
|
|||
result.songVariations = input.songVariations;
|
||||
result.difficulties = input.difficulties;
|
||||
result.stage = input.stage;
|
||||
result.noteSkin = input.noteSkin;
|
||||
|
||||
// Added
|
||||
result.ratings = ['default' => 1];
|
||||
result.album = null;
|
||||
|
||||
// Renamed
|
||||
result.noteStyle = input.noteSkin;
|
||||
|
||||
// Fetch the first playable character and migrate it.
|
||||
var firstCharKey:Null<String> = input.playableChars.size() == 0 ? null : input.playableChars.keys().array()[0];
|
||||
|
|
|
@ -42,6 +42,7 @@ class SongMetadata_v2_0_0
|
|||
@:default(false)
|
||||
public var looped:Bool;
|
||||
|
||||
@:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
|
||||
public var generatedBy:String;
|
||||
|
||||
public var timeFormat:SongData.SongTimeFormat;
|
||||
|
@ -70,6 +71,13 @@ class SongPlayData_v2_0_0
|
|||
*/
|
||||
public var playableChars:Map<String, SongPlayableChar_v2_0_0>;
|
||||
|
||||
/**
|
||||
* In metadata version `v2.2.0`, this was renamed to `noteStyle`.
|
||||
*/
|
||||
public var noteSkin:String;
|
||||
|
||||
// In 2.2.0, the ratings value was added.
|
||||
// In 2.2.0, the album value was added.
|
||||
// ==========
|
||||
// UNMODIFIED VALUES
|
||||
// ==========
|
||||
|
@ -77,7 +85,6 @@ class SongPlayData_v2_0_0
|
|||
public var difficulties:Array<String>;
|
||||
|
||||
public var stage:String;
|
||||
public var noteSkin:String;
|
||||
|
||||
public function new() {}
|
||||
|
||||
|
|
108
source/funkin/data/song/migrator/SongData_v2_1_0.hx
Normal file
108
source/funkin/data/song/migrator/SongData_v2_1_0.hx
Normal file
|
@ -0,0 +1,108 @@
|
|||
package funkin.data.song.migrator;
|
||||
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import thx.semver.Version;
|
||||
|
||||
@:nullSafety
|
||||
class SongMetadata_v2_1_0
|
||||
{
|
||||
// ==========
|
||||
// MODIFIED VALUES
|
||||
// ===========
|
||||
|
||||
/**
|
||||
* In metadata `v2.2.0`, `SongPlayData` was refactored.
|
||||
*/
|
||||
public var playData:SongPlayData_v2_1_0;
|
||||
|
||||
// ==========
|
||||
// UNMODIFIED VALUES
|
||||
// ==========
|
||||
@:jcustomparse(funkin.data.DataParse.semverVersion)
|
||||
@:jcustomwrite(funkin.data.DataWrite.semverVersion)
|
||||
public var version:Version;
|
||||
|
||||
@:default("Unknown")
|
||||
public var songName:String;
|
||||
|
||||
@:default("Unknown")
|
||||
public var artist:String;
|
||||
|
||||
@:optional
|
||||
@:default(96)
|
||||
public var divisions:Null<Int>; // Optional field
|
||||
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var looped:Bool;
|
||||
|
||||
@:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
|
||||
public var generatedBy:String;
|
||||
|
||||
public var timeFormat:SongData.SongTimeFormat;
|
||||
|
||||
public var timeChanges:Array<SongData.SongTimeChange>;
|
||||
|
||||
/**
|
||||
* Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
|
||||
*/
|
||||
@:jignored
|
||||
public var variation:String;
|
||||
|
||||
public function new(songName:String, artist:String, ?variation:String)
|
||||
{
|
||||
this.version = SongRegistry.SONG_METADATA_VERSION;
|
||||
this.songName = songName;
|
||||
this.artist = artist;
|
||||
this.timeFormat = 'ms';
|
||||
this.divisions = null;
|
||||
this.timeChanges = [new SongTimeChange(0, 100)];
|
||||
this.looped = false;
|
||||
this.playData = new SongPlayData_v2_1_0();
|
||||
this.playData.songVariations = [];
|
||||
this.playData.difficulties = [];
|
||||
this.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
|
||||
this.playData.stage = 'mainStage';
|
||||
this.playData.noteSkin = 'funkin';
|
||||
this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
|
||||
// Variation ID.
|
||||
this.variation = (variation == null) ? Constants.DEFAULT_VARIATION : variation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongMetadata[LEGACY:v2.1.0](${this.songName} by ${this.artist}, variation ${this.variation})';
|
||||
}
|
||||
}
|
||||
|
||||
class SongPlayData_v2_1_0
|
||||
{
|
||||
/**
|
||||
* In `v2.2.0`, this value was renamed to `noteStyle`.
|
||||
*/
|
||||
public var noteSkin:String;
|
||||
|
||||
// In 2.2.0, the ratings value was added.
|
||||
// In 2.2.0, the album value was added.
|
||||
// ==========
|
||||
// UNMODIFIED VALUES
|
||||
// ==========
|
||||
public var songVariations:Array<String>;
|
||||
public var difficulties:Array<String>;
|
||||
public var characters:SongData.SongCharacterData;
|
||||
public var stage:String;
|
||||
|
||||
public function new() {}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongPlayData[LEGACY:v2.1.0](${this.songVariations}, ${this.difficulties})';
|
||||
}
|
||||
}
|
|
@ -1471,7 +1471,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
case 'school': 'pixel';
|
||||
case 'schoolEvil': 'pixel';
|
||||
default: 'funkin';
|
||||
default: Constants.DEFAULT_NOTE_STYLE;
|
||||
}
|
||||
var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
|
||||
if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
|
||||
|
@ -2389,8 +2389,8 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
#if sys
|
||||
// spitter for ravy, teehee!!
|
||||
|
||||
var output = SerializerUtil.toJSON(inputSpitter);
|
||||
var writer = new json2object.JsonWriter<Array<ScoreInput>>();
|
||||
var output = writer.write(inputSpitter, ' ');
|
||||
sys.io.File.saveContent("./scores.json", output);
|
||||
#end
|
||||
|
||||
|
|
|
@ -96,11 +96,13 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
if (_data != null && _data.playData != null)
|
||||
{
|
||||
for (vari in _data.playData.songVariations)
|
||||
{
|
||||
variations.push(vari);
|
||||
}
|
||||
|
||||
for (meta in fetchVariationMetadata(id))
|
||||
_metadata.push(meta);
|
||||
var variMeta = fetchVariationMetadata(id, vari);
|
||||
if (variMeta != null) _metadata.push(variMeta);
|
||||
}
|
||||
}
|
||||
|
||||
if (_metadata.length == 0)
|
||||
{
|
||||
|
@ -178,7 +180,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
difficulty.generatedBy = metadata.generatedBy;
|
||||
|
||||
difficulty.stage = metadata.playData.stage;
|
||||
difficulty.noteStyle = metadata.playData.noteSkin;
|
||||
difficulty.noteStyle = metadata.playData.noteStyle;
|
||||
|
||||
difficulties.set(diffId, difficulty);
|
||||
|
||||
|
@ -337,17 +339,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
return SongRegistry.instance.parseEntryMetadataWithMigration(id, Constants.DEFAULT_VARIATION, version);
|
||||
}
|
||||
|
||||
function fetchVariationMetadata(id:String):Array<SongMetadata>
|
||||
function fetchVariationMetadata(id:String, vari:String):Null<SongMetadata>
|
||||
{
|
||||
var result:Array<SongMetadata> = [];
|
||||
for (vari in variations)
|
||||
{
|
||||
var version:Null<thx.semver.Version> = SongRegistry.instance.fetchEntryMetadataVersion(id, vari);
|
||||
if (version == null) continue;
|
||||
var meta:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataWithMigration(id, vari, version);
|
||||
if (meta != null) result.push(meta);
|
||||
}
|
||||
return result;
|
||||
var version:Null<thx.semver.Version> = SongRegistry.instance.fetchEntryMetadataVersion(id, vari);
|
||||
if (version == null) return null;
|
||||
var meta:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataWithMigration(id, vari, version);
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -176,13 +176,13 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
continue;
|
||||
}
|
||||
|
||||
if (Std.isOfType(dataProp.scale, Array))
|
||||
switch (dataProp.scale)
|
||||
{
|
||||
propSprite.scale.set(dataProp.scale[0], dataProp.scale[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
propSprite.scale.set(dataProp.scale);
|
||||
case Left(value):
|
||||
propSprite.scale.set(value);
|
||||
|
||||
case Right(values):
|
||||
propSprite.scale.set(values[0], values[1]);
|
||||
}
|
||||
propSprite.updateHitbox();
|
||||
|
||||
|
@ -194,8 +194,15 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
// If pixel, disable antialiasing.
|
||||
propSprite.antialiasing = !dataProp.isPixel;
|
||||
|
||||
propSprite.scrollFactor.x = dataProp.scroll[0];
|
||||
propSprite.scrollFactor.y = dataProp.scroll[1];
|
||||
switch (dataProp.scroll)
|
||||
{
|
||||
case Left(value):
|
||||
propSprite.scrollFactor.x = value;
|
||||
propSprite.scrollFactor.y = value;
|
||||
case Right(values):
|
||||
propSprite.scrollFactor.x = values[0];
|
||||
propSprite.scrollFactor.y = values[1];
|
||||
}
|
||||
|
||||
propSprite.zIndex = dataProp.zIndex;
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package funkin.play.stage;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
import flixel.util.typeLimit.OneOfTwo;
|
||||
import funkin.play.stage.ScriptedStage;
|
||||
import funkin.play.stage.Stage;
|
||||
import funkin.util.VersionUtil;
|
||||
|
@ -157,15 +156,26 @@ class StageDataParser
|
|||
return rawJson;
|
||||
}
|
||||
|
||||
static function migrateStageData(rawJson:String, stageId:String)
|
||||
static function migrateStageData(rawJson:String, stageId:String):Null<StageData>
|
||||
{
|
||||
// If you update the stage data format in a breaking way,
|
||||
// handle migration here by checking the `version` value.
|
||||
|
||||
try
|
||||
{
|
||||
var stageData:StageData = cast Json.parse(rawJson);
|
||||
return stageData;
|
||||
var parser = new json2object.JsonParser<StageData>();
|
||||
parser.fromJson(rawJson, '$stageId.json');
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
trace('[STAGE] Failed to parse stage data');
|
||||
|
||||
for (error in parser.errors)
|
||||
funkin.data.DataError.printError(error);
|
||||
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
|
@ -269,24 +279,29 @@ class StageDataParser
|
|||
inputProp.danceEvery = DEFAULT_DANCEEVERY;
|
||||
}
|
||||
|
||||
if (inputProp.scale == null)
|
||||
{
|
||||
inputProp.scale = DEFAULT_SCALE;
|
||||
}
|
||||
|
||||
if (inputProp.animType == null)
|
||||
{
|
||||
inputProp.animType = DEFAULT_ANIMTYPE;
|
||||
}
|
||||
|
||||
if (Std.isOfType(inputProp.scale, Float))
|
||||
switch (inputProp.scale)
|
||||
{
|
||||
inputProp.scale = [inputProp.scale, inputProp.scale];
|
||||
case null:
|
||||
inputProp.scale = Right([DEFAULT_SCALE, DEFAULT_SCALE]);
|
||||
case Left(value):
|
||||
inputProp.scale = Right([value, value]);
|
||||
case Right(_):
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
if (inputProp.scroll == null)
|
||||
switch (inputProp.scroll)
|
||||
{
|
||||
inputProp.scroll = DEFAULT_SCROLL;
|
||||
case null:
|
||||
inputProp.scroll = Right(DEFAULT_SCROLL);
|
||||
case Left(value):
|
||||
inputProp.scroll = Right([value, value]);
|
||||
case Right(_):
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
if (inputProp.alpha == null)
|
||||
|
@ -294,11 +309,6 @@ class StageDataParser
|
|||
inputProp.alpha = DEFAULT_ALPHA;
|
||||
}
|
||||
|
||||
if (Std.isOfType(inputProp.scroll, Float))
|
||||
{
|
||||
inputProp.scroll = [inputProp.scroll, inputProp.scroll];
|
||||
}
|
||||
|
||||
if (inputProp.animations == null)
|
||||
{
|
||||
inputProp.animations = [];
|
||||
|
@ -392,23 +402,39 @@ class StageDataParser
|
|||
}
|
||||
}
|
||||
|
||||
typedef StageData =
|
||||
class StageData
|
||||
{
|
||||
/**
|
||||
* The sematic version number of the stage data JSON format.
|
||||
* Supports fancy comparisons like NPM does it's neat.
|
||||
*/
|
||||
var version:String;
|
||||
public var version:String;
|
||||
|
||||
var name:String;
|
||||
var cameraZoom:Null<Float>;
|
||||
var props:Array<StageDataProp>;
|
||||
var characters:
|
||||
{
|
||||
bf:StageDataCharacter,
|
||||
dad:StageDataCharacter,
|
||||
gf:StageDataCharacter,
|
||||
};
|
||||
public var name:String;
|
||||
public var cameraZoom:Null<Float>;
|
||||
public var props:Array<StageDataProp>;
|
||||
public var characters:StageDataCharacters;
|
||||
|
||||
public function new()
|
||||
{
|
||||
this.version = StageDataParser.STAGE_DATA_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this StageData into a JSON string.
|
||||
*/
|
||||
public function serialize(pretty:Bool = true):String
|
||||
{
|
||||
var writer = new json2object.JsonWriter<StageData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
}
|
||||
|
||||
typedef StageDataCharacters =
|
||||
{
|
||||
var bf:StageDataCharacter;
|
||||
var dad:StageDataCharacter;
|
||||
var gf:StageDataCharacter;
|
||||
};
|
||||
|
||||
typedef StageDataProp =
|
||||
|
@ -417,6 +443,7 @@ typedef StageDataProp =
|
|||
* The name of the prop for later lookup by scripts.
|
||||
* Optional; if unspecified, the prop can't be referenced by scripts.
|
||||
*/
|
||||
@:optional
|
||||
var name:String;
|
||||
|
||||
/**
|
||||
|
@ -435,27 +462,35 @@ typedef StageDataProp =
|
|||
* This is just like CSS, it isn't hard.
|
||||
* @default 0
|
||||
*/
|
||||
var zIndex:Null<Int>;
|
||||
@:optional
|
||||
@:default(0)
|
||||
var zIndex:Int;
|
||||
|
||||
/**
|
||||
* If set to true, anti-aliasing will be forcibly disabled on the sprite.
|
||||
* This prevents blurry images on pixel-art levels.
|
||||
* @default false
|
||||
*/
|
||||
var isPixel:Null<Bool>;
|
||||
@:optional
|
||||
@:default(false)
|
||||
var isPixel:Bool;
|
||||
|
||||
/**
|
||||
* Either the scale of the prop as a float, or the [w, h] scale as an array of two floats.
|
||||
* Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory.
|
||||
* @default 1
|
||||
*/
|
||||
var scale:OneOfTwo<Float, Array<Float>>;
|
||||
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
|
||||
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
|
||||
@:optional
|
||||
var scale:haxe.ds.Either<Float, Array<Float>>;
|
||||
|
||||
/**
|
||||
* The alpha of the prop, as a float.
|
||||
* @default 1.0
|
||||
*/
|
||||
var alpha:Null<Float>;
|
||||
@:optional
|
||||
@:default(1.0)
|
||||
var alpha:Float;
|
||||
|
||||
/**
|
||||
* If not zero, this prop will play an animation every X beats of the song.
|
||||
|
@ -464,7 +499,9 @@ typedef StageDataProp =
|
|||
*
|
||||
* @default 0
|
||||
*/
|
||||
var danceEvery:Null<Int>;
|
||||
@:default(0)
|
||||
@:optional
|
||||
var danceEvery:Int;
|
||||
|
||||
/**
|
||||
* How much the prop scrolls relative to the camera. Used to create a parallax effect.
|
||||
|
@ -474,25 +511,32 @@ typedef StageDataProp =
|
|||
* [0, 0] means the prop is not moved.
|
||||
* @default [0, 0]
|
||||
*/
|
||||
var scroll:OneOfTwo<Float, Array<Float>>;
|
||||
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
|
||||
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
|
||||
@:optional
|
||||
var scroll:haxe.ds.Either<Float, Array<Float>>;
|
||||
|
||||
/**
|
||||
* An optional array of animations which the prop can play.
|
||||
* @default Prop has no animations.
|
||||
*/
|
||||
@:optional
|
||||
var animations:Array<AnimationData>;
|
||||
|
||||
/**
|
||||
* If animations are used, this is the name of the animation to play first.
|
||||
* @default Don't play an animation.
|
||||
*/
|
||||
var startingAnimation:String;
|
||||
@:optional
|
||||
var startingAnimation:Null<String>;
|
||||
|
||||
/**
|
||||
* The animation type to use.
|
||||
* Options: "sparrow", "packer"
|
||||
* @default "sparrow"
|
||||
*/
|
||||
@:default("sparrow")
|
||||
@:optional
|
||||
var animType:String;
|
||||
};
|
||||
|
||||
|
@ -503,16 +547,22 @@ typedef StageDataCharacter =
|
|||
* Again, just like CSS.
|
||||
* @default 0
|
||||
*/
|
||||
?zIndex:Int,
|
||||
@:optional
|
||||
@:default(0)
|
||||
var zIndex:Int;
|
||||
|
||||
/**
|
||||
* The position to render the character at.
|
||||
*/
|
||||
position:Array<Float>,
|
||||
@:optional
|
||||
@:default([0, 0])
|
||||
var position:Array<Float>;
|
||||
|
||||
/**
|
||||
* The camera offsets to apply when focusing on the character on this stage.
|
||||
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
|
||||
*/
|
||||
cameraOffsets:Array<Float>,
|
||||
@:optional
|
||||
@:default([0, 0])
|
||||
var cameraOffsets:Array<Float>;
|
||||
};
|
||||
|
|
|
@ -13,8 +13,7 @@ class SaveDataMigrator
|
|||
*/
|
||||
public static function migrate(inputData:Dynamic):Save
|
||||
{
|
||||
// This deserializes directly into a `Version` object, not a `String`.
|
||||
var version:Null<Version> = inputData?.version ?? null;
|
||||
var version:Null<thx.semver.Version> = VersionUtil.parseVersion(inputData?.version ?? null);
|
||||
|
||||
if (version == null)
|
||||
{
|
||||
|
@ -24,7 +23,7 @@ class SaveDataMigrator
|
|||
}
|
||||
else
|
||||
{
|
||||
if (VersionUtil.validateVersionStr(version, Save.SAVE_DATA_VERSION_RULE))
|
||||
if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
|
||||
{
|
||||
// Simply cast the structured data.
|
||||
var save:Save = inputData;
|
||||
|
|
|
@ -265,7 +265,7 @@ class ChartEditorAudioHandler
|
|||
{
|
||||
var data:Null<Bytes> = state.audioVocalTrackData.get(key);
|
||||
if (data == null) continue;
|
||||
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-${key}.ogg', data));
|
||||
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Voices-${key}.ogg', data));
|
||||
}
|
||||
|
||||
return zipEntries;
|
||||
|
|
|
@ -182,8 +182,8 @@ class SelectItemsCommand implements ChartEditorCommand
|
|||
state.currentEventSelection.push(event);
|
||||
}
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
// state.noteDisplayDirty = true;
|
||||
// state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
|
@ -191,8 +191,8 @@ class SelectItemsCommand implements ChartEditorCommand
|
|||
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes);
|
||||
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events);
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
// state.noteDisplayDirty = true;
|
||||
// state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
|
@ -452,8 +452,8 @@ class DeselectItemsCommand implements ChartEditorCommand
|
|||
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes);
|
||||
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events);
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
// state.noteDisplayDirty = true;
|
||||
// state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
|
@ -468,8 +468,8 @@ class DeselectItemsCommand implements ChartEditorCommand
|
|||
state.currentEventSelection.push(event);
|
||||
}
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
// state.noteDisplayDirty = true;
|
||||
// state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
|
|
|
@ -51,12 +51,13 @@ class ChartEditorDialogHandler
|
|||
{
|
||||
static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT:String = Paths.ui('chart-editor/dialogs/about');
|
||||
static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT:String = Paths.ui('chart-editor/dialogs/welcome');
|
||||
static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart');
|
||||
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst');
|
||||
static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata');
|
||||
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals');
|
||||
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry');
|
||||
static final CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart');
|
||||
static final CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-entry');
|
||||
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts');
|
||||
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry');
|
||||
static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart');
|
||||
static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide');
|
||||
static final CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-variation');
|
||||
|
@ -83,6 +84,11 @@ class ChartEditorDialogHandler
|
|||
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable);
|
||||
if (dialog == null) throw 'Could not locate Welcome dialog';
|
||||
|
||||
dialog.onDialogClosed = function(_event) {
|
||||
// Called when the Welcome dialog is closed while it is closable.
|
||||
state.stopWelcomeMusic();
|
||||
}
|
||||
|
||||
// Create New Song "Easy/Normal/Hard"
|
||||
var linkCreateBasic:Null<Link> = dialog.findComponent('splashCreateFromSongBasic', Link);
|
||||
if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasic link in Welcome dialog';
|
||||
|
@ -129,7 +135,7 @@ class ChartEditorDialogHandler
|
|||
state.stopWelcomeMusic();
|
||||
|
||||
// Open the "Open Chart" dialog
|
||||
openBrowseWizard(state, false);
|
||||
openBrowseFNFC(state, false);
|
||||
}
|
||||
|
||||
var splashTemplateContainer:Null<VBox> = dialog.findComponent('splashTemplateContainer', VBox);
|
||||
|
@ -168,6 +174,126 @@ class ChartEditorDialogHandler
|
|||
return dialog;
|
||||
}
|
||||
|
||||
public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog>
|
||||
{
|
||||
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT, true, closable);
|
||||
if (dialog == null) throw 'Could not locate Upload Chart dialog';
|
||||
|
||||
dialog.onDialogClosed = function(_event) {
|
||||
if (_event.button == DialogButton.APPLY)
|
||||
{
|
||||
// Simply let the dialog close.
|
||||
}
|
||||
else
|
||||
{
|
||||
// User cancelled the wizard! Back to the welcome dialog.
|
||||
openWelcomeDialog(state);
|
||||
}
|
||||
};
|
||||
|
||||
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
|
||||
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Chart dialog';
|
||||
|
||||
buttonCancel.onClick = function(_event) {
|
||||
dialog.hideDialog(DialogButton.CANCEL);
|
||||
}
|
||||
|
||||
var chartBox:Null<Box> = dialog.findComponent('chartBox', Box);
|
||||
if (chartBox == null) throw 'Could not locate chartBox in Upload Chart dialog';
|
||||
|
||||
chartBox.onMouseOver = function(_event) {
|
||||
chartBox.swapClass('upload-bg', 'upload-bg-hover');
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
|
||||
chartBox.onMouseOut = function(_event) {
|
||||
chartBox.swapClass('upload-bg-hover', 'upload-bg');
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
|
||||
var onDropFile:String->Void;
|
||||
|
||||
chartBox.onClick = function(_event) {
|
||||
Dialogs.openBinaryFile('Open Chart', [
|
||||
{label: 'Friday Night Funkin\' Chart (.fnfc)', extension: 'fnfc'}], function(selectedFile:SelectedFileInfo) {
|
||||
if (selectedFile != null && selectedFile.bytes != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes))
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded chart (${selectedFile.name})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
|
||||
if (selectedFile.fullPath != null) state.currentWorkingFilePath = selectedFile.fullPath;
|
||||
dialog.hideDialog(DialogButton.APPLY);
|
||||
removeDropHandler(onDropFile);
|
||||
}
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Failed to load chart (${selectedFile.name}): ${err}',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDropFile = function(pathStr:String) {
|
||||
var path:Path = new Path(pathStr);
|
||||
trace('Dropped file (${path})');
|
||||
|
||||
try
|
||||
{
|
||||
if (ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString()))
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded chart (${path.toString()})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
|
||||
dialog.hideDialog(DialogButton.APPLY);
|
||||
removeDropHandler(onDropFile);
|
||||
}
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Failure',
|
||||
body: 'Failed to load chart (${path.toString()}): ${err}',
|
||||
type: NotificationType.Error,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
};
|
||||
|
||||
addDropHandler(chartBox, onDropFile);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the wizard for opening an existing chart from individual files.
|
||||
* @param state
|
||||
|
@ -622,9 +748,9 @@ class ChartEditorDialogHandler
|
|||
if (inputNoteStyle == null) throw 'Could not locate inputNoteStyle DropDown in Song Metadata dialog';
|
||||
inputNoteStyle.onChange = function(event:UIEvent) {
|
||||
if (event.data.id == null) return;
|
||||
newSongMetadata.playData.noteSkin = event.data.id;
|
||||
newSongMetadata.playData.noteStyle = event.data.id;
|
||||
};
|
||||
var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, newSongMetadata.playData.noteSkin);
|
||||
var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, newSongMetadata.playData.noteStyle);
|
||||
inputNoteStyle.value = startingValueNoteStyle;
|
||||
|
||||
var inputCharacterPlayer:Null<FunkinDropDown> = dialog.findComponent('inputCharacterPlayer', FunkinDropDown);
|
||||
|
@ -765,9 +891,9 @@ class ChartEditorDialogHandler
|
|||
});
|
||||
#end
|
||||
#if FILE_DROP_SUPPORTED
|
||||
vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
|
||||
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
|
||||
#else
|
||||
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${path.file}.${path.ext}';
|
||||
vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}';
|
||||
#end
|
||||
|
||||
dialogNoVocals.hidden = true;
|
||||
|
@ -820,9 +946,9 @@ class ChartEditorDialogHandler
|
|||
});
|
||||
#end
|
||||
#if FILE_DROP_SUPPORTED
|
||||
vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
|
||||
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
|
||||
#else
|
||||
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
|
||||
vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}';
|
||||
#end
|
||||
|
||||
dialogNoVocals.hidden = true;
|
||||
|
@ -877,7 +1003,7 @@ class ChartEditorDialogHandler
|
|||
@:haxe.warning('-WVarInit')
|
||||
public static function openChartDialog(state:ChartEditorState, closable:Bool = true):Dialog
|
||||
{
|
||||
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT, true, closable);
|
||||
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT, true, closable);
|
||||
if (dialog == null) throw 'Could not locate Open Chart dialog';
|
||||
|
||||
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
|
||||
|
@ -915,7 +1041,7 @@ class ChartEditorDialogHandler
|
|||
}
|
||||
|
||||
// Build an entry for -chart.json.
|
||||
var songDefaultChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
|
||||
var songDefaultChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
|
||||
var songDefaultChartDataEntryLabel:Null<Label> = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label);
|
||||
if (songDefaultChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
|
||||
#if FILE_DROP_SUPPORTED
|
||||
|
@ -931,7 +1057,7 @@ class ChartEditorDialogHandler
|
|||
for (variation in variations)
|
||||
{
|
||||
// Build entries for -metadata-<variation>.json.
|
||||
var songVariationMetadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
|
||||
var songVariationMetadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
|
||||
var songVariationMetadataEntryLabel:Null<Label> = songVariationMetadataEntry.findComponent('chartEntryLabel', Label);
|
||||
if (songVariationMetadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
|
||||
#if FILE_DROP_SUPPORTED
|
||||
|
@ -955,7 +1081,7 @@ class ChartEditorDialogHandler
|
|||
chartContainerB.addComponent(songVariationMetadataEntry);
|
||||
|
||||
// Build entries for -chart-<variation>.json.
|
||||
var songVariationChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
|
||||
var songVariationChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
|
||||
var songVariationChartDataEntryLabel:Null<Label> = songVariationChartDataEntry.findComponent('chartEntryLabel', Label);
|
||||
if (songVariationChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
|
||||
#if FILE_DROP_SUPPORTED
|
||||
|
@ -1230,7 +1356,7 @@ class ChartEditorDialogHandler
|
|||
});
|
||||
}
|
||||
|
||||
var metadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
|
||||
var metadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
|
||||
var metadataEntryLabel:Null<Label> = metadataEntry.findComponent('chartEntryLabel', Label);
|
||||
if (metadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
|
||||
|
||||
|
@ -1447,7 +1573,7 @@ class ChartEditorDialogHandler
|
|||
|
||||
var dialogNoteStyle:Null<DropDown> = dialog.findComponent('dialogNoteStyle', DropDown);
|
||||
if (dialogNoteStyle == null) throw 'Could not locate dialogNoteStyle DropDown in Add Variation dialog';
|
||||
dialogNoteStyle.value = state.currentSongMetadata.playData.noteSkin;
|
||||
dialogNoteStyle.value = state.currentSongMetadata.playData.noteStyle;
|
||||
|
||||
var dialogCharacterPlayer:Null<DropDown> = dialog.findComponent('dialogCharacterPlayer', DropDown);
|
||||
if (dialogCharacterPlayer == null) throw 'Could not locate dialogCharacterPlayer DropDown in Add Variation dialog';
|
||||
|
@ -1479,7 +1605,7 @@ class ChartEditorDialogHandler
|
|||
var pendingVariation:SongMetadata = new SongMetadata(dialogSongName.text, dialogSongArtist.text, dialogVariationName.text.toLowerCase());
|
||||
|
||||
pendingVariation.playData.stage = dialogStage.value.id;
|
||||
pendingVariation.playData.noteSkin = dialogNoteStyle.value;
|
||||
pendingVariation.playData.noteStyle = dialogNoteStyle.value;
|
||||
pendingVariation.timeChanges[0].bpm = dialogBPM.value;
|
||||
|
||||
state.songMetadata.set(pendingVariation.variation, pendingVariation);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.util.VersionUtil;
|
||||
import haxe.ui.notifications.NotificationType;
|
||||
import funkin.util.DateUtil;
|
||||
import haxe.io.Path;
|
||||
|
@ -7,10 +8,12 @@ import funkin.util.SerializerUtil;
|
|||
import haxe.ui.notifications.NotificationManager;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.util.FileUtil;
|
||||
import haxe.io.Bytes;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.song.importer.ChartManifestData;
|
||||
|
||||
/**
|
||||
* Contains functions for importing, loading, saving, and exporting charts.
|
||||
|
@ -104,7 +107,7 @@ class ChartEditorImportExportHandler
|
|||
}
|
||||
|
||||
/**
|
||||
* Loads song metadata and chart data into the editor.
|
||||
* Loads a chart from parsed song metadata and chart data into the editor.
|
||||
* @param newSongMetadata The song metadata to load.
|
||||
* @param newSongChartData The song chart data to load.
|
||||
*/
|
||||
|
@ -135,15 +138,179 @@ class ChartEditorImportExportHandler
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param force Whether to force the export without prompting the user for a file location.
|
||||
*/
|
||||
public static function exportAllSongData(state:ChartEditorState, force:Bool = false):Void
|
||||
public static function loadFromFNFCPath(state:ChartEditorState, path:String):Bool
|
||||
{
|
||||
var bytes:Null<Bytes> = FileUtil.readBytesFromPath(path);
|
||||
if (bytes == null) return false;
|
||||
|
||||
trace('Loaded ${bytes.length} bytes from $path');
|
||||
|
||||
var result:Bool = loadFromFNFC(state, bytes);
|
||||
if (result)
|
||||
{
|
||||
state.currentWorkingFilePath = path;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a chart's metadata, chart data, and audio from an FNFC archive..
|
||||
* @param state
|
||||
* @param bytes
|
||||
* @param instId
|
||||
* @return Bool
|
||||
*/
|
||||
public static function loadFromFNFC(state:ChartEditorState, bytes:Bytes):Bool
|
||||
{
|
||||
var songMetadatas:Map<String, SongMetadata> = [];
|
||||
var songChartDatas:Map<String, SongChartData> = [];
|
||||
|
||||
var fileEntries:Array<haxe.zip.Entry> = FileUtil.readZIPFromBytes(bytes);
|
||||
var mappedFileEntries:Map<String, haxe.zip.Entry> = FileUtil.mapZIPEntriesByName(fileEntries);
|
||||
|
||||
var manifestBytes:Null<Bytes> = mappedFileEntries.get('manifest.json')?.data;
|
||||
if (manifestBytes == null) throw 'Could not locate manifest.';
|
||||
var manifestString = manifestBytes.toString();
|
||||
var manifest:Null<ChartManifestData> = ChartManifestData.deserialize(manifestString);
|
||||
if (manifest == null) throw 'Could not read manifest.';
|
||||
|
||||
// Get the song ID.
|
||||
var songId:String = manifest.songId;
|
||||
|
||||
var baseMetadataPath:String = manifest.getMetadataFileName();
|
||||
var baseChartDataPath:String = manifest.getChartDataFileName();
|
||||
|
||||
var baseMetadataBytes:Null<Bytes> = mappedFileEntries.get(baseMetadataPath)?.data;
|
||||
if (baseMetadataBytes == null) throw 'Could not locate metadata (default).';
|
||||
var baseMetadataString:String = baseMetadataBytes.toString();
|
||||
var baseMetadataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(baseMetadataString);
|
||||
if (baseMetadataVersion == null) throw 'Could not read metadata version (default).';
|
||||
|
||||
var baseMetadata:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataRawWithMigration(baseMetadataString, baseMetadataPath, baseMetadataVersion);
|
||||
if (baseMetadata == null) throw 'Could not read metadata (default).';
|
||||
songMetadatas.set(Constants.DEFAULT_VARIATION, baseMetadata);
|
||||
|
||||
var baseChartDataBytes:Null<Bytes> = mappedFileEntries.get(baseChartDataPath)?.data;
|
||||
if (baseChartDataBytes == null) throw 'Could not locate chart data (default).';
|
||||
var baseChartDataString:String = baseChartDataBytes.toString();
|
||||
var baseChartDataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(baseChartDataString);
|
||||
if (baseChartDataVersion == null) throw 'Could not read chart data (default) version.';
|
||||
|
||||
var baseChartData:Null<SongChartData> = SongRegistry.instance.parseEntryChartDataRawWithMigration(baseChartDataString, baseChartDataPath,
|
||||
baseChartDataVersion);
|
||||
if (baseChartData == null) throw 'Could not read chart data (default).';
|
||||
songChartDatas.set(Constants.DEFAULT_VARIATION, baseChartData);
|
||||
|
||||
var variationList:Array<String> = baseMetadata.playData.songVariations;
|
||||
|
||||
for (variation in variationList)
|
||||
{
|
||||
var variMetadataPath:String = manifest.getMetadataFileName(variation);
|
||||
var variChartDataPath:String = manifest.getChartDataFileName(variation);
|
||||
|
||||
var variMetadataBytes:Null<Bytes> = mappedFileEntries.get(variMetadataPath)?.data;
|
||||
if (variMetadataBytes == null) throw 'Could not locate metadata ($variation).';
|
||||
var variMetadataString:String = variMetadataBytes.toString();
|
||||
var variMetadataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(variMetadataString);
|
||||
if (variMetadataVersion == null) throw 'Could not read metadata ($variation) version.';
|
||||
|
||||
var variMetadata:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataRawWithMigration(baseMetadataString, variMetadataPath, variMetadataVersion);
|
||||
if (variMetadata == null) throw 'Could not read metadata ($variation).';
|
||||
songMetadatas.set(variation, variMetadata);
|
||||
|
||||
var variChartDataBytes:Null<Bytes> = mappedFileEntries.get(variChartDataPath)?.data;
|
||||
if (variChartDataBytes == null) throw 'Could not locate chart data ($variation).';
|
||||
var variChartDataString:String = variChartDataBytes.toString();
|
||||
var variChartDataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(variChartDataString);
|
||||
if (variChartDataVersion == null) throw 'Could not read chart data version ($variation).';
|
||||
|
||||
var variChartData:Null<SongChartData> = SongRegistry.instance.parseEntryChartDataRawWithMigration(variChartDataString, variChartDataPath,
|
||||
variChartDataVersion);
|
||||
if (variChartData == null) throw 'Could not read chart data ($variation).';
|
||||
songChartDatas.set(variation, variChartData);
|
||||
}
|
||||
|
||||
ChartEditorAudioHandler.stopExistingInstrumental(state);
|
||||
ChartEditorAudioHandler.stopExistingVocals(state);
|
||||
|
||||
// Load instrumentals
|
||||
for (variation in [Constants.DEFAULT_VARIATION].concat(variationList))
|
||||
{
|
||||
var variMetadata:Null<SongMetadata> = songMetadatas.get(variation);
|
||||
if (variMetadata == null) continue;
|
||||
|
||||
var instId:String = variMetadata?.playData?.characters?.instrumental ?? '';
|
||||
var playerCharId:String = variMetadata?.playData?.characters?.player ?? Constants.DEFAULT_CHARACTER;
|
||||
var opponentCharId:Null<String> = variMetadata?.playData?.characters?.opponent;
|
||||
|
||||
var instFileName:String = manifest.getInstFileName(instId);
|
||||
var instFileBytes:Null<Bytes> = mappedFileEntries.get(instFileName)?.data;
|
||||
if (instFileBytes != null)
|
||||
{
|
||||
if (!ChartEditorAudioHandler.loadInstFromBytes(state, instFileBytes, instId))
|
||||
{
|
||||
throw 'Could not load instrumental ($instFileName).';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw 'Could not find instrumental ($instFileName).';
|
||||
}
|
||||
|
||||
var playerVocalsFileName:String = manifest.getVocalsFileName(playerCharId);
|
||||
var playerVocalsFileBytes:Null<Bytes> = mappedFileEntries.get(playerVocalsFileName)?.data;
|
||||
if (playerVocalsFileBytes != null)
|
||||
{
|
||||
if (!ChartEditorAudioHandler.loadVocalsFromBytes(state, playerVocalsFileBytes, playerCharId, instId))
|
||||
{
|
||||
throw 'Could not load vocals ($playerCharId).';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw 'Could not find vocals ($playerVocalsFileName).';
|
||||
}
|
||||
|
||||
if (opponentCharId != null)
|
||||
{
|
||||
var opponentVocalsFileName:String = manifest.getVocalsFileName(opponentCharId);
|
||||
var opponentVocalsFileBytes:Null<Bytes> = mappedFileEntries.get(opponentVocalsFileName)?.data;
|
||||
if (opponentVocalsFileBytes != null)
|
||||
{
|
||||
if (!ChartEditorAudioHandler.loadVocalsFromBytes(state, opponentVocalsFileBytes, opponentCharId, instId))
|
||||
{
|
||||
throw 'Could not load vocals ($opponentCharId).';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw 'Could not load vocals ($playerCharId-$instId).';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply chart data.
|
||||
trace(songMetadatas);
|
||||
trace(songChartDatas);
|
||||
loadSong(state, songMetadatas, songChartDatas);
|
||||
|
||||
state.switchToCurrentInstrumental();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param force Whether to export without prompting. `false` will prompt the user for a location.
|
||||
* @param targetPath where to export if `force` is `true`. If `null`, will export to the `backups` folder.
|
||||
*/
|
||||
public static function exportAllSongData(state:ChartEditorState, force:Bool = false, ?targetPath:String):Void
|
||||
{
|
||||
var tmp = false;
|
||||
var zipEntries:Array<haxe.zip.Entry> = [];
|
||||
|
||||
for (variation in state.availableVariations)
|
||||
var variations = state.availableVariations;
|
||||
|
||||
for (variation in variations)
|
||||
{
|
||||
var variationId:String = variation;
|
||||
if (variation == '' || variation == 'default' || variation == 'normal')
|
||||
|
@ -162,50 +329,51 @@ class ChartEditorImportExportHandler
|
|||
{
|
||||
var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
|
||||
if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata-$variationId.json',
|
||||
SerializerUtil.toJSON(variationMetadata)));
|
||||
variationMetadata.serialize()));
|
||||
var variationChart:Null<SongChartData> = state.songChartData.get(variation);
|
||||
if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json',
|
||||
SerializerUtil.toJSON(variationChart)));
|
||||
if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json', variationChart.serialize()));
|
||||
}
|
||||
}
|
||||
|
||||
if (state.audioInstTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromInstrumentals(state));
|
||||
if (state.audioVocalTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromVocals(state));
|
||||
if (state.audioInstTrackData != null) zipEntries = zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromInstrumentals(state));
|
||||
if (state.audioVocalTrackData != null) zipEntries = zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromVocals(state));
|
||||
|
||||
var manifest:ChartManifestData = new ChartManifestData(state.currentSongId);
|
||||
zipEntries.push(FileUtil.makeZIPEntry('manifest.json', manifest.serialize()));
|
||||
|
||||
trace('Exporting ${zipEntries.length} files to ZIP...');
|
||||
|
||||
if (force)
|
||||
{
|
||||
var targetPath:String = if (tmp)
|
||||
if (targetPath == null)
|
||||
{
|
||||
Path.join([
|
||||
FileUtil.getTempDir(),
|
||||
'chart-editor-exit-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}'
|
||||
]);
|
||||
}
|
||||
else
|
||||
{
|
||||
Path.join([
|
||||
targetPath = Path.join([
|
||||
'./backups/',
|
||||
'chart-editor-exit-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}'
|
||||
'chart-editor-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}'
|
||||
]);
|
||||
}
|
||||
|
||||
// We have to force write because the program will die before the save dialog is closed.
|
||||
trace('Force exporting to $targetPath...');
|
||||
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prompt and save.
|
||||
var onSave:Array<String>->Void = function(paths:Array<String>) {
|
||||
trace('Successfully exported files.');
|
||||
};
|
||||
|
||||
// Prompt and save.
|
||||
var onSave:Array<String>->Void = function(paths:Array<String>) {
|
||||
trace('Successfully exported files.');
|
||||
};
|
||||
var onCancel:Void->Void = function() {
|
||||
trace('Export cancelled.');
|
||||
};
|
||||
|
||||
var onCancel:Void->Void = function() {
|
||||
trace('Export cancelled.');
|
||||
};
|
||||
|
||||
FileUtil.saveMultipleFiles(zipEntries, onSave, onCancel, '${state.currentSongId}-chart.${Constants.EXT_CHART}');
|
||||
trace('Exporting to user-defined location...');
|
||||
try
|
||||
{
|
||||
FileUtil.saveChartAsFNFC(zipEntries, onSave, onCancel, '${state.currentSongId}.${Constants.EXT_CHART}');
|
||||
}
|
||||
catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1011,17 +1011,17 @@ class ChartEditorState extends HaxeUIState
|
|||
|
||||
function get_currentSongNoteStyle():String
|
||||
{
|
||||
if (currentSongMetadata.playData.noteSkin == null)
|
||||
if (currentSongMetadata.playData.noteStyle == null)
|
||||
{
|
||||
// Initialize to the default value if not set.
|
||||
currentSongMetadata.playData.noteSkin = 'funkin';
|
||||
currentSongMetadata.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE;
|
||||
}
|
||||
return currentSongMetadata.playData.noteSkin;
|
||||
return currentSongMetadata.playData.noteStyle;
|
||||
}
|
||||
|
||||
function set_currentSongNoteStyle(value:String):String
|
||||
{
|
||||
return currentSongMetadata.playData.noteSkin = value;
|
||||
return currentSongMetadata.playData.noteStyle = value;
|
||||
}
|
||||
|
||||
var currentSongStage(get, set):String;
|
||||
|
@ -1232,10 +1232,22 @@ class ChartEditorState extends HaxeUIState
|
|||
|
||||
var renderedSelectionSquares:FlxTypedSpriteGroup<FlxSprite> = new FlxTypedSpriteGroup<FlxSprite>();
|
||||
|
||||
public function new()
|
||||
/**
|
||||
* The params which were passed in when the Chart Editor was initialized.
|
||||
*/
|
||||
var params:Null<ChartEditorParams>;
|
||||
|
||||
/**
|
||||
* The current file path which the chart editor is working with.
|
||||
*/
|
||||
public var currentWorkingFilePath:Null<String>;
|
||||
|
||||
public function new(?params:ChartEditorParams)
|
||||
{
|
||||
// Load the HaxeUI XML file.
|
||||
super(CHART_EDITOR_LAYOUT);
|
||||
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
override function create():Void
|
||||
|
@ -1251,7 +1263,7 @@ class ChartEditorState extends HaxeUIState
|
|||
fixCamera();
|
||||
|
||||
// Get rid of any music from the previous state.
|
||||
FlxG.sound.music.stop();
|
||||
if (FlxG.sound.music != null) FlxG.sound.music.stop();
|
||||
|
||||
// Play the welcome music.
|
||||
setupWelcomeMusic();
|
||||
|
@ -1277,7 +1289,33 @@ class ChartEditorState extends HaxeUIState
|
|||
|
||||
refresh();
|
||||
|
||||
ChartEditorDialogHandler.openWelcomeDialog(this, false);
|
||||
if (params != null && params.fnfcTargetPath != null)
|
||||
{
|
||||
// Chart editor was opened from the command line. Open the FNFC file now!
|
||||
if (ChartEditorImportExportHandler.loadFromFNFCPath(this, params.fnfcTargetPath))
|
||||
{
|
||||
// Don't open the welcome dialog!
|
||||
|
||||
#if !mac
|
||||
NotificationManager.instance.addNotification(
|
||||
{
|
||||
title: 'Success',
|
||||
body: 'Loaded chart (${params.fnfcTargetPath})',
|
||||
type: NotificationType.Success,
|
||||
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
|
||||
});
|
||||
#end
|
||||
}
|
||||
else
|
||||
{
|
||||
// Song failed to load, open the Welcome dialog so we aren't in a broken state.
|
||||
ChartEditorDialogHandler.openWelcomeDialog(this, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ChartEditorDialogHandler.openWelcomeDialog(this, false);
|
||||
}
|
||||
}
|
||||
|
||||
function setupWelcomeMusic()
|
||||
|
@ -1632,11 +1670,15 @@ class ChartEditorState extends HaxeUIState
|
|||
noteSnapQuantIndex++;
|
||||
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
|
||||
});
|
||||
addUIRightClickListener('playbarNoteSnap', function(_) {
|
||||
noteSnapQuantIndex--;
|
||||
if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1;
|
||||
});
|
||||
|
||||
// Add functionality to the menu items.
|
||||
|
||||
addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
|
||||
addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseWizard(this, true));
|
||||
addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseFNFC(this, true));
|
||||
addUIClickListener('menubarItemSaveChartAs', _ -> ChartEditorImportExportHandler.exportAllSongData(this));
|
||||
addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
|
||||
addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true));
|
||||
|
@ -1776,7 +1818,7 @@ class ChartEditorState extends HaxeUIState
|
|||
addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) {
|
||||
var volume:Float = (event?.value ?? 0) / 100.0;
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
|
||||
vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%';
|
||||
vocalsVolumeLabel.text = 'Voices - ${Std.int(event.value)}%';
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1913,6 +1955,15 @@ class ChartEditorState extends HaxeUIState
|
|||
{
|
||||
FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels);
|
||||
FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels);
|
||||
|
||||
// Add a debug value which displays the current size of the note pool.
|
||||
// The pool will grow as more notes need to be rendered at once.
|
||||
// If this gets too big, something needs to be optimized somewhere! -Eric
|
||||
if (renderedNotes != null && renderedNotes.members != null) FlxG.watch.addQuick("tapNotesRendered", renderedNotes.members.length);
|
||||
if (renderedHoldNotes != null && renderedHoldNotes.members != null) FlxG.watch.addQuick("holdNotesRendered", renderedHoldNotes.members.length);
|
||||
if (renderedEvents != null && renderedEvents.members != null) FlxG.watch.addQuick("eventsRendered", renderedEvents.members.length);
|
||||
if (currentNoteSelection != null) FlxG.watch.addQuick("notesSelected", currentNoteSelection.length);
|
||||
if (currentEventSelection != null) FlxG.watch.addQuick("eventsSelected", currentEventSelection.length);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3037,15 +3088,6 @@ class ChartEditorState extends HaxeUIState
|
|||
// Sort the events DESCENDING. This keeps the sustain behind the associated note.
|
||||
renderedEvents.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort()
|
||||
}
|
||||
|
||||
// Add a debug value which displays the current size of the note pool.
|
||||
// The pool will grow as more notes need to be rendered at once.
|
||||
// If this gets too big, something needs to be optimized somewhere! -Eric
|
||||
FlxG.watch.addQuick("tapNotesRendered", renderedNotes.members.length);
|
||||
FlxG.watch.addQuick("holdNotesRendered", renderedHoldNotes.members.length);
|
||||
FlxG.watch.addQuick("eventsRendered", renderedEvents.members.length);
|
||||
FlxG.watch.addQuick("notesSelected", currentNoteSelection.length);
|
||||
FlxG.watch.addQuick("eventsSelected", currentEventSelection.length);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3152,7 +3194,7 @@ class ChartEditorState extends HaxeUIState
|
|||
// CTRL + O = Open Chart
|
||||
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.O)
|
||||
{
|
||||
ChartEditorDialogHandler.openBrowseWizard(this, true);
|
||||
ChartEditorDialogHandler.openBrowseFNFC(this, true);
|
||||
}
|
||||
|
||||
// CTRL + SHIFT + S = Save As
|
||||
|
@ -3168,10 +3210,13 @@ class ChartEditorState extends HaxeUIState
|
|||
}
|
||||
}
|
||||
|
||||
@:nullSafety(Off)
|
||||
function quitChartEditor():Void
|
||||
{
|
||||
autoSave();
|
||||
stopWelcomeMusic();
|
||||
// TODO: PR Flixel to make onComplete nullable.
|
||||
if (audioInstTrack != null) audioInstTrack.onComplete = null;
|
||||
FlxG.switchState(new MainMenuState());
|
||||
}
|
||||
|
||||
|
@ -3691,7 +3736,7 @@ class ChartEditorState extends HaxeUIState
|
|||
if (inputStage != null) inputStage.value = currentSongMetadata.playData.stage;
|
||||
|
||||
var inputNoteStyle:Null<DropDown> = toolbox.findComponent('inputNoteStyle', DropDown);
|
||||
if (inputNoteStyle != null) inputNoteStyle.value = currentSongMetadata.playData.noteSkin;
|
||||
if (inputNoteStyle != null) inputNoteStyle.value = currentSongMetadata.playData.noteStyle;
|
||||
|
||||
var inputBPM:Null<NumberStepper> = toolbox.findComponent('inputBPM', NumberStepper);
|
||||
if (inputBPM != null) inputBPM.value = currentSongMetadata.timeChanges[0].bpm;
|
||||
|
@ -4351,3 +4396,11 @@ enum LiveInputStyle
|
|||
NumberKeys;
|
||||
WASD;
|
||||
}
|
||||
|
||||
typedef ChartEditorParams =
|
||||
{
|
||||
/**
|
||||
* If non-null, load this song immediately instead of the welcome screen.
|
||||
*/
|
||||
var ?fnfcTargetPath:String;
|
||||
};
|
||||
|
|
|
@ -108,6 +108,23 @@ class HaxeUIState extends MusicBeatState
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an onRightClick listener to a HaxeUI menu bar item.
|
||||
*/
|
||||
function addUIRightClickListener(key:String, callback:MouseEvent->Void):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.onRightClick = callback;
|
||||
}
|
||||
}
|
||||
|
||||
function setComponentText(key:String, text:String):Void
|
||||
{
|
||||
var target:Component = findComponent(key);
|
||||
|
|
134
source/funkin/util/CLIUtil.hx
Normal file
134
source/funkin/util/CLIUtil.hx
Normal file
|
@ -0,0 +1,134 @@
|
|||
package funkin.util;
|
||||
|
||||
/**
|
||||
* Utilties for interpreting command line arguments.
|
||||
*/
|
||||
@:nullSafety
|
||||
class CLIUtil
|
||||
{
|
||||
/**
|
||||
* If we don't do this, dragging and dropping a file onto the executable
|
||||
* causes it to be unable to find the assets folder.
|
||||
*/
|
||||
public static function resetWorkingDir():Void
|
||||
{
|
||||
#if sys
|
||||
var exeDir:String = haxe.io.Path.directory(Sys.programPath());
|
||||
trace('Changing working directory from ${Sys.getCwd()} to ${exeDir}');
|
||||
Sys.setCwd(exeDir);
|
||||
#end
|
||||
}
|
||||
|
||||
public static function processArgs():CLIParams
|
||||
{
|
||||
#if sys
|
||||
return interpretArgs(cleanArgs(Sys.args()));
|
||||
#else
|
||||
return buildDefaultParams();
|
||||
#end
|
||||
}
|
||||
|
||||
static function interpretArgs(args:Array<String>):CLIParams
|
||||
{
|
||||
var result = buildDefaultParams();
|
||||
|
||||
result.args = [for (arg in args) arg]; // Copy the array.
|
||||
|
||||
while (args.length > 0)
|
||||
{
|
||||
var arg:Null<String> = args.shift();
|
||||
if (arg == null) continue;
|
||||
|
||||
if (arg.startsWith('-'))
|
||||
{
|
||||
switch (arg)
|
||||
{
|
||||
// Flags
|
||||
case '-h' | '--help':
|
||||
printUsage();
|
||||
case '-v' | '--version':
|
||||
trace(Constants.GENERATED_BY);
|
||||
case '--chart':
|
||||
if (args.length == 0)
|
||||
{
|
||||
trace('No chart path provided.');
|
||||
printUsage();
|
||||
}
|
||||
else
|
||||
{
|
||||
result.chart.shouldLoadChart = true;
|
||||
result.chart.chartPath = args.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make an attempt to interpret the argument.
|
||||
|
||||
if (arg.endsWith(Constants.EXT_CHART))
|
||||
{
|
||||
result.chart.shouldLoadChart = true;
|
||||
result.chart.chartPath = arg;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Unrecognized argument: ${arg}');
|
||||
printUsage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static function printUsage():Void
|
||||
{
|
||||
trace('Usage: Funkin.exe [--chart <chart>]');
|
||||
}
|
||||
|
||||
static function buildDefaultParams():CLIParams
|
||||
{
|
||||
return {
|
||||
args: [],
|
||||
|
||||
chart:
|
||||
{
|
||||
shouldLoadChart: false,
|
||||
chartPath: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the arguments passed to the application before parsing them.
|
||||
* @param args The arguments to clean up.
|
||||
* @return The cleaned up arguments.
|
||||
*/
|
||||
static function cleanArgs(args:Array<String>):Array<String>
|
||||
{
|
||||
var result:Array<String> = [];
|
||||
|
||||
if (args == null || args.length == 0) return result;
|
||||
|
||||
return args.map(function(arg:String):String {
|
||||
if (arg == null) return '';
|
||||
|
||||
return arg.trim();
|
||||
}).filter(function(arg:String):Bool {
|
||||
return arg != null && arg != '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
typedef CLIParams =
|
||||
{
|
||||
var args:Array<String>;
|
||||
|
||||
var chart:CLIChartParams;
|
||||
}
|
||||
|
||||
typedef CLIChartParams =
|
||||
{
|
||||
var shouldLoadChart:Bool;
|
||||
var chartPath:Null<String>;
|
||||
};
|
|
@ -14,6 +14,9 @@ import openfl.events.IOErrorEvent;
|
|||
*/
|
||||
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");
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -173,10 +176,11 @@ class FileUtil
|
|||
*
|
||||
* @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
|
||||
public static function saveFile(data:Bytes, ?typeFilter:Array<FileFilter>, ?onSave:String->Void, ?onCancel:Void->Void, ?defaultFileName:String,
|
||||
?dialogTitle:String):Bool
|
||||
{
|
||||
#if desktop
|
||||
var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null;
|
||||
var filter:String = convertTypeFilter(typeFilter);
|
||||
|
||||
var fileDialog:FileDialog = new FileDialog();
|
||||
if (onSave != null) fileDialog.onSelect.add(onSave);
|
||||
|
@ -231,8 +235,7 @@ class FileUtil
|
|||
}
|
||||
catch (_)
|
||||
{
|
||||
trace('Failed to write file (probably already exists): $filePath' + filePath);
|
||||
continue;
|
||||
throw 'Failed to write file (probably already exists): $filePath';
|
||||
}
|
||||
paths.push(filePath);
|
||||
}
|
||||
|
@ -269,7 +272,26 @@ class FileUtil
|
|||
};
|
||||
|
||||
// Prompt the user to save the ZIP file.
|
||||
saveFile(zipBytes, onSave, onCancel, defaultPath, 'Save files as ZIP...');
|
||||
saveFile(zipBytes, [FILE_FILTER_ZIP], onSave, onCancel, defaultPath, 'Save files as ZIP...');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of file entries and prompts the user to save them as a FNFC file.
|
||||
*/
|
||||
public static function saveChartAsFNFC(resources:Array<Entry>, ?onSave:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String,
|
||||
force:Bool = false):Bool
|
||||
{
|
||||
// Create a ZIP file.
|
||||
var zipBytes:Bytes = createZIPFromEntries(resources);
|
||||
|
||||
var onSave:String->Void = function(path:String) {
|
||||
onSave([path]);
|
||||
};
|
||||
|
||||
// Prompt the user to save the ZIP file.
|
||||
saveFile(zipBytes, [FILE_FILTER_FNFC], onSave, onCancel, defaultPath, 'Save chart as FNFC...');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -322,7 +344,8 @@ class FileUtil
|
|||
public static function readBytesFromPath(path:String):Bytes
|
||||
{
|
||||
#if sys
|
||||
return Bytes.ofString(sys.io.File.getContent(path));
|
||||
if (!sys.FileSystem.exists(path)) return null;
|
||||
return sys.io.File.getBytes(path);
|
||||
#else
|
||||
return null;
|
||||
#end
|
||||
|
@ -559,6 +582,36 @@ class FileUtil
|
|||
return o.getBytes();
|
||||
}
|
||||
|
||||
public static function readZIPFromBytes(input:Bytes):Array<Entry>
|
||||
{
|
||||
trace('TEST: ' + input.length);
|
||||
trace(input.sub(0, 30).toHex());
|
||||
|
||||
var bytesInput = new haxe.io.BytesInput(input);
|
||||
var zippedEntries = haxe.zip.Reader.readZip(bytesInput);
|
||||
|
||||
var results:Array<Entry> = [];
|
||||
for (entry in zippedEntries)
|
||||
{
|
||||
if (entry.compressed)
|
||||
{
|
||||
entry.data = haxe.zip.Reader.unzip(entry);
|
||||
}
|
||||
results.push(entry);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public static function mapZIPEntriesByName(input:Array<Entry>):Map<String, Entry>
|
||||
{
|
||||
var results:Map<String, Entry> = [];
|
||||
for (entry in input)
|
||||
{
|
||||
results.set(entry.fileName, entry);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ZIP file entry from a file name and its string contents.
|
||||
*
|
||||
|
|
|
@ -21,6 +21,8 @@ class SerializerUtil
|
|||
|
||||
/**
|
||||
* Convert a Haxe object to a JSON string.
|
||||
* NOTE: Use `json2object.JsonWriter<T>` WHEREVER POSSIBLE. Do not use this one unless you ABSOLUTELY HAVE TO it's SLOW!
|
||||
* And don't even THINK about using `haxe.Json.stringify` without the replacer!
|
||||
*/
|
||||
public static function toJSON(input:Dynamic, pretty:Bool = true):String
|
||||
{
|
||||
|
|
|
@ -61,4 +61,22 @@ class VersionUtil
|
|||
var version:thx.semver.Version = versionStr; // Implicit, not explicit, cast.
|
||||
return version;
|
||||
}
|
||||
|
||||
public static function parseVersion(input:Dynamic):Null<thx.semver.Version>
|
||||
{
|
||||
if (input == null) return null;
|
||||
|
||||
if (Std.isOfType(input, String))
|
||||
{
|
||||
var inputStr:String = input;
|
||||
var version:thx.semver.Version = inputStr;
|
||||
return version;
|
||||
}
|
||||
else
|
||||
{
|
||||
var semVer:thx.semver.Version.SemVer = input;
|
||||
var version:thx.semver.Version = semVer;
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,6 +95,6 @@ class NoteStyleRegistryTest extends FunkinTest
|
|||
|
||||
// Verify the underlying call.
|
||||
|
||||
nsrMock.fetchEntry(NoteStyleRegistry.DEFAULT_NOTE_STYLE_ID).verify(times(1));
|
||||
nsrMock.fetchEntry(Constants.DEFAULT_NOTE_STYLE).verify(times(1));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue