mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-14 19:25:16 -05:00
Merge branch 'rewrite/master' into input-offsets
This commit is contained in:
commit
d903edcb59
63 changed files with 4668 additions and 1267 deletions
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
|
@ -110,6 +110,11 @@
|
|||
"target": "windows",
|
||||
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Conversation Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DDIALOGUE"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Chart Editor)",
|
||||
"target": "windows",
|
||||
|
@ -125,6 +130,11 @@
|
|||
"target": "windows",
|
||||
"args": ["-debug", "-DLATENCY"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Waveform Test)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DWAVEFORM"]
|
||||
},
|
||||
{
|
||||
"label": "HTML5 / Debug",
|
||||
"target": "html5",
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
<library name="week7" preload="false" />
|
||||
<library name="weekend1" preload="false" />
|
||||
</section>
|
||||
<library name="art" preload="false" />
|
||||
<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg" if="web" />
|
||||
<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3" unless="web" />
|
||||
<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg" if="web" />
|
||||
|
@ -82,14 +83,15 @@
|
|||
If we can exclude the `mods` folder from the manifest, we can re-enable this line.
|
||||
<assets path='example_mods' rename='mods' embed='false' exclude="*.md" />
|
||||
-->
|
||||
<assets path="art/readme.txt" rename="do NOT readme.txt" />
|
||||
<assets path="CHANGELOG.md" rename="changelog.txt" />
|
||||
<assets path="art/readme.txt" rename="do NOT readme.txt" library="art"/>
|
||||
<assets path="CHANGELOG.md" rename="changelog.txt" library="art"/>
|
||||
<!-- NOTE FOR FUTURE SELF SINCE FONTS ARE ALWAYS FUCKY
|
||||
TO FIX ONE OF THEM, I CONVERTED IT TO OTF. DUNNO IF YOU NEED TO
|
||||
THEN UHHH I USED THE NAME OF THE FONT WITH SETFORMAT() ON THE TEXT!!!
|
||||
NOT USING A DIRECT THING TO THE ASSET!!!
|
||||
-->
|
||||
<assets path="assets/fonts" embed="true" />
|
||||
|
||||
<!-- _______________________________ Libraries ______________________________ -->
|
||||
<haxelib name="lime" /> <!-- Game engine backend -->
|
||||
<haxelib name="openfl" /> <!-- Game engine backend -->
|
||||
|
@ -108,7 +110,6 @@
|
|||
<haxelib name="hxCodec" /> <!-- Video playback -->
|
||||
|
||||
<haxelib name="json2object" /> <!-- JSON parsing -->
|
||||
<haxelib name="tink_json" /> <!-- JSON parsing (DEPRECATED) -->
|
||||
<haxelib name="thx.semver" /> <!-- Version string handling -->
|
||||
|
||||
<haxelib name="hmm" /> <!-- Read library version data at compile time so it can be baked into logs -->
|
||||
|
|
2
assets
2
assets
|
@ -1 +1 @@
|
|||
Subproject commit 30ad76c15d88bdff2b41ec017e3f3089ede52411
|
||||
Subproject commit 594853037cbea06caa5c141b0d9ed3736818e592
|
11
hmm.json
11
hmm.json
|
@ -18,7 +18,7 @@
|
|||
"name": "flixel-addons",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "fd3aecdeb5635fa0428dffee204fc78fc26b5885",
|
||||
"ref": "a523c3b56622f0640933944171efed46929e360e",
|
||||
"url": "https://github.com/FunkinCrew/flixel-addons"
|
||||
},
|
||||
{
|
||||
|
@ -54,7 +54,7 @@
|
|||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "a551159",
|
||||
"ref": "8a7846b",
|
||||
"url": "https://github.com/haxeui/haxeui-core"
|
||||
},
|
||||
{
|
||||
|
@ -149,18 +149,13 @@
|
|||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "cb11a95d0159271eb3587428cf4b9602e46dc469",
|
||||
"ref": "6cec79e4f322fbb262170594ed67ab72b4714810",
|
||||
"url": "https://github.com/larsiusprime/polymod"
|
||||
},
|
||||
{
|
||||
"name": "thx.semver",
|
||||
"type": "haxelib",
|
||||
"version": "0.2.2"
|
||||
},
|
||||
{
|
||||
"name": "tink_json",
|
||||
"type": "haxelib",
|
||||
"version": "0.11.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -33,6 +33,8 @@ class Main extends Sprite
|
|||
|
||||
public static function main():Void
|
||||
{
|
||||
haxe.Log.trace = funkin.util.logging.AnsiTrace.trace;
|
||||
funkin.util.logging.AnsiTrace.traceBF();
|
||||
Lib.current.addChild(new Main());
|
||||
}
|
||||
|
||||
|
|
|
@ -21,9 +21,9 @@ import funkin.data.level.LevelRegistry;
|
|||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
|
||||
import funkin.play.cutscene.dialogue.SpeakerDataParser;
|
||||
import funkin.data.dialogue.ConversationRegistry;
|
||||
import funkin.data.dialogue.DialogueBoxRegistry;
|
||||
import funkin.data.dialogue.SpeakerRegistry;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
|
@ -208,22 +208,29 @@ class InitState extends FlxState
|
|||
// GAME DATA PARSING
|
||||
//
|
||||
|
||||
// NOTE: Registries and data parsers must be imported and not referenced with fully qualified names,
|
||||
// NOTE: Registries must be imported and not referenced with fully qualified names,
|
||||
// to ensure build macros work properly.
|
||||
trace('Parsing game data...');
|
||||
var perfStart = haxe.Timer.stamp();
|
||||
SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry.
|
||||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
NoteStyleRegistry.instance.loadEntries();
|
||||
SongEventRegistry.loadEventCache();
|
||||
ConversationDataParser.loadConversationCache();
|
||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
||||
SpeakerDataParser.loadSpeakerCache();
|
||||
ConversationRegistry.instance.loadEntries();
|
||||
DialogueBoxRegistry.instance.loadEntries();
|
||||
SpeakerRegistry.instance.loadEntries();
|
||||
StageRegistry.instance.loadEntries();
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
|
||||
// TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers.
|
||||
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
|
||||
|
||||
ModuleHandler.buildModuleCallbacks();
|
||||
ModuleHandler.loadModuleCache();
|
||||
|
||||
ModuleHandler.callOnCreate();
|
||||
|
||||
var perfEnd = haxe.Timer.stamp();
|
||||
|
||||
trace('Parsing game data took ${Math.floor((perfEnd - perfStart) * 1000)}ms.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -241,8 +248,12 @@ class InitState extends FlxState
|
|||
startLevel(defineLevel(), defineDifficulty());
|
||||
#elseif FREEPLAY // -DFREEPLAY
|
||||
FlxG.switchState(new FreeplayState());
|
||||
#elseif DIALOGUE // -DDIALOGUE
|
||||
FlxG.switchState(new funkin.ui.debug.dialogue.ConversationDebugState());
|
||||
#elseif ANIMATE // -DANIMATE
|
||||
FlxG.switchState(new funkin.ui.debug.anim.FlxAnimateTest());
|
||||
#elseif WAVEFORM // -DWAVEFORM
|
||||
FlxG.switchState(new funkin.ui.debug.WaveformTestState());
|
||||
#elseif CHARTING // -DCHARTING
|
||||
FlxG.switchState(new funkin.ui.debug.charting.ChartEditorState());
|
||||
#elseif STAGEBUILD // -DSTAGEBUILD
|
||||
|
|
|
@ -7,6 +7,10 @@ import flash.utils.ByteArray;
|
|||
import flixel.sound.FlxSound;
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.system.FlxAssets.FlxSoundAsset;
|
||||
import funkin.util.tools.ICloneable;
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
import flixel.math.FlxMath;
|
||||
import openfl.Assets;
|
||||
#if (openfl >= "8.0.0")
|
||||
import openfl.utils.AssetType;
|
||||
|
@ -17,10 +21,38 @@ import openfl.utils.AssetType;
|
|||
* - Delayed playback via negative song position.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FunkinSound extends FlxSound
|
||||
class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
||||
{
|
||||
static final MAX_VOLUME:Float = 2.0;
|
||||
|
||||
static var cache(default, null):FlxTypedGroup<FunkinSound> = new FlxTypedGroup<FunkinSound>();
|
||||
|
||||
public var muted(default, set):Bool = false;
|
||||
|
||||
function set_muted(value:Bool):Bool
|
||||
{
|
||||
if (value == muted) return value;
|
||||
muted = value;
|
||||
updateTransform();
|
||||
return value;
|
||||
}
|
||||
|
||||
override function set_volume(value:Float):Float
|
||||
{
|
||||
// Uncap the volume.
|
||||
fixMaxVolume();
|
||||
_volume = FlxMath.bound(value, 0.0, MAX_VOLUME);
|
||||
updateTransform();
|
||||
return _volume;
|
||||
}
|
||||
|
||||
public var paused(get, never):Bool;
|
||||
|
||||
function get_paused():Bool
|
||||
{
|
||||
return this._paused;
|
||||
}
|
||||
|
||||
public var isPlaying(get, never):Bool;
|
||||
|
||||
function get_isPlaying():Bool
|
||||
|
@ -28,6 +60,24 @@ class FunkinSound extends FlxSound
|
|||
return this.playing || this._shouldPlay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waveform data for this sound.
|
||||
* This is lazily loaded, so it will be built the first time it is accessed.
|
||||
*/
|
||||
public var waveformData(get, never):WaveformData;
|
||||
|
||||
var _waveformData:Null<WaveformData> = null;
|
||||
|
||||
function get_waveformData():WaveformData
|
||||
{
|
||||
if (_waveformData == null)
|
||||
{
|
||||
_waveformData = WaveformDataParser.interpretFlxSound(this);
|
||||
if (_waveformData == null) throw 'Could not interpret waveform data!';
|
||||
}
|
||||
return _waveformData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we in a state where the song should play but time is negative?
|
||||
*/
|
||||
|
@ -63,6 +113,30 @@ class FunkinSound extends FlxSound
|
|||
}
|
||||
}
|
||||
|
||||
public function togglePlayback():FunkinSound
|
||||
{
|
||||
if (playing)
|
||||
{
|
||||
pause();
|
||||
}
|
||||
else
|
||||
{
|
||||
resume();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
function fixMaxVolume():Void
|
||||
{
|
||||
#if lime_openal
|
||||
// This code is pretty fragile, it reaches through 5 layers of private access.
|
||||
@:privateAccess
|
||||
var handle = this?._channel?.__source?.__backend?.handle;
|
||||
if (handle == null) return;
|
||||
lime.media.openal.AL.sourcef(handle, lime.media.openal.AL.MAX_GAIN, MAX_VOLUME);
|
||||
#end
|
||||
}
|
||||
|
||||
public override function play(forceRestart:Bool = false, startTime:Float = 0, ?endTime:Float):FunkinSound
|
||||
{
|
||||
if (!exists) return this;
|
||||
|
@ -140,6 +214,37 @@ class FunkinSound extends FlxSound
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call after adjusting the volume to update the sound channel's settings.
|
||||
*/
|
||||
@:allow(flixel.sound.FlxSoundGroup)
|
||||
override function updateTransform():Void
|
||||
{
|
||||
_transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end
|
||||
(group != null ? group.volume : 1) * _volume * _volumeAdjust;
|
||||
|
||||
if (_channel != null) _channel.soundTransform = _transform;
|
||||
}
|
||||
|
||||
public function clone():FunkinSound
|
||||
{
|
||||
var sound:FunkinSound = new FunkinSound();
|
||||
|
||||
// Clone the sound by creating one with the same data buffer.
|
||||
// Reusing the `Sound` object directly causes issues with playback.
|
||||
@:privateAccess
|
||||
sound._sound = openfl.media.Sound.fromAudioBuffer(this._sound.__buffer);
|
||||
|
||||
// Call init to ensure the FlxSound is properly initialized.
|
||||
sound.init(this.looped, this.autoDestroy, this.onComplete);
|
||||
|
||||
// Oh yeah, the waveform data is the same too!
|
||||
@:privateAccess
|
||||
sound._waveformData = this._waveformData;
|
||||
|
||||
return sound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `FunkinSound` object.
|
||||
*
|
||||
|
|
|
@ -3,6 +3,7 @@ package funkin.audio;
|
|||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.sound.FlxSound;
|
||||
import funkin.audio.FunkinSound;
|
||||
import flixel.tweens.FlxTween;
|
||||
|
||||
/**
|
||||
* A group of FunkinSounds that are all synced together.
|
||||
|
@ -14,8 +15,12 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
|
||||
public var volume(get, set):Float;
|
||||
|
||||
public var muted(get, set):Bool;
|
||||
|
||||
public var pitch(get, set):Float;
|
||||
|
||||
public var playing(get, never):Bool;
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
@ -122,6 +127,26 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade in all the sounds in the group.
|
||||
*/
|
||||
public function fadeIn(duration:Float, ?from:Float = 0.0, ?to:Float = 1.0, ?onComplete:FlxTween->Void):Void
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
sound.fadeIn(duration, from, to, onComplete);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade out all the sounds in the group.
|
||||
*/
|
||||
public function fadeOut(duration:Float, ?to:Float = 0.0, ?onComplete:FlxTween->Void):Void
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
sound.fadeOut(duration, to, onComplete);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all the sounds in the group.
|
||||
*/
|
||||
|
@ -165,6 +190,13 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
return time;
|
||||
}
|
||||
|
||||
function get_playing():Bool
|
||||
{
|
||||
if (getFirstAlive != null) return getFirstAlive().playing;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
function get_volume():Float
|
||||
{
|
||||
if (getFirstAlive() != null) return getFirstAlive().volume;
|
||||
|
@ -182,6 +214,22 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
return volume;
|
||||
}
|
||||
|
||||
function get_muted():Bool
|
||||
{
|
||||
if (getFirstAlive() != null) return getFirstAlive().muted;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
function set_muted(muted:Bool):Bool
|
||||
{
|
||||
forEachAlive(function(snd:FunkinSound) {
|
||||
snd.muted = muted;
|
||||
});
|
||||
|
||||
return muted;
|
||||
}
|
||||
|
||||
function get_pitch():Float
|
||||
{
|
||||
#if FLX_PITCH
|
||||
|
|
|
@ -2,6 +2,8 @@ package funkin.audio;
|
|||
|
||||
import funkin.audio.FunkinSound;
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
|
||||
class VoicesGroup extends SoundGroup
|
||||
{
|
||||
|
@ -104,6 +106,50 @@ class VoicesGroup extends SoundGroup
|
|||
return opponentVolume = volume;
|
||||
}
|
||||
|
||||
public function getPlayerVoice(index:Int = 0):Null<FunkinSound>
|
||||
{
|
||||
return playerVoices.members[index];
|
||||
}
|
||||
|
||||
public function getOpponentVoice(index:Int = 0):Null<FunkinSound>
|
||||
{
|
||||
return opponentVoices.members[index];
|
||||
}
|
||||
|
||||
public function getPlayerVoiceWaveform():Null<WaveformData>
|
||||
{
|
||||
if (playerVoices.members.length == 0) return null;
|
||||
|
||||
return playerVoices.members[0].waveformData;
|
||||
}
|
||||
|
||||
public function getOpponentVoiceWaveform():Null<WaveformData>
|
||||
{
|
||||
if (opponentVoices.members.length == 0) return null;
|
||||
|
||||
return opponentVoices.members[0].waveformData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The length of the player's vocal track, in milliseconds.
|
||||
*/
|
||||
public function getPlayerVoiceLength():Float
|
||||
{
|
||||
if (playerVoices.members.length == 0) return 0.0;
|
||||
|
||||
return playerVoices.members[0].length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The length of the opponent's vocal track, in milliseconds.
|
||||
*/
|
||||
public function getOpponentVoiceLength():Float
|
||||
{
|
||||
if (opponentVoices.members.length == 0) return 0.0;
|
||||
|
||||
return opponentVoices.members[0].length;
|
||||
}
|
||||
|
||||
public override function clear():Void
|
||||
{
|
||||
playerVoices.clear();
|
||||
|
|
|
@ -102,7 +102,7 @@ class PolygonSpectogram extends MeshRender
|
|||
coolPoint.x = (curAud.balanced * waveAmplitude);
|
||||
coolPoint.y = (i / funnyPixels * daHeight);
|
||||
|
||||
add_quad(prevPoint.x, prevPoint.y, prevPoint.x
|
||||
build_quad(prevPoint.x, prevPoint.y, prevPoint.x
|
||||
+ thickness, prevPoint.y, coolPoint.x, coolPoint.y, coolPoint.x
|
||||
+ thickness, coolPoint.y
|
||||
+ thickness);
|
||||
|
|
336
source/funkin/audio/waveform/WaveformData.hx
Normal file
336
source/funkin/audio/waveform/WaveformData.hx
Normal file
|
@ -0,0 +1,336 @@
|
|||
package funkin.audio.waveform;
|
||||
|
||||
import funkin.util.MathUtil;
|
||||
|
||||
@:nullSafety
|
||||
class WaveformData
|
||||
{
|
||||
static final DEFAULT_VERSION:Int = 2;
|
||||
|
||||
/**
|
||||
* The version of the waveform data format.
|
||||
* @default `2` (-1 if not specified/invalid)
|
||||
*/
|
||||
public var version(default, null):Int = -1;
|
||||
|
||||
/**
|
||||
* The number of channels in the waveform.
|
||||
*/
|
||||
public var channels(default, null):Int = 1;
|
||||
|
||||
@:alias('sample_rate')
|
||||
public var sampleRate(default, null):Int = 44100;
|
||||
|
||||
/**
|
||||
* Number of input audio samples per output waveform data point.
|
||||
* At base zoom level this is number of samples per pixel.
|
||||
* Lower values can more accurately represent the waveform when zoomed in, but take more data.
|
||||
*/
|
||||
@:alias('samples_per_pixel')
|
||||
public var samplesPerPoint(default, null):Int = 256;
|
||||
|
||||
/**
|
||||
* Number of bits to use for each sample value. Valid values are `8` and `16`.
|
||||
*/
|
||||
public var bits(default, null):Int = 16;
|
||||
|
||||
/**
|
||||
* The length of the data array, in points.
|
||||
*/
|
||||
public var length(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Array of Int16 values representing the waveform.
|
||||
* TODO: Use an `openfl.Vector` for performance.
|
||||
*/
|
||||
public var data(default, null):Array<Int> = [];
|
||||
|
||||
@:jignored
|
||||
var channelData:Null<Array<WaveformDataChannel>> = null;
|
||||
|
||||
public function new(?version:Int, channels:Int, sampleRate:Int, samplesPerPoint:Int, bits:Int, length:Int, data:Array<Int>)
|
||||
{
|
||||
this.version = version ?? DEFAULT_VERSION;
|
||||
this.channels = channels;
|
||||
this.sampleRate = sampleRate;
|
||||
this.samplesPerPoint = samplesPerPoint;
|
||||
this.bits = bits;
|
||||
this.length = length;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
function buildChannelData():Array<WaveformDataChannel>
|
||||
{
|
||||
channelData = [];
|
||||
for (i in 0...channels)
|
||||
{
|
||||
channelData.push(new WaveformDataChannel(this, i));
|
||||
}
|
||||
return channelData;
|
||||
}
|
||||
|
||||
public function channel(index:Int)
|
||||
{
|
||||
return (channelData == null) ? buildChannelData()[index] : channelData[index];
|
||||
}
|
||||
|
||||
public function get(index:Int):Int
|
||||
{
|
||||
return data[index] ?? 0;
|
||||
}
|
||||
|
||||
public function set(index:Int, value:Int)
|
||||
{
|
||||
data[index] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum possible value for a waveform data point.
|
||||
* The minimum possible value is (-1 * maxSampleValue)
|
||||
*/
|
||||
public function maxSampleValue():Int
|
||||
{
|
||||
if (_maxSampleValue != 0) return _maxSampleValue;
|
||||
return _maxSampleValue = Std.int(Math.pow(2, bits));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the value because `Math.pow` is expensive and the value gets used a lot.
|
||||
*/
|
||||
@:jignored
|
||||
var _maxSampleValue:Int = 0;
|
||||
|
||||
/**
|
||||
* @return The length of the waveform in samples.
|
||||
*/
|
||||
public function lenSamples():Int
|
||||
{
|
||||
return length * samplesPerPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The length of the waveform in seconds.
|
||||
*/
|
||||
public function lenSeconds():Float
|
||||
{
|
||||
return inline lenSamples() / sampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the time in seconds, return the waveform data point index.
|
||||
*/
|
||||
public function secondsToIndex(seconds:Float):Int
|
||||
{
|
||||
return Std.int(seconds * inline pointsPerSecond());
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a waveform data point index, return the time in seconds.
|
||||
*/
|
||||
public function indexToSeconds(index:Int):Float
|
||||
{
|
||||
return index / inline pointsPerSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of data points this waveform data provides per second of audio.
|
||||
*/
|
||||
public inline function pointsPerSecond():Float
|
||||
{
|
||||
return sampleRate / samplesPerPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the percentage progress through the waveform, return the waveform data point index.
|
||||
*/
|
||||
public function percentToIndex(percent:Float):Int
|
||||
{
|
||||
return Std.int(percent * length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a waveform data point index, return the percentage progress through the waveform.
|
||||
*/
|
||||
public function indexToPercent(index:Int):Float
|
||||
{
|
||||
return index / length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resample the waveform data to create a new WaveformData object matching the desired `samplesPerPoint` value.
|
||||
* This is useful for zooming in/out of the waveform in a performant manner.
|
||||
*
|
||||
* @param newSamplesPerPoint The new value for `samplesPerPoint`.
|
||||
*/
|
||||
public function resample(newSamplesPerPoint:Int):WaveformData
|
||||
{
|
||||
var result = this.clone();
|
||||
|
||||
var ratio = newSamplesPerPoint / samplesPerPoint;
|
||||
if (ratio == 1) return result;
|
||||
if (ratio < 1) trace('[WARNING] Downsampling will result in a low precision.');
|
||||
|
||||
var inputSampleCount = this.lenSamples();
|
||||
var outputSampleCount = Std.int(inputSampleCount * ratio);
|
||||
|
||||
var inputPointCount = this.length;
|
||||
var outputPointCount = Std.int(inputPointCount / ratio);
|
||||
var outputChannelCount = this.channels;
|
||||
|
||||
// TODO: Actually figure out the dumbass logic for this.
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new WaveformData whose data represents the two waveforms overlayed.
|
||||
*/
|
||||
public function merge(that:WaveformData):WaveformData
|
||||
{
|
||||
var result = this.clone([]);
|
||||
|
||||
for (channelIndex in 0...this.channels)
|
||||
{
|
||||
var thisChannel = this.channel(channelIndex);
|
||||
var thatChannel = that.channel(channelIndex);
|
||||
var resultChannel = result.channel(channelIndex);
|
||||
|
||||
for (index in 0...this.length)
|
||||
{
|
||||
var thisMinSample = thisChannel.minSample(index);
|
||||
var thatMinSample = thatChannel.minSample(index);
|
||||
|
||||
var thisMaxSample = thisChannel.maxSample(index);
|
||||
var thatMaxSample = thatChannel.maxSample(index);
|
||||
|
||||
resultChannel.setMinSample(index, Std.int(Math.min(thisMinSample, thatMinSample)));
|
||||
resultChannel.setMaxSample(index, Std.int(Math.max(thisMaxSample, thatMaxSample)));
|
||||
}
|
||||
}
|
||||
|
||||
@:privateAccess
|
||||
result.length = this.length;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new WaveformData whose parameters match the current object.
|
||||
*/
|
||||
public function clone(?newData:Array<Int> = null):WaveformData
|
||||
{
|
||||
if (newData == null)
|
||||
{
|
||||
newData = this.data.clone();
|
||||
}
|
||||
|
||||
var clone = new WaveformData(this.version, this.channels, this.sampleRate, this.samplesPerPoint, this.bits, newData.length, newData);
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
@:nullSafety
|
||||
class WaveformDataChannel
|
||||
{
|
||||
var parent:WaveformData;
|
||||
var channelId:Int;
|
||||
|
||||
public function new(parent:WaveformData, channelId:Int)
|
||||
{
|
||||
this.parent = parent;
|
||||
this.channelId = channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a given minimum point at an index.
|
||||
*/
|
||||
public function minSample(i:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2;
|
||||
return inline parent.get(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function minSampleMapped(i:Int)
|
||||
{
|
||||
return inline minSample(i) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum value within the range of samples.
|
||||
* NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead.
|
||||
*/
|
||||
public function minSampleRange(start:Int, end:Int)
|
||||
{
|
||||
var min = inline parent.maxSampleValue();
|
||||
for (i in start...end)
|
||||
{
|
||||
var sample = inline minSample(i);
|
||||
if (sample < min) min = sample;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum value within the range of samples, mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function minSampleRangeMapped(start:Int, end:Int)
|
||||
{
|
||||
return inline minSampleRange(start, end) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a given maximum point at an index.
|
||||
*/
|
||||
public function maxSample(i:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2 + 1;
|
||||
return inline parent.get(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function maxSampleMapped(i:Int)
|
||||
{
|
||||
return inline maxSample(i) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum value within the range of samples.
|
||||
* NOTE: Inefficient for large ranges. Use `WaveformData.remap` instead.
|
||||
*/
|
||||
public function maxSampleRange(start:Int, end:Int)
|
||||
{
|
||||
var max = -(inline parent.maxSampleValue());
|
||||
for (i in start...end)
|
||||
{
|
||||
var sample = inline maxSample(i);
|
||||
if (sample > max) max = sample;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum value within the range of samples, mapped to a value between 0 and 1.
|
||||
*/
|
||||
public function maxSampleRangeMapped(start:Int, end:Int)
|
||||
{
|
||||
return inline maxSampleRange(start, end) / inline parent.maxSampleValue();
|
||||
}
|
||||
|
||||
public function setMinSample(i:Int, value:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2;
|
||||
inline parent.set(offset, value);
|
||||
}
|
||||
|
||||
public function setMaxSample(i:Int, value:Int)
|
||||
{
|
||||
var offset = (i * parent.channels + this.channelId) * 2 + 1;
|
||||
inline parent.set(offset, value);
|
||||
}
|
||||
}
|
145
source/funkin/audio/waveform/WaveformDataParser.hx
Normal file
145
source/funkin/audio/waveform/WaveformDataParser.hx
Normal file
|
@ -0,0 +1,145 @@
|
|||
package funkin.audio.waveform;
|
||||
|
||||
class WaveformDataParser
|
||||
{
|
||||
static final INT16_MAX:Int = 32767;
|
||||
static final INT16_MIN:Int = -32768;
|
||||
|
||||
static final INT8_MAX:Int = 127;
|
||||
static final INT8_MIN:Int = -128;
|
||||
|
||||
public static function interpretFlxSound(sound:flixel.sound.FlxSound):Null<WaveformData>
|
||||
{
|
||||
if (sound == null) return null;
|
||||
|
||||
// Method 1. This only works if the sound has been played before.
|
||||
@:privateAccess
|
||||
var soundBuffer:Null<lime.media.AudioBuffer> = sound?._channel?.__source?.buffer;
|
||||
|
||||
if (soundBuffer == null)
|
||||
{
|
||||
// Method 2. This works if the sound has not been played before.
|
||||
@:privateAccess
|
||||
soundBuffer = sound?._sound?.__buffer;
|
||||
|
||||
if (soundBuffer == null)
|
||||
{
|
||||
trace('[WAVEFORM] Failed to interpret FlxSound: ${sound}');
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// trace('[WAVEFORM] Method 2 worked.');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// trace('[WAVEFORM] Method 1 worked.');
|
||||
}
|
||||
|
||||
return interpretAudioBuffer(soundBuffer);
|
||||
}
|
||||
|
||||
public static function interpretAudioBuffer(soundBuffer:lime.media.AudioBuffer):Null<WaveformData>
|
||||
{
|
||||
var sampleRate = soundBuffer.sampleRate;
|
||||
var channels = soundBuffer.channels;
|
||||
var bitsPerSample = soundBuffer.bitsPerSample;
|
||||
var samplesPerPoint:Int = 256; // I don't think we need to configure this.
|
||||
var pointsPerSecond:Float = sampleRate / samplesPerPoint; // 172 samples per second for most songs is plenty precise while still being performant..
|
||||
|
||||
// TODO: Make this work better on HTML5.
|
||||
var soundData:lime.utils.Int16Array = cast soundBuffer.data;
|
||||
|
||||
var soundDataRawLength:Int = soundData.length;
|
||||
var soundDataSampleCount:Int = Std.int(Math.ceil(soundDataRawLength / channels / (bitsPerSample == 16 ? 2 : 1)));
|
||||
var outputPointCount:Int = Std.int(Math.ceil(soundDataSampleCount / samplesPerPoint));
|
||||
|
||||
// trace('Interpreting audio buffer:');
|
||||
// trace(' sampleRate: ${sampleRate}');
|
||||
// trace(' channels: ${channels}');
|
||||
// trace(' bitsPerSample: ${bitsPerSample}');
|
||||
// trace(' samplesPerPoint: ${samplesPerPoint}');
|
||||
// trace(' pointsPerSecond: ${pointsPerSecond}');
|
||||
// trace(' soundDataRawLength: ${soundDataRawLength}');
|
||||
// trace(' soundDataSampleCount: ${soundDataSampleCount}');
|
||||
// trace(' soundDataRawLength/4: ${soundDataRawLength / 4}');
|
||||
// trace(' outputPointCount: ${outputPointCount}');
|
||||
|
||||
var minSampleValue:Int = bitsPerSample == 16 ? INT16_MIN : INT8_MIN;
|
||||
var maxSampleValue:Int = bitsPerSample == 16 ? INT16_MAX : INT8_MAX;
|
||||
|
||||
var outputData:Array<Int> = [];
|
||||
|
||||
var perfStart = haxe.Timer.stamp();
|
||||
|
||||
for (pointIndex in 0...outputPointCount)
|
||||
{
|
||||
// minChannel1, maxChannel1, minChannel2, maxChannel2, ...
|
||||
var values:Array<Int> = [];
|
||||
|
||||
for (i in 0...channels)
|
||||
{
|
||||
values.push(bitsPerSample == 16 ? INT16_MAX : INT8_MAX);
|
||||
values.push(bitsPerSample == 16 ? INT16_MIN : INT8_MIN);
|
||||
}
|
||||
|
||||
var rangeStart = pointIndex * samplesPerPoint;
|
||||
var rangeEnd = rangeStart + samplesPerPoint;
|
||||
if (rangeEnd > soundDataSampleCount) rangeEnd = soundDataSampleCount;
|
||||
|
||||
for (sampleIndex in rangeStart...rangeEnd)
|
||||
{
|
||||
for (channelIndex in 0...channels)
|
||||
{
|
||||
var sampleIndex:Int = sampleIndex * channels + channelIndex;
|
||||
var sampleValue = soundData[sampleIndex];
|
||||
|
||||
if (sampleValue < values[channelIndex * 2]) values[(channelIndex * 2)] = sampleValue;
|
||||
if (sampleValue > values[channelIndex * 2 + 1]) values[(channelIndex * 2) + 1] = sampleValue;
|
||||
}
|
||||
}
|
||||
|
||||
// We now have the min and max values for the range.
|
||||
for (value in values)
|
||||
outputData.push(value);
|
||||
}
|
||||
|
||||
var outputDataLength:Int = Std.int(outputData.length / channels / 2);
|
||||
var result = new WaveformData(null, channels, sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData);
|
||||
|
||||
var perfEnd = haxe.Timer.stamp();
|
||||
trace('[WAVEFORM] Interpreted audio buffer in ${perfEnd - perfStart} seconds.');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static function parseWaveformData(path:String):Null<WaveformData>
|
||||
{
|
||||
var rawJson:String = openfl.Assets.getText(path).trim();
|
||||
return parseWaveformDataString(rawJson, path);
|
||||
}
|
||||
|
||||
public static function parseWaveformDataString(contents:String, ?fileName:String):Null<WaveformData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<WaveformData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
trace('[WAVEFORM] Parsing waveform data: ${contents}');
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
static function printErrors(errors:Array<json2object.Error>, id:String = ''):Void
|
||||
{
|
||||
trace('[WAVEFORM] Failed to parse waveform data: ${id}');
|
||||
|
||||
for (error in errors)
|
||||
funkin.data.DataError.printError(error);
|
||||
}
|
||||
}
|
449
source/funkin/audio/waveform/WaveformSprite.hx
Normal file
449
source/funkin/audio/waveform/WaveformSprite.hx
Normal file
|
@ -0,0 +1,449 @@
|
|||
package funkin.audio.waveform;
|
||||
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
import funkin.graphics.rendering.MeshRender;
|
||||
import flixel.util.FlxColor;
|
||||
|
||||
class WaveformSprite extends MeshRender
|
||||
{
|
||||
static final DEFAULT_COLOR:FlxColor = FlxColor.WHITE;
|
||||
static final DEFAULT_DURATION:Float = 5.0;
|
||||
static final DEFAULT_ORIENTATION:WaveformOrientation = HORIZONTAL;
|
||||
static final DEFAULT_X:Float = 0.0;
|
||||
static final DEFAULT_Y:Float = 0.0;
|
||||
static final DEFAULT_WIDTH:Float = 100.0;
|
||||
static final DEFAULT_HEIGHT:Float = 100.0;
|
||||
|
||||
/**
|
||||
* Set this to true to tell the waveform to rebuild itself.
|
||||
* Do this any time the data or drawable area of the waveform changes.
|
||||
* This often (but not always) needs to be done every frame.
|
||||
*/
|
||||
var isWaveformDirty:Bool = true;
|
||||
|
||||
/**
|
||||
* If true, force the waveform to redraw every frame.
|
||||
* Useful if the waveform's clipRect is constantly changing.
|
||||
*/
|
||||
public var forceUpdate:Bool = false;
|
||||
|
||||
public var waveformData(default, set):Null<WaveformData>;
|
||||
|
||||
function set_waveformData(value:Null<WaveformData>):Null<WaveformData>
|
||||
{
|
||||
if (waveformData == value) return value;
|
||||
|
||||
waveformData = value;
|
||||
isWaveformDirty = true;
|
||||
return waveformData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The color to render the waveform with.
|
||||
*/
|
||||
public var waveformColor(default, set):FlxColor;
|
||||
|
||||
function set_waveformColor(value:FlxColor):FlxColor
|
||||
{
|
||||
if (waveformColor == value) return value;
|
||||
|
||||
waveformColor = value;
|
||||
// We don't need to dirty the waveform geometry, just rebuild the texture.
|
||||
rebuildGraphic();
|
||||
return waveformColor;
|
||||
}
|
||||
|
||||
public var orientation(default, set):WaveformOrientation;
|
||||
|
||||
function set_orientation(value:WaveformOrientation):WaveformOrientation
|
||||
{
|
||||
if (orientation == value) return value;
|
||||
|
||||
orientation = value;
|
||||
isWaveformDirty = true;
|
||||
return orientation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time, in seconds, at which the waveform starts.
|
||||
*/
|
||||
public var time(default, set):Float;
|
||||
|
||||
function set_time(value:Float)
|
||||
{
|
||||
if (time == value) return value;
|
||||
|
||||
time = value;
|
||||
isWaveformDirty = true;
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* The duration, in seconds, that the waveform represents.
|
||||
* The section of waveform from `time` to `time + duration` and `width` are used to determine how many samples each pixel represents.
|
||||
*/
|
||||
public var duration(default, set):Float;
|
||||
|
||||
function set_duration(value:Float)
|
||||
{
|
||||
if (duration == value) return value;
|
||||
|
||||
duration = value;
|
||||
isWaveformDirty = true;
|
||||
return duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the physical size of the waveform with `this.height = value`.
|
||||
*/
|
||||
override function set_height(value:Float):Float
|
||||
{
|
||||
if (height == value) return super.set_height(value);
|
||||
|
||||
isWaveformDirty = true;
|
||||
return super.set_height(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the physical size of the waveform with `this.width = value`.
|
||||
*/
|
||||
override function set_width(value:Float):Float
|
||||
{
|
||||
if (width == value) return super.set_width(value);
|
||||
|
||||
isWaveformDirty = true;
|
||||
return super.set_width(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum size, in pixels, that a waveform will display with.
|
||||
* Useful for preventing the waveform from becoming too small to see.
|
||||
*
|
||||
* NOTE: This is technically doubled since it's applied above and below the center of the waveform.
|
||||
*/
|
||||
public var minWaveformSize:Int = 1;
|
||||
|
||||
/**
|
||||
* A multiplier on the size of the waveform.
|
||||
* Still capped at the width and height set for the sprite.
|
||||
*/
|
||||
public var amplitude:Float = 1.0;
|
||||
|
||||
public function new(?waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
{
|
||||
super(DEFAULT_X, DEFAULT_Y, DEFAULT_COLOR);
|
||||
this.waveformColor = color ?? DEFAULT_COLOR;
|
||||
this.width = DEFAULT_WIDTH;
|
||||
this.height = DEFAULT_HEIGHT;
|
||||
|
||||
this.waveformData = waveformData;
|
||||
this.orientation = orientation ?? DEFAULT_ORIENTATION;
|
||||
this.time = 0.0;
|
||||
this.duration = duration ?? DEFAULT_DURATION;
|
||||
|
||||
this.forceUpdate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually tell the waveform to rebuild itself, even if none of its properties have changed.
|
||||
*/
|
||||
public function markDirty():Void
|
||||
{
|
||||
isWaveformDirty = true;
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
if (forceUpdate || isWaveformDirty)
|
||||
{
|
||||
// Recalculate the waveform vertices.
|
||||
drawWaveform();
|
||||
isWaveformDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildGraphic():Void
|
||||
{
|
||||
// The waveform is rendered using a single colored pixel as a texture.
|
||||
// If you want something more elaborate, make sure to modify `build_vertex` below to use the UVs you want.
|
||||
makeGraphic(1, 1, this.waveformColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param offsetX Horizontal offset to draw the waveform at, in samples.
|
||||
*/
|
||||
function drawWaveform():Void
|
||||
{
|
||||
// For each sample in the waveform...
|
||||
// Add a MAX vertex and a MIN vertex.
|
||||
// If previous MAX/MIN is empty, store.
|
||||
// If previous MAX/MIN is not empty, draw a quad using current and previous MAX/MIN. Then store current MAX/MIN.
|
||||
// Continue until end of waveform.
|
||||
|
||||
this.clear();
|
||||
|
||||
if (waveformData == null) return;
|
||||
|
||||
// Center point of the waveform. When horizontal this is half the height, when vertical this is half the width.
|
||||
var waveformCenterPos:Int = orientation == HORIZONTAL ? Std.int(this.height / 2) : Std.int(this.width / 2);
|
||||
|
||||
var oneSecondInIndices:Int = waveformData.secondsToIndex(1);
|
||||
|
||||
var startTime:Float = time;
|
||||
var endTime:Float = time + duration;
|
||||
|
||||
var startIndex:Int = waveformData.secondsToIndex(startTime);
|
||||
var endIndex:Int = waveformData.secondsToIndex(endTime);
|
||||
|
||||
var pixelsPerIndex:Float = (orientation == HORIZONTAL ? this.width : this.height) / (endIndex - startIndex);
|
||||
var indexesPerPixel:Float = 1 / pixelsPerIndex;
|
||||
|
||||
var topLeftVertexIndex:Int = -1;
|
||||
var topRightVertexIndex:Int = -1;
|
||||
var bottomLeftVertexIndex:Int = -1;
|
||||
var bottomRightVertexIndex:Int = -1;
|
||||
|
||||
if (clipRect != null)
|
||||
{
|
||||
topLeftVertexIndex = this.build_vertex(clipRect.x, clipRect.y);
|
||||
topRightVertexIndex = this.build_vertex(clipRect.x + clipRect.width, clipRect.y);
|
||||
bottomLeftVertexIndex = this.build_vertex(clipRect.x, clipRect.y + clipRect.height);
|
||||
bottomRightVertexIndex = this.build_vertex(clipRect.x + clipRect.width, clipRect.y + clipRect.height);
|
||||
}
|
||||
|
||||
if (pixelsPerIndex >= 1.0)
|
||||
{
|
||||
// Each index is at least one pixel wide, so we render each index.
|
||||
var prevVertexTopIndex:Int = -1;
|
||||
var prevVertexBottomIndex:Int = -1;
|
||||
for (i in startIndex...endIndex)
|
||||
{
|
||||
var pixelPos:Int = Std.int((i - startIndex) * pixelsPerIndex);
|
||||
|
||||
var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y);
|
||||
|
||||
if (isBeforeClipRect) continue;
|
||||
|
||||
var isAfterClipRect:Bool = (clipRect != null)
|
||||
&& ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height));
|
||||
|
||||
if (isAfterClipRect)
|
||||
{
|
||||
break;
|
||||
};
|
||||
|
||||
var sampleMax:Float = Math.min(waveformData.channel(0).maxSampleMapped(i) * amplitude, 1.0);
|
||||
var sampleMin:Float = Math.max(waveformData.channel(0).minSampleMapped(i) * amplitude, -1.0);
|
||||
var sampleMaxSize:Float = sampleMax * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMaxSize < minWaveformSize) sampleMaxSize = minWaveformSize;
|
||||
var sampleMinSize:Float = sampleMin * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMinSize > -minWaveformSize) sampleMinSize = -minWaveformSize;
|
||||
var vertexTopY:Int = Std.int(waveformCenterPos - sampleMaxSize);
|
||||
var vertexBottomY:Int = Std.int(waveformCenterPos - sampleMinSize);
|
||||
|
||||
if (vertexBottomY - vertexTopY < minWaveformSize) vertexTopY = vertexBottomY - minWaveformSize;
|
||||
|
||||
var vertexTopIndex:Int = -1;
|
||||
var vertexBottomIndex:Int = -1;
|
||||
|
||||
if (clipRect != null)
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(pixelPos, vertexTopY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(pixelPos, vertexBottomY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(vertexTopY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(vertexBottomY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(pixelPos, vertexTopY);
|
||||
vertexBottomIndex = this.build_vertex(pixelPos, vertexBottomY);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(vertexTopY, pixelPos);
|
||||
vertexBottomIndex = this.build_vertex(vertexBottomY, pixelPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't render if we don't have a previous different set of vertices to create a quad from.
|
||||
if (prevVertexTopIndex != -1
|
||||
&& prevVertexBottomIndex != -1
|
||||
&& prevVertexTopIndex != vertexTopIndex
|
||||
&& prevVertexBottomIndex != vertexBottomIndex)
|
||||
{
|
||||
switch (orientation) // the line of code that makes you gay
|
||||
{
|
||||
case HORIZONTAL:
|
||||
this.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex);
|
||||
case VERTICAL:
|
||||
this.add_quad(prevVertexBottomIndex, prevVertexTopIndex, vertexTopIndex, vertexBottomIndex);
|
||||
}
|
||||
}
|
||||
|
||||
prevVertexTopIndex = vertexTopIndex;
|
||||
prevVertexBottomIndex = vertexBottomIndex;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Indexes are less than one pixel wide, so for each pixel we render the maximum of the samples that fall within it.
|
||||
var prevVertexTopIndex:Int = -1;
|
||||
var prevVertexBottomIndex:Int = -1;
|
||||
var waveformLengthPixels:Int = orientation == HORIZONTAL ? Std.int(this.width) : Std.int(this.height);
|
||||
for (i in 0...waveformLengthPixels)
|
||||
{
|
||||
var pixelPos:Int = i;
|
||||
|
||||
var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y);
|
||||
|
||||
if (isBeforeClipRect) continue;
|
||||
|
||||
var isAfterClipRect:Bool = (clipRect != null)
|
||||
&& ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height));
|
||||
|
||||
if (isAfterClipRect)
|
||||
{
|
||||
break;
|
||||
};
|
||||
|
||||
// Wrap Std.int around the whole range calculation, not just indexesPerPixel, otherwise you get weird issues with zooming.
|
||||
var rangeStart:Int = Std.int(i * indexesPerPixel + startIndex);
|
||||
var rangeEnd:Int = Std.int((i + 1) * indexesPerPixel + startIndex);
|
||||
|
||||
var sampleMax:Float = Math.min(waveformData.channel(0).maxSampleRangeMapped(rangeStart, rangeEnd) * amplitude, 1.0);
|
||||
var sampleMin:Float = Math.max(waveformData.channel(0).minSampleRangeMapped(rangeStart, rangeEnd) * amplitude, -1.0);
|
||||
var sampleMaxSize:Float = sampleMax * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMaxSize < minWaveformSize) sampleMaxSize = minWaveformSize;
|
||||
var sampleMinSize:Float = sampleMin * (orientation == HORIZONTAL ? this.height : this.width) / 2;
|
||||
if (sampleMinSize > -minWaveformSize) sampleMinSize = -minWaveformSize;
|
||||
var vertexTopY:Int = Std.int(waveformCenterPos - sampleMaxSize);
|
||||
var vertexBottomY:Int = Std.int(waveformCenterPos - sampleMinSize);
|
||||
|
||||
var vertexTopIndex:Int = -1;
|
||||
var vertexBottomIndex:Int = -1;
|
||||
|
||||
if (clipRect != null)
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(pixelPos, vertexTopY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(pixelPos, vertexBottomY, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = buildClippedVertex(vertexTopY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex, bottomRightVertexIndex);
|
||||
vertexBottomIndex = buildClippedVertex(vertexBottomY, pixelPos, topLeftVertexIndex, topRightVertexIndex, bottomLeftVertexIndex,
|
||||
bottomRightVertexIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (orientation == HORIZONTAL)
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(pixelPos, vertexTopY);
|
||||
vertexBottomIndex = this.build_vertex(pixelPos, vertexBottomY);
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexTopIndex = this.build_vertex(vertexTopY, pixelPos);
|
||||
vertexBottomIndex = this.build_vertex(vertexBottomY, pixelPos);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevVertexTopIndex != -1 && prevVertexBottomIndex != -1)
|
||||
{
|
||||
switch (orientation)
|
||||
{
|
||||
case HORIZONTAL:
|
||||
this.add_quad(prevVertexTopIndex, vertexTopIndex, vertexBottomIndex, prevVertexBottomIndex);
|
||||
case VERTICAL:
|
||||
this.add_quad(prevVertexBottomIndex, prevVertexTopIndex, vertexTopIndex, vertexBottomIndex);
|
||||
}
|
||||
}
|
||||
prevVertexTopIndex = vertexTopIndex;
|
||||
prevVertexBottomIndex = vertexBottomIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildClippedVertex(x:Int, y:Int, topLeftVertexIndex:Int, topRightVertexIndex:Int, bottomLeftVertexIndex:Int, bottomRightVertexIndex:Int):Int
|
||||
{
|
||||
var shouldClipXLeft = x < clipRect.x;
|
||||
var shouldClipXRight = x > (clipRect.x + clipRect.width);
|
||||
var shouldClipYTop = y < clipRect.y;
|
||||
var shouldClipYBottom = y > (clipRect.y + clipRect.height);
|
||||
|
||||
// If the vertex is fully outside the clipRect, use a pre-existing vertex.
|
||||
// Else, if the vertex is outside the clipRect on one axis, create a new vertex constrained on that axis.
|
||||
// Else, create a whole new vertex.
|
||||
if (shouldClipXLeft && shouldClipYTop)
|
||||
{
|
||||
return topLeftVertexIndex;
|
||||
}
|
||||
else if (shouldClipXRight && shouldClipYTop)
|
||||
{
|
||||
return topRightVertexIndex;
|
||||
}
|
||||
else if (shouldClipXLeft && shouldClipYBottom)
|
||||
{
|
||||
return bottomLeftVertexIndex;
|
||||
}
|
||||
else if (shouldClipXRight && shouldClipYBottom)
|
||||
{
|
||||
return bottomRightVertexIndex;
|
||||
}
|
||||
else if (shouldClipXLeft)
|
||||
{
|
||||
return this.build_vertex(clipRect.x, y);
|
||||
}
|
||||
else if (shouldClipXRight)
|
||||
{
|
||||
return this.build_vertex(clipRect.x + clipRect.width, y);
|
||||
}
|
||||
else if (shouldClipYTop)
|
||||
{
|
||||
return this.build_vertex(x, clipRect.y);
|
||||
}
|
||||
else if (shouldClipYBottom)
|
||||
{
|
||||
return this.build_vertex(x, clipRect.y + clipRect.height);
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.build_vertex(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
{
|
||||
return new WaveformSprite(data, orientation, color, duration);
|
||||
}
|
||||
|
||||
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
|
||||
{
|
||||
// TODO: Build waveform data from FunkinSound.
|
||||
var data = null;
|
||||
|
||||
return buildFromWaveformData(data, orientation, color, duration);
|
||||
}
|
||||
}
|
||||
|
||||
enum WaveformOrientation
|
||||
{
|
||||
HORIZONTAL;
|
||||
VERTICAL;
|
||||
}
|
|
@ -46,6 +46,9 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
this.entries = new Map<String, T>();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Create a `loadEntriesAsync()` function.
|
||||
*/
|
||||
public function loadEntries():Void
|
||||
{
|
||||
clearEntries();
|
||||
|
@ -54,7 +57,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
// SCRIPTED ENTRIES
|
||||
//
|
||||
var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
|
||||
log('Registering ${scriptedEntryClassNames.length} scripted entries...');
|
||||
log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
|
||||
|
||||
for (entryCls in scriptedEntryClassNames)
|
||||
{
|
||||
|
@ -78,7 +81,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
|
||||
return !entries.exists(entryId);
|
||||
});
|
||||
log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...');
|
||||
log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
|
||||
for (entryId in unscriptedEntryIds)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -120,6 +120,71 @@ class DataParse
|
|||
}
|
||||
}
|
||||
|
||||
public static function backdropData(json:Json, name:String):funkin.data.dialogue.ConversationData.BackdropData
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
var result:Dynamic = {};
|
||||
var backdropType:String = '';
|
||||
|
||||
for (field in fields)
|
||||
{
|
||||
switch (field.name)
|
||||
{
|
||||
case 'type':
|
||||
backdropType = Tools.getValue(field.value);
|
||||
}
|
||||
Reflect.setField(result, field.name, Tools.getValue(field.value));
|
||||
}
|
||||
|
||||
switch (backdropType)
|
||||
{
|
||||
case 'solid':
|
||||
return SOLID(result);
|
||||
default:
|
||||
throw 'Expected Backdrop property $name to be specify a valid "type", but it was "${backdropType}".';
|
||||
}
|
||||
|
||||
return null;
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
public static function outroData(json:Json, name:String):Null<funkin.data.dialogue.ConversationData.OutroData>
|
||||
{
|
||||
switch (json.value)
|
||||
{
|
||||
case JObject(fields):
|
||||
var result:Dynamic = {};
|
||||
var outroType:String = '';
|
||||
|
||||
for (field in fields)
|
||||
{
|
||||
switch (field.name)
|
||||
{
|
||||
case 'type':
|
||||
outroType = Tools.getValue(field.value);
|
||||
}
|
||||
Reflect.setField(result, field.name, Tools.getValue(field.value));
|
||||
}
|
||||
|
||||
switch (outroType)
|
||||
{
|
||||
case 'none':
|
||||
return NONE(result);
|
||||
case 'fade':
|
||||
return FADE(result);
|
||||
default:
|
||||
throw 'Expected Outro property $name to be specify a valid "type", but it was "${outroType}".';
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser which outputs a `Either<Float, LegacyScrollSpeeds>`.
|
||||
* Used by the FNF legacy JSON importer.
|
||||
|
|
168
source/funkin/data/dialogue/ConversationData.hx
Normal file
168
source/funkin/data/dialogue/ConversationData.hx
Normal file
|
@ -0,0 +1,168 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
|
||||
/**
|
||||
* A type definition for the data for a specific conversation.
|
||||
* It includes things like what dialogue boxes to use, what text to display, and what animations to play.
|
||||
* @see https://lib.haxe.org/p/json2object/
|
||||
*/
|
||||
typedef ConversationData =
|
||||
{
|
||||
/**
|
||||
* Semantic version for conversation data.
|
||||
*/
|
||||
public var version:String;
|
||||
|
||||
/**
|
||||
* Data on the backdrop for the conversation.
|
||||
*/
|
||||
@:jcustomparse(funkin.data.DataParse.backdropData)
|
||||
public var backdrop:BackdropData;
|
||||
|
||||
/**
|
||||
* Data on the outro for the conversation.
|
||||
*/
|
||||
@:jcustomparse(funkin.data.DataParse.outroData)
|
||||
@:optional
|
||||
public var outro:Null<OutroData>;
|
||||
|
||||
/**
|
||||
* Data on the music for the conversation.
|
||||
*/
|
||||
@:optional
|
||||
public var music:Null<MusicData>;
|
||||
|
||||
/**
|
||||
* Data for each line of dialogue in the conversation.
|
||||
*/
|
||||
public var dialogue:Array<DialogueEntryData>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data on the backdrop for the conversation, behind the dialogue box.
|
||||
* A custom parser distinguishes between backdrop types based on the `type` field.
|
||||
*/
|
||||
enum BackdropData
|
||||
{
|
||||
SOLID(data:BackdropData_Solid); // 'solid'
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for a Solid color backdrop.
|
||||
*/
|
||||
typedef BackdropData_Solid =
|
||||
{
|
||||
/**
|
||||
* Used to distinguish between backdrop types. Should always be `solid` for this type.
|
||||
*/
|
||||
var type:String;
|
||||
|
||||
/**
|
||||
* The color of the backdrop.
|
||||
*/
|
||||
var color:String;
|
||||
|
||||
/**
|
||||
* Fade-in time for the backdrop.
|
||||
* @default No fade-in
|
||||
*/
|
||||
@:optional
|
||||
@:default(0.0)
|
||||
var fadeTime:Float;
|
||||
};
|
||||
|
||||
enum OutroData
|
||||
{
|
||||
NONE(data:OutroData_None); // 'none'
|
||||
FADE(data:OutroData_Fade); // 'fade'
|
||||
}
|
||||
|
||||
typedef OutroData_None =
|
||||
{
|
||||
/**
|
||||
* Used to distinguish between outro types. Should always be `none` for this type.
|
||||
*/
|
||||
var type:String;
|
||||
}
|
||||
|
||||
typedef OutroData_Fade =
|
||||
{
|
||||
/**
|
||||
* Used to distinguish between outro types. Should always be `fade` for this type.
|
||||
*/
|
||||
var type:String;
|
||||
|
||||
/**
|
||||
* The time to fade out the conversation.
|
||||
* @default 1 second
|
||||
*/
|
||||
@:optional
|
||||
@:default(1.0)
|
||||
var fadeTime:Float;
|
||||
}
|
||||
|
||||
typedef MusicData =
|
||||
{
|
||||
/**
|
||||
* The asset to play for the music.
|
||||
*/
|
||||
var asset:String;
|
||||
|
||||
/**
|
||||
* The time to fade in the music.
|
||||
*/
|
||||
@:optional
|
||||
@:default(0.0)
|
||||
var fadeTime:Float;
|
||||
|
||||
@:optional
|
||||
@:default(false)
|
||||
var looped:Bool;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data on a single line of dialogue in a conversation.
|
||||
*/
|
||||
typedef DialogueEntryData =
|
||||
{
|
||||
/**
|
||||
* Which speaker is speaking.
|
||||
* @see `SpeakerData.hx`
|
||||
*/
|
||||
public var speaker:String;
|
||||
|
||||
/**
|
||||
* The animation the speaker should play for this line of dialogue.
|
||||
*/
|
||||
public var speakerAnimation:String;
|
||||
|
||||
/**
|
||||
* Which dialogue box to use for this line of dialogue.
|
||||
* @see `DialogueBoxData.hx`
|
||||
*/
|
||||
public var box:String;
|
||||
|
||||
/**
|
||||
* Which animation to play for the dialogue box.
|
||||
*/
|
||||
public var boxAnimation:String;
|
||||
|
||||
/**
|
||||
* The text that will display for this line of dialogue.
|
||||
* Text will automatically wrap.
|
||||
* When the user advances the dialogue, the next entry in the array will concatenate on.
|
||||
* Advancing when the last entry is displayed will move to the next `DialogueEntryData`,
|
||||
* or end the conversation if there are no more.
|
||||
*/
|
||||
public var text:Array<String>;
|
||||
|
||||
/**
|
||||
* The relative speed at which text gets "typed out".
|
||||
* Setting `speed` to `1.5` would make it look like the character is speaking quickly,
|
||||
* and setting `speed` to `0.5` would make it look like the character is emphasizing each word.
|
||||
*/
|
||||
@:optional
|
||||
@:default(1.0)
|
||||
public var speed:Float;
|
||||
};
|
81
source/funkin/data/dialogue/ConversationRegistry.hx
Normal file
81
source/funkin/data/dialogue/ConversationRegistry.hx
Normal file
|
@ -0,0 +1,81 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.play.cutscene.dialogue.Conversation;
|
||||
import funkin.data.dialogue.ConversationData;
|
||||
import funkin.play.cutscene.dialogue.ScriptedConversation;
|
||||
|
||||
class ConversationRegistry extends BaseRegistry<Conversation, ConversationData>
|
||||
{
|
||||
/**
|
||||
* The current version string for the dialogue box data format.
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateConversationData()` function.
|
||||
*/
|
||||
public static final CONVERSATION_DATA_VERSION:thx.semver.Version = "1.0.0";
|
||||
|
||||
public static final CONVERSATION_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
|
||||
|
||||
public static final instance:ConversationRegistry = new ConversationRegistry();
|
||||
|
||||
public function new()
|
||||
{
|
||||
super('CONVERSATION', 'dialogue/conversations', CONVERSATION_DATA_VERSION_RULE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||
*/
|
||||
public function parseEntryData(id:String):Null<ConversationData>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<ConversationData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
|
||||
switch (loadEntryFile(id))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate the JSON data and produce the corresponding data object.
|
||||
*
|
||||
* NOTE: Must be implemented on the implementation class.
|
||||
* @param contents The JSON as a string.
|
||||
* @param fileName An optional file name for error reporting.
|
||||
*/
|
||||
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<ConversationData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<ConversationData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
function createScriptedEntry(clsName:String):Conversation
|
||||
{
|
||||
return ScriptedConversation.init(clsName, "unknown");
|
||||
}
|
||||
|
||||
function getScriptedClassNames():Array<String>
|
||||
{
|
||||
return ScriptedConversation.listScriptClasses();
|
||||
}
|
||||
}
|
128
source/funkin/data/dialogue/DialogueBoxData.hx
Normal file
128
source/funkin/data/dialogue/DialogueBoxData.hx
Normal file
|
@ -0,0 +1,128 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
|
||||
/**
|
||||
* A type definition for the data for a conversation text box.
|
||||
* It includes things like the sprite to use, and the font and color for the text.
|
||||
* The actual text is included in the ConversationData.
|
||||
* @see https://lib.haxe.org/p/json2object/
|
||||
*/
|
||||
typedef DialogueBoxData =
|
||||
{
|
||||
/**
|
||||
* Semantic version for dialogue box data.
|
||||
*/
|
||||
public var version:String;
|
||||
|
||||
/**
|
||||
* A human readable name for the dialogue box type.
|
||||
*/
|
||||
public var name:String;
|
||||
|
||||
/**
|
||||
* The asset path for the sprite to use for the dialogue box.
|
||||
* Takes a static sprite or a sprite sheet.
|
||||
*/
|
||||
public var assetPath:String;
|
||||
|
||||
/**
|
||||
* Whether to horizontally flip the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var flipX:Bool;
|
||||
|
||||
/**
|
||||
* Whether to vertically flip the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var flipY:Bool;
|
||||
|
||||
/**
|
||||
* Whether to disable anti-aliasing for the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var isPixel:Bool;
|
||||
|
||||
/**
|
||||
* The relative horizontal and vertical offsets for the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default([0, 0])
|
||||
public var offsets:Array<Float>;
|
||||
|
||||
/**
|
||||
* Info about how to display text in the dialogue box.
|
||||
*/
|
||||
public var text:DialogueBoxTextData;
|
||||
|
||||
/**
|
||||
* Multiply the size of the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(1)
|
||||
public var scale:Float;
|
||||
|
||||
/**
|
||||
* If using a spritesheet for the dialogue box, the animations to use.
|
||||
*/
|
||||
@:optional
|
||||
@:default([])
|
||||
public var animations:Array<AnimationData>;
|
||||
}
|
||||
|
||||
typedef DialogueBoxTextData =
|
||||
{
|
||||
/**
|
||||
* The position of the text in teh box.
|
||||
*/
|
||||
@:optional
|
||||
@:default([0, 0])
|
||||
var offsets:Array<Float>;
|
||||
|
||||
/**
|
||||
* The width of the
|
||||
*/
|
||||
@:optional
|
||||
@:default(300)
|
||||
var width:Int;
|
||||
|
||||
/**
|
||||
* The font size to use for the text.
|
||||
*/
|
||||
@:optional
|
||||
@:default(32)
|
||||
var size:Int;
|
||||
|
||||
/**
|
||||
* The color to use for the text.
|
||||
* Use a string that can be translated to a color, like `#FF0000` for red.
|
||||
*/
|
||||
@:optional
|
||||
@:default("#000000")
|
||||
var color:String;
|
||||
|
||||
/**
|
||||
* The font to use for the text.
|
||||
* @since v1.1.0
|
||||
* @default `Arial`, make sure to switch this!
|
||||
*/
|
||||
@:optional
|
||||
@:default("Arial")
|
||||
var fontFamily:String;
|
||||
|
||||
/**
|
||||
* The color to use for the shadow of the text. Use transparent to disable.
|
||||
*/
|
||||
var shadowColor:String;
|
||||
|
||||
/**
|
||||
* The width of the shadow of the text.
|
||||
*/
|
||||
@:optional
|
||||
@:default(0)
|
||||
var shadowWidth:Int;
|
||||
};
|
81
source/funkin/data/dialogue/DialogueBoxRegistry.hx
Normal file
81
source/funkin/data/dialogue/DialogueBoxRegistry.hx
Normal file
|
@ -0,0 +1,81 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.play.cutscene.dialogue.DialogueBox;
|
||||
import funkin.data.dialogue.DialogueBoxData;
|
||||
import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
|
||||
|
||||
class DialogueBoxRegistry extends BaseRegistry<DialogueBox, DialogueBoxData>
|
||||
{
|
||||
/**
|
||||
* The current version string for the dialogue box data format.
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateDialogueBoxData()` function.
|
||||
*/
|
||||
public static final DIALOGUEBOX_DATA_VERSION:thx.semver.Version = "1.1.0";
|
||||
|
||||
public static final DIALOGUEBOX_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x";
|
||||
|
||||
public static final instance:DialogueBoxRegistry = new DialogueBoxRegistry();
|
||||
|
||||
public function new()
|
||||
{
|
||||
super('DIALOGUEBOX', 'dialogue/boxes', DIALOGUEBOX_DATA_VERSION_RULE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||
*/
|
||||
public function parseEntryData(id:String):Null<DialogueBoxData>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<DialogueBoxData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
|
||||
switch (loadEntryFile(id))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate the JSON data and produce the corresponding data object.
|
||||
*
|
||||
* NOTE: Must be implemented on the implementation class.
|
||||
* @param contents The JSON as a string.
|
||||
* @param fileName An optional file name for error reporting.
|
||||
*/
|
||||
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<DialogueBoxData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<DialogueBoxData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
function createScriptedEntry(clsName:String):DialogueBox
|
||||
{
|
||||
return ScriptedDialogueBox.init(clsName, "unknown");
|
||||
}
|
||||
|
||||
function getScriptedClassNames():Array<String>
|
||||
{
|
||||
return ScriptedDialogueBox.listScriptClasses();
|
||||
}
|
||||
}
|
68
source/funkin/data/dialogue/SpeakerData.hx
Normal file
68
source/funkin/data/dialogue/SpeakerData.hx
Normal file
|
@ -0,0 +1,68 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
|
||||
/**
|
||||
* A type definition for a specific speaker in a conversation.
|
||||
* It includes things like what sprite to use and its available animations.
|
||||
* @see https://lib.haxe.org/p/json2object/
|
||||
*/
|
||||
typedef SpeakerData =
|
||||
{
|
||||
/**
|
||||
* Semantic version of the speaker data.
|
||||
*/
|
||||
public var version:String;
|
||||
|
||||
/**
|
||||
* A human-readable name for the speaker.
|
||||
*/
|
||||
public var name:String;
|
||||
|
||||
/**
|
||||
* The path to the asset to use for the speaker's sprite.
|
||||
*/
|
||||
public var assetPath:String;
|
||||
|
||||
/**
|
||||
* Whether the sprite should be flipped horizontally.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var flipX:Bool;
|
||||
|
||||
/**
|
||||
* Whether the sprite should be flipped vertically.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var flipY:Bool;
|
||||
|
||||
/**
|
||||
* Whether to disable anti-aliasing for the dialogue box sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var isPixel:Bool;
|
||||
|
||||
/**
|
||||
* The offsets to apply to the sprite's position.
|
||||
*/
|
||||
@:optional
|
||||
@:default([0, 0])
|
||||
public var offsets:Array<Float>;
|
||||
|
||||
/**
|
||||
* The scale to apply to the sprite.
|
||||
*/
|
||||
@:optional
|
||||
@:default(1.0)
|
||||
public var scale:Float;
|
||||
|
||||
/**
|
||||
* The available animations for the speaker.
|
||||
*/
|
||||
@:optional
|
||||
@:default([])
|
||||
public var animations:Array<AnimationData>;
|
||||
}
|
81
source/funkin/data/dialogue/SpeakerRegistry.hx
Normal file
81
source/funkin/data/dialogue/SpeakerRegistry.hx
Normal file
|
@ -0,0 +1,81 @@
|
|||
package funkin.data.dialogue;
|
||||
|
||||
import funkin.play.cutscene.dialogue.Speaker;
|
||||
import funkin.data.dialogue.SpeakerData;
|
||||
import funkin.play.cutscene.dialogue.ScriptedSpeaker;
|
||||
|
||||
class SpeakerRegistry extends BaseRegistry<Speaker, SpeakerData>
|
||||
{
|
||||
/**
|
||||
* The current version string for the speaker data format.
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateSpeakerData()` function.
|
||||
*/
|
||||
public static final SPEAKER_DATA_VERSION:thx.semver.Version = "1.0.0";
|
||||
|
||||
public static final SPEAKER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
|
||||
|
||||
public static final instance:SpeakerRegistry = new SpeakerRegistry();
|
||||
|
||||
public function new()
|
||||
{
|
||||
super('SPEAKER', 'dialogue/speakers', SPEAKER_DATA_VERSION_RULE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||
*/
|
||||
public function parseEntryData(id:String):Null<SpeakerData>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<SpeakerData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
|
||||
switch (loadEntryFile(id))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate the JSON data and produce the corresponding data object.
|
||||
*
|
||||
* NOTE: Must be implemented on the implementation class.
|
||||
* @param contents The JSON as a string.
|
||||
* @param fileName An optional file name for error reporting.
|
||||
*/
|
||||
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<SpeakerData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SpeakerData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
function createScriptedEntry(clsName:String):Speaker
|
||||
{
|
||||
return ScriptedSpeaker.init(clsName, "unknown");
|
||||
}
|
||||
|
||||
function getScriptedClassNames():Array<String>
|
||||
{
|
||||
return ScriptedSpeaker.listScriptClasses();
|
||||
}
|
||||
}
|
|
@ -6,9 +6,14 @@ import funkin.data.song.SongData.SongEventData;
|
|||
import funkin.util.macro.ClassMacro;
|
||||
import funkin.play.event.ScriptedSongEvent;
|
||||
|
||||
@:forward(name, tittlte, type, keys, min, max, step, defaultValue, iterator)
|
||||
@:forward(name, title, type, keys, min, max, step, units, defaultValue, iterator)
|
||||
abstract SongEventSchema(SongEventSchemaRaw)
|
||||
{
|
||||
/**
|
||||
* These units look better when placed immediately next to the value, rather than after a space.
|
||||
*/
|
||||
static final NO_SPACE_UNITS:Array<String> = ['x', '°', '%'];
|
||||
|
||||
public function new(?fields:Array<SongEventSchemaField>)
|
||||
{
|
||||
this = fields;
|
||||
|
@ -42,7 +47,7 @@ abstract SongEventSchema(SongEventSchemaRaw)
|
|||
return this[k] = v;
|
||||
}
|
||||
|
||||
public function stringifyFieldValue(name:String, value:Dynamic):String
|
||||
public function stringifyFieldValue(name:String, value:Dynamic, addUnits:Bool = true):String
|
||||
{
|
||||
var field:SongEventSchemaField = getByName(name);
|
||||
if (field == null) return 'Unknown';
|
||||
|
@ -52,21 +57,36 @@ abstract SongEventSchema(SongEventSchemaRaw)
|
|||
case SongEventFieldType.STRING:
|
||||
return Std.string(value);
|
||||
case SongEventFieldType.INTEGER:
|
||||
return Std.string(value);
|
||||
var returnValue:String = Std.string(value);
|
||||
if (addUnits) return addUnitsToString(returnValue, field);
|
||||
return returnValue;
|
||||
case SongEventFieldType.FLOAT:
|
||||
return Std.string(value);
|
||||
var returnValue:String = Std.string(value);
|
||||
if (addUnits) return addUnitsToString(returnValue, field);
|
||||
return returnValue;
|
||||
case SongEventFieldType.BOOL:
|
||||
return Std.string(value);
|
||||
case SongEventFieldType.ENUM:
|
||||
var valueString:String = Std.string(value);
|
||||
for (key in field.keys.keys())
|
||||
{
|
||||
if (field.keys.get(key) == value) return key;
|
||||
// Comparing these values as strings because comparing Dynamic variables is jank.
|
||||
if (Std.string(field.keys.get(key)) == valueString) return key;
|
||||
}
|
||||
return Std.string(value);
|
||||
return valueString;
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function addUnitsToString(value:String, field:SongEventSchemaField)
|
||||
{
|
||||
if (field.units == null || field.units == '') return value;
|
||||
|
||||
var unit:String = field.units;
|
||||
|
||||
return value + (NO_SPACE_UNITS.contains(unit) ? '' : ' ') + '${unit}';
|
||||
}
|
||||
}
|
||||
|
||||
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
|
||||
|
@ -115,6 +135,12 @@ typedef SongEventSchemaField =
|
|||
*/
|
||||
?step:Float,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The units that the value is expressed in (pixels, percent, etc).
|
||||
*/
|
||||
?units:String,
|
||||
|
||||
/**
|
||||
* An optional default value for the field.
|
||||
*/
|
||||
|
|
|
@ -418,10 +418,10 @@ class SongPlayData implements ICloneable<SongPlayData>
|
|||
|
||||
/**
|
||||
* The difficulty ratings for this song as displayed in Freeplay.
|
||||
* Key is a difficulty ID or `default`.
|
||||
* Key is a difficulty ID.
|
||||
*/
|
||||
@:optional
|
||||
@:default(['default' => 1])
|
||||
@:default(['normal' => 0])
|
||||
public var ratings:Map<String, Int>;
|
||||
|
||||
/**
|
||||
|
@ -431,6 +431,24 @@ class SongPlayData implements ICloneable<SongPlayData>
|
|||
@:optional
|
||||
public var album:Null<String>;
|
||||
|
||||
/**
|
||||
* The start time for the audio preview in Freeplay.
|
||||
* Defaults to 0 seconds in.
|
||||
* @since `2.2.2`
|
||||
*/
|
||||
@:optional
|
||||
@:default(0)
|
||||
public var previewStart:Int;
|
||||
|
||||
/**
|
||||
* The end time for the audio preview in Freeplay.
|
||||
* Defaults to 15 seconds in.
|
||||
* @since `2.2.2`
|
||||
*/
|
||||
@:optional
|
||||
@:default(15000)
|
||||
public var previewEnd:Int;
|
||||
|
||||
public function new()
|
||||
{
|
||||
ratings = new Map<String, Int>();
|
||||
|
@ -438,6 +456,7 @@ class SongPlayData implements ICloneable<SongPlayData>
|
|||
|
||||
public function clone():SongPlayData
|
||||
{
|
||||
// TODO: This sucks! If you forget to update this you get weird behavior.
|
||||
var result:SongPlayData = new SongPlayData();
|
||||
result.songVariations = this.songVariations.clone();
|
||||
result.difficulties = this.difficulties.clone();
|
||||
|
@ -446,6 +465,8 @@ class SongPlayData implements ICloneable<SongPlayData>
|
|||
result.noteStyle = this.noteStyle;
|
||||
result.ratings = this.ratings.clone();
|
||||
result.album = this.album;
|
||||
result.previewStart = this.previewStart;
|
||||
result.previewEnd = this.previewEnd;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -777,7 +798,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
|||
|
||||
var title = eventSchema.getByName(key)?.title ?? 'UnknownField';
|
||||
|
||||
if (eventSchema.stringifyFieldValue(key, value) != null) trace(eventSchema.stringifyFieldValue(key, value));
|
||||
// if (eventSchema.stringifyFieldValue(key, value) != null) trace(eventSchema.stringifyFieldValue(key, value));
|
||||
var valueStr = eventSchema.stringifyFieldValue(key, value) ?? 'UnknownValue';
|
||||
|
||||
result += '\n- ${title}: ${valueStr}';
|
||||
|
|
|
@ -153,8 +153,8 @@ class SongDataUtils
|
|||
public static function buildNoteClipboard(notes:Array<SongNoteData>, ?timeOffset:Int = null):Array<SongNoteData>
|
||||
{
|
||||
if (notes.length == 0) return notes;
|
||||
if (timeOffset == null) timeOffset = -Std.int(notes[0].time);
|
||||
return offsetSongNoteData(sortNotes(notes), timeOffset);
|
||||
if (timeOffset == null) timeOffset = Std.int(notes[0].time);
|
||||
return offsetSongNoteData(sortNotes(notes), -timeOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -165,8 +165,8 @@ class SongDataUtils
|
|||
public static function buildEventClipboard(events:Array<SongEventData>, ?timeOffset:Int = null):Array<SongEventData>
|
||||
{
|
||||
if (events.length == 0) return events;
|
||||
if (timeOffset == null) timeOffset = -Std.int(events[0].time);
|
||||
return offsetSongEventData(sortEvents(events), timeOffset);
|
||||
if (timeOffset == null) timeOffset = Std.int(events[0].time);
|
||||
return offsetSongEventData(sortEvents(events), -timeOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,7 +20,7 @@ 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.2.1";
|
||||
public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.2";
|
||||
|
||||
public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x";
|
||||
|
||||
|
@ -58,7 +58,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
// SCRIPTED ENTRIES
|
||||
//
|
||||
var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
|
||||
log('Registering ${scriptedEntryClassNames.length} scripted entries...');
|
||||
log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
|
||||
|
||||
for (entryCls in scriptedEntryClassNames)
|
||||
{
|
||||
|
@ -84,7 +84,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
|
||||
return !entries.exists(entryId);
|
||||
});
|
||||
log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...');
|
||||
log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
|
||||
for (entryId in unscriptedEntryIds)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -12,22 +12,19 @@ class MeshRender extends FlxStrip
|
|||
public var vertex_count(default, null):Int = 0;
|
||||
public var index_count(default, null):Int = 0;
|
||||
|
||||
var tri_offset:Int = 0;
|
||||
|
||||
public function new(x, y, ?col:FlxColor = FlxColor.WHITE)
|
||||
{
|
||||
super(x, y);
|
||||
makeGraphic(1, 1, col);
|
||||
}
|
||||
|
||||
public inline function start()
|
||||
/**
|
||||
* Add a vertex.
|
||||
*/
|
||||
public inline function build_vertex(x:Float, y:Float, u:Float = 0, v:Float = 0):Int
|
||||
{
|
||||
tri_offset = vertex_count;
|
||||
}
|
||||
|
||||
public inline function add_vertex(x:Float, y:Float, u:Float = 0, v:Float = 0)
|
||||
{
|
||||
final pos = vertex_count << 1;
|
||||
final index = vertex_count;
|
||||
final pos = index << 1;
|
||||
|
||||
vertices[pos] = x;
|
||||
vertices[pos + 1] = y;
|
||||
|
@ -36,48 +33,72 @@ class MeshRender extends FlxStrip
|
|||
uvtData[pos + 1] = v;
|
||||
|
||||
vertex_count++;
|
||||
return index;
|
||||
}
|
||||
|
||||
public function add_tri(a:Int, b:Int, c:Int)
|
||||
/**
|
||||
* Build a triangle from three vertex indexes.
|
||||
* @param a
|
||||
* @param b
|
||||
* @param c
|
||||
*/
|
||||
public function add_tri(a:Int, b:Int, c:Int):Void
|
||||
{
|
||||
indices[index_count] = a + tri_offset;
|
||||
indices[index_count + 1] = b + tri_offset;
|
||||
indices[index_count + 2] = c + tri_offset;
|
||||
indices[index_count] = a;
|
||||
indices[index_count + 1] = b;
|
||||
indices[index_count + 2] = c;
|
||||
|
||||
index_count += 3;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* top left - a
|
||||
*
|
||||
* top right - b
|
||||
*
|
||||
* bottom left - c
|
||||
*
|
||||
* bottom right - d
|
||||
*/
|
||||
public function add_quad(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, dx:Float, dy:Float, au:Float = 0, av:Float = 0, bu:Float = 0,
|
||||
bv:Float = 0, cu:Float = 0, cv:Float = 0, du:Float = 0, dv:Float = 0)
|
||||
public function build_tri(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, au:Float = 0, av:Float = 0, bu:Float = 0, bv:Float = 0, cu:Float = 0,
|
||||
cv:Float = 0):Void
|
||||
{
|
||||
start();
|
||||
// top left
|
||||
add_vertex(bx, by, bu, bv);
|
||||
// top right
|
||||
add_vertex(ax, ay, au, av);
|
||||
// bottom left
|
||||
add_vertex(cx, cy, cu, cv);
|
||||
// bottom right
|
||||
add_vertex(dx, dy, du, dv);
|
||||
add_tri(build_vertex(ax, ay, au, av), build_vertex(bx, by, bu, bv), build_vertex(cx, cy, cu, cv));
|
||||
}
|
||||
|
||||
add_tri(0, 1, 2);
|
||||
add_tri(0, 2, 3);
|
||||
/**
|
||||
* @param a top left vertex
|
||||
* @param b top right vertex
|
||||
* @param c bottom right vertex
|
||||
* @param d bottom left vertex
|
||||
*/
|
||||
public function add_quad(a:Int, b:Int, c:Int, d:Int):Void
|
||||
{
|
||||
add_tri(a, b, c);
|
||||
add_tri(a, c, d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a quad from four points.
|
||||
*
|
||||
* top right - a
|
||||
* top left - b
|
||||
* bottom right - c
|
||||
* bottom left - d
|
||||
*/
|
||||
public function build_quad(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, dx:Float, dy:Float, au:Float = 0, av:Float = 0, bu:Float = 0,
|
||||
bv:Float = 0, cu:Float = 0, cv:Float = 0, du:Float = 0, dv:Float = 0):Void
|
||||
{
|
||||
// top left
|
||||
var b = build_vertex(bx, by, bu, bv);
|
||||
// top right
|
||||
var a = build_vertex(ax, ay, au, av);
|
||||
// bottom left
|
||||
var c = build_vertex(cx, cy, cu, cv);
|
||||
// bottom right
|
||||
var d = build_vertex(dx, dy, du, dv);
|
||||
|
||||
add_tri(a, b, c);
|
||||
add_tri(a, c, d);
|
||||
}
|
||||
|
||||
public function clear()
|
||||
{
|
||||
vertices.length = 0;
|
||||
indices.length = 0;
|
||||
uvtData.length = 0;
|
||||
colors.length = 0;
|
||||
vertex_count = 0;
|
||||
index_count = 0;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package funkin.modding;
|
|||
|
||||
import funkin.util.macro.ClassMacro;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.data.stage.StageData;
|
||||
import polymod.Polymod;
|
||||
|
@ -13,10 +12,11 @@ import funkin.data.stage.StageRegistry;
|
|||
import funkin.util.FileUtil;
|
||||
import funkin.data.level.LevelRegistry;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
|
||||
import funkin.data.dialogue.ConversationRegistry;
|
||||
import funkin.data.dialogue.DialogueBoxRegistry;
|
||||
import funkin.data.dialogue.SpeakerRegistry;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.save.Save;
|
||||
import funkin.play.cutscene.dialogue.SpeakerDataParser;
|
||||
import funkin.data.song.SongRegistry;
|
||||
|
||||
class PolymodHandler
|
||||
|
@ -208,8 +208,8 @@ class PolymodHandler
|
|||
{
|
||||
return {
|
||||
assetLibraryPaths: [
|
||||
"default" => "preload", "shared" => "", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2", "week3" => "week3",
|
||||
"week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
|
||||
"default" => "preload", "shared" => "shared", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2",
|
||||
"week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
|
||||
],
|
||||
coreAssetRedirect: CORE_FOLDER,
|
||||
}
|
||||
|
@ -273,11 +273,11 @@ class PolymodHandler
|
|||
LevelRegistry.instance.loadEntries();
|
||||
NoteStyleRegistry.instance.loadEntries();
|
||||
SongEventRegistry.loadEventCache();
|
||||
ConversationDataParser.loadConversationCache();
|
||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
||||
SpeakerDataParser.loadSpeakerCache();
|
||||
ConversationRegistry.instance.loadEntries();
|
||||
DialogueBoxRegistry.instance.loadEntries();
|
||||
SpeakerRegistry.instance.loadEntries();
|
||||
StageRegistry.instance.loadEntries();
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
|
||||
ModuleHandler.loadModuleCache();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ import funkin.modding.events.ScriptEventDispatcher;
|
|||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.cutscene.dialogue.Conversation;
|
||||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||
import funkin.data.dialogue.ConversationRegistry;
|
||||
import funkin.play.cutscene.VanillaCutscenes;
|
||||
import funkin.play.cutscene.VideoCutscene;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
|
@ -1662,7 +1662,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
isInCutscene = true;
|
||||
|
||||
currentConversation = ConversationDataParser.fetchConversation(conversationId);
|
||||
currentConversation = ConversationRegistry.instance.fetchEntry(conversationId);
|
||||
if (currentConversation == null) return;
|
||||
|
||||
currentConversation.completeCallback = onConversationComplete;
|
||||
|
|
|
@ -43,7 +43,7 @@ class CharacterDataParser
|
|||
{
|
||||
// Clear any stages that are cached if there were any.
|
||||
clearCharacterCache();
|
||||
trace('Loading character cache...');
|
||||
trace('[CHARACTER] Parsing all entries...');
|
||||
|
||||
//
|
||||
// UNSCRIPTED CHARACTERS
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import funkin.data.IRegistryEntry;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.sound.FlxSound;
|
||||
|
@ -13,27 +15,30 @@ import funkin.modding.IScriptedClass.IEventHandler;
|
|||
import funkin.play.cutscene.dialogue.DialogueBox;
|
||||
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.play.cutscene.dialogue.ConversationData.DialogueEntryData;
|
||||
import flixel.addons.display.FlxPieDial;
|
||||
import funkin.data.dialogue.ConversationData;
|
||||
import funkin.data.dialogue.ConversationData.DialogueEntryData;
|
||||
import funkin.data.dialogue.ConversationRegistry;
|
||||
import funkin.data.dialogue.SpeakerData;
|
||||
import funkin.data.dialogue.SpeakerRegistry;
|
||||
import funkin.data.dialogue.DialogueBoxData;
|
||||
import funkin.data.dialogue.DialogueBoxRegistry;
|
||||
|
||||
/**
|
||||
* A high-level handler for dialogue.
|
||||
*
|
||||
* This shit is great for modders but it's pretty elaborate for how much it'll actually be used, lolol. -Eric
|
||||
*/
|
||||
class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
||||
class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry<ConversationData>
|
||||
{
|
||||
static final CONVERSATION_SKIP_TIMER:Float = 1.5;
|
||||
|
||||
var skipHeldTimer:Float = 0.0;
|
||||
|
||||
/**
|
||||
* DATA
|
||||
* The ID of the conversation.
|
||||
*/
|
||||
/**
|
||||
* The ID of the associated dialogue.
|
||||
*/
|
||||
public final conversationId:String;
|
||||
public final id:String;
|
||||
|
||||
/**
|
||||
* The current state of the conversation.
|
||||
|
@ -41,9 +46,9 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
var state:ConversationState = ConversationState.Start;
|
||||
|
||||
/**
|
||||
* The data for the associated dialogue.
|
||||
* Conversation data as parsed from the JSON file.
|
||||
*/
|
||||
var conversationData:ConversationData;
|
||||
public final _data:ConversationData;
|
||||
|
||||
/**
|
||||
* The current entry in the dialogue.
|
||||
|
@ -54,7 +59,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function get_currentDialogueEntryCount():Int
|
||||
{
|
||||
return conversationData.dialogue.length;
|
||||
return _data.dialogue.length;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,10 +78,10 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function get_currentDialogueEntryData():DialogueEntryData
|
||||
{
|
||||
if (conversationData == null || conversationData.dialogue == null) return null;
|
||||
if (currentDialogueEntry < 0 || currentDialogueEntry >= conversationData.dialogue.length) return null;
|
||||
if (_data == null || _data.dialogue == null) return null;
|
||||
if (currentDialogueEntry < 0 || currentDialogueEntry >= _data.dialogue.length) return null;
|
||||
|
||||
return conversationData.dialogue[currentDialogueEntry];
|
||||
return _data.dialogue[currentDialogueEntry];
|
||||
}
|
||||
|
||||
var currentDialogueLineString(get, never):String;
|
||||
|
@ -94,7 +99,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
/**
|
||||
* GRAPHICS
|
||||
*/
|
||||
var backdrop:FlxSprite;
|
||||
var backdrop:FunkinSprite;
|
||||
|
||||
var currentSpeaker:Speaker;
|
||||
|
||||
|
@ -102,14 +107,17 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
var skipTimer:FlxPieDial;
|
||||
|
||||
public function new(conversationId:String)
|
||||
public function new(id:String)
|
||||
{
|
||||
super();
|
||||
|
||||
this.conversationId = conversationId;
|
||||
this.conversationData = ConversationDataParser.parseConversationData(this.conversationId);
|
||||
this.id = id;
|
||||
this._data = _fetchData(id);
|
||||
|
||||
if (conversationData == null) throw 'Could not load conversation data for conversation ID "$conversationId"';
|
||||
if (_data == null)
|
||||
{
|
||||
throw 'Could not parse conversation data for id: $id';
|
||||
}
|
||||
}
|
||||
|
||||
public function onCreate(event:ScriptEvent):Void
|
||||
|
@ -125,14 +133,14 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function setupMusic():Void
|
||||
{
|
||||
if (conversationData.music == null) return;
|
||||
if (_data.music == null) return;
|
||||
|
||||
music = new FlxSound().loadEmbedded(Paths.music(conversationData.music.asset), true, true);
|
||||
music = new FlxSound().loadEmbedded(Paths.music(_data.music.asset), true, true);
|
||||
music.volume = 0;
|
||||
|
||||
if (conversationData.music.fadeTime > 0.0)
|
||||
if (_data.music.fadeTime > 0.0)
|
||||
{
|
||||
FlxTween.tween(music, {volume: 1.0}, conversationData.music.fadeTime, {ease: FlxEase.linear});
|
||||
FlxTween.tween(music, {volume: 1.0}, _data.music.fadeTime, {ease: FlxEase.linear});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -145,19 +153,20 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function setupBackdrop():Void
|
||||
{
|
||||
backdrop = new FlxSprite(0, 0);
|
||||
backdrop = new FunkinSprite(0, 0);
|
||||
|
||||
if (conversationData.backdrop == null) return;
|
||||
if (_data.backdrop == null) return;
|
||||
|
||||
// Play intro
|
||||
switch (conversationData?.backdrop.type)
|
||||
switch (_data.backdrop)
|
||||
{
|
||||
case SOLID:
|
||||
backdrop.makeGraphic(Std.int(FlxG.width), Std.int(FlxG.height), FlxColor.fromString(conversationData.backdrop.data.color));
|
||||
if (conversationData.backdrop.data.fadeTime > 0.0)
|
||||
case SOLID(backdropData):
|
||||
var targetColor:FlxColor = FlxColor.fromString(backdropData.color);
|
||||
backdrop.makeSolidColor(Std.int(FlxG.width), Std.int(FlxG.height), targetColor);
|
||||
if (backdropData.fadeTime > 0.0)
|
||||
{
|
||||
backdrop.alpha = 0.0;
|
||||
FlxTween.tween(backdrop, {alpha: 1.0}, conversationData.backdrop.data.fadeTime, {ease: FlxEase.linear});
|
||||
FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: FlxEase.linear});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -190,9 +199,9 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
var nextSpeakerId:String = currentDialogueEntryData.speaker;
|
||||
|
||||
// Skip the next steps if the current speaker is already displayed.
|
||||
if (currentSpeaker != null && nextSpeakerId == currentSpeaker.speakerId) return;
|
||||
if (currentSpeaker != null && nextSpeakerId == currentSpeaker.id) return;
|
||||
|
||||
var nextSpeaker:Speaker = SpeakerDataParser.fetchSpeaker(nextSpeakerId);
|
||||
var nextSpeaker:Speaker = SpeakerRegistry.instance.fetchEntry(nextSpeakerId);
|
||||
|
||||
if (currentSpeaker != null)
|
||||
{
|
||||
|
@ -241,7 +250,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
var nextDialogueBoxId:String = currentDialogueEntryData?.box;
|
||||
|
||||
// Skip the next steps if the current speaker is already displayed.
|
||||
if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.dialogueBoxId) return;
|
||||
if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.id) return;
|
||||
|
||||
if (currentDialogueBox != null)
|
||||
{
|
||||
|
@ -250,7 +259,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
currentDialogueBox = null;
|
||||
}
|
||||
|
||||
var nextDialogueBox:DialogueBox = DialogueBoxDataParser.fetchDialogueBox(nextDialogueBoxId);
|
||||
var nextDialogueBox:DialogueBox = DialogueBoxRegistry.instance.fetchEntry(nextDialogueBoxId);
|
||||
|
||||
if (nextDialogueBox == null)
|
||||
{
|
||||
|
@ -378,20 +387,18 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
public function startOutro():Void
|
||||
{
|
||||
switch (conversationData?.outro?.type)
|
||||
switch (_data?.outro)
|
||||
{
|
||||
case FADE:
|
||||
var fadeTime:Float = conversationData?.outro.data.fadeTime ?? 1.0;
|
||||
|
||||
outroTween = FlxTween.tween(this, {alpha: 0.0}, fadeTime,
|
||||
case FADE(outroData):
|
||||
outroTween = FlxTween.tween(this, {alpha: 0.0}, outroData.fadeTime,
|
||||
{
|
||||
type: ONESHOT, // holy shit like the game no way
|
||||
startDelay: 0,
|
||||
onComplete: (_) -> endOutro(),
|
||||
});
|
||||
|
||||
FlxTween.tween(this.music, {volume: 0.0}, fadeTime);
|
||||
case NONE:
|
||||
FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime);
|
||||
case NONE(_):
|
||||
// Immediately clean up.
|
||||
endOutro();
|
||||
default:
|
||||
|
@ -400,7 +407,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
}
|
||||
}
|
||||
|
||||
public var completeCallback:Void->Void;
|
||||
public var completeCallback:() -> Void;
|
||||
|
||||
public function endOutro():Void
|
||||
{
|
||||
|
@ -596,7 +603,12 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
public override function toString():String
|
||||
{
|
||||
return 'Conversation($conversationId)';
|
||||
return 'Conversation($id)';
|
||||
}
|
||||
|
||||
static function _fetchData(id:String):Null<ConversationData>
|
||||
{
|
||||
return ConversationRegistry.instance.parseEntryDataWithMigration(id, ConversationRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,240 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import funkin.util.SerializerUtil;
|
||||
|
||||
/**
|
||||
* Data about a conversation.
|
||||
* Includes what speakers are in the conversation, and what phrases they say.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class ConversationData
|
||||
{
|
||||
public var version:String;
|
||||
public var backdrop:BackdropData;
|
||||
public var outro:OutroData;
|
||||
public var music:MusicData;
|
||||
public var dialogue:Array<DialogueEntryData>;
|
||||
|
||||
public function new(version:String, backdrop:BackdropData, outro:OutroData, music:MusicData, dialogue:Array<DialogueEntryData>)
|
||||
{
|
||||
this.version = version;
|
||||
this.backdrop = backdrop;
|
||||
this.outro = outro;
|
||||
this.music = music;
|
||||
this.dialogue = dialogue;
|
||||
}
|
||||
|
||||
public static function fromString(i:String):ConversationData
|
||||
{
|
||||
if (i == null || i == '') return null;
|
||||
var data:
|
||||
{
|
||||
version:String,
|
||||
backdrop:Dynamic, // TODO: tink.Json doesn't like when these are typed
|
||||
?outro:Dynamic, // TODO: tink.Json doesn't like when these are typed
|
||||
?music:Dynamic, // TODO: tink.Json doesn't like when these are typed
|
||||
dialogue:Array<Dynamic> // TODO: tink.Json doesn't like when these are typed
|
||||
} = tink.Json.parse(i);
|
||||
return fromJson(data);
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):ConversationData
|
||||
{
|
||||
// TODO: Check version and perform migrations if necessary.
|
||||
if (j == null) return null;
|
||||
return new ConversationData(j.version, BackdropData.fromJson(j.backdrop), OutroData.fromJson(j.outro), MusicData.fromJson(j.music),
|
||||
j.dialogue.map(d -> DialogueEntryData.fromJson(d)));
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
version: this.version,
|
||||
backdrop: this.backdrop.toJson(),
|
||||
dialogue: this.dialogue.map(d -> d.toJson())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data about a single dialogue entry.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.DialogueEntryData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class DialogueEntryData
|
||||
{
|
||||
/**
|
||||
* The speaker who says this phrase.
|
||||
*/
|
||||
public var speaker:String;
|
||||
|
||||
/**
|
||||
* The animation the speaker will play.
|
||||
*/
|
||||
public var speakerAnimation:String;
|
||||
|
||||
/**
|
||||
* The text box that will appear.
|
||||
*/
|
||||
public var box:String;
|
||||
|
||||
/**
|
||||
* The animation the dialogue box will play.
|
||||
*/
|
||||
public var boxAnimation:String;
|
||||
|
||||
/**
|
||||
* The lines of text that will appear in the text box.
|
||||
*/
|
||||
public var text:Array<String>;
|
||||
|
||||
/**
|
||||
* The relative speed at which the text will scroll.
|
||||
* @default 1.0
|
||||
*/
|
||||
public var speed:Float = 1.0;
|
||||
|
||||
public function new(speaker:String, speakerAnimation:String, box:String, boxAnimation:String, text:Array<String>, speed:Float = null)
|
||||
{
|
||||
this.speaker = speaker;
|
||||
this.speakerAnimation = speakerAnimation;
|
||||
this.box = box;
|
||||
this.boxAnimation = boxAnimation;
|
||||
this.text = text;
|
||||
if (speed != null) this.speed = speed;
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):DialogueEntryData
|
||||
{
|
||||
if (j == null) return null;
|
||||
return new DialogueEntryData(j.speaker, j.speakerAnimation, j.box, j.boxAnimation, j.text, j.speed);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
var result:Dynamic =
|
||||
{
|
||||
speaker: this.speaker,
|
||||
speakerAnimation: this.speakerAnimation,
|
||||
box: this.box,
|
||||
boxAnimation: this.boxAnimation,
|
||||
text: this.text,
|
||||
};
|
||||
|
||||
if (this.speed != 1.0) result.speed = this.speed;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data about a backdrop.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.BackdropData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class BackdropData
|
||||
{
|
||||
public var type:BackdropType;
|
||||
public var data:Dynamic;
|
||||
|
||||
public function new(typeStr:String, data:Dynamic)
|
||||
{
|
||||
this.type = typeStr;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):BackdropData
|
||||
{
|
||||
if (j == null) return null;
|
||||
return new BackdropData(j.type, j.data);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
type: this.type,
|
||||
data: this.data
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum abstract BackdropType(String) from String to String
|
||||
{
|
||||
public var SOLID:BackdropType = 'solid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Data about a music track.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.MusicData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class MusicData
|
||||
{
|
||||
public var asset:String;
|
||||
|
||||
public var fadeTime:Float;
|
||||
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var looped:Bool;
|
||||
|
||||
public function new(asset:String, looped:Bool, fadeTime:Float = 0.0)
|
||||
{
|
||||
this.asset = asset;
|
||||
this.looped = looped;
|
||||
this.fadeTime = fadeTime;
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):MusicData
|
||||
{
|
||||
if (j == null) return null;
|
||||
return new MusicData(j.asset, j.looped, j.fadeTime);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
asset: this.asset,
|
||||
looped: this.looped,
|
||||
fadeTime: this.fadeTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data about an outro.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.OutroData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class OutroData
|
||||
{
|
||||
public var type:OutroType;
|
||||
public var data:Dynamic;
|
||||
|
||||
public function new(?typeStr:String, data:Dynamic)
|
||||
{
|
||||
this.type = typeStr ?? OutroType.NONE;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):OutroData
|
||||
{
|
||||
if (j == null) return null;
|
||||
return new OutroData(j.type, j.data);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
type: this.type,
|
||||
data: this.data
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum abstract OutroType(String) from String to String
|
||||
{
|
||||
public var NONE:OutroType = 'none';
|
||||
public var FADE:OutroType = 'fade';
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import openfl.Assets;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.play.cutscene.dialogue.ScriptedConversation;
|
||||
|
||||
/**
|
||||
* Contains utilities for loading and parsing conversation data.
|
||||
* TODO: Refactor to use the json2object + BaseRegistry system that actually validates things for you.
|
||||
*/
|
||||
class ConversationDataParser
|
||||
{
|
||||
public static final CONVERSATION_DATA_VERSION:String = '1.0.0';
|
||||
public static final CONVERSATION_DATA_VERSION_RULE:String = '1.0.x';
|
||||
|
||||
static final conversationCache:Map<String, Conversation> = new Map<String, Conversation>();
|
||||
static final conversationScriptedClass:Map<String, String> = new Map<String, String>();
|
||||
|
||||
static final DEFAULT_CONVERSATION_ID:String = 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* Parses and preloads the game's conversation data and scripts when the game starts.
|
||||
*
|
||||
* If you want to force conversations to be reloaded, you can just call this function again.
|
||||
*/
|
||||
public static function loadConversationCache():Void
|
||||
{
|
||||
clearConversationCache();
|
||||
trace('Loading dialogue conversation cache...');
|
||||
|
||||
//
|
||||
// SCRIPTED CONVERSATIONS
|
||||
//
|
||||
var scriptedConversationClassNames:Array<String> = ScriptedConversation.listScriptClasses();
|
||||
trace(' Instantiating ${scriptedConversationClassNames.length} scripted conversations...');
|
||||
for (conversationCls in scriptedConversationClassNames)
|
||||
{
|
||||
var conversation:Conversation = ScriptedConversation.init(conversationCls, DEFAULT_CONVERSATION_ID);
|
||||
if (conversation != null)
|
||||
{
|
||||
trace(' Loaded scripted conversation: ${conversationCls}');
|
||||
// Disable the rendering logic for conversation until it's loaded.
|
||||
// Note that kill() =/= destroy()
|
||||
conversation.kill();
|
||||
|
||||
// Then store it.
|
||||
conversationCache.set(conversation.conversationId, conversation);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to instantiate scripted conversation class: ${conversationCls}');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UNSCRIPTED CONVERSATIONS
|
||||
//
|
||||
// Scripts refers to code here, not the actual dialogue.
|
||||
var conversationIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/conversations/');
|
||||
// Filter out conversations that are scripted.
|
||||
var unscriptedConversationIds:Array<String> = conversationIdList.filter(function(conversationId:String):Bool {
|
||||
return !conversationCache.exists(conversationId);
|
||||
});
|
||||
trace(' Fetching data for ${unscriptedConversationIds.length} conversations...');
|
||||
for (conversationId in unscriptedConversationIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var conversation:Conversation = new Conversation(conversationId);
|
||||
// Say something offensive to kill the conversation.
|
||||
// We will revive it later.
|
||||
conversation.kill();
|
||||
if (conversation != null)
|
||||
{
|
||||
trace(' Loaded conversation data: ${conversation.conversationId}');
|
||||
conversationCache.set(conversation.conversationId, conversation);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data for a conversation and returns a Conversation instance,
|
||||
* ready to be displayed.
|
||||
* @param conversationId The ID of the conversation to fetch.
|
||||
* @return The conversation instance, or null if the conversation was not found.
|
||||
*/
|
||||
public static function fetchConversation(conversationId:String):Null<Conversation>
|
||||
{
|
||||
if (conversationId != null && conversationId != '' && conversationCache.exists(conversationId))
|
||||
{
|
||||
trace('Successfully fetched conversation: ${conversationId}');
|
||||
var conversation:Conversation = conversationCache.get(conversationId);
|
||||
// ...ANYway...
|
||||
conversation.revive();
|
||||
return conversation;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Failed to fetch conversation, not found in cache: ${conversationId}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function clearConversationCache():Void
|
||||
{
|
||||
if (conversationCache != null)
|
||||
{
|
||||
for (conversation in conversationCache)
|
||||
{
|
||||
conversation.destroy();
|
||||
}
|
||||
conversationCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static function listConversationIds():Array<String>
|
||||
{
|
||||
return conversationCache.keys().array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a conversation's JSON file, parse its data, and return it.
|
||||
*
|
||||
* @param conversationId The conversation to load.
|
||||
* @return The conversation data, or null if validation failed.
|
||||
*/
|
||||
public static function parseConversationData(conversationId:String):Null<ConversationData>
|
||||
{
|
||||
trace('Parsing conversation data: ${conversationId}');
|
||||
var rawJson:String = loadConversationFile(conversationId);
|
||||
|
||||
try
|
||||
{
|
||||
var conversationData:ConversationData = ConversationData.fromString(rawJson);
|
||||
return conversationData;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace('Failed to parse conversation ($conversationId).');
|
||||
trace(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function loadConversationFile(conversationPath:String):String
|
||||
{
|
||||
var conversationFilePath:String = Paths.json('dialogue/conversations/${conversationPath}');
|
||||
var rawJson:String = Assets.getText(conversationFilePath).trim();
|
||||
|
||||
while (!rawJson.endsWith('}') && rawJson.length > 0)
|
||||
{
|
||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
||||
}
|
||||
|
||||
return rawJson;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import funkin.data.IRegistryEntry;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import flixel.text.FlxText;
|
||||
|
@ -9,18 +10,21 @@ import funkin.util.assets.FlxAnimationUtil;
|
|||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.data.dialogue.DialogueBoxData;
|
||||
import funkin.data.dialogue.DialogueBoxRegistry;
|
||||
|
||||
class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
||||
class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry<DialogueBoxData>
|
||||
{
|
||||
public final dialogueBoxId:String;
|
||||
public final id:String;
|
||||
|
||||
public var dialogueBoxName(get, never):String;
|
||||
|
||||
function get_dialogueBoxName():String
|
||||
{
|
||||
return boxData?.name ?? 'UNKNOWN';
|
||||
return _data.name ?? 'UNKNOWN';
|
||||
}
|
||||
|
||||
var boxData:DialogueBoxData;
|
||||
public final _data:DialogueBoxData;
|
||||
|
||||
/**
|
||||
* Offset the speaker's sprite by this much when playing each animation.
|
||||
|
@ -88,13 +92,16 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
return this.speed;
|
||||
}
|
||||
|
||||
public function new(dialogueBoxId:String)
|
||||
public function new(id:String)
|
||||
{
|
||||
super();
|
||||
this.dialogueBoxId = dialogueBoxId;
|
||||
this.boxData = DialogueBoxDataParser.parseDialogueBoxData(this.dialogueBoxId);
|
||||
this.id = id;
|
||||
this._data = _fetchData(id);
|
||||
|
||||
if (boxData == null) throw 'Could not load dialogue box data for box ID "$dialogueBoxId"';
|
||||
if (_data == null)
|
||||
{
|
||||
throw 'Could not parse dialogue box data for id: $id';
|
||||
}
|
||||
}
|
||||
|
||||
public function onCreate(event:ScriptEvent):Void
|
||||
|
@ -115,18 +122,18 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function loadSpritesheet():Void
|
||||
{
|
||||
trace('[DIALOGUE BOX] Loading spritesheet ${boxData.assetPath} for ${dialogueBoxId}');
|
||||
trace('[DIALOGUE BOX] Loading spritesheet ${_data.assetPath} for ${id}');
|
||||
|
||||
var tex:FlxFramesCollection = Paths.getSparrowAtlas(boxData.assetPath);
|
||||
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
|
||||
if (tex == null)
|
||||
{
|
||||
trace('Could not load Sparrow sprite: ${boxData.assetPath}');
|
||||
trace('Could not load Sparrow sprite: ${_data.assetPath}');
|
||||
return;
|
||||
}
|
||||
|
||||
this.boxSprite.frames = tex;
|
||||
|
||||
if (boxData.isPixel)
|
||||
if (_data.isPixel)
|
||||
{
|
||||
this.boxSprite.antialiasing = false;
|
||||
}
|
||||
|
@ -135,9 +142,10 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
this.boxSprite.antialiasing = true;
|
||||
}
|
||||
|
||||
this.flipX = boxData.flipX;
|
||||
this.globalOffsets = boxData.offsets;
|
||||
this.setScale(boxData.scale);
|
||||
this.flipX = _data.flipX;
|
||||
this.flipY = _data.flipY;
|
||||
this.globalOffsets = _data.offsets;
|
||||
this.setScale(_data.scale);
|
||||
}
|
||||
|
||||
public function setText(newText:String):Void
|
||||
|
@ -184,11 +192,11 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
|
||||
function loadAnimations():Void
|
||||
{
|
||||
trace('[DIALOGUE BOX] Loading ${boxData.animations.length} animations for ${dialogueBoxId}');
|
||||
trace('[DIALOGUE BOX] Loading ${_data.animations.length} animations for ${id}');
|
||||
|
||||
FlxAnimationUtil.addAtlasAnimations(this.boxSprite, boxData.animations);
|
||||
FlxAnimationUtil.addAtlasAnimations(this.boxSprite, _data.animations);
|
||||
|
||||
for (anim in boxData.animations)
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
if (anim.offsets == null)
|
||||
{
|
||||
|
@ -201,7 +209,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
}
|
||||
|
||||
var animNames:Array<String> = this.boxSprite?.animation?.getNameList() ?? [];
|
||||
trace('[DIALOGUE BOX] Successfully loaded ${animNames.length} animations for ${dialogueBoxId}');
|
||||
trace('[DIALOGUE BOX] Successfully loaded ${animNames.length} animations for ${id}');
|
||||
|
||||
boxSprite.animation.callback = this.onAnimationFrame;
|
||||
boxSprite.animation.finishCallback = this.onAnimationFinished;
|
||||
|
@ -234,16 +242,16 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
function loadText():Void
|
||||
{
|
||||
textDisplay = new FlxTypeText(0, 0, 300, '', 32);
|
||||
textDisplay.fieldWidth = boxData.text.width;
|
||||
textDisplay.setFormat('Pixel Arial 11 Bold', boxData.text.size, FlxColor.fromString(boxData.text.color), LEFT, SHADOW,
|
||||
FlxColor.fromString(boxData.text.shadowColor ?? '#00000000'), false);
|
||||
textDisplay.borderSize = boxData.text.shadowWidth ?? 2;
|
||||
textDisplay.fieldWidth = _data.text.width;
|
||||
textDisplay.setFormat(_data.text.fontFamily, _data.text.size, FlxColor.fromString(_data.text.color), LEFT, SHADOW,
|
||||
FlxColor.fromString(_data.text.shadowColor ?? '#00000000'), false);
|
||||
textDisplay.borderSize = _data.text.shadowWidth ?? 2;
|
||||
textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)];
|
||||
|
||||
textDisplay.completeCallback = onTypingComplete;
|
||||
|
||||
textDisplay.x += boxData.text.offsets[0];
|
||||
textDisplay.y += boxData.text.offsets[1];
|
||||
textDisplay.x += _data.text.offsets[0];
|
||||
textDisplay.y += _data.text.offsets[1];
|
||||
|
||||
add(textDisplay);
|
||||
}
|
||||
|
@ -374,4 +382,14 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
|||
}
|
||||
|
||||
public function onScriptEvent(event:ScriptEvent):Void {}
|
||||
|
||||
public override function toString():String
|
||||
{
|
||||
return 'DialogueBox($id)';
|
||||
}
|
||||
|
||||
static function _fetchData(id:String):Null<DialogueBoxData>
|
||||
{
|
||||
return DialogueBoxRegistry.instance.parseEntryDataWithMigration(id, DialogueBoxRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
import funkin.util.SerializerUtil;
|
||||
|
||||
/**
|
||||
* Data about a text box.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.DialogueBoxData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class DialogueBoxData
|
||||
{
|
||||
public var version:String;
|
||||
public var name:String;
|
||||
public var assetPath:String;
|
||||
public var flipX:Bool;
|
||||
public var flipY:Bool;
|
||||
public var isPixel:Bool;
|
||||
public var offsets:Array<Float>;
|
||||
public var text:DialogueBoxTextData;
|
||||
public var scale:Float;
|
||||
public var animations:Array<AnimationData>;
|
||||
|
||||
public function new(version:String, name:String, assetPath:String, flipX:Bool = false, flipY:Bool = false, isPixel:Bool = false, offsets:Null<Array<Float>>,
|
||||
text:DialogueBoxTextData, scale:Float = 1.0, animations:Array<AnimationData>)
|
||||
{
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.assetPath = assetPath;
|
||||
this.flipX = flipX;
|
||||
this.flipY = flipY;
|
||||
this.isPixel = isPixel;
|
||||
this.offsets = offsets ?? [0, 0];
|
||||
this.text = text;
|
||||
this.scale = scale;
|
||||
this.animations = animations;
|
||||
}
|
||||
|
||||
public static function fromString(i:String):DialogueBoxData
|
||||
{
|
||||
if (i == null || i == '') return null;
|
||||
var data:
|
||||
{
|
||||
version:String,
|
||||
name:String,
|
||||
assetPath:String,
|
||||
flipX:Bool,
|
||||
flipY:Bool,
|
||||
isPixel:Bool,
|
||||
?offsets:Array<Float>,
|
||||
text:Dynamic,
|
||||
scale:Float,
|
||||
animations:Array<AnimationData>
|
||||
} = tink.Json.parse(i);
|
||||
return fromJson(data);
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):DialogueBoxData
|
||||
{
|
||||
// TODO: Check version and perform migrations if necessary.
|
||||
if (j == null) return null;
|
||||
return new DialogueBoxData(j.version, j.name, j.assetPath, j.flipX, j.flipY, j.isPixel, j.offsets, DialogueBoxTextData.fromJson(j.text), j.scale,
|
||||
j.animations);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
version: this.version,
|
||||
name: this.name,
|
||||
assetPath: this.assetPath,
|
||||
flipX: this.flipX,
|
||||
flipY: this.flipY,
|
||||
isPixel: this.isPixel,
|
||||
offsets: this.offsets,
|
||||
scale: this.scale,
|
||||
animations: this.animations
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data about text in a text box.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.DialogueBoxTextData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class DialogueBoxTextData
|
||||
{
|
||||
public var offsets:Array<Float>;
|
||||
public var width:Int;
|
||||
public var size:Int;
|
||||
public var color:String;
|
||||
public var shadowColor:Null<String>;
|
||||
public var shadowWidth:Null<Int>;
|
||||
|
||||
public function new(offsets:Null<Array<Float>>, ?width:Int, ?size:Int, color:String, ?shadowColor:String, shadowWidth:Null<Int>)
|
||||
{
|
||||
this.offsets = offsets ?? [0, 0];
|
||||
this.width = width ?? 300;
|
||||
this.size = size ?? 32;
|
||||
this.color = color;
|
||||
this.shadowColor = shadowColor;
|
||||
this.shadowWidth = shadowWidth;
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):DialogueBoxTextData
|
||||
{
|
||||
// TODO: Check version and perform migrations if necessary.
|
||||
if (j == null) return null;
|
||||
return new DialogueBoxTextData(j.offsets, j.width, j.size, j.color, j.shadowColor, j.shadowWidth);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
return {
|
||||
offsets: this.offsets,
|
||||
width: this.width,
|
||||
size: this.size,
|
||||
color: this.color,
|
||||
shadowColor: this.shadowColor,
|
||||
shadowWidth: this.shadowWidth,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import openfl.Assets;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.play.cutscene.dialogue.DialogueBox;
|
||||
import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
|
||||
|
||||
/**
|
||||
* Contains utilities for loading and parsing dialogueBox data.
|
||||
*/
|
||||
class DialogueBoxDataParser
|
||||
{
|
||||
public static final DIALOGUE_BOX_DATA_VERSION:String = '1.0.0';
|
||||
public static final DIALOGUE_BOX_DATA_VERSION_RULE:String = '1.0.x';
|
||||
|
||||
static final dialogueBoxCache:Map<String, DialogueBox> = new Map<String, DialogueBox>();
|
||||
|
||||
static final dialogueBoxScriptedClass:Map<String, String> = new Map<String, String>();
|
||||
|
||||
static final DEFAULT_DIALOGUE_BOX_ID:String = 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* Parses and preloads the game's dialogueBox data and scripts when the game starts.
|
||||
*
|
||||
* If you want to force dialogue boxes to be reloaded, you can just call this function again.
|
||||
*/
|
||||
public static function loadDialogueBoxCache():Void
|
||||
{
|
||||
clearDialogueBoxCache();
|
||||
trace('Loading dialogue box cache...');
|
||||
|
||||
//
|
||||
// SCRIPTED CONVERSATIONS
|
||||
//
|
||||
var scriptedDialogueBoxClassNames:Array<String> = ScriptedDialogueBox.listScriptClasses();
|
||||
trace(' Instantiating ${scriptedDialogueBoxClassNames.length} scripted dialogue boxes...');
|
||||
for (dialogueBoxCls in scriptedDialogueBoxClassNames)
|
||||
{
|
||||
var dialogueBox:DialogueBox = ScriptedDialogueBox.init(dialogueBoxCls, DEFAULT_DIALOGUE_BOX_ID);
|
||||
if (dialogueBox != null)
|
||||
{
|
||||
trace(' Loaded scripted dialogue box: ${dialogueBox.dialogueBoxName}');
|
||||
// Disable the rendering logic for dialogueBox until it's loaded.
|
||||
// Note that kill() =/= destroy()
|
||||
dialogueBox.kill();
|
||||
|
||||
// Then store it.
|
||||
dialogueBoxCache.set(dialogueBox.dialogueBoxId, dialogueBox);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to instantiate scripted dialogueBox class: ${dialogueBoxCls}');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UNSCRIPTED CONVERSATIONS
|
||||
//
|
||||
// Scripts refers to code here, not the actual dialogue.
|
||||
var dialogueBoxIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/boxes/');
|
||||
// Filter out dialogue boxes that are scripted.
|
||||
var unscriptedDialogueBoxIds:Array<String> = dialogueBoxIdList.filter(function(dialogueBoxId:String):Bool {
|
||||
return !dialogueBoxCache.exists(dialogueBoxId);
|
||||
});
|
||||
trace(' Fetching data for ${unscriptedDialogueBoxIds.length} dialogue boxes...');
|
||||
for (dialogueBoxId in unscriptedDialogueBoxIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dialogueBox:DialogueBox = new DialogueBox(dialogueBoxId);
|
||||
if (dialogueBox != null)
|
||||
{
|
||||
trace(' Loaded dialogueBox data: ${dialogueBox.dialogueBoxName}');
|
||||
dialogueBoxCache.set(dialogueBox.dialogueBoxId, dialogueBox);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data for a dialogueBox and returns a DialogueBox instance,
|
||||
* ready to be displayed.
|
||||
* @param dialogueBoxId The ID of the dialogueBox to fetch.
|
||||
* @return The dialogueBox instance, or null if the dialogueBox was not found.
|
||||
*/
|
||||
public static function fetchDialogueBox(dialogueBoxId:String):Null<DialogueBox>
|
||||
{
|
||||
if (dialogueBoxId != null && dialogueBoxId != '' && dialogueBoxCache.exists(dialogueBoxId))
|
||||
{
|
||||
trace('Successfully fetched dialogueBox: ${dialogueBoxId}');
|
||||
var dialogueBox:DialogueBox = dialogueBoxCache.get(dialogueBoxId);
|
||||
dialogueBox.revive();
|
||||
return dialogueBox;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Failed to fetch dialogueBox, not found in cache: ${dialogueBoxId}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function clearDialogueBoxCache():Void
|
||||
{
|
||||
if (dialogueBoxCache != null)
|
||||
{
|
||||
for (dialogueBox in dialogueBoxCache)
|
||||
{
|
||||
dialogueBox.destroy();
|
||||
}
|
||||
dialogueBoxCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static function listDialogueBoxIds():Array<String>
|
||||
{
|
||||
return dialogueBoxCache.keys().array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a dialogueBox's JSON file, parse its data, and return it.
|
||||
*
|
||||
* @param dialogueBoxId The dialogueBox to load.
|
||||
* @return The dialogueBox data, or null if validation failed.
|
||||
*/
|
||||
public static function parseDialogueBoxData(dialogueBoxId:String):Null<DialogueBoxData>
|
||||
{
|
||||
var rawJson:String = loadDialogueBoxFile(dialogueBoxId);
|
||||
|
||||
try
|
||||
{
|
||||
var dialogueBoxData:DialogueBoxData = DialogueBoxData.fromString(rawJson);
|
||||
return dialogueBoxData;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace('Failed to parse dialogueBox ($dialogueBoxId).');
|
||||
trace(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function loadDialogueBoxFile(dialogueBoxPath:String):String
|
||||
{
|
||||
var dialogueBoxFilePath:String = Paths.json('dialogue/boxes/${dialogueBoxPath}');
|
||||
var rawJson:String = Assets.getText(dialogueBoxFilePath).trim();
|
||||
|
||||
while (!rawJson.endsWith('}') && rawJson.length > 0)
|
||||
{
|
||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
||||
}
|
||||
|
||||
return rawJson;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,10 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
/**
|
||||
* A script that can be tied to a Conversation.
|
||||
* Create a scripted class that extends Conversation to use this.
|
||||
* This allows you to customize how a specific conversation appears and behaves.
|
||||
* Someone clever could use this to add branching dialogue I think.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedConversation extends Conversation implements polymod.hscript.HScriptedClass {}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
/**
|
||||
* A script that can be tied to a DialogueBox.
|
||||
* Create a scripted class that extends DialogueBox to use this.
|
||||
* This allows you to customize how a specific dialogue box appears.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedDialogueBox extends DialogueBox implements polymod.hscript.HScriptedClass {}
|
||||
|
|
|
@ -1,27 +1,30 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import funkin.data.IRegistryEntry;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
||||
import funkin.data.dialogue.SpeakerData;
|
||||
import funkin.data.dialogue.SpeakerRegistry;
|
||||
|
||||
/**
|
||||
* The character sprite which displays during dialogue.
|
||||
*
|
||||
* Most conversations have two speakers, with one being flipped.
|
||||
*/
|
||||
class Speaker extends FlxSprite implements IDialogueScriptedClass
|
||||
class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRegistryEntry<SpeakerData>
|
||||
{
|
||||
/**
|
||||
* The internal ID for this speaker.
|
||||
*/
|
||||
public final speakerId:String;
|
||||
public final id:String;
|
||||
|
||||
/**
|
||||
* The full data for a speaker.
|
||||
*/
|
||||
var speakerData:SpeakerData;
|
||||
public final _data:SpeakerData;
|
||||
|
||||
/**
|
||||
* A readable name for this speaker.
|
||||
|
@ -30,7 +33,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
|
||||
function get_speakerName():String
|
||||
{
|
||||
return speakerData.name;
|
||||
return _data.name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,14 +78,17 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
return globalOffsets = value;
|
||||
}
|
||||
|
||||
public function new(speakerId:String)
|
||||
public function new(id:String)
|
||||
{
|
||||
super();
|
||||
|
||||
this.speakerId = speakerId;
|
||||
this.speakerData = SpeakerDataParser.parseSpeakerData(this.speakerId);
|
||||
this.id = id;
|
||||
this._data = _fetchData(id);
|
||||
|
||||
if (speakerData == null) throw 'Could not load speaker data for speaker ID "$speakerId"';
|
||||
if (_data == null)
|
||||
{
|
||||
throw 'Could not parse speaker data for id: $id';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,18 +108,18 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
|
||||
function loadSpritesheet():Void
|
||||
{
|
||||
trace('[SPEAKER] Loading spritesheet ${speakerData.assetPath} for ${speakerId}');
|
||||
trace('[SPEAKER] Loading spritesheet ${_data.assetPath} for ${id}');
|
||||
|
||||
var tex:FlxFramesCollection = Paths.getSparrowAtlas(speakerData.assetPath);
|
||||
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
|
||||
if (tex == null)
|
||||
{
|
||||
trace('Could not load Sparrow sprite: ${speakerData.assetPath}');
|
||||
trace('Could not load Sparrow sprite: ${_data.assetPath}');
|
||||
return;
|
||||
}
|
||||
|
||||
this.frames = tex;
|
||||
|
||||
if (speakerData.isPixel)
|
||||
if (_data.isPixel)
|
||||
{
|
||||
this.antialiasing = false;
|
||||
}
|
||||
|
@ -122,9 +128,10 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
this.antialiasing = true;
|
||||
}
|
||||
|
||||
this.flipX = speakerData.flipX;
|
||||
this.globalOffsets = speakerData.offsets;
|
||||
this.setScale(speakerData.scale);
|
||||
this.flipX = _data.flipX;
|
||||
this.flipY = _data.flipY;
|
||||
this.globalOffsets = _data.offsets;
|
||||
this.setScale(_data.scale);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -141,11 +148,11 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
|
||||
function loadAnimations():Void
|
||||
{
|
||||
trace('[SPEAKER] Loading ${speakerData.animations.length} animations for ${speakerId}');
|
||||
trace('[SPEAKER] Loading ${_data.animations.length} animations for ${id}');
|
||||
|
||||
FlxAnimationUtil.addAtlasAnimations(this, speakerData.animations);
|
||||
FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
|
||||
|
||||
for (anim in speakerData.animations)
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
if (anim.offsets == null)
|
||||
{
|
||||
|
@ -158,7 +165,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
}
|
||||
|
||||
var animNames:Array<String> = this.animation.getNameList();
|
||||
trace('[SPEAKER] Successfully loaded ${animNames.length} animations for ${speakerId}');
|
||||
trace('[SPEAKER] Successfully loaded ${animNames.length} animations for ${id}');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -271,4 +278,14 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
|
|||
}
|
||||
|
||||
public function onScriptEvent(event:ScriptEvent):Void {}
|
||||
|
||||
public override function toString():String
|
||||
{
|
||||
return 'Speaker($id)';
|
||||
}
|
||||
|
||||
static function _fetchData(id:String):Null<SpeakerData>
|
||||
{
|
||||
return SpeakerRegistry.instance.parseEntryDataWithMigration(id, SpeakerRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
|
||||
/**
|
||||
* Data about a conversation.
|
||||
* Includes what speakers are in the conversation, and what phrases they say.
|
||||
*/
|
||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.SpeakerData.fromJson(j))
|
||||
@:jsonStringify(v -> v.toJson())
|
||||
class SpeakerData
|
||||
{
|
||||
public var version:String;
|
||||
public var name:String;
|
||||
public var assetPath:String;
|
||||
public var flipX:Bool;
|
||||
public var isPixel:Bool;
|
||||
public var offsets:Array<Float>;
|
||||
public var scale:Float;
|
||||
public var animations:Array<AnimationData>;
|
||||
|
||||
public function new(version:String, name:String, assetPath:String, animations:Array<AnimationData>, ?offsets:Array<Float>, flipX:Bool = false,
|
||||
isPixel:Bool = false, ?scale:Float = 1.0)
|
||||
{
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.assetPath = assetPath;
|
||||
this.animations = animations;
|
||||
|
||||
this.offsets = offsets;
|
||||
if (this.offsets == null || this.offsets == []) this.offsets = [0, 0];
|
||||
|
||||
this.flipX = flipX;
|
||||
this.isPixel = isPixel;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public static function fromString(i:String):SpeakerData
|
||||
{
|
||||
if (i == null || i == '') return null;
|
||||
var data:
|
||||
{
|
||||
version:String,
|
||||
name:String,
|
||||
assetPath:String,
|
||||
animations:Array<AnimationData>,
|
||||
?offsets:Array<Float>,
|
||||
?flipX:Bool,
|
||||
?isPixel:Bool,
|
||||
?scale:Float
|
||||
} = tink.Json.parse(i);
|
||||
return fromJson(data);
|
||||
}
|
||||
|
||||
public static function fromJson(j:Dynamic):SpeakerData
|
||||
{
|
||||
// TODO: Check version and perform migrations if necessary.
|
||||
if (j == null) return null;
|
||||
return new SpeakerData(j.version, j.name, j.assetPath, j.animations, j.offsets, j.flipX, j.isPixel, j.scale);
|
||||
}
|
||||
|
||||
public function toJson():Dynamic
|
||||
{
|
||||
var result:Dynamic =
|
||||
{
|
||||
version: this.version,
|
||||
name: this.name,
|
||||
assetPath: this.assetPath,
|
||||
animations: this.animations,
|
||||
flipX: this.flipX,
|
||||
isPixel: this.isPixel
|
||||
};
|
||||
|
||||
if (this.scale != 1.0) result.scale = this.scale;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import openfl.Assets;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import funkin.play.cutscene.dialogue.Speaker;
|
||||
import funkin.play.cutscene.dialogue.ScriptedSpeaker;
|
||||
|
||||
/**
|
||||
* Contains utilities for loading and parsing speaker data.
|
||||
*/
|
||||
class SpeakerDataParser
|
||||
{
|
||||
public static final SPEAKER_DATA_VERSION:String = '1.0.0';
|
||||
public static final SPEAKER_DATA_VERSION_RULE:String = '1.0.x';
|
||||
|
||||
static final speakerCache:Map<String, Speaker> = new Map<String, Speaker>();
|
||||
|
||||
static final speakerScriptedClass:Map<String, String> = new Map<String, String>();
|
||||
|
||||
static final DEFAULT_SPEAKER_ID:String = 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* Parses and preloads the game's speaker data and scripts when the game starts.
|
||||
*
|
||||
* If you want to force speakers to be reloaded, you can just call this function again.
|
||||
*/
|
||||
public static function loadSpeakerCache():Void
|
||||
{
|
||||
clearSpeakerCache();
|
||||
trace('Loading dialogue speaker cache...');
|
||||
|
||||
//
|
||||
// SCRIPTED CONVERSATIONS
|
||||
//
|
||||
var scriptedSpeakerClassNames:Array<String> = ScriptedSpeaker.listScriptClasses();
|
||||
trace(' Instantiating ${scriptedSpeakerClassNames.length} scripted speakers...');
|
||||
for (speakerCls in scriptedSpeakerClassNames)
|
||||
{
|
||||
var speaker:Speaker = ScriptedSpeaker.init(speakerCls, DEFAULT_SPEAKER_ID);
|
||||
if (speaker != null)
|
||||
{
|
||||
trace(' Loaded scripted speaker: ${speaker.speakerName}');
|
||||
// Disable the rendering logic for speaker until it's loaded.
|
||||
// Note that kill() =/= destroy()
|
||||
speaker.kill();
|
||||
|
||||
// Then store it.
|
||||
speakerCache.set(speaker.speakerId, speaker);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to instantiate scripted speaker class: ${speakerCls}');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UNSCRIPTED CONVERSATIONS
|
||||
//
|
||||
// Scripts refers to code here, not the actual dialogue.
|
||||
var speakerIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/speakers/');
|
||||
// Filter out speakers that are scripted.
|
||||
var unscriptedSpeakerIds:Array<String> = speakerIdList.filter(function(speakerId:String):Bool {
|
||||
return !speakerCache.exists(speakerId);
|
||||
});
|
||||
trace(' Fetching data for ${unscriptedSpeakerIds.length} speakers...');
|
||||
for (speakerId in unscriptedSpeakerIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var speaker:Speaker = new Speaker(speakerId);
|
||||
if (speaker != null)
|
||||
{
|
||||
trace(' Loaded speaker data: ${speaker.speakerName}');
|
||||
speakerCache.set(speaker.speakerId, speaker);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data for a speaker and returns a Speaker instance,
|
||||
* ready to be displayed.
|
||||
* @param speakerId The ID of the speaker to fetch.
|
||||
* @return The speaker instance, or null if the speaker was not found.
|
||||
*/
|
||||
public static function fetchSpeaker(speakerId:String):Null<Speaker>
|
||||
{
|
||||
if (speakerId != null && speakerId != '' && speakerCache.exists(speakerId))
|
||||
{
|
||||
trace('Successfully fetched speaker: ${speakerId}');
|
||||
var speaker:Speaker = speakerCache.get(speakerId);
|
||||
speaker.revive();
|
||||
return speaker;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Failed to fetch speaker, not found in cache: ${speakerId}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function clearSpeakerCache():Void
|
||||
{
|
||||
if (speakerCache != null)
|
||||
{
|
||||
for (speaker in speakerCache)
|
||||
{
|
||||
speaker.destroy();
|
||||
}
|
||||
speakerCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static function listSpeakerIds():Array<String>
|
||||
{
|
||||
return speakerCache.keys().array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a speaker's JSON file, parse its data, and return it.
|
||||
*
|
||||
* @param speakerId The speaker to load.
|
||||
* @return The speaker data, or null if validation failed.
|
||||
*/
|
||||
public static function parseSpeakerData(speakerId:String):Null<SpeakerData>
|
||||
{
|
||||
var rawJson:String = loadSpeakerFile(speakerId);
|
||||
|
||||
try
|
||||
{
|
||||
var speakerData:SpeakerData = SpeakerData.fromString(rawJson);
|
||||
return speakerData;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace('Failed to parse speaker ($speakerId).');
|
||||
trace(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function loadSpeakerFile(speakerPath:String):String
|
||||
{
|
||||
var speakerFilePath:String = Paths.json('dialogue/speakers/${speakerPath}');
|
||||
var rawJson:String = Assets.getText(speakerFilePath).trim();
|
||||
|
||||
while (!rawJson.endsWith('}') && rawJson.length > 0)
|
||||
{
|
||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
||||
}
|
||||
|
||||
return rawJson;
|
||||
}
|
||||
}
|
|
@ -135,10 +135,10 @@ class FocusCameraSongEvent extends SongEvent
|
|||
return new SongEventSchema([
|
||||
{
|
||||
name: "char",
|
||||
title: "Character",
|
||||
title: "Target",
|
||||
defaultValue: 0,
|
||||
type: SongEventFieldType.ENUM,
|
||||
keys: ["Position" => -1, "Boyfriend" => 0, "Dad" => 1, "Girlfriend" => 2]
|
||||
keys: ["Position" => -1, "Player" => 0, "Opponent" => 1, "Girlfriend" => 2]
|
||||
},
|
||||
{
|
||||
name: "x",
|
||||
|
@ -146,6 +146,7 @@ class FocusCameraSongEvent extends SongEvent
|
|||
defaultValue: 0,
|
||||
step: 10.0,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: "px"
|
||||
},
|
||||
{
|
||||
name: "y",
|
||||
|
@ -153,6 +154,7 @@ class FocusCameraSongEvent extends SongEvent
|
|||
defaultValue: 0,
|
||||
step: 10.0,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: "px"
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -78,14 +78,16 @@ class SetCameraBopSongEvent extends SongEvent
|
|||
title: 'Intensity',
|
||||
defaultValue: 1.0,
|
||||
step: 0.1,
|
||||
type: SongEventFieldType.FLOAT
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: 'x'
|
||||
},
|
||||
{
|
||||
name: 'rate',
|
||||
title: 'Rate (beats/zoom)',
|
||||
title: 'Rate',
|
||||
defaultValue: 4,
|
||||
step: 1,
|
||||
type: SongEventFieldType.INTEGER,
|
||||
units: 'beats/zoom'
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -106,14 +106,16 @@ class ZoomCameraSongEvent extends SongEvent
|
|||
title: 'Zoom Level',
|
||||
defaultValue: 1.0,
|
||||
step: 0.1,
|
||||
type: SongEventFieldType.FLOAT
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: 'x'
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
title: 'Duration (in steps)',
|
||||
title: 'Duration',
|
||||
defaultValue: 4.0,
|
||||
step: 0.5,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: 'steps'
|
||||
},
|
||||
{
|
||||
name: 'ease',
|
||||
|
|
191
source/funkin/ui/debug/WaveformTestState.hx
Normal file
191
source/funkin/ui/debug/WaveformTestState.hx
Normal file
|
@ -0,0 +1,191 @@
|
|||
package funkin.ui.debug;
|
||||
|
||||
import flixel.math.FlxRect;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
import funkin.audio.waveform.WaveformSprite;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
import funkin.graphics.rendering.MeshRender;
|
||||
|
||||
class WaveformTestState extends MusicBeatState
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
var waveformData:WaveformData;
|
||||
var waveformData2:WaveformData;
|
||||
|
||||
var waveformAudio:FunkinSound;
|
||||
|
||||
// var waveformSprite:WaveformSprite;
|
||||
// var waveformSprite2:WaveformSprite;
|
||||
var timeMarker:FlxSprite;
|
||||
|
||||
var polygonSprite:MeshRender;
|
||||
var vertexCount:Int = 3;
|
||||
|
||||
public override function create():Void
|
||||
{
|
||||
super.create();
|
||||
|
||||
var testSprite = new FlxSprite(0, 0);
|
||||
testSprite.loadGraphic(Paths.image('funkay'));
|
||||
testSprite.updateHitbox();
|
||||
testSprite.clipRect = new FlxRect(0, 0, FlxG.width, FlxG.height);
|
||||
// add(testSprite);
|
||||
|
||||
waveformAudio = FunkinSound.load(Paths.inst('bopeebo', '-erect'));
|
||||
|
||||
waveformData = WaveformDataParser.interpretFlxSound(waveformAudio);
|
||||
|
||||
polygonSprite = new MeshRender(FlxG.width / 2, FlxG.height / 2, FlxColor.WHITE);
|
||||
|
||||
setPolygonVertices(vertexCount);
|
||||
add(polygonSprite);
|
||||
|
||||
// waveformSprite = WaveformSprite.buildFromWaveformData(waveformData, HORIZONTAL, FlxColor.fromString("#ADD8E6"));
|
||||
// waveformSprite.duration = 5.0 * 160;
|
||||
// waveformSprite.width = FlxG.width * 160;
|
||||
// waveformSprite.height = FlxG.height; // / 2;
|
||||
// waveformSprite.amplitude = 2.0;
|
||||
// waveformSprite.minWaveformSize = 25;
|
||||
// waveformSprite.clipRect = new FlxRect(0, 0, FlxG.width, FlxG.height);
|
||||
// add(waveformSprite);
|
||||
//
|
||||
// waveformSprite2 = WaveformSprite.buildFromWaveformData(waveformData2, HORIZONTAL, FlxColor.fromString("#FF0000"), 5.0);
|
||||
// waveformSprite2.width = FlxG.width;
|
||||
// waveformSprite2.height = FlxG.height / 2;
|
||||
// waveformSprite2.y = FlxG.height / 2;
|
||||
// add(waveformSprite2);
|
||||
|
||||
timeMarker = new FlxSprite(0, FlxG.height * 1 / 6);
|
||||
timeMarker.makeGraphic(1, Std.int(FlxG.height * 2 / 3), FlxColor.RED);
|
||||
add(timeMarker);
|
||||
|
||||
// drawWaveform(time, duration);
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
if (FlxG.keys.justPressed.SPACE)
|
||||
{
|
||||
if (waveformAudio.isPlaying)
|
||||
{
|
||||
waveformAudio.stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
waveformAudio.play();
|
||||
}
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.ENTER)
|
||||
{
|
||||
// if (waveformSprite.orientation == HORIZONTAL)
|
||||
// {
|
||||
// // waveformSprite.orientation = VERTICAL;
|
||||
// // waveformSprite2.orientation = VERTICAL;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // waveformSprite.orientation = HORIZONTAL;
|
||||
// // waveformSprite2.orientation = HORIZONTAL;
|
||||
// }
|
||||
}
|
||||
|
||||
if (waveformAudio.isPlaying)
|
||||
{
|
||||
// waveformSprite takes a time in fractional seconds, not milliseconds.
|
||||
var timeSeconds = waveformAudio.time / 1000;
|
||||
// waveformSprite.time = timeSeconds;
|
||||
// waveformSprite2.time = timeSeconds;
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.UP)
|
||||
{
|
||||
vertexCount += 1;
|
||||
setPolygonVertices(vertexCount);
|
||||
// waveformSprite.duration += 1.0;
|
||||
// waveformSprite2.duration += 1.0;
|
||||
}
|
||||
if (FlxG.keys.justPressed.DOWN)
|
||||
{
|
||||
vertexCount -= 1;
|
||||
setPolygonVertices(vertexCount);
|
||||
// waveformSprite.duration -= 1.0;
|
||||
// waveformSprite2.duration -= 1.0;
|
||||
}
|
||||
if (FlxG.keys.justPressed.LEFT)
|
||||
{
|
||||
// waveformSprite.time -= 1.0;
|
||||
// waveformSprite2.time -= 1.0;
|
||||
}
|
||||
if (FlxG.keys.justPressed.RIGHT)
|
||||
{
|
||||
// waveformSprite.time += 1.0;
|
||||
// waveformSprite2.time += 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
function setPolygonVertices(count:Int)
|
||||
{
|
||||
polygonSprite.clear();
|
||||
|
||||
var size = 100.0;
|
||||
|
||||
// Build a polygon with count vertices.
|
||||
|
||||
var vertices:Array<Array<Float>> = [];
|
||||
|
||||
var angle = 0.0;
|
||||
|
||||
for (i in 0...count)
|
||||
{
|
||||
var x = Math.cos(angle) * size;
|
||||
var y = Math.sin(angle) * size;
|
||||
|
||||
vertices.push([x, y]);
|
||||
|
||||
angle += 2 * Math.PI / count;
|
||||
}
|
||||
|
||||
trace('vertices: ${vertices}');
|
||||
|
||||
var centerVertex = polygonSprite.build_vertex(0, 0);
|
||||
var firstVertex = -1;
|
||||
var lastVertex = -1;
|
||||
|
||||
for (vertex in vertices)
|
||||
{
|
||||
var x = vertex[0];
|
||||
var y = vertex[1];
|
||||
|
||||
var newVertex = polygonSprite.build_vertex(x, y);
|
||||
|
||||
if (firstVertex == -1)
|
||||
{
|
||||
firstVertex = newVertex;
|
||||
}
|
||||
|
||||
if (lastVertex != -1)
|
||||
{
|
||||
polygonSprite.add_tri(centerVertex, lastVertex, newVertex);
|
||||
}
|
||||
|
||||
lastVertex = newVertex;
|
||||
}
|
||||
|
||||
polygonSprite.add_tri(centerVertex, lastVertex, firstVertex);
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
{
|
||||
super.destroy();
|
||||
}
|
||||
}
|
|
@ -1,45 +1,52 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.util.logging.CrashHandler;
|
||||
import haxe.ui.containers.HBox;
|
||||
import haxe.ui.containers.Grid;
|
||||
import haxe.ui.containers.ScrollView;
|
||||
import haxe.ui.containers.menus.MenuBar;
|
||||
import flixel.addons.display.FlxSliceSprite;
|
||||
import flixel.addons.display.FlxTiledSprite;
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import flixel.FlxCamera;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.FlxSubState;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import flixel.input.keyboard.FlxKey;
|
||||
import flixel.input.mouse.FlxMouseEvent;
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.math.FlxRect;
|
||||
import flixel.sound.FlxSound;
|
||||
import flixel.system.debug.log.LogStyle;
|
||||
import flixel.system.FlxAssets.FlxSoundAsset;
|
||||
import flixel.text.FlxText;
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.tweens.misc.VarTween;
|
||||
import haxe.ui.Toolkit;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxSort;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.audio.visualize.PolygonSpectogram;
|
||||
import funkin.audio.visualize.PolygonSpectogram;
|
||||
import funkin.audio.VoicesGroup;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.audio.waveform.WaveformSprite;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongOffsets;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.input.Cursor;
|
||||
import funkin.input.TurboKeyHandler;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
|
@ -50,22 +57,14 @@ import funkin.play.components.HealthIcon;
|
|||
import funkin.play.notes.NoteSprite;
|
||||
import funkin.play.PlayState;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import funkin.ui.debug.charting.commands.ChartEditorCommand;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.save.Save;
|
||||
import funkin.ui.debug.charting.commands.AddEventsCommand;
|
||||
import funkin.ui.debug.charting.commands.AddNotesCommand;
|
||||
import funkin.ui.debug.charting.commands.ChartEditorCommand;
|
||||
import funkin.ui.debug.charting.commands.ChartEditorCommand;
|
||||
import funkin.ui.debug.charting.commands.CutItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.ChartEditorCommand;
|
||||
import funkin.ui.debug.charting.commands.CopyItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.CutItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.DeselectAllItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.DeselectItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
|
||||
|
@ -83,17 +82,22 @@ import funkin.ui.debug.charting.commands.SelectItemsCommand;
|
|||
import funkin.ui.debug.charting.commands.SetItemSelectionCommand;
|
||||
import funkin.ui.debug.charting.components.ChartEditorEventSprite;
|
||||
import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite;
|
||||
import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
|
||||
import funkin.ui.debug.charting.components.ChartEditorNotePreview;
|
||||
import funkin.ui.debug.charting.components.ChartEditorNoteSprite;
|
||||
import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
|
||||
import funkin.ui.debug.charting.components.ChartEditorPlaybarHead;
|
||||
import funkin.ui.debug.charting.components.ChartEditorSelectionSquareSprite;
|
||||
import funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorFreeplayToolbox;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox;
|
||||
import funkin.ui.haxeui.components.CharacterPlayer;
|
||||
import funkin.ui.haxeui.HaxeUIState;
|
||||
import funkin.ui.mainmenu.MainMenuState;
|
||||
import funkin.util.Constants;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.util.logging.CrashHandler;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.util.WindowUtil;
|
||||
import haxe.DynamicAccess;
|
||||
|
@ -101,23 +105,26 @@ import haxe.io.Bytes;
|
|||
import haxe.io.Path;
|
||||
import haxe.ui.backend.flixel.UIRuntimeState;
|
||||
import haxe.ui.backend.flixel.UIState;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.Label;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.Image;
|
||||
import haxe.ui.components.Label;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.containers.Box;
|
||||
import haxe.ui.containers.dialogs.CollapsibleDialog;
|
||||
import haxe.ui.containers.Frame;
|
||||
import haxe.ui.containers.Box;
|
||||
import haxe.ui.containers.Grid;
|
||||
import haxe.ui.containers.HBox;
|
||||
import haxe.ui.containers.menus.Menu;
|
||||
import haxe.ui.containers.menus.MenuBar;
|
||||
import haxe.ui.containers.menus.MenuItem;
|
||||
import haxe.ui.containers.menus.MenuBar;
|
||||
import haxe.ui.containers.menus.MenuCheckBox;
|
||||
import haxe.ui.containers.menus.MenuItem;
|
||||
import haxe.ui.containers.ScrollView;
|
||||
import haxe.ui.containers.TreeView;
|
||||
import haxe.ui.containers.TreeViewNode;
|
||||
import haxe.ui.components.Image;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox;
|
||||
import haxe.ui.core.Component;
|
||||
import haxe.ui.core.Screen;
|
||||
import haxe.ui.events.DragEvent;
|
||||
|
@ -125,13 +132,8 @@ import haxe.ui.events.MouseEvent;
|
|||
import haxe.ui.events.UIEvent;
|
||||
import haxe.ui.events.UIEvent;
|
||||
import haxe.ui.focus.FocusManager;
|
||||
import haxe.ui.Toolkit;
|
||||
import openfl.display.BitmapData;
|
||||
import funkin.audio.visualize.PolygonSpectogram;
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import funkin.audio.visualize.PolygonVisGroup;
|
||||
import flixel.input.mouse.FlxMouseEvent;
|
||||
import flixel.text.FlxText;
|
||||
import flixel.system.debug.log.LogStyle;
|
||||
|
||||
using Lambda;
|
||||
|
||||
|
@ -153,17 +155,19 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
*/
|
||||
// ==============================
|
||||
// Layouts
|
||||
public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata');
|
||||
|
||||
public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata');
|
||||
public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties');
|
||||
public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata');
|
||||
public static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty');
|
||||
|
||||
public static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/player-preview');
|
||||
public static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/opponent-preview');
|
||||
public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata');
|
||||
public static final CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT:String = Paths.ui('chart-editor/toolbox/offsets');
|
||||
public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata');
|
||||
public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata');
|
||||
public static final CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT:String = Paths.ui('chart-editor/toolbox/freeplay');
|
||||
public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties');
|
||||
|
||||
// Validation
|
||||
public static final SUPPORTED_MUSIC_FORMATS:Array<String> = ['ogg'];
|
||||
public static final SUPPORTED_MUSIC_FORMATS:Array<String> = #if sys ['ogg'] #else ['mp3'] #end;
|
||||
|
||||
// Layout
|
||||
|
||||
|
@ -393,13 +397,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS);
|
||||
measureTicks.y = gridTiledSprite.y;
|
||||
|
||||
if (audioVisGroup != null && audioVisGroup.playerVis != null)
|
||||
for (member in audioWaveforms.members)
|
||||
{
|
||||
audioVisGroup.playerVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS - GRID_TOP_PAD);
|
||||
}
|
||||
if (audioVisGroup != null && audioVisGroup.opponentVis != null)
|
||||
{
|
||||
audioVisGroup.opponentVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS - GRID_TOP_PAD);
|
||||
member.time = scrollPositionInMs / Constants.MS_PER_SEC;
|
||||
|
||||
// Doing this desyncs the waveforms from the grid.
|
||||
// member.y = Math.max(this.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -501,8 +504,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
function get_playheadPositionInMs():Float
|
||||
{
|
||||
if (audioVisGroup != null && audioVisGroup.playerVis != null)
|
||||
audioVisGroup.playerVis.realtimeStartOffset = -Conductor.instance.getStepTimeInMs(playheadPositionInSteps);
|
||||
return Conductor.instance.getStepTimeInMs(playheadPositionInSteps);
|
||||
}
|
||||
|
||||
|
@ -510,7 +511,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
{
|
||||
playheadPositionInSteps = Conductor.instance.getTimeInSteps(value);
|
||||
|
||||
if (audioVisGroup != null && audioVisGroup.playerVis != null) audioVisGroup.playerVis.realtimeStartOffset = -value;
|
||||
return value;
|
||||
}
|
||||
|
||||
|
@ -690,6 +690,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
*/
|
||||
var activeToolboxes:Map<String, CollapsibleDialog> = new Map<String, CollapsibleDialog>();
|
||||
|
||||
/**
|
||||
* The camera component we're using for this state.
|
||||
*/
|
||||
var uiCamera:FlxCamera;
|
||||
|
||||
// Audio
|
||||
|
||||
/**
|
||||
|
@ -1101,14 +1106,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
* `null` until vocal track(s) are loaded.
|
||||
* When switching characters, the elements of the VoicesGroup will be swapped to match the new character.
|
||||
*/
|
||||
var audioVocalTrackGroup:Null<VoicesGroup> = null;
|
||||
var audioVocalTrackGroup:VoicesGroup = new VoicesGroup();
|
||||
|
||||
/**
|
||||
* The audio vis for the inst/vocals.
|
||||
* The audio waveform visualization for the inst/vocals.
|
||||
* `null` until vocal track(s) are loaded.
|
||||
* When switching characters, the elements of the PolygonVisGroup will be swapped to match the new character.
|
||||
* When switching characters, the elements will be swapped to match the new character.
|
||||
*/
|
||||
var audioVisGroup:Null<PolygonVisGroup> = null;
|
||||
var audioWaveforms:FlxTypedSpriteGroup<WaveformSprite> = new FlxTypedSpriteGroup<WaveformSprite>();
|
||||
|
||||
/**
|
||||
* A map of the audio tracks for each character's vocals.
|
||||
|
@ -1291,6 +1296,29 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
return currentSongChartData.events = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience property to get the rating for this difficulty in the Freeplay menu.
|
||||
*/
|
||||
var currentSongChartDifficultyRating(get, set):Int;
|
||||
|
||||
function get_currentSongChartDifficultyRating():Int
|
||||
{
|
||||
var result:Null<Int> = currentSongMetadata.playData.ratings.get(selectedDifficulty);
|
||||
if (result == null)
|
||||
{
|
||||
// Initialize to the default value if not set.
|
||||
currentSongMetadata.playData.ratings.set(selectedDifficulty, 0);
|
||||
return 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function set_currentSongChartDifficultyRating(value:Int):Int
|
||||
{
|
||||
currentSongMetadata.playData.ratings.set(selectedDifficulty, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
var currentSongNoteStyle(get, set):String;
|
||||
|
||||
function get_currentSongNoteStyle():String
|
||||
|
@ -1308,6 +1336,30 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
return currentSongMetadata.playData.noteStyle = value;
|
||||
}
|
||||
|
||||
var currentSongFreeplayPreviewStart(get, set):Int;
|
||||
|
||||
function get_currentSongFreeplayPreviewStart():Int
|
||||
{
|
||||
return currentSongMetadata.playData.previewStart;
|
||||
}
|
||||
|
||||
function set_currentSongFreeplayPreviewStart(value:Int):Int
|
||||
{
|
||||
return currentSongMetadata.playData.previewStart = value;
|
||||
}
|
||||
|
||||
var currentSongFreeplayPreviewEnd(get, set):Int;
|
||||
|
||||
function get_currentSongFreeplayPreviewEnd():Int
|
||||
{
|
||||
return currentSongMetadata.playData.previewEnd;
|
||||
}
|
||||
|
||||
function set_currentSongFreeplayPreviewEnd(value:Int):Int
|
||||
{
|
||||
return currentSongMetadata.playData.previewEnd = value;
|
||||
}
|
||||
|
||||
var currentSongStage(get, set):String;
|
||||
|
||||
function get_currentSongStage():String
|
||||
|
@ -1441,19 +1493,28 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
return value;
|
||||
}
|
||||
|
||||
var currentVocalOffset(get, set):Float;
|
||||
var currentVocalOffsetPlayer(get, set):Float;
|
||||
|
||||
function get_currentVocalOffset():Float
|
||||
function get_currentVocalOffsetPlayer():Float
|
||||
{
|
||||
// Currently there's only one vocal offset, so we just grab the player's offset since both should be the same.
|
||||
// Should probably make it so we can set offsets for player + opponent individually, though.
|
||||
return currentSongOffsets.getVocalOffset(currentPlayerChar);
|
||||
}
|
||||
|
||||
function set_currentVocalOffset(value:Float):Float
|
||||
function set_currentVocalOffsetPlayer(value:Float):Float
|
||||
{
|
||||
// Currently there's only one vocal offset, so we just apply it to both characters.
|
||||
currentSongOffsets.setVocalOffset(currentPlayerChar, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
var currentVocalOffsetOpponent(get, set):Float;
|
||||
|
||||
function get_currentVocalOffsetOpponent():Float
|
||||
{
|
||||
return currentSongOffsets.getVocalOffset(currentOpponentChar);
|
||||
}
|
||||
|
||||
function set_currentVocalOffsetOpponent(value:Float):Float
|
||||
{
|
||||
currentSongOffsets.setVocalOffset(currentOpponentChar, value);
|
||||
return value;
|
||||
}
|
||||
|
@ -1858,11 +1919,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
*/
|
||||
var notePreviewViewportBitmap:Null<BitmapData> = null;
|
||||
|
||||
/**r
|
||||
/**
|
||||
* The IMAGE used for the measure ticks. Updated by ChartEditorThemeHandler.
|
||||
*/
|
||||
var measureTickBitmap:Null<BitmapData> = null;
|
||||
|
||||
/**
|
||||
* The IMAGE used for the offset ticks. Updated by ChartEditorThemeHandler.
|
||||
*/
|
||||
var offsetTickBitmap:Null<BitmapData> = null;
|
||||
|
||||
/**
|
||||
* The tiled sprite used to display the grid.
|
||||
* The height is the length of the song, and scrolling is done by simply the sprite.
|
||||
|
@ -2028,7 +2094,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
loadPreferences();
|
||||
|
||||
fixCamera();
|
||||
uiCamera = new FlxCamera();
|
||||
FlxG.cameras.reset(uiCamera);
|
||||
|
||||
buildDefaultSongData();
|
||||
|
||||
|
@ -2246,8 +2313,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
// Initialize the song chart data.
|
||||
songChartData = new Map<String, SongChartData>();
|
||||
|
||||
audioVocalTrackGroup = new VoicesGroup();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2334,8 +2399,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
add(healthIconBF);
|
||||
healthIconBF.zIndex = 30;
|
||||
|
||||
audioVisGroup = new PolygonVisGroup();
|
||||
add(audioVisGroup);
|
||||
add(audioWaveforms);
|
||||
}
|
||||
|
||||
function buildMeasureTicks():Void
|
||||
|
@ -2879,13 +2943,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
menubarItemVolumeVocalsPlayer.onChange = event -> {
|
||||
var volume:Float = event.value.toFloat() / 100.0;
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.playerVolume = volume;
|
||||
audioVocalTrackGroup.playerVolume = volume;
|
||||
menubarLabelVolumeVocalsPlayer.text = 'Player - ${Std.int(event.value)}%';
|
||||
};
|
||||
|
||||
menubarItemVolumeVocalsOpponent.onChange = event -> {
|
||||
var volume:Float = event.value.toFloat() / 100.0;
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.opponentVolume = volume;
|
||||
audioVocalTrackGroup.opponentVolume = volume;
|
||||
menubarLabelVolumeVocalsOpponent.text = 'Enemy - ${Std.int(event.value)}%';
|
||||
};
|
||||
|
||||
|
@ -2894,7 +2958,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25.
|
||||
#if FLX_PITCH
|
||||
if (audioInstTrack != null) audioInstTrack.pitch = pitch;
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch;
|
||||
audioVocalTrackGroup.pitch = pitch;
|
||||
#end
|
||||
var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places.
|
||||
menubarLabelPlaybackSpeed.text = 'Playback Speed - ${pitchDisplay}x';
|
||||
|
@ -2902,8 +2966,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value);
|
||||
menubarItemToggleToolboxMetadata.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value);
|
||||
menubarItemToggleToolboxOffsets.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT, event.value);
|
||||
menubarItemToggleToolboxNotes.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value);
|
||||
menubarItemToggleToolboxEventData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT, event.value);
|
||||
menubarItemToggleToolboxFreeplay.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT, event.value);
|
||||
menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value);
|
||||
menubarItemToggleToolboxPlayerPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value);
|
||||
menubarItemToggleToolboxOpponentPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value);
|
||||
|
@ -3204,7 +3270,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
Conductor.instance.update(audioInstTrack.time);
|
||||
handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
|
||||
// Resync vocals.
|
||||
if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
|
||||
if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
|
||||
{
|
||||
audioVocalTrackGroup.time = audioInstTrack.time;
|
||||
}
|
||||
|
@ -3222,7 +3288,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
Conductor.instance.update(audioInstTrack.time);
|
||||
handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
|
||||
// Resync vocals.
|
||||
if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
|
||||
if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
|
||||
{
|
||||
audioVocalTrackGroup.time = audioInstTrack.time;
|
||||
}
|
||||
|
@ -5287,7 +5353,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
Paths.setCurrentLevel('weekend1');
|
||||
}
|
||||
|
||||
subStateClosed.add(fixCamera);
|
||||
subStateClosed.add(reviveUICamera);
|
||||
subStateClosed.add(resetConductorAfterTest);
|
||||
|
||||
FlxTransitionableState.skipNextTransIn = false;
|
||||
|
@ -5310,7 +5376,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
{
|
||||
FlxG.sound.music = audioInstTrack;
|
||||
}
|
||||
if (audioVocalTrackGroup != null) targetState.vocals = audioVocalTrackGroup;
|
||||
targetState.vocals = audioVocalTrackGroup;
|
||||
|
||||
// Kill and replace the UI camera so it doesn't get destroyed during the state transition.
|
||||
uiCamera.kill();
|
||||
FlxG.cameras.remove(uiCamera, false);
|
||||
FlxG.cameras.reset(new FlxCamera());
|
||||
|
||||
this.persistentUpdate = false;
|
||||
this.persistentDraw = false;
|
||||
|
@ -5401,13 +5472,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
}
|
||||
|
||||
/**
|
||||
* Fix a camera issue caused when closing the PlayState used when testing.
|
||||
* Revive the UI camera and re-establish it as the main camera so UI elements depending on it don't explode.
|
||||
*/
|
||||
function fixCamera(_:FlxSubState = null):Void
|
||||
function reviveUICamera(_:FlxSubState = null):Void
|
||||
{
|
||||
FlxG.cameras.reset(new FlxCamera());
|
||||
FlxG.camera.focusOn(new FlxPoint(FlxG.width / 2, FlxG.height / 2));
|
||||
FlxG.camera.zoom = 1.0;
|
||||
uiCamera.revive();
|
||||
FlxG.cameras.reset(uiCamera);
|
||||
|
||||
add(this.root);
|
||||
}
|
||||
|
@ -5422,7 +5492,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
if (audioInstTrack != null)
|
||||
{
|
||||
audioInstTrack.play(false, audioInstTrack.time);
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time);
|
||||
audioVocalTrackGroup.play(false, audioInstTrack.time);
|
||||
}
|
||||
|
||||
playbarPlay.text = '||'; // Pause
|
||||
|
@ -5660,7 +5730,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
audioInstTrack.time = scrollPositionInMs + playheadPositionInMs - Conductor.instance.instrumentalOffset;
|
||||
// Update the songPosition in the Conductor.
|
||||
Conductor.instance.update(audioInstTrack.time);
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = audioInstTrack.time;
|
||||
audioVocalTrackGroup.time = audioInstTrack.time;
|
||||
}
|
||||
|
||||
// We need to update the note sprites because we changed the scroll position.
|
||||
|
@ -5881,7 +5951,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
function stopAudioPlayback():Void
|
||||
{
|
||||
if (audioInstTrack != null) audioInstTrack.pause();
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause();
|
||||
audioVocalTrackGroup.pause();
|
||||
|
||||
playbarPlay.text = '>';
|
||||
}
|
||||
|
@ -5916,7 +5986,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
// Keep the track at the end.
|
||||
audioInstTrack.time = audioInstTrack.length;
|
||||
}
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause();
|
||||
audioVocalTrackGroup.pause();
|
||||
};
|
||||
}
|
||||
else
|
||||
|
@ -5930,12 +6000,32 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
healthIconsDirty = true;
|
||||
}
|
||||
|
||||
function hardRefreshOffsetsToolbox():Void
|
||||
{
|
||||
var offsetsToolbox:ChartEditorOffsetsToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT);
|
||||
if (offsetsToolbox != null)
|
||||
{
|
||||
offsetsToolbox.refreshAudioPreview();
|
||||
offsetsToolbox.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function hardRefreshFreeplayToolbox():Void
|
||||
{
|
||||
var freeplayToolbox:ChartEditorFreeplayToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT);
|
||||
if (freeplayToolbox != null)
|
||||
{
|
||||
freeplayToolbox.refreshAudioPreview();
|
||||
freeplayToolbox.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the voices group.
|
||||
*/
|
||||
public function clearVocals():Void
|
||||
{
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.clear();
|
||||
audioVocalTrackGroup.clear();
|
||||
}
|
||||
|
||||
function isNoteSelected(note:Null<SongNoteData>):Bool
|
||||
|
@ -5960,7 +6050,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
// Stop the music.
|
||||
if (welcomeMusic != null) welcomeMusic.destroy();
|
||||
if (audioInstTrack != null) audioInstTrack.destroy();
|
||||
if (audioVocalTrackGroup != null) audioVocalTrackGroup.destroy();
|
||||
audioVocalTrackGroup.destroy();
|
||||
}
|
||||
|
||||
function applyCanQuickSave():Void
|
||||
|
|
|
@ -46,26 +46,14 @@ class CopyItemsCommand implements ChartEditorCommand
|
|||
|
||||
function performVisuals(state:ChartEditorState):Void
|
||||
{
|
||||
var hasNotes:Bool = false;
|
||||
var hasEvents:Bool = false;
|
||||
|
||||
// Wiggle copied notes.
|
||||
if (state.currentNoteSelection.length > 0)
|
||||
{
|
||||
// Display the "Copied Notes" text.
|
||||
if (state.txtCopyNotif != null)
|
||||
{
|
||||
state.txtCopyNotif.visible = true;
|
||||
state.txtCopyNotif.text = "Copied " + state.currentNoteSelection.length + " notes to clipboard";
|
||||
state.txtCopyNotif.x = FlxG.mouse.x - (state.txtCopyNotif.width / 2);
|
||||
state.txtCopyNotif.y = FlxG.mouse.y - 16;
|
||||
FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5,
|
||||
{
|
||||
type: FlxTween.ONESHOT,
|
||||
ease: FlxEase.quadOut,
|
||||
onComplete: function(_) {
|
||||
state.txtCopyNotif.visible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
hasNotes = true;
|
||||
|
||||
// Wiggle the notes.
|
||||
for (note in state.renderedNotes.members)
|
||||
{
|
||||
if (state.isNoteSelected(note.noteData))
|
||||
|
@ -91,8 +79,13 @@ class CopyItemsCommand implements ChartEditorCommand
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wiggle copied events.
|
||||
if (state.currentEventSelection.length > 0)
|
||||
{
|
||||
hasEvents = true;
|
||||
|
||||
// Wiggle the events.
|
||||
for (event in state.renderedEvents.members)
|
||||
{
|
||||
if (state.isEventSelected(event.eventData))
|
||||
|
@ -119,6 +112,39 @@ class CopyItemsCommand implements ChartEditorCommand
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display the "Copied Notes" text.
|
||||
if ((hasNotes || hasEvents) && state.txtCopyNotif != null)
|
||||
{
|
||||
var copiedString:String = '';
|
||||
if (hasNotes)
|
||||
{
|
||||
var copiedNotes:Int = state.currentNoteSelection.length;
|
||||
copiedString += '${copiedNotes} note';
|
||||
if (copiedNotes > 1) copiedString += 's';
|
||||
|
||||
if (hasEvents) copiedString += ' and ';
|
||||
}
|
||||
if (hasEvents)
|
||||
{
|
||||
var copiedEvents:Int = state.currentEventSelection.length;
|
||||
copiedString += '${state.currentEventSelection.length} event';
|
||||
if (copiedEvents > 1) copiedString += 's';
|
||||
}
|
||||
|
||||
state.txtCopyNotif.visible = true;
|
||||
state.txtCopyNotif.text = 'Copied ${copiedString} to clipboard';
|
||||
state.txtCopyNotif.x = FlxG.mouse.x - (state.txtCopyNotif.width / 2);
|
||||
state.txtCopyNotif.y = FlxG.mouse.y - 16;
|
||||
FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5,
|
||||
{
|
||||
type: FlxTween.ONESHOT,
|
||||
ease: FlxEase.quadOut,
|
||||
onComplete: function(_) {
|
||||
state.txtCopyNotif.visible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
package funkin.ui.debug.charting.commands;
|
||||
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.tweens.FlxTween;
|
||||
|
||||
/**
|
||||
* Command that copies a given set of notes and song events to the clipboard,
|
||||
* without deleting them from the chart editor.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class SetAudioOffsetCommand implements ChartEditorCommand
|
||||
{
|
||||
var type:AudioOffsetType;
|
||||
var oldOffset:Float = 0;
|
||||
var newOffset:Float;
|
||||
var refreshOffsetsToolbox:Bool;
|
||||
|
||||
public function new(type:AudioOffsetType, newOffset:Float, refreshOffsetsToolbox:Bool = true)
|
||||
{
|
||||
this.type = type;
|
||||
this.newOffset = newOffset;
|
||||
this.refreshOffsetsToolbox = refreshOffsetsToolbox;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case INSTRUMENTAL:
|
||||
oldOffset = state.currentInstrumentalOffset;
|
||||
state.currentInstrumentalOffset = newOffset;
|
||||
|
||||
// Update rendering.
|
||||
Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset;
|
||||
state.songLengthInMs = (state.audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset;
|
||||
case PLAYER:
|
||||
oldOffset = state.currentVocalOffsetPlayer;
|
||||
state.currentVocalOffsetPlayer = newOffset;
|
||||
|
||||
// Update rendering.
|
||||
state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffsetPlayer;
|
||||
case OPPONENT:
|
||||
oldOffset = state.currentVocalOffsetOpponent;
|
||||
state.currentVocalOffsetOpponent = newOffset;
|
||||
|
||||
// Update rendering.
|
||||
state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffsetOpponent;
|
||||
}
|
||||
|
||||
// Update the offsets toolbox.
|
||||
if (refreshOffsetsToolbox)
|
||||
{
|
||||
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT);
|
||||
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT);
|
||||
}
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case INSTRUMENTAL:
|
||||
state.currentInstrumentalOffset = oldOffset;
|
||||
|
||||
// Update rendering.
|
||||
Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset;
|
||||
state.songLengthInMs = (state.audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset;
|
||||
case PLAYER:
|
||||
state.currentVocalOffsetPlayer = oldOffset;
|
||||
|
||||
// Update rendering.
|
||||
state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffsetPlayer;
|
||||
case OPPONENT:
|
||||
state.currentVocalOffsetOpponent = oldOffset;
|
||||
|
||||
// Update rendering.
|
||||
state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffsetOpponent;
|
||||
}
|
||||
|
||||
// Update the offsets toolbox.
|
||||
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT);
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (newOffset != oldOffset);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case INSTRUMENTAL:
|
||||
return 'Set Inst. Audio Offset to $newOffset';
|
||||
case PLAYER:
|
||||
return 'Set Player Audio Offset to $newOffset';
|
||||
case OPPONENT:
|
||||
return 'Set Opponent Audio Offset to $newOffset';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AudioOffsetType
|
||||
{
|
||||
INSTRUMENTAL;
|
||||
PLAYER;
|
||||
OPPONENT;
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package funkin.ui.debug.charting.commands;
|
||||
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
|
||||
/**
|
||||
* Command that sets the start time or end time of the Freeplay preview.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class SetFreeplayPreviewCommand implements ChartEditorCommand
|
||||
{
|
||||
var previousStartTime:Int = 0;
|
||||
var previousEndTime:Int = 0;
|
||||
var newStartTime:Null<Int> = null;
|
||||
var newEndTime:Null<Int> = null;
|
||||
|
||||
public function new(newStartTime:Null<Int>, newEndTime:Null<Int>)
|
||||
{
|
||||
this.newStartTime = newStartTime;
|
||||
this.newEndTime = newEndTime;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
this.previousStartTime = state.currentSongFreeplayPreviewStart;
|
||||
this.previousEndTime = state.currentSongFreeplayPreviewEnd;
|
||||
|
||||
if (newStartTime != null) state.currentSongFreeplayPreviewStart = newStartTime;
|
||||
if (newEndTime != null) state.currentSongFreeplayPreviewEnd = newEndTime;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSongFreeplayPreviewStart = previousStartTime;
|
||||
state.currentSongFreeplayPreviewEnd = previousEndTime;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
return (newStartTime != null && newStartTime != previousStartTime) || (newEndTime != null && newEndTime != previousEndTime);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var setStart = newStartTime != null && newStartTime != previousStartTime;
|
||||
var setEnd = newEndTime != null && newEndTime != previousEndTime;
|
||||
|
||||
if (setStart && !setEnd)
|
||||
{
|
||||
return "Set Freeplay Preview Start Time";
|
||||
}
|
||||
else if (setEnd && !setStart)
|
||||
{
|
||||
return "Set Freeplay Preview End Time";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "Set Freeplay Preview Start and End Times";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,11 +3,14 @@ package funkin.ui.debug.charting.handlers;
|
|||
import flixel.system.FlxAssets.FlxSoundAsset;
|
||||
import flixel.system.FlxSound;
|
||||
import funkin.audio.VoicesGroup;
|
||||
import funkin.audio.visualize.PolygonVisGroup;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.util.assets.SoundUtil;
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
import funkin.audio.waveform.WaveformSprite;
|
||||
import flixel.util.FlxColor;
|
||||
import haxe.io.Bytes;
|
||||
import haxe.io.Path;
|
||||
import openfl.utils.Assets;
|
||||
|
@ -125,15 +128,42 @@ class ChartEditorAudioHandler
|
|||
|
||||
public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool
|
||||
{
|
||||
var perfA = haxe.Timer.stamp();
|
||||
|
||||
var result:Bool = playInstrumental(state, instId);
|
||||
if (!result) return false;
|
||||
|
||||
var perfB = haxe.Timer.stamp();
|
||||
|
||||
stopExistingVocals(state);
|
||||
|
||||
var perfC = haxe.Timer.stamp();
|
||||
|
||||
result = playVocals(state, BF, playerId, instId);
|
||||
|
||||
var perfD = haxe.Timer.stamp();
|
||||
|
||||
// if (!result) return false;
|
||||
result = playVocals(state, DAD, opponentId, instId);
|
||||
// if (!result) return false;
|
||||
|
||||
var perfE = haxe.Timer.stamp();
|
||||
|
||||
state.hardRefreshOffsetsToolbox();
|
||||
|
||||
var perfF = haxe.Timer.stamp();
|
||||
|
||||
state.hardRefreshFreeplayToolbox();
|
||||
|
||||
var perfG = haxe.Timer.stamp();
|
||||
|
||||
trace('Switched to instrumental in ${perfB - perfA} seconds.');
|
||||
trace('Stopped existing vocals in ${perfC - perfB} seconds.');
|
||||
trace('Played BF vocals in ${perfD - perfC} seconds.');
|
||||
trace('Played DAD vocals in ${perfE - perfD} seconds.');
|
||||
trace('Hard refreshed offsets toolbox in ${perfF - perfE} seconds.');
|
||||
trace('Hard refreshed freeplay toolbox in ${perfG - perfF} seconds.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -144,7 +174,10 @@ class ChartEditorAudioHandler
|
|||
{
|
||||
if (instId == '') instId = 'default';
|
||||
var instTrackData:Null<Bytes> = state.audioInstTrackData.get(instId);
|
||||
var perfA = haxe.Timer.stamp();
|
||||
var instTrack:Null<FunkinSound> = SoundUtil.buildSoundFromBytes(instTrackData);
|
||||
var perfB = haxe.Timer.stamp();
|
||||
trace('Built instrumental track in ${perfB - perfA} seconds.');
|
||||
if (instTrack == null) return false;
|
||||
|
||||
stopExistingInstrumental(state);
|
||||
|
@ -172,10 +205,12 @@ class ChartEditorAudioHandler
|
|||
{
|
||||
var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
|
||||
var vocalTrackData:Null<Bytes> = state.audioVocalTrackData.get(trackId);
|
||||
var perfStart = haxe.Timer.stamp();
|
||||
var vocalTrack:Null<FunkinSound> = SoundUtil.buildSoundFromBytes(vocalTrackData);
|
||||
var perfEnd = haxe.Timer.stamp();
|
||||
trace('Built vocal track in ${perfEnd - perfStart} seconds.');
|
||||
|
||||
if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup();
|
||||
if (state.audioVisGroup == null) state.audioVisGroup = new PolygonVisGroup();
|
||||
|
||||
if (vocalTrack != null)
|
||||
{
|
||||
|
@ -183,26 +218,57 @@ class ChartEditorAudioHandler
|
|||
{
|
||||
case BF:
|
||||
state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
|
||||
state.audioVisGroup.addPlayerVis(vocalTrack);
|
||||
state.audioVisGroup.playerVis.x = 885;
|
||||
state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
|
||||
state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
|
||||
state.audioVisGroup.playerVis.detail = 1;
|
||||
state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
|
||||
|
||||
state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffset;
|
||||
var perfStart = haxe.Timer.stamp();
|
||||
var waveformData:Null<WaveformData> = vocalTrack.waveformData;
|
||||
var perfEnd = haxe.Timer.stamp();
|
||||
trace('Interpreted waveform data in ${perfEnd - perfStart} seconds.');
|
||||
|
||||
if (waveformData != null)
|
||||
{
|
||||
var duration:Float = Conductor.instance.getStepTimeInMs(16) * 0.001;
|
||||
var waveformSprite:WaveformSprite = new WaveformSprite(waveformData, VERTICAL, FlxColor.WHITE);
|
||||
waveformSprite.x = 840;
|
||||
waveformSprite.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
|
||||
waveformSprite.height = (ChartEditorState.GRID_SIZE) * 16;
|
||||
waveformSprite.width = (ChartEditorState.GRID_SIZE) * 2;
|
||||
waveformSprite.time = 0;
|
||||
waveformSprite.duration = duration;
|
||||
state.audioWaveforms.add(waveformSprite);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[WARN] Failed to parse waveform data for vocal track.');
|
||||
}
|
||||
|
||||
state.audioVocalTrackGroup.playerVoicesOffset = state.currentVocalOffsetPlayer;
|
||||
return true;
|
||||
case DAD:
|
||||
state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
|
||||
state.audioVisGroup.addOpponentVis(vocalTrack);
|
||||
state.audioVisGroup.opponentVis.x = 405;
|
||||
|
||||
state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
|
||||
state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
|
||||
state.audioVisGroup.opponentVis.detail = 1;
|
||||
state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
|
||||
var perfStart = haxe.Timer.stamp();
|
||||
var waveformData:Null<WaveformData> = vocalTrack.waveformData;
|
||||
var perfEnd = haxe.Timer.stamp();
|
||||
trace('Interpreted waveform data in ${perfEnd - perfStart} seconds.');
|
||||
|
||||
state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffset;
|
||||
if (waveformData != null)
|
||||
{
|
||||
var duration:Float = Conductor.instance.getStepTimeInMs(16) * 0.001;
|
||||
var waveformSprite:WaveformSprite = new WaveformSprite(waveformData, VERTICAL, FlxColor.WHITE);
|
||||
waveformSprite.x = 360;
|
||||
waveformSprite.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
|
||||
waveformSprite.height = (ChartEditorState.GRID_SIZE) * 16;
|
||||
waveformSprite.width = (ChartEditorState.GRID_SIZE) * 2;
|
||||
waveformSprite.time = 0;
|
||||
waveformSprite.duration = duration;
|
||||
state.audioWaveforms.add(waveformSprite);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[WARN] Failed to parse waveform data for vocal track.');
|
||||
}
|
||||
|
||||
state.audioVocalTrackGroup.opponentVoicesOffset = state.currentVocalOffsetOpponent;
|
||||
|
||||
return true;
|
||||
case OTHER:
|
||||
|
@ -219,13 +285,10 @@ class ChartEditorAudioHandler
|
|||
|
||||
public static function stopExistingVocals(state:ChartEditorState):Void
|
||||
{
|
||||
if (state.audioVocalTrackGroup != null)
|
||||
state.audioVocalTrackGroup.clear();
|
||||
if (state.audioWaveforms != null)
|
||||
{
|
||||
state.audioVocalTrackGroup.clear();
|
||||
}
|
||||
if (state.audioVisGroup != null)
|
||||
{
|
||||
state.audioVisGroup.clearAllVis();
|
||||
state.audioWaveforms.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,8 @@ class ChartEditorImportExportHandler
|
|||
*/
|
||||
public static function loadSongAsTemplate(state:ChartEditorState, songId:String):Void
|
||||
{
|
||||
trace('===============START');
|
||||
|
||||
var song:Null<Song> = SongRegistry.instance.fetchEntry(songId);
|
||||
|
||||
if (song == null) return;
|
||||
|
@ -98,11 +100,14 @@ class ChartEditorImportExportHandler
|
|||
state.isHaxeUIDialogOpen = false;
|
||||
state.currentWorkingFilePath = null; // New file, so no path.
|
||||
state.switchToCurrentInstrumental();
|
||||
|
||||
state.postLoadInstrumental();
|
||||
|
||||
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||
|
||||
state.success('Success', 'Loaded song (${rawSongMetadata[0].songName})');
|
||||
|
||||
trace('===============END');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,11 +137,8 @@ class ChartEditorImportExportHandler
|
|||
state.audioInstTrack.stop();
|
||||
state.audioInstTrack = null;
|
||||
}
|
||||
if (state.audioVocalTrackGroup != null)
|
||||
{
|
||||
state.audioVocalTrackGroup.stop();
|
||||
state.audioVocalTrackGroup.clear();
|
||||
}
|
||||
state.audioVocalTrackGroup.stop();
|
||||
state.audioVocalTrackGroup.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -82,6 +82,7 @@ class ChartEditorThemeHandler
|
|||
updateBackground(state);
|
||||
updateGridBitmap(state);
|
||||
updateMeasureTicks(state);
|
||||
updateOffsetTicks(state);
|
||||
updateSelectionSquare(state);
|
||||
updateNotePreview(state);
|
||||
}
|
||||
|
@ -231,6 +232,9 @@ class ChartEditorThemeHandler
|
|||
// Else, gridTiledSprite will be built later.
|
||||
}
|
||||
|
||||
/**
|
||||
* Vertical measure ticks.
|
||||
*/
|
||||
static function updateMeasureTicks(state:ChartEditorState):Void
|
||||
{
|
||||
var measureTickWidth:Int = 6;
|
||||
|
@ -286,6 +290,59 @@ class ChartEditorThemeHandler
|
|||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick16Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontal offset ticks.
|
||||
*/
|
||||
static function updateOffsetTicks(state:ChartEditorState):Void
|
||||
{
|
||||
var majorTickWidth:Int = 6;
|
||||
var minorTickWidth:Int = 3;
|
||||
|
||||
var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure); // 10 minor ticks wide.
|
||||
var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE); // 1 grid squares tall.
|
||||
state.offsetTickBitmap = new BitmapData(ticksWidth, ticksHeight, true);
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), GRID_BEAT_DIVIDER_COLOR_DARK);
|
||||
|
||||
// Draw the major ticks.
|
||||
var leftTickX:Float = 0;
|
||||
var middleTickX:Float = state.offsetTickBitmap.width / 2 - (majorTickWidth / 2);
|
||||
var rightTickX:Float = state.offsetTickBitmap.width - (majorTickWidth / 2);
|
||||
var majorTickLength:Float = state.offsetTickBitmap.height;
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(leftTickX, 0, majorTickWidth / 2, majorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(middleTickX, 0, majorTickWidth, majorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(rightTickX, 0, majorTickWidth / 2, majorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
|
||||
// Draw the minor ticks.
|
||||
var minorTick2X:Float = state.offsetTickBitmap.width * 1 / 10 - (minorTickWidth / 2);
|
||||
var minorTick3X:Float = state.offsetTickBitmap.width * 2 / 10 - (minorTickWidth / 2);
|
||||
var minorTick4X:Float = state.offsetTickBitmap.width * 3 / 10 - (minorTickWidth / 2);
|
||||
var minorTick5X:Float = state.offsetTickBitmap.width * 4 / 10 - (minorTickWidth / 2);
|
||||
var minorTick7X:Float = state.offsetTickBitmap.width * 6 / 10 - (minorTickWidth / 2);
|
||||
var minorTick8X:Float = state.offsetTickBitmap.width * 7 / 10 - (minorTickWidth / 2);
|
||||
var minorTick9X:Float = state.offsetTickBitmap.width * 8 / 10 - (minorTickWidth / 2);
|
||||
var minorTick10X:Float = state.offsetTickBitmap.width * 9 / 10 - (minorTickWidth / 2);
|
||||
var minorTickLength:Float = state.offsetTickBitmap.height * 1 / 3;
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(minorTick2X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(minorTick3X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(minorTick4X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(minorTick5X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(minorTick7X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(minorTick8X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(minorTick9X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.offsetTickBitmap.fillRect(new Rectangle(minorTick10X, 0, minorTickWidth, minorTickLength), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
|
||||
// Draw the offset ticks.
|
||||
// var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT); // 1 grid squares wide.
|
||||
// var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE); // 1 measure tall.
|
||||
// state.offsetTickBitmap = new BitmapData(ticksWidth, ticksHeight, true);
|
||||
// state.offsetTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), GRID_BEAT_DIVIDER_COLOR_DARK);
|
||||
//
|
||||
//// Draw the offset ticks.
|
||||
// state.offsetTickBitmap.fillRect(new Rectangle(0, 0, offsetTickWidth / 2, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
// var rightTickX:Float = state.offsetTickBitmap.width - (offsetTickWidth / 2);
|
||||
// state.offsetTickBitmap.fillRect(new Rectangle(rightTickX, 0, offsetTickWidth / 2, state.offsetTickBitmap.height), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
}
|
||||
|
||||
static function updateSelectionSquare(state:ChartEditorState):Void
|
||||
{
|
||||
var selectionSquareBorderColor:FlxColor = switch (state.currentTheme)
|
||||
|
|
|
@ -35,6 +35,8 @@ import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
|||
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorFreeplayToolbox;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox;
|
||||
import haxe.ui.containers.Frame;
|
||||
|
@ -89,6 +91,10 @@ class ChartEditorToolboxHandler
|
|||
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
||||
// TODO: Fix this.
|
||||
cast(toolbox, ChartEditorBaseToolbox).refresh();
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT:
|
||||
cast(toolbox, ChartEditorBaseToolbox).refresh();
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT:
|
||||
cast(toolbox, ChartEditorBaseToolbox).refresh();
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
|
||||
onShowToolboxPlayerPreview(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
|
||||
|
@ -124,8 +130,6 @@ class ChartEditorToolboxHandler
|
|||
onHideToolboxEventData(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
|
||||
onHideToolboxPlaytestProperties(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
||||
onHideToolboxMetadata(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
|
||||
onHideToolboxPlayerPreview(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
|
||||
|
@ -202,6 +206,10 @@ class ChartEditorToolboxHandler
|
|||
toolbox = buildToolboxDifficultyLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
||||
toolbox = buildToolboxMetadataLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT:
|
||||
toolbox = buildToolboxOffsetsLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT:
|
||||
toolbox = buildToolboxFreeplayLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
|
||||
toolbox = buildToolboxPlayerPreviewLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
|
||||
|
@ -304,8 +312,6 @@ class ChartEditorToolboxHandler
|
|||
|
||||
static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
@ -373,6 +379,24 @@ class ChartEditorToolboxHandler
|
|||
return toolbox;
|
||||
}
|
||||
|
||||
static function buildToolboxOffsetsLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
|
||||
{
|
||||
var toolbox:ChartEditorBaseToolbox = ChartEditorOffsetsToolbox.build(state);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function buildToolboxFreeplayLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
|
||||
{
|
||||
var toolbox:ChartEditorBaseToolbox = ChartEditorFreeplayToolbox.build(state);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function buildToolboxEventDataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
|
||||
{
|
||||
var toolbox:ChartEditorBaseToolbox = ChartEditorEventDataToolbox.build(state);
|
||||
|
|
|
@ -18,6 +18,7 @@ import haxe.ui.core.Component;
|
|||
import funkin.data.event.SongEventRegistry;
|
||||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.containers.Box;
|
||||
import haxe.ui.containers.HBox;
|
||||
import haxe.ui.containers.Frame;
|
||||
import haxe.ui.events.UIEvent;
|
||||
import haxe.ui.data.ArrayDataSource;
|
||||
|
@ -214,7 +215,20 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
|
|||
input.text = field.type;
|
||||
}
|
||||
|
||||
target.addComponent(input);
|
||||
// Putting in a box so we can add a unit label easily if there is one.
|
||||
var inputBox:HBox = new HBox();
|
||||
inputBox.addComponent(input);
|
||||
|
||||
// Add a unit label if applicable.
|
||||
if (field.units != null && field.units != "")
|
||||
{
|
||||
var units:Label = new Label();
|
||||
units.text = field.units;
|
||||
units.verticalAlign = "center";
|
||||
inputBox.addComponent(units);
|
||||
}
|
||||
|
||||
target.addComponent(inputBox);
|
||||
|
||||
// Update the value of the event data.
|
||||
input.onChange = function(event:UIEvent) {
|
||||
|
|
|
@ -0,0 +1,693 @@
|
|||
package funkin.ui.debug.charting.toolboxes;
|
||||
|
||||
import funkin.audio.SoundGroup;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
import haxe.ui.components.Label;
|
||||
import flixel.addons.display.FlxTiledSprite;
|
||||
import flixel.math.FlxMath;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.backend.flixel.components.SpriteWrapper;
|
||||
import funkin.ui.debug.charting.commands.SetFreeplayPreviewCommand;
|
||||
import funkin.ui.haxeui.components.WaveformPlayer;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
import haxe.ui.containers.VBox;
|
||||
import haxe.ui.containers.Absolute;
|
||||
import haxe.ui.containers.ScrollView;
|
||||
import funkin.ui.freeplay.FreeplayState;
|
||||
import haxe.ui.containers.Frame;
|
||||
import haxe.ui.core.Screen;
|
||||
import haxe.ui.events.DragEvent;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
||||
/**
|
||||
* The toolbox which allows modifying information like Song Title, Scroll Speed, Characters/Stages, and starting BPM.
|
||||
*/
|
||||
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/freeplay.xml"))
|
||||
class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox
|
||||
{
|
||||
var waveformContainer:Absolute;
|
||||
var waveformScrollview:ScrollView;
|
||||
var waveformMusic:WaveformPlayer;
|
||||
var freeplayButtonZoomIn:Button;
|
||||
var freeplayButtonZoomOut:Button;
|
||||
var freeplayButtonPause:Button;
|
||||
var freeplayButtonPlay:Button;
|
||||
var freeplayButtonStop:Button;
|
||||
var freeplayPreviewStart:NumberStepper;
|
||||
var freeplayPreviewEnd:NumberStepper;
|
||||
var freeplayTicksContainer:Absolute;
|
||||
var playheadSprite:SpriteWrapper;
|
||||
var previewSelectionSprite:SpriteWrapper;
|
||||
|
||||
static final TICK_LABEL_X_OFFSET:Float = 4.0;
|
||||
|
||||
static final PLAYHEAD_RIGHT_PAD:Float = 10.0;
|
||||
|
||||
static final BASE_SCALE:Float = 64.0;
|
||||
static final STARTING_SCALE:Float = 1024.0;
|
||||
static final MIN_SCALE:Float = 4.0;
|
||||
static final WAVEFORM_ZOOM_MULT:Float = 1.5;
|
||||
|
||||
static final MAGIC_SCALE_BASE_TIME:Float = 5.0;
|
||||
|
||||
var waveformScale:Float = STARTING_SCALE;
|
||||
|
||||
var playheadAbsolutePos(get, set):Float;
|
||||
|
||||
function get_playheadAbsolutePos():Float
|
||||
{
|
||||
return playheadSprite.left;
|
||||
}
|
||||
|
||||
function set_playheadAbsolutePos(value:Float):Float
|
||||
{
|
||||
return playheadSprite.left = value;
|
||||
}
|
||||
|
||||
var playheadRelativePos(get, set):Float;
|
||||
|
||||
function get_playheadRelativePos():Float
|
||||
{
|
||||
return playheadSprite.left - waveformScrollview.hscrollPos;
|
||||
}
|
||||
|
||||
function set_playheadRelativePos(value:Float):Float
|
||||
{
|
||||
return playheadSprite.left = waveformScrollview.hscrollPos + value;
|
||||
}
|
||||
|
||||
var previewBoxStartPosAbsolute(get, set):Float;
|
||||
|
||||
function get_previewBoxStartPosAbsolute():Float
|
||||
{
|
||||
return previewSelectionSprite.left;
|
||||
}
|
||||
|
||||
function set_previewBoxStartPosAbsolute(value:Float):Float
|
||||
{
|
||||
return previewSelectionSprite.left = value;
|
||||
}
|
||||
|
||||
var previewBoxEndPosAbsolute(get, set):Float;
|
||||
|
||||
function get_previewBoxEndPosAbsolute():Float
|
||||
{
|
||||
return previewSelectionSprite.left + previewSelectionSprite.width;
|
||||
}
|
||||
|
||||
function set_previewBoxEndPosAbsolute(value:Float):Float
|
||||
{
|
||||
if (value < previewBoxStartPosAbsolute) return previewSelectionSprite.left = previewBoxStartPosAbsolute;
|
||||
return previewSelectionSprite.width = value - previewBoxStartPosAbsolute;
|
||||
}
|
||||
|
||||
var previewBoxStartPosRelative(get, set):Float;
|
||||
|
||||
function get_previewBoxStartPosRelative():Float
|
||||
{
|
||||
return previewSelectionSprite.left - waveformScrollview.hscrollPos;
|
||||
}
|
||||
|
||||
function set_previewBoxStartPosRelative(value:Float):Float
|
||||
{
|
||||
return previewSelectionSprite.left = waveformScrollview.hscrollPos + value;
|
||||
}
|
||||
|
||||
var previewBoxEndPosRelative(get, set):Float;
|
||||
|
||||
function get_previewBoxEndPosRelative():Float
|
||||
{
|
||||
return previewSelectionSprite.left + previewSelectionSprite.width - waveformScrollview.hscrollPos;
|
||||
}
|
||||
|
||||
function set_previewBoxEndPosRelative(value:Float):Float
|
||||
{
|
||||
if (value < previewBoxStartPosRelative) return previewSelectionSprite.left = previewBoxStartPosRelative;
|
||||
return previewSelectionSprite.width = value - previewBoxStartPosRelative;
|
||||
}
|
||||
|
||||
/**
|
||||
* The amount you need to multiply the zoom by such that, at the base zoom level, one tick is equal to `MAGIC_SCALE_BASE_TIME` seconds.
|
||||
*/
|
||||
var waveformMagicFactor:Float = 1.0;
|
||||
|
||||
var audioPreviewTracks:SoundGroup;
|
||||
|
||||
var tickTiledSprite:FlxTiledSprite;
|
||||
|
||||
var freeplayPreviewVolume(get, null):Float;
|
||||
|
||||
function get_freeplayPreviewVolume():Float
|
||||
{
|
||||
return freeplayMusicVolume.value * 2 / 100;
|
||||
}
|
||||
|
||||
var tickLabels:Array<Label> = [];
|
||||
|
||||
public function new(chartEditorState2:ChartEditorState)
|
||||
{
|
||||
super(chartEditorState2);
|
||||
|
||||
initialize();
|
||||
|
||||
this.onDialogClosed = onClose;
|
||||
}
|
||||
|
||||
function onClose(event:UIEvent)
|
||||
{
|
||||
chartEditorState.menubarItemToggleToolboxFreeplay.selected = false;
|
||||
}
|
||||
|
||||
function initialize():Void
|
||||
{
|
||||
// Starting position.
|
||||
// TODO: Save and load this.
|
||||
this.x = 150;
|
||||
this.y = 250;
|
||||
|
||||
freeplayMusicVolume.onChange = (_) -> {
|
||||
setTrackVolume(freeplayPreviewVolume);
|
||||
};
|
||||
freeplayMusicMute.onClick = (_) -> {
|
||||
toggleMuteTrack();
|
||||
};
|
||||
freeplayButtonZoomIn.onClick = (_) -> {
|
||||
zoomWaveformIn();
|
||||
};
|
||||
freeplayButtonZoomOut.onClick = (_) -> {
|
||||
zoomWaveformOut();
|
||||
};
|
||||
freeplayButtonPause.onClick = (_) -> {
|
||||
pauseAudioPreview();
|
||||
};
|
||||
freeplayButtonPlay.onClick = (_) -> {
|
||||
playAudioPreview();
|
||||
};
|
||||
freeplayButtonStop.onClick = (_) -> {
|
||||
stopAudioPreview();
|
||||
};
|
||||
testPreview.onClick = (_) -> {
|
||||
performPreview();
|
||||
};
|
||||
freeplayPreviewStart.onChange = (event:UIEvent) -> {
|
||||
if (event.value == chartEditorState.currentSongFreeplayPreviewStart) return;
|
||||
if (waveformDragStartPos != null) return; // The values are changing because we are dragging the preview.
|
||||
|
||||
chartEditorState.performCommand(new SetFreeplayPreviewCommand(event.value, null));
|
||||
refresh();
|
||||
}
|
||||
freeplayPreviewEnd.onChange = (event:UIEvent) -> {
|
||||
if (event.value == chartEditorState.currentSongFreeplayPreviewEnd) return;
|
||||
if (waveformDragStartPos != null) return; // The values are changing because we are dragging the preview.
|
||||
|
||||
chartEditorState.performCommand(new SetFreeplayPreviewCommand(null, event.value));
|
||||
refresh();
|
||||
}
|
||||
waveformScrollview.onScroll = (_) -> {
|
||||
if (!audioPreviewTracks.playing)
|
||||
{
|
||||
// Move the playhead if it would go out of view.
|
||||
var prevPlayheadRelativePos = playheadRelativePos;
|
||||
playheadRelativePos = FlxMath.bound(playheadRelativePos, 0, waveformScrollview.width - PLAYHEAD_RIGHT_PAD);
|
||||
trace('newPos: ${playheadRelativePos}');
|
||||
var diff = playheadRelativePos - prevPlayheadRelativePos;
|
||||
|
||||
if (diff != 0)
|
||||
{
|
||||
// We have to change the song time to match the playhead position when we move it.
|
||||
var currentWaveformIndex:Int = Std.int(playheadAbsolutePos * (waveformScale / BASE_SCALE * waveformMagicFactor));
|
||||
var targetSongTimeSeconds:Float = waveformMusic.waveform.waveformData.indexToSeconds(currentWaveformIndex);
|
||||
audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC;
|
||||
}
|
||||
|
||||
addOffsetsToAudioPreview();
|
||||
}
|
||||
else
|
||||
{
|
||||
// The scrollview probably changed because the song position changed.
|
||||
// If we try to move the song now it will glitch.
|
||||
}
|
||||
|
||||
// Either way, clipRect has changed, so we need to refresh the waveforms.
|
||||
refresh();
|
||||
};
|
||||
|
||||
initializeTicks();
|
||||
|
||||
refreshAudioPreview();
|
||||
refresh();
|
||||
refreshTicks();
|
||||
|
||||
waveformMusic.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
|
||||
onStartDragWaveform();
|
||||
});
|
||||
|
||||
freeplayTicksContainer.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
|
||||
onStartDragPlayhead();
|
||||
});
|
||||
}
|
||||
|
||||
function initializeTicks():Void
|
||||
{
|
||||
tickTiledSprite = new FlxTiledSprite(chartEditorState.offsetTickBitmap, 100, chartEditorState.offsetTickBitmap.height, true, false);
|
||||
freeplayTicksSprite.sprite = tickTiledSprite;
|
||||
tickTiledSprite.width = 5000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull the audio tracks from the chart editor state and create copies of them to play in the Offsets Toolbox.
|
||||
* These must be DEEP CLONES or else the editor will affect the audio preview!
|
||||
*/
|
||||
public function refreshAudioPreview():Void
|
||||
{
|
||||
if (audioPreviewTracks == null)
|
||||
{
|
||||
audioPreviewTracks = new SoundGroup();
|
||||
// Make sure audioPreviewTracks (and all its children) receives update() calls.
|
||||
chartEditorState.add(audioPreviewTracks);
|
||||
}
|
||||
else
|
||||
{
|
||||
audioPreviewTracks.stop();
|
||||
audioPreviewTracks.clear();
|
||||
}
|
||||
|
||||
var instTrack = chartEditorState.audioInstTrack.clone();
|
||||
audioPreviewTracks.add(instTrack);
|
||||
|
||||
var playerVoice = chartEditorState.audioVocalTrackGroup.getPlayerVoice();
|
||||
if (playerVoice != null) audioPreviewTracks.add(playerVoice.clone());
|
||||
|
||||
var opponentVoice = chartEditorState.audioVocalTrackGroup.getOpponentVoice();
|
||||
if (opponentVoice != null) audioPreviewTracks.add(opponentVoice.clone());
|
||||
|
||||
// Build player waveform.
|
||||
// waveformMusic.waveform.forceUpdate = true;
|
||||
var perfStart = haxe.Timer.stamp();
|
||||
var waveformData1 = playerVoice.waveformData;
|
||||
var waveformData2 = opponentVoice.waveformData;
|
||||
var waveformData3 = chartEditorState.audioInstTrack.waveformData;
|
||||
var waveformData = waveformData1.merge(waveformData2).merge(waveformData3);
|
||||
trace('Waveform data merging took: ${haxe.Timer.stamp() - perfStart} seconds');
|
||||
|
||||
waveformMusic.waveform.waveformData = waveformData;
|
||||
// Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it.
|
||||
waveformMusic.waveform.duration = instTrack.length / Constants.MS_PER_SEC;
|
||||
|
||||
addOffsetsToAudioPreview();
|
||||
}
|
||||
|
||||
public function refreshTicks():Void
|
||||
{
|
||||
while (tickLabels.length > 0)
|
||||
{
|
||||
var label = tickLabels.pop();
|
||||
freeplayTicksContainer.removeComponent(label);
|
||||
}
|
||||
|
||||
var labelYPos:Float = chartEditorState.offsetTickBitmap.height / 2;
|
||||
var labelHeight:Float = chartEditorState.offsetTickBitmap.height / 2;
|
||||
|
||||
var numberOfTicks:Int = Math.floor(waveformMusic.waveform.width / chartEditorState.offsetTickBitmap.width * 2) + 1;
|
||||
|
||||
for (index in 0...numberOfTicks)
|
||||
{
|
||||
var tickPos = chartEditorState.offsetTickBitmap.width / 2 * index;
|
||||
var tickTime = tickPos * (waveformScale / BASE_SCALE * waveformMagicFactor) / waveformMusic.waveform.waveformData.pointsPerSecond();
|
||||
|
||||
var tickLabel:Label = new Label();
|
||||
tickLabel.text = formatTime(tickTime);
|
||||
tickLabel.styleNames = "offset-ticks-label";
|
||||
tickLabel.height = labelHeight;
|
||||
// Positioning within offsetTicksContainer is absolute (relative to the container itself).
|
||||
tickLabel.top = labelYPos;
|
||||
tickLabel.left = tickPos + TICK_LABEL_X_OFFSET;
|
||||
|
||||
freeplayTicksContainer.addComponent(tickLabel);
|
||||
tickLabels.push(tickLabel);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(seconds:Float):String
|
||||
{
|
||||
if (seconds <= 0) return "0.0";
|
||||
|
||||
var integerSeconds = Math.floor(seconds);
|
||||
var decimalSeconds = Math.floor((seconds - integerSeconds) * 10);
|
||||
|
||||
if (integerSeconds < 60)
|
||||
{
|
||||
return '${integerSeconds}.${decimalSeconds}';
|
||||
}
|
||||
else
|
||||
{
|
||||
var integerMinutes = Math.floor(integerSeconds / 60);
|
||||
var remainingSeconds = integerSeconds % 60;
|
||||
var remainingSecondsPad:String = remainingSeconds < 10 ? '0$remainingSeconds' : '$remainingSeconds';
|
||||
|
||||
return '${integerMinutes}:${remainingSecondsPad}${decimalSeconds > 0 ? '.$decimalSeconds' : ''}';
|
||||
}
|
||||
}
|
||||
|
||||
function buildTickLabel():Void {}
|
||||
|
||||
public function onStartDragPlayhead():Void
|
||||
{
|
||||
Screen.instance.registerEvent(MouseEvent.MOUSE_MOVE, onDragPlayhead);
|
||||
Screen.instance.registerEvent(MouseEvent.MOUSE_UP, onStopDragPlayhead);
|
||||
|
||||
movePlayheadToMouse();
|
||||
}
|
||||
|
||||
public function onDragPlayhead(event:MouseEvent):Void
|
||||
{
|
||||
movePlayheadToMouse();
|
||||
}
|
||||
|
||||
public function onStopDragPlayhead(event:MouseEvent):Void
|
||||
{
|
||||
// Stop dragging.
|
||||
Screen.instance.unregisterEvent(MouseEvent.MOUSE_MOVE, onDragPlayhead);
|
||||
Screen.instance.unregisterEvent(MouseEvent.MOUSE_UP, onStopDragPlayhead);
|
||||
}
|
||||
|
||||
function movePlayheadToMouse():Void
|
||||
{
|
||||
// Determine the position of the mouse relative to the
|
||||
var mouseXPos = FlxG.mouse.x;
|
||||
|
||||
var relativeMouseXPos = mouseXPos - waveformScrollview.screenX;
|
||||
var targetPlayheadPos = relativeMouseXPos + waveformScrollview.hscrollPos;
|
||||
|
||||
// Move the playhead to the mouse position.
|
||||
playheadAbsolutePos = targetPlayheadPos;
|
||||
|
||||
// Move the audio preview to the playhead position.
|
||||
var currentWaveformIndex:Int = Std.int(playheadAbsolutePos * (waveformScale / BASE_SCALE * waveformMagicFactor));
|
||||
var targetSongTimeSeconds:Float = waveformMusic.waveform.waveformData.indexToSeconds(currentWaveformIndex);
|
||||
audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC;
|
||||
}
|
||||
|
||||
var waveformDragStartPos:Null<Float> = null;
|
||||
|
||||
var waveformDragPreviewStartPos:Float;
|
||||
var waveformDragPreviewEndPos:Float;
|
||||
|
||||
public function onStartDragWaveform():Void
|
||||
{
|
||||
waveformDragStartPos = FlxG.mouse.x;
|
||||
|
||||
Screen.instance.registerEvent(MouseEvent.MOUSE_MOVE, onDragWaveform);
|
||||
Screen.instance.registerEvent(MouseEvent.MOUSE_UP, onStopDragWaveform);
|
||||
}
|
||||
|
||||
public function onDragWaveform(event:MouseEvent):Void
|
||||
{
|
||||
// Set waveformDragPreviewStartPos and waveformDragPreviewEndPos to the position the drag started and the current mouse position.
|
||||
// This only affects the visuals.
|
||||
|
||||
var currentAbsMousePos = FlxG.mouse.x;
|
||||
var dragDiff = currentAbsMousePos - waveformDragStartPos;
|
||||
|
||||
var currentRelativeMousePos = currentAbsMousePos - waveformScrollview.screenX;
|
||||
var relativeStartPos = waveformDragStartPos - waveformScrollview.screenX;
|
||||
|
||||
var isDraggingRight = dragDiff > 0;
|
||||
var hasDraggedEnough = Math.abs(dragDiff) > 10;
|
||||
|
||||
if (hasDraggedEnough)
|
||||
{
|
||||
if (isDraggingRight)
|
||||
{
|
||||
waveformDragPreviewStartPos = relativeStartPos;
|
||||
waveformDragPreviewEndPos = currentRelativeMousePos;
|
||||
}
|
||||
else
|
||||
{
|
||||
waveformDragPreviewStartPos = currentRelativeMousePos;
|
||||
waveformDragPreviewEndPos = relativeStartPos;
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
public function onStopDragWaveform(event:MouseEvent):Void
|
||||
{
|
||||
Screen.instance.unregisterEvent(MouseEvent.MOUSE_MOVE, onDragWaveform);
|
||||
Screen.instance.unregisterEvent(MouseEvent.MOUSE_UP, onStopDragWaveform);
|
||||
|
||||
var previewStartPosAbsolute = waveformDragPreviewStartPos + waveformScrollview.hscrollPos;
|
||||
var previewStartPosIndex:Int = Std.int(previewStartPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor));
|
||||
var previewStartPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewStartPosIndex) * Constants.MS_PER_SEC);
|
||||
|
||||
var previewEndPosAbsolute = waveformDragPreviewEndPos + waveformScrollview.hscrollPos;
|
||||
var previewEndPosIndex:Int = Std.int(previewEndPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor));
|
||||
var previewEndPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewEndPosIndex) * Constants.MS_PER_SEC);
|
||||
|
||||
chartEditorState.performCommand(new SetFreeplayPreviewCommand(previewStartPosMs, previewEndPosMs));
|
||||
|
||||
waveformDragStartPos = null;
|
||||
waveformDragPreviewStartPos = 0;
|
||||
waveformDragPreviewEndPos = 0;
|
||||
|
||||
refresh();
|
||||
addOffsetsToAudioPreview();
|
||||
}
|
||||
|
||||
public function playAudioPreview():Void
|
||||
{
|
||||
if (isPerformingPreview) stopPerformingPreview();
|
||||
|
||||
audioPreviewTracks.volume = freeplayPreviewVolume;
|
||||
audioPreviewTracks.play(false, audioPreviewTracks.time);
|
||||
}
|
||||
|
||||
public function addOffsetsToAudioPreview():Void
|
||||
{
|
||||
var trackInst = audioPreviewTracks.members[0];
|
||||
if (trackInst != null)
|
||||
{
|
||||
trackInst.time -= chartEditorState.currentInstrumentalOffset;
|
||||
}
|
||||
|
||||
var trackPlayer = audioPreviewTracks.members[1];
|
||||
if (trackPlayer != null)
|
||||
{
|
||||
trackPlayer.time -= chartEditorState.currentVocalOffsetPlayer;
|
||||
}
|
||||
|
||||
var trackOpponent = audioPreviewTracks.members[2];
|
||||
if (trackOpponent != null)
|
||||
{
|
||||
trackOpponent.time -= chartEditorState.currentVocalOffsetOpponent;
|
||||
}
|
||||
}
|
||||
|
||||
public function pauseAudioPreview():Void
|
||||
{
|
||||
if (isPerformingPreview) stopPerformingPreview();
|
||||
|
||||
audioPreviewTracks.pause();
|
||||
}
|
||||
|
||||
public function stopAudioPreview():Void
|
||||
{
|
||||
if (isPerformingPreview) stopPerformingPreview();
|
||||
|
||||
audioPreviewTracks.stop();
|
||||
|
||||
audioPreviewTracks.time = 0;
|
||||
|
||||
waveformScrollview.hscrollPos = 0;
|
||||
playheadAbsolutePos = 0 + playheadSprite.width;
|
||||
refresh();
|
||||
addOffsetsToAudioPreview();
|
||||
}
|
||||
|
||||
public function zoomWaveformIn():Void
|
||||
{
|
||||
if (isPerformingPreview) stopPerformingPreview();
|
||||
|
||||
if (waveformScale > MIN_SCALE)
|
||||
{
|
||||
waveformScale = waveformScale / WAVEFORM_ZOOM_MULT;
|
||||
if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE;
|
||||
|
||||
trace('Zooming in, scale: ${waveformScale}');
|
||||
|
||||
// Update the playhead too!
|
||||
playheadAbsolutePos = playheadAbsolutePos * WAVEFORM_ZOOM_MULT;
|
||||
|
||||
// Recenter the scroll view on the playhead.
|
||||
var vaguelyCenterPlayheadOffset = waveformScrollview.width / 8;
|
||||
waveformScrollview.hscrollPos = playheadAbsolutePos - vaguelyCenterPlayheadOffset;
|
||||
|
||||
refresh();
|
||||
refreshTicks();
|
||||
}
|
||||
else
|
||||
{
|
||||
waveformScale = MIN_SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
public function zoomWaveformOut():Void
|
||||
{
|
||||
waveformScale = waveformScale * WAVEFORM_ZOOM_MULT;
|
||||
if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE;
|
||||
|
||||
trace('Zooming out, scale: ${waveformScale}');
|
||||
|
||||
// Update the playhead too!
|
||||
playheadAbsolutePos = playheadAbsolutePos / WAVEFORM_ZOOM_MULT;
|
||||
|
||||
// Recenter the scroll view on the playhead.
|
||||
var vaguelyCenterPlayheadOffset = waveformScrollview.width / 8;
|
||||
waveformScrollview.hscrollPos = playheadAbsolutePos - vaguelyCenterPlayheadOffset;
|
||||
|
||||
refresh();
|
||||
refreshTicks();
|
||||
}
|
||||
|
||||
public function setTrackVolume(volume:Float):Void
|
||||
{
|
||||
audioPreviewTracks.volume = volume;
|
||||
}
|
||||
|
||||
public function muteTrack():Void
|
||||
{
|
||||
audioPreviewTracks.muted = true;
|
||||
}
|
||||
|
||||
public function unmuteTrack():Void
|
||||
{
|
||||
audioPreviewTracks.muted = false;
|
||||
}
|
||||
|
||||
public function toggleMuteTrack():Void
|
||||
{
|
||||
audioPreviewTracks.muted = !audioPreviewTracks.muted;
|
||||
}
|
||||
|
||||
var isPerformingPreview:Bool = false;
|
||||
var isFadingOutPreview:Bool = false;
|
||||
|
||||
public function performPreview():Void
|
||||
{
|
||||
isPerformingPreview = true;
|
||||
isFadingOutPreview = false;
|
||||
audioPreviewTracks.play(true, chartEditorState.currentSongFreeplayPreviewStart);
|
||||
audioPreviewTracks.fadeIn(FreeplayState.FADE_IN_DURATION, FreeplayState.FADE_IN_START_VOLUME * freeplayPreviewVolume,
|
||||
FreeplayState.FADE_IN_END_VOLUME * freeplayPreviewVolume, null);
|
||||
}
|
||||
|
||||
public function stopPerformingPreview():Void
|
||||
{
|
||||
isPerformingPreview = false;
|
||||
isFadingOutPreview = false;
|
||||
audioPreviewTracks.volume = freeplayPreviewVolume;
|
||||
audioPreviewTracks.pause();
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
if (isPerformingPreview && !audioPreviewTracks.playing)
|
||||
{
|
||||
stopPerformingPreview();
|
||||
}
|
||||
|
||||
if (isPerformingPreview && audioPreviewTracks.playing)
|
||||
{
|
||||
var startFadeOutTime = chartEditorState.currentSongFreeplayPreviewEnd - (FreeplayState.FADE_OUT_DURATION * Constants.MS_PER_SEC);
|
||||
trace('startFadeOutTime: ${audioPreviewTracks.time} >= ${startFadeOutTime}');
|
||||
if (!isFadingOutPreview && audioPreviewTracks.time >= startFadeOutTime)
|
||||
{
|
||||
isFadingOutPreview = true;
|
||||
audioPreviewTracks.fadeOut(FreeplayState.FADE_OUT_DURATION, FreeplayState.FADE_OUT_END_VOLUME * freeplayPreviewVolume, (_) -> {
|
||||
trace('Stop performing preview! ${audioPreviewTracks.time}');
|
||||
stopPerformingPreview();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (audioPreviewTracks.playing)
|
||||
{
|
||||
var targetScrollPos:Float = waveformMusic.waveform.waveformData.secondsToIndex(audioPreviewTracks.time / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor);
|
||||
// waveformScrollview.hscrollPos = targetScrollPos;
|
||||
var prevPlayheadAbsolutePos = playheadAbsolutePos;
|
||||
playheadAbsolutePos = targetScrollPos;
|
||||
var playheadDiff = playheadAbsolutePos - prevPlayheadAbsolutePos;
|
||||
|
||||
// BEHAVIOR C.
|
||||
// Copy Audacity!
|
||||
// If the playhead is out of view, jump forward or backward by one screen width until it's in view.
|
||||
if (playheadAbsolutePos < waveformScrollview.hscrollPos)
|
||||
{
|
||||
waveformScrollview.hscrollPos -= waveformScrollview.width;
|
||||
}
|
||||
if (playheadAbsolutePos > waveformScrollview.hscrollPos + waveformScrollview.width)
|
||||
{
|
||||
waveformScrollview.hscrollPos += waveformScrollview.width;
|
||||
}
|
||||
}
|
||||
freeplayLabelTime.text = formatTime(audioPreviewTracks.time / Constants.MS_PER_SEC);
|
||||
if (waveformDragStartPos != null && (waveformDragPreviewStartPos > 0 && waveformDragPreviewEndPos > 0))
|
||||
{
|
||||
var previewStartPosAbsolute = waveformDragPreviewStartPos + waveformScrollview.hscrollPos;
|
||||
var previewStartPosIndex:Int = Std.int(previewStartPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor));
|
||||
var previewStartPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewStartPosIndex) * Constants.MS_PER_SEC);
|
||||
|
||||
var previewEndPosAbsolute = waveformDragPreviewEndPos + waveformScrollview.hscrollPos;
|
||||
var previewEndPosIndex:Int = Std.int(previewEndPosAbsolute * (waveformScale / BASE_SCALE * waveformMagicFactor));
|
||||
var previewEndPosMs:Int = Std.int(waveformMusic.waveform.waveformData.indexToSeconds(previewEndPosIndex) * Constants.MS_PER_SEC);
|
||||
|
||||
// Set the values in milliseconds.
|
||||
freeplayPreviewStart.value = previewStartPosMs;
|
||||
freeplayPreviewEnd.value = previewEndPosMs;
|
||||
|
||||
previewBoxStartPosAbsolute = previewStartPosAbsolute;
|
||||
previewBoxEndPosAbsolute = previewEndPosAbsolute;
|
||||
}
|
||||
else
|
||||
{
|
||||
previewBoxStartPosAbsolute = waveformMusic.waveform.waveformData.secondsToIndex(chartEditorState.currentSongFreeplayPreviewStart / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor);
|
||||
previewBoxEndPosAbsolute = waveformMusic.waveform.waveformData.secondsToIndex(chartEditorState.currentSongFreeplayPreviewEnd / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor);
|
||||
|
||||
freeplayPreviewStart.value = chartEditorState.currentSongFreeplayPreviewStart;
|
||||
freeplayPreviewEnd.value = chartEditorState.currentSongFreeplayPreviewEnd;
|
||||
}
|
||||
}
|
||||
|
||||
public override function refresh():Void
|
||||
{
|
||||
super.refresh();
|
||||
|
||||
waveformMagicFactor = MAGIC_SCALE_BASE_TIME / (chartEditorState.offsetTickBitmap.width / waveformMusic.waveform.waveformData.pointsPerSecond());
|
||||
|
||||
var currentZoomFactor = waveformScale / BASE_SCALE * waveformMagicFactor;
|
||||
|
||||
var maxWidth:Int = -1;
|
||||
|
||||
waveformMusic.waveform.time = -chartEditorState.currentInstrumentalOffset / Constants.MS_PER_SEC;
|
||||
waveformMusic.waveform.width = (waveformMusic.waveform.waveformData?.length ?? 1000) / currentZoomFactor;
|
||||
if (waveformMusic.waveform.width > maxWidth) maxWidth = Std.int(waveformMusic.waveform.width);
|
||||
waveformMusic.waveform.height = 65;
|
||||
waveformMusic.waveform.markDirty();
|
||||
|
||||
waveformContainer.width = maxWidth;
|
||||
tickTiledSprite.width = maxWidth;
|
||||
}
|
||||
|
||||
public static function build(chartEditorState:ChartEditorState):ChartEditorFreeplayToolbox
|
||||
{
|
||||
return new ChartEditorFreeplayToolbox(chartEditorState);
|
||||
}
|
||||
}
|
|
@ -35,8 +35,6 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
var buttonCharacterGirlfriend:Button;
|
||||
var buttonCharacterOpponent:Button;
|
||||
var inputBPM:NumberStepper;
|
||||
var inputOffsetInst:NumberStepper;
|
||||
var inputOffsetVocal:NumberStepper;
|
||||
var labelScrollSpeed:Label;
|
||||
var inputScrollSpeed:Slider;
|
||||
var frameVariation:Frame;
|
||||
|
@ -138,25 +136,6 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
chartEditorState.updateTimeSignature();
|
||||
};
|
||||
|
||||
inputOffsetInst.onChange = function(event:UIEvent) {
|
||||
if (event.value == null) return;
|
||||
|
||||
chartEditorState.currentInstrumentalOffset = event.value;
|
||||
Conductor.instance.instrumentalOffset = event.value;
|
||||
// Update song length.
|
||||
chartEditorState.songLengthInMs = (chartEditorState.audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset;
|
||||
};
|
||||
|
||||
inputOffsetVocal.onChange = function(event:UIEvent) {
|
||||
if (event.value == null) return;
|
||||
|
||||
chartEditorState.currentVocalOffset = event.value;
|
||||
if (chartEditorState.audioVocalTrackGroup != null)
|
||||
{
|
||||
chartEditorState.audioVocalTrackGroup.playerVoicesOffset = event.value;
|
||||
chartEditorState.audioVocalTrackGroup.opponentVoicesOffset = event.value;
|
||||
}
|
||||
};
|
||||
inputScrollSpeed.onChange = function(event:UIEvent) {
|
||||
var valid:Bool = event.target.value != null && event.target.value > 0;
|
||||
|
||||
|
@ -172,6 +151,10 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x';
|
||||
};
|
||||
|
||||
inputDifficultyRating.onChange = function(event:UIEvent) {
|
||||
chartEditorState.currentSongChartDifficultyRating = event.target.value;
|
||||
};
|
||||
|
||||
buttonCharacterOpponent.onClick = function(_) {
|
||||
chartEditorState.openCharacterDropdown(CharacterType.DAD, false);
|
||||
};
|
||||
|
@ -196,8 +179,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
|
||||
inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle;
|
||||
inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
|
||||
inputOffsetInst.value = chartEditorState.currentSongMetadata.offsets.getInstrumentalOffset();
|
||||
inputOffsetVocal.value = chartEditorState.currentSongMetadata.offsets.getVocalOffset(chartEditorState.currentSongMetadata.playData.characters.player);
|
||||
inputDifficultyRating.value = chartEditorState.currentSongChartDifficultyRating;
|
||||
inputScrollSpeed.value = chartEditorState.currentSongChartScrollSpeed;
|
||||
labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x';
|
||||
frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';
|
||||
|
|
|
@ -0,0 +1,845 @@
|
|||
package funkin.ui.debug.charting.toolboxes;
|
||||
|
||||
import funkin.audio.SoundGroup;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
import haxe.ui.components.Label;
|
||||
import flixel.addons.display.FlxTiledSprite;
|
||||
import flixel.math.FlxMath;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.backend.flixel.components.SpriteWrapper;
|
||||
import funkin.ui.debug.charting.commands.SetAudioOffsetCommand;
|
||||
import funkin.ui.haxeui.components.WaveformPlayer;
|
||||
import funkin.audio.waveform.WaveformDataParser;
|
||||
import haxe.ui.containers.VBox;
|
||||
import haxe.ui.containers.Absolute;
|
||||
import haxe.ui.containers.ScrollView;
|
||||
import haxe.ui.containers.Frame;
|
||||
import haxe.ui.core.Screen;
|
||||
import haxe.ui.events.DragEvent;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
||||
/**
|
||||
* The toolbox which allows modifying information like Song Title, Scroll Speed, Characters/Stages, and starting BPM.
|
||||
*/
|
||||
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
|
||||
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/offsets.xml"))
|
||||
class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox
|
||||
{
|
||||
var waveformContainer:Absolute;
|
||||
var waveformScrollview:ScrollView;
|
||||
var waveformPlayer:WaveformPlayer;
|
||||
var waveformOpponent:WaveformPlayer;
|
||||
var waveformInstrumental:WaveformPlayer;
|
||||
var offsetButtonZoomIn:Button;
|
||||
var offsetButtonZoomOut:Button;
|
||||
var offsetButtonPause:Button;
|
||||
var offsetButtonPlay:Button;
|
||||
var offsetButtonStop:Button;
|
||||
var offsetStepperPlayer:NumberStepper;
|
||||
var offsetStepperOpponent:NumberStepper;
|
||||
var offsetStepperInstrumental:NumberStepper;
|
||||
var offsetTicksContainer:Absolute;
|
||||
var playheadSprite:SpriteWrapper;
|
||||
|
||||
static final TICK_LABEL_X_OFFSET:Float = 4.0;
|
||||
|
||||
static final PLAYHEAD_RIGHT_PAD:Float = 10.0;
|
||||
|
||||
static final BASE_SCALE:Float = 64.0;
|
||||
static final MIN_SCALE:Float = 4.0;
|
||||
static final WAVEFORM_ZOOM_MULT:Float = 1.5;
|
||||
|
||||
static final MAGIC_SCALE_BASE_TIME:Float = 5.0;
|
||||
|
||||
var waveformScale:Float = BASE_SCALE;
|
||||
|
||||
var playheadAbsolutePos(get, set):Float;
|
||||
|
||||
function get_playheadAbsolutePos():Float
|
||||
{
|
||||
return playheadSprite.left;
|
||||
}
|
||||
|
||||
function set_playheadAbsolutePos(value:Float):Float
|
||||
{
|
||||
return playheadSprite.left = value;
|
||||
}
|
||||
|
||||
var playheadRelativePos(get, set):Float;
|
||||
|
||||
function get_playheadRelativePos():Float
|
||||
{
|
||||
return playheadSprite.left - waveformScrollview.hscrollPos;
|
||||
}
|
||||
|
||||
function set_playheadRelativePos(value:Float):Float
|
||||
{
|
||||
return playheadSprite.left = waveformScrollview.hscrollPos + value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The amount you need to multiply the zoom by such that, at the base zoom level, one tick is equal to `MAGIC_SCALE_BASE_TIME` seconds.
|
||||
*/
|
||||
var waveformMagicFactor:Float = 1.0;
|
||||
|
||||
var audioPreviewTracks:SoundGroup;
|
||||
|
||||
var tickTiledSprite:FlxTiledSprite;
|
||||
|
||||
var tickLabels:Array<Label> = [];
|
||||
|
||||
// Local store of the audio offsets, so we can detect when they change.
|
||||
var audioPreviewPlayerOffset:Float = 0;
|
||||
var audioPreviewOpponentOffset:Float = 0;
|
||||
var audioPreviewInstrumentalOffset:Float = 0;
|
||||
|
||||
public function new(chartEditorState2:ChartEditorState)
|
||||
{
|
||||
super(chartEditorState2);
|
||||
|
||||
initialize();
|
||||
|
||||
this.onDialogClosed = onClose;
|
||||
}
|
||||
|
||||
function onClose(event:UIEvent)
|
||||
{
|
||||
chartEditorState.menubarItemToggleToolboxOffsets.selected = false;
|
||||
}
|
||||
|
||||
function initialize():Void
|
||||
{
|
||||
// Starting position.
|
||||
// TODO: Save and load this.
|
||||
this.x = 150;
|
||||
this.y = 250;
|
||||
|
||||
offsetPlayerVolume.onChange = (_) -> {
|
||||
var targetVolume = offsetPlayerVolume.value * 2 / 100;
|
||||
setTrackVolume(PLAYER, targetVolume);
|
||||
};
|
||||
offsetPlayerMute.onClick = (_) -> {
|
||||
toggleMuteTrack(PLAYER);
|
||||
};
|
||||
offsetPlayerSolo.onClick = (_) -> {
|
||||
soloTrack(PLAYER);
|
||||
};
|
||||
offsetOpponentVolume.onChange = (_) -> {
|
||||
var targetVolume = offsetOpponentVolume.value * 2 / 100;
|
||||
setTrackVolume(OPPONENT, targetVolume);
|
||||
};
|
||||
offsetOpponentMute.onClick = (_) -> {
|
||||
toggleMuteTrack(OPPONENT);
|
||||
};
|
||||
offsetOpponentSolo.onClick = (_) -> {
|
||||
soloTrack(OPPONENT);
|
||||
};
|
||||
offsetInstrumentalVolume.onChange = (_) -> {
|
||||
var targetVolume = offsetInstrumentalVolume.value * 2 / 100;
|
||||
setTrackVolume(INSTRUMENTAL, targetVolume);
|
||||
};
|
||||
offsetInstrumentalMute.onClick = (_) -> {
|
||||
toggleMuteTrack(INSTRUMENTAL);
|
||||
};
|
||||
offsetInstrumentalSolo.onClick = (_) -> {
|
||||
soloTrack(INSTRUMENTAL);
|
||||
};
|
||||
offsetButtonZoomIn.onClick = (_) -> {
|
||||
zoomWaveformIn();
|
||||
};
|
||||
offsetButtonZoomOut.onClick = (_) -> {
|
||||
zoomWaveformOut();
|
||||
};
|
||||
offsetButtonPause.onClick = (_) -> {
|
||||
pauseAudioPreview();
|
||||
};
|
||||
offsetButtonPlay.onClick = (_) -> {
|
||||
playAudioPreview();
|
||||
};
|
||||
offsetButtonStop.onClick = (_) -> {
|
||||
stopAudioPreview();
|
||||
};
|
||||
offsetStepperPlayer.onChange = (event:UIEvent) -> {
|
||||
if (event.value == chartEditorState.currentVocalOffsetPlayer) return;
|
||||
if (dragWaveform != null) return;
|
||||
|
||||
chartEditorState.performCommand(new SetAudioOffsetCommand(PLAYER, event.value));
|
||||
refresh();
|
||||
}
|
||||
offsetStepperOpponent.onChange = (event:UIEvent) -> {
|
||||
if (event.value == chartEditorState.currentVocalOffsetOpponent) return;
|
||||
if (dragWaveform != null) return;
|
||||
|
||||
chartEditorState.performCommand(new SetAudioOffsetCommand(OPPONENT, event.value));
|
||||
refresh();
|
||||
}
|
||||
offsetStepperInstrumental.onChange = (event:UIEvent) -> {
|
||||
if (event.value == chartEditorState.currentInstrumentalOffset) return;
|
||||
if (dragWaveform != null) return;
|
||||
|
||||
chartEditorState.performCommand(new SetAudioOffsetCommand(INSTRUMENTAL, event.value));
|
||||
refresh();
|
||||
}
|
||||
waveformScrollview.onScroll = (_) -> {
|
||||
if (!audioPreviewTracks.playing)
|
||||
{
|
||||
// Move the playhead if it would go out of view.
|
||||
var prevPlayheadRelativePos = playheadRelativePos;
|
||||
playheadRelativePos = FlxMath.bound(playheadRelativePos, 0, waveformScrollview.width - PLAYHEAD_RIGHT_PAD);
|
||||
var diff = playheadRelativePos - prevPlayheadRelativePos;
|
||||
|
||||
if (diff != 0)
|
||||
{
|
||||
// We have to change the song time to match the playhead position when we move it.
|
||||
var currentWaveformIndex:Int = Std.int(playheadAbsolutePos * (waveformScale / BASE_SCALE * waveformMagicFactor));
|
||||
var targetSongTimeSeconds:Float = waveformPlayer.waveform.waveformData.indexToSeconds(currentWaveformIndex);
|
||||
audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC;
|
||||
}
|
||||
|
||||
addOffsetsToAudioPreview();
|
||||
}
|
||||
else
|
||||
{
|
||||
// The scrollview probably changed because the song position changed.
|
||||
// If we try to move the song now it will glitch.
|
||||
}
|
||||
|
||||
// Either way, clipRect has changed, so we need to refresh the waveforms.
|
||||
refresh();
|
||||
};
|
||||
|
||||
initializeTicks();
|
||||
|
||||
refreshAudioPreview();
|
||||
refresh();
|
||||
refreshTicks();
|
||||
|
||||
waveformPlayer.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
|
||||
onStartDragWaveform(PLAYER);
|
||||
});
|
||||
waveformOpponent.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
|
||||
onStartDragWaveform(OPPONENT);
|
||||
});
|
||||
waveformInstrumental.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
|
||||
onStartDragWaveform(INSTRUMENTAL);
|
||||
});
|
||||
|
||||
offsetTicksContainer.registerEvent(MouseEvent.MOUSE_DOWN, (_) -> {
|
||||
onStartDragPlayhead();
|
||||
});
|
||||
}
|
||||
|
||||
function initializeTicks():Void
|
||||
{
|
||||
tickTiledSprite = new FlxTiledSprite(chartEditorState.offsetTickBitmap, 100, chartEditorState.offsetTickBitmap.height, true, false);
|
||||
offsetTicksSprite.sprite = tickTiledSprite;
|
||||
tickTiledSprite.width = 5000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull the audio tracks from the chart editor state and create copies of them to play in the Offsets Toolbox.
|
||||
* These must be DEEP CLONES or else the editor will affect the audio preview!
|
||||
*/
|
||||
public function refreshAudioPreview():Void
|
||||
{
|
||||
if (audioPreviewTracks == null)
|
||||
{
|
||||
audioPreviewTracks = new SoundGroup();
|
||||
// Make sure audioPreviewTracks (and all its children) receives update() calls.
|
||||
chartEditorState.add(audioPreviewTracks);
|
||||
}
|
||||
else
|
||||
{
|
||||
audioPreviewTracks.stop();
|
||||
audioPreviewTracks.clear();
|
||||
}
|
||||
|
||||
var instTrack = chartEditorState.audioInstTrack.clone();
|
||||
audioPreviewTracks.add(instTrack);
|
||||
|
||||
var playerVoice = chartEditorState.audioVocalTrackGroup.getPlayerVoice();
|
||||
if (playerVoice != null) audioPreviewTracks.add(playerVoice.clone());
|
||||
|
||||
var opponentVoice = chartEditorState.audioVocalTrackGroup.getOpponentVoice();
|
||||
if (opponentVoice != null) audioPreviewTracks.add(opponentVoice.clone());
|
||||
|
||||
// Build player waveform.
|
||||
// waveformPlayer.waveform.forceUpdate = true;
|
||||
waveformPlayer.waveform.waveformData = playerVoice.waveformData;
|
||||
// Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it.
|
||||
waveformPlayer.waveform.duration = playerVoice.length / Constants.MS_PER_SEC;
|
||||
|
||||
// Build opponent waveform.
|
||||
// waveformOpponent.waveform.forceUpdate = true;
|
||||
waveformOpponent.waveform.waveformData = opponentVoice.waveformData;
|
||||
waveformOpponent.waveform.duration = opponentVoice.length / Constants.MS_PER_SEC;
|
||||
|
||||
// Build instrumental waveform.
|
||||
// waveformInstrumental.waveform.forceUpdate = true;
|
||||
waveformInstrumental.waveform.waveformData = chartEditorState.audioInstTrack.waveformData;
|
||||
waveformInstrumental.waveform.duration = instTrack.length / Constants.MS_PER_SEC;
|
||||
|
||||
addOffsetsToAudioPreview();
|
||||
}
|
||||
|
||||
public function refreshTicks():Void
|
||||
{
|
||||
while (tickLabels.length > 0)
|
||||
{
|
||||
var label = tickLabels.pop();
|
||||
offsetTicksContainer.removeComponent(label);
|
||||
}
|
||||
|
||||
var labelYPos:Float = chartEditorState.offsetTickBitmap.height / 2;
|
||||
var labelHeight:Float = chartEditorState.offsetTickBitmap.height / 2;
|
||||
|
||||
var numberOfTicks:Int = Math.floor(waveformInstrumental.waveform.width / chartEditorState.offsetTickBitmap.width * 2) + 1;
|
||||
|
||||
for (index in 0...numberOfTicks)
|
||||
{
|
||||
var tickPos = chartEditorState.offsetTickBitmap.width / 2 * index;
|
||||
var tickTime = tickPos * (waveformScale / BASE_SCALE * waveformMagicFactor) / waveformInstrumental.waveform.waveformData.pointsPerSecond();
|
||||
|
||||
var tickLabel:Label = new Label();
|
||||
tickLabel.text = formatTime(tickTime);
|
||||
tickLabel.styleNames = "offset-ticks-label";
|
||||
tickLabel.height = labelHeight;
|
||||
// Positioning within offsetTicksContainer is absolute (relative to the container itself).
|
||||
tickLabel.top = labelYPos;
|
||||
tickLabel.left = tickPos + TICK_LABEL_X_OFFSET;
|
||||
|
||||
offsetTicksContainer.addComponent(tickLabel);
|
||||
tickLabels.push(tickLabel);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(seconds:Float):String
|
||||
{
|
||||
if (seconds <= 0) return "0.0";
|
||||
|
||||
var integerSeconds = Math.floor(seconds);
|
||||
var decimalSeconds = Math.floor((seconds - integerSeconds) * 10);
|
||||
|
||||
if (integerSeconds < 60)
|
||||
{
|
||||
return '${integerSeconds}.${decimalSeconds}';
|
||||
}
|
||||
else
|
||||
{
|
||||
var integerMinutes = Math.floor(integerSeconds / 60);
|
||||
var remainingSeconds = integerSeconds % 60;
|
||||
var remainingSecondsPad:String = remainingSeconds < 10 ? '0$remainingSeconds' : '$remainingSeconds';
|
||||
|
||||
return '${integerMinutes}:${remainingSecondsPad}${decimalSeconds > 0 ? '.$decimalSeconds' : ''}';
|
||||
}
|
||||
}
|
||||
|
||||
function buildTickLabel():Void {}
|
||||
|
||||
public function onStartDragPlayhead():Void
|
||||
{
|
||||
Screen.instance.registerEvent(MouseEvent.MOUSE_MOVE, onDragPlayhead);
|
||||
Screen.instance.registerEvent(MouseEvent.MOUSE_UP, onStopDragPlayhead);
|
||||
|
||||
movePlayheadToMouse();
|
||||
}
|
||||
|
||||
public function onDragPlayhead(event:MouseEvent):Void
|
||||
{
|
||||
movePlayheadToMouse();
|
||||
}
|
||||
|
||||
public function onStopDragPlayhead(event:MouseEvent):Void
|
||||
{
|
||||
// Stop dragging.
|
||||
Screen.instance.unregisterEvent(MouseEvent.MOUSE_MOVE, onDragPlayhead);
|
||||
Screen.instance.unregisterEvent(MouseEvent.MOUSE_UP, onStopDragPlayhead);
|
||||
}
|
||||
|
||||
function movePlayheadToMouse():Void
|
||||
{
|
||||
// Determine the position of the mouse relative to the
|
||||
var mouseXPos = FlxG.mouse.x;
|
||||
|
||||
var relativeMouseXPos = mouseXPos - waveformScrollview.screenX;
|
||||
var targetPlayheadPos = relativeMouseXPos + waveformScrollview.hscrollPos;
|
||||
|
||||
// Move the playhead to the mouse position.
|
||||
playheadAbsolutePos = targetPlayheadPos;
|
||||
|
||||
// Move the audio preview to the playhead position.
|
||||
var currentWaveformIndex:Int = Std.int(playheadAbsolutePos * (waveformScale / BASE_SCALE * waveformMagicFactor));
|
||||
var targetSongTimeSeconds:Float = waveformPlayer.waveform.waveformData.indexToSeconds(currentWaveformIndex);
|
||||
audioPreviewTracks.time = targetSongTimeSeconds * Constants.MS_PER_SEC;
|
||||
}
|
||||
|
||||
public function onStartDragWaveform(waveform:Waveform):Void
|
||||
{
|
||||
dragMousePosition = FlxG.mouse.x;
|
||||
dragWaveform = waveform;
|
||||
|
||||
Screen.instance.registerEvent(MouseEvent.MOUSE_MOVE, onDragWaveform);
|
||||
Screen.instance.registerEvent(MouseEvent.MOUSE_UP, onStopDragWaveform);
|
||||
}
|
||||
|
||||
var dragMousePosition:Float = 0;
|
||||
var dragWaveform:Waveform = null;
|
||||
var dragOffsetMs:Float = 0;
|
||||
|
||||
public function onDragWaveform(event:MouseEvent):Void
|
||||
{
|
||||
var newDragMousePosition = FlxG.mouse.x;
|
||||
var deltaMousePosition = newDragMousePosition - dragMousePosition;
|
||||
|
||||
if (deltaMousePosition == 0) return;
|
||||
|
||||
var deltaPixels:Float = deltaMousePosition * (waveformScale / BASE_SCALE * waveformMagicFactor);
|
||||
var deltaMilliseconds:Float = switch (dragWaveform)
|
||||
{
|
||||
case PLAYER:
|
||||
deltaPixels / waveformPlayer.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC;
|
||||
case OPPONENT:
|
||||
deltaPixels / waveformOpponent.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC;
|
||||
case INSTRUMENTAL:
|
||||
deltaPixels / waveformInstrumental.waveform.waveformData.pointsPerSecond() * Constants.MS_PER_SEC;
|
||||
};
|
||||
|
||||
switch (dragWaveform)
|
||||
{
|
||||
case PLAYER:
|
||||
// chartEditorState.currentVocalOffsetPlayer += deltaMilliseconds;
|
||||
dragOffsetMs += deltaMilliseconds;
|
||||
offsetStepperPlayer.value += deltaMilliseconds;
|
||||
case OPPONENT:
|
||||
// chartEditorState.currentVocalOffsetOpponent += deltaMilliseconds;
|
||||
dragOffsetMs += deltaMilliseconds;
|
||||
offsetStepperOpponent.value += deltaMilliseconds;
|
||||
case INSTRUMENTAL:
|
||||
// chartEditorState.currentInstrumentalOffset += deltaMilliseconds;
|
||||
dragOffsetMs += deltaMilliseconds;
|
||||
offsetStepperInstrumental.value += deltaMilliseconds;
|
||||
}
|
||||
|
||||
dragMousePosition = newDragMousePosition;
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
public function onStopDragWaveform(event:MouseEvent):Void
|
||||
{
|
||||
// Stop dragging.
|
||||
Screen.instance.unregisterEvent(MouseEvent.MOUSE_MOVE, onDragWaveform);
|
||||
Screen.instance.unregisterEvent(MouseEvent.MOUSE_UP, onStopDragWaveform);
|
||||
|
||||
// Apply the offset change after dragging happens.
|
||||
// We only do this once per drag so we don't get 20 commands a second in the history.
|
||||
if (dragOffsetMs != 0)
|
||||
{
|
||||
// false to not refresh this toolbox, we will manually do that later.
|
||||
switch (dragWaveform)
|
||||
{
|
||||
case PLAYER:
|
||||
chartEditorState.performCommand(new SetAudioOffsetCommand(PLAYER, chartEditorState.currentVocalOffsetPlayer + dragOffsetMs, false));
|
||||
case OPPONENT:
|
||||
chartEditorState.performCommand(new SetAudioOffsetCommand(OPPONENT, chartEditorState.currentVocalOffsetOpponent + dragOffsetMs, false));
|
||||
case INSTRUMENTAL:
|
||||
chartEditorState.performCommand(new SetAudioOffsetCommand(INSTRUMENTAL, chartEditorState.currentInstrumentalOffset + dragOffsetMs, false));
|
||||
}
|
||||
}
|
||||
|
||||
dragOffsetMs = 0;
|
||||
dragMousePosition = 0;
|
||||
dragWaveform = null;
|
||||
|
||||
refresh();
|
||||
addOffsetsToAudioPreview();
|
||||
}
|
||||
|
||||
public function playAudioPreview():Void
|
||||
{
|
||||
audioPreviewTracks.play(false, audioPreviewTracks.time);
|
||||
}
|
||||
|
||||
public function addOffsetsToAudioPreview():Void
|
||||
{
|
||||
var trackInst = audioPreviewTracks.members[0];
|
||||
if (trackInst != null)
|
||||
{
|
||||
audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset;
|
||||
trackInst.time -= audioPreviewInstrumentalOffset;
|
||||
}
|
||||
|
||||
var trackPlayer = audioPreviewTracks.members[1];
|
||||
if (trackPlayer != null)
|
||||
{
|
||||
audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer;
|
||||
trackPlayer.time -= audioPreviewPlayerOffset;
|
||||
}
|
||||
|
||||
var trackOpponent = audioPreviewTracks.members[2];
|
||||
if (trackOpponent != null)
|
||||
{
|
||||
audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent;
|
||||
trackOpponent.time -= audioPreviewOpponentOffset;
|
||||
}
|
||||
}
|
||||
|
||||
public function pauseAudioPreview():Void
|
||||
{
|
||||
audioPreviewTracks.pause();
|
||||
}
|
||||
|
||||
public function stopAudioPreview():Void
|
||||
{
|
||||
audioPreviewTracks.stop();
|
||||
|
||||
audioPreviewTracks.time = 0;
|
||||
|
||||
var trackInst = audioPreviewTracks.members[0];
|
||||
if (trackInst != null)
|
||||
{
|
||||
audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset;
|
||||
trackInst.time = -audioPreviewInstrumentalOffset;
|
||||
}
|
||||
|
||||
var trackPlayer = audioPreviewTracks.members[1];
|
||||
if (trackPlayer != null)
|
||||
{
|
||||
audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer;
|
||||
trackPlayer.time = -audioPreviewPlayerOffset;
|
||||
}
|
||||
|
||||
var trackOpponent = audioPreviewTracks.members[2];
|
||||
if (trackOpponent != null)
|
||||
{
|
||||
audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent;
|
||||
trackOpponent.time = -audioPreviewOpponentOffset;
|
||||
}
|
||||
|
||||
waveformScrollview.hscrollPos = 0;
|
||||
playheadAbsolutePos = 0 + playheadSprite.width;
|
||||
refresh();
|
||||
addOffsetsToAudioPreview();
|
||||
}
|
||||
|
||||
public function zoomWaveformIn():Void
|
||||
{
|
||||
if (waveformScale > MIN_SCALE)
|
||||
{
|
||||
waveformScale = waveformScale / WAVEFORM_ZOOM_MULT;
|
||||
if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE;
|
||||
|
||||
// Update the playhead too!
|
||||
playheadAbsolutePos = playheadAbsolutePos * WAVEFORM_ZOOM_MULT;
|
||||
|
||||
// Recenter the scroll view on the playhead.
|
||||
var vaguelyCenterPlayheadOffset = waveformScrollview.width / 8;
|
||||
waveformScrollview.hscrollPos = playheadAbsolutePos - vaguelyCenterPlayheadOffset;
|
||||
|
||||
refresh();
|
||||
refreshTicks();
|
||||
}
|
||||
else
|
||||
{
|
||||
waveformScale = MIN_SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
public function zoomWaveformOut():Void
|
||||
{
|
||||
waveformScale = waveformScale * WAVEFORM_ZOOM_MULT;
|
||||
if (waveformScale < MIN_SCALE) waveformScale = MIN_SCALE;
|
||||
|
||||
// Update the playhead too!
|
||||
playheadAbsolutePos = playheadAbsolutePos / WAVEFORM_ZOOM_MULT;
|
||||
|
||||
// Recenter the scroll view on the playhead.
|
||||
var vaguelyCenterPlayheadOffset = waveformScrollview.width / 8;
|
||||
waveformScrollview.hscrollPos = playheadAbsolutePos - vaguelyCenterPlayheadOffset;
|
||||
|
||||
refresh();
|
||||
refreshTicks();
|
||||
}
|
||||
|
||||
public function setTrackVolume(target:Waveform, volume:Float):Void
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case Waveform.INSTRUMENTAL:
|
||||
var trackInst = audioPreviewTracks.members[0];
|
||||
if (trackInst != null)
|
||||
{
|
||||
trackInst.volume = volume;
|
||||
}
|
||||
case Waveform.PLAYER:
|
||||
var trackPlayer = audioPreviewTracks.members[1];
|
||||
if (trackPlayer != null)
|
||||
{
|
||||
trackPlayer.volume = volume;
|
||||
}
|
||||
case Waveform.OPPONENT:
|
||||
var trackOpponent = audioPreviewTracks.members[2];
|
||||
if (trackOpponent != null)
|
||||
{
|
||||
trackOpponent.volume = volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function muteTrack(target:Waveform):Void
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case Waveform.INSTRUMENTAL:
|
||||
var trackInst = audioPreviewTracks.members[0];
|
||||
if (trackInst != null)
|
||||
{
|
||||
trackInst.muted = true;
|
||||
offsetInstrumentalMute.text = trackInst.muted ? "Unmute" : "Mute";
|
||||
}
|
||||
case Waveform.PLAYER:
|
||||
var trackPlayer = audioPreviewTracks.members[1];
|
||||
if (trackPlayer != null)
|
||||
{
|
||||
trackPlayer.muted = true;
|
||||
offsetPlayerMute.text = trackPlayer.muted ? "Unmute" : "Mute";
|
||||
}
|
||||
case Waveform.OPPONENT:
|
||||
var trackOpponent = audioPreviewTracks.members[2];
|
||||
if (trackOpponent != null)
|
||||
{
|
||||
trackOpponent.muted = true;
|
||||
offsetOpponentMute.text = trackOpponent.muted ? "Unmute" : "Mute";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function unmuteTrack(target:Waveform):Void
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case Waveform.INSTRUMENTAL:
|
||||
var trackInst = audioPreviewTracks.members[0];
|
||||
if (trackInst != null)
|
||||
{
|
||||
trackInst.muted = false;
|
||||
offsetInstrumentalMute.text = trackInst.muted ? "Unmute" : "Mute";
|
||||
}
|
||||
case Waveform.PLAYER:
|
||||
var trackPlayer = audioPreviewTracks.members[1];
|
||||
if (trackPlayer != null)
|
||||
{
|
||||
trackPlayer.muted = false;
|
||||
offsetPlayerMute.text = trackPlayer.muted ? "Unmute" : "Mute";
|
||||
}
|
||||
case Waveform.OPPONENT:
|
||||
var trackOpponent = audioPreviewTracks.members[2];
|
||||
if (trackOpponent != null)
|
||||
{
|
||||
trackOpponent.muted = false;
|
||||
offsetOpponentMute.text = trackOpponent.muted ? "Unmute" : "Mute";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleMuteTrack(target:Waveform):Void
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case Waveform.INSTRUMENTAL:
|
||||
var trackInst = audioPreviewTracks.members[0];
|
||||
if (trackInst != null)
|
||||
{
|
||||
trackInst.muted = !trackInst.muted;
|
||||
offsetInstrumentalMute.text = trackInst.muted ? "Unmute" : "Mute";
|
||||
}
|
||||
case Waveform.PLAYER:
|
||||
var trackPlayer = audioPreviewTracks.members[1];
|
||||
if (trackPlayer != null)
|
||||
{
|
||||
trackPlayer.muted = !trackPlayer.muted;
|
||||
offsetPlayerMute.text = trackPlayer.muted ? "Unmute" : "Mute";
|
||||
}
|
||||
case Waveform.OPPONENT:
|
||||
var trackOpponent = audioPreviewTracks.members[2];
|
||||
if (trackOpponent != null)
|
||||
{
|
||||
trackOpponent.muted = !trackOpponent.muted;
|
||||
offsetOpponentMute.text = trackOpponent.muted ? "Unmute" : "Mute";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicking the solo button will unmute the track and mute all other tracks.
|
||||
* @param target
|
||||
*/
|
||||
public function soloTrack(target:Waveform):Void
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case Waveform.PLAYER:
|
||||
muteTrack(Waveform.OPPONENT);
|
||||
muteTrack(Waveform.INSTRUMENTAL);
|
||||
unmuteTrack(Waveform.PLAYER);
|
||||
case Waveform.OPPONENT:
|
||||
muteTrack(Waveform.PLAYER);
|
||||
muteTrack(Waveform.INSTRUMENTAL);
|
||||
unmuteTrack(Waveform.OPPONENT);
|
||||
case Waveform.INSTRUMENTAL:
|
||||
muteTrack(Waveform.PLAYER);
|
||||
muteTrack(Waveform.OPPONENT);
|
||||
unmuteTrack(Waveform.INSTRUMENTAL);
|
||||
}
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
if (audioPreviewTracks.playing)
|
||||
{
|
||||
trace('Playback time: ${audioPreviewTracks.time}');
|
||||
|
||||
var targetScrollPos:Float = waveformInstrumental.waveform.waveformData.secondsToIndex(audioPreviewTracks.time / Constants.MS_PER_SEC) / (waveformScale / BASE_SCALE * waveformMagicFactor);
|
||||
// waveformScrollview.hscrollPos = targetScrollPos;
|
||||
var prevPlayheadAbsolutePos = playheadAbsolutePos;
|
||||
playheadAbsolutePos = targetScrollPos;
|
||||
var playheadDiff = playheadAbsolutePos - prevPlayheadAbsolutePos;
|
||||
|
||||
// BEHAVIOR A.
|
||||
// Just move the scroll view with the playhead, constraining it so that the playhead is always visible.
|
||||
// waveformScrollview.hscrollPos += playheadDiff;
|
||||
// waveformScrollview.hscrollPos = FlxMath.bound(waveformScrollview.hscrollPos, playheadAbsolutePos - playheadSprite.width, playheadAbsolutePos);
|
||||
|
||||
// BEHAVIOR B.
|
||||
// Keep `playheadAbsolutePos` within the bounds of the screen.
|
||||
// The scroll view will eventually move to where the playhead is 1/8th of the way from the left. This looks kinda nice!
|
||||
// TODO: This causes a hard snap to scroll when the playhead is to the right of the playheadCenterPoint.
|
||||
// var playheadCenterPoint = waveformScrollview.width / 8;
|
||||
// waveformScrollview.hscrollPos = FlxMath.bound(waveformScrollview.hscrollPos, playheadAbsolutePos - playheadCenterPoint, playheadAbsolutePos);
|
||||
|
||||
// playheadRelativePos = 0;
|
||||
|
||||
// BEHAVIOR C.
|
||||
// Copy Audacity!
|
||||
// If the playhead is out of view, jump forward or backward by one screen width until it's in view.
|
||||
if (playheadAbsolutePos < waveformScrollview.hscrollPos)
|
||||
{
|
||||
waveformScrollview.hscrollPos -= waveformScrollview.width;
|
||||
}
|
||||
if (playheadAbsolutePos > waveformScrollview.hscrollPos + waveformScrollview.width)
|
||||
{
|
||||
waveformScrollview.hscrollPos += waveformScrollview.width;
|
||||
}
|
||||
}
|
||||
|
||||
if (chartEditorState.currentInstrumentalOffset != audioPreviewInstrumentalOffset)
|
||||
{
|
||||
var track = audioPreviewTracks.members[0];
|
||||
if (track != null)
|
||||
{
|
||||
track.time += audioPreviewInstrumentalOffset;
|
||||
track.time -= chartEditorState.currentInstrumentalOffset;
|
||||
audioPreviewInstrumentalOffset = chartEditorState.currentInstrumentalOffset;
|
||||
}
|
||||
}
|
||||
if (chartEditorState.currentVocalOffsetPlayer != audioPreviewPlayerOffset)
|
||||
{
|
||||
var track = audioPreviewTracks.members[1];
|
||||
if (track != null)
|
||||
{
|
||||
track.time += audioPreviewPlayerOffset;
|
||||
track.time -= chartEditorState.currentVocalOffsetPlayer;
|
||||
audioPreviewPlayerOffset = chartEditorState.currentVocalOffsetPlayer;
|
||||
}
|
||||
}
|
||||
if (chartEditorState.currentVocalOffsetOpponent != audioPreviewOpponentOffset)
|
||||
{
|
||||
var track = audioPreviewTracks.members[2];
|
||||
if (track != null)
|
||||
{
|
||||
track.time += audioPreviewOpponentOffset;
|
||||
track.time -= chartEditorState.currentVocalOffsetOpponent;
|
||||
audioPreviewOpponentOffset = chartEditorState.currentVocalOffsetOpponent;
|
||||
}
|
||||
}
|
||||
offsetLabelTime.text = formatTime(audioPreviewTracks.time / Constants.MS_PER_SEC);
|
||||
// Keep the playhead in view.
|
||||
// playheadRelativePos = FlxMath.bound(playheadRelativePos, waveformScrollview.hscrollPos + 1,
|
||||
// Math.min(waveformScrollview.hscrollPos + waveformScrollview.width, waveformContainer.width));
|
||||
}
|
||||
|
||||
public override function refresh():Void
|
||||
{
|
||||
super.refresh();
|
||||
|
||||
waveformMagicFactor = MAGIC_SCALE_BASE_TIME / (chartEditorState.offsetTickBitmap.width / waveformInstrumental.waveform.waveformData.pointsPerSecond());
|
||||
|
||||
var currentZoomFactor = waveformScale / BASE_SCALE * waveformMagicFactor;
|
||||
|
||||
var maxWidth:Int = -1;
|
||||
|
||||
offsetStepperPlayer.value = chartEditorState.currentVocalOffsetPlayer;
|
||||
offsetStepperOpponent.value = chartEditorState.currentVocalOffsetOpponent;
|
||||
offsetStepperInstrumental.value = chartEditorState.currentInstrumentalOffset;
|
||||
|
||||
waveformPlayer.waveform.time = -chartEditorState.currentVocalOffsetPlayer / Constants.MS_PER_SEC; // Negative offsets make the song start early.
|
||||
waveformPlayer.waveform.width = (waveformPlayer.waveform.waveformData?.length ?? 1000) / currentZoomFactor;
|
||||
if (waveformPlayer.waveform.width > maxWidth) maxWidth = Std.int(waveformPlayer.waveform.width);
|
||||
waveformPlayer.waveform.height = 65;
|
||||
|
||||
waveformOpponent.waveform.time = -chartEditorState.currentVocalOffsetOpponent / Constants.MS_PER_SEC;
|
||||
waveformOpponent.waveform.width = (waveformOpponent.waveform.waveformData?.length ?? 1000) / currentZoomFactor;
|
||||
if (waveformOpponent.waveform.width > maxWidth) maxWidth = Std.int(waveformOpponent.waveform.width);
|
||||
waveformOpponent.waveform.height = 65;
|
||||
|
||||
waveformInstrumental.waveform.time = -chartEditorState.currentInstrumentalOffset / Constants.MS_PER_SEC;
|
||||
waveformInstrumental.waveform.width = (waveformInstrumental.waveform.waveformData?.length ?? 1000) / currentZoomFactor;
|
||||
if (waveformInstrumental.waveform.width > maxWidth) maxWidth = Std.int(waveformInstrumental.waveform.width);
|
||||
waveformInstrumental.waveform.height = 65;
|
||||
|
||||
// Live update the drag, but don't actually change the underlying offset until we release the mouse to finish dragging.
|
||||
if (dragWaveform != null) switch (dragWaveform)
|
||||
{
|
||||
case PLAYER:
|
||||
// chartEditorState.currentVocalOffsetPlayer += deltaMilliseconds;
|
||||
waveformPlayer.waveform.time -= dragOffsetMs / Constants.MS_PER_SEC;
|
||||
offsetStepperPlayer.value += dragOffsetMs;
|
||||
case OPPONENT:
|
||||
// chartEditorState.currentVocalOffsetOpponent += deltaMilliseconds;
|
||||
waveformOpponent.waveform.time -= dragOffsetMs / Constants.MS_PER_SEC;
|
||||
offsetStepperOpponent.value += dragOffsetMs;
|
||||
case INSTRUMENTAL:
|
||||
// chartEditorState.currentInstrumentalOffset += deltaMilliseconds;
|
||||
waveformInstrumental.waveform.time -= dragOffsetMs / Constants.MS_PER_SEC;
|
||||
offsetStepperInstrumental.value += dragOffsetMs;
|
||||
default:
|
||||
// No drag, no
|
||||
}
|
||||
|
||||
waveformPlayer.waveform.markDirty();
|
||||
waveformOpponent.waveform.markDirty();
|
||||
waveformInstrumental.waveform.markDirty();
|
||||
|
||||
waveformContainer.width = maxWidth;
|
||||
tickTiledSprite.width = maxWidth;
|
||||
}
|
||||
|
||||
public static function build(chartEditorState:ChartEditorState):ChartEditorOffsetsToolbox
|
||||
{
|
||||
return new ChartEditorOffsetsToolbox(chartEditorState);
|
||||
}
|
||||
}
|
||||
|
||||
enum Waveform
|
||||
{
|
||||
PLAYER;
|
||||
OPPONENT;
|
||||
INSTRUMENTAL;
|
||||
}
|
|
@ -1,10 +1,19 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
package funkin.ui.debug.dialogue;
|
||||
|
||||
import flixel.FlxState;
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.ui.MusicBeatState;
|
||||
import funkin.data.dialogue.ConversationData;
|
||||
import funkin.data.dialogue.ConversationRegistry;
|
||||
import funkin.data.dialogue.DialogueBoxData;
|
||||
import funkin.data.dialogue.DialogueBoxRegistry;
|
||||
import funkin.data.dialogue.SpeakerData;
|
||||
import funkin.data.dialogue.SpeakerRegistry;
|
||||
import funkin.play.cutscene.dialogue.Conversation;
|
||||
import funkin.play.cutscene.dialogue.DialogueBox;
|
||||
import funkin.play.cutscene.dialogue.Speaker;
|
||||
|
||||
/**
|
||||
* A state with displays a conversation with no background.
|
||||
|
@ -27,7 +36,7 @@ class ConversationDebugState extends MusicBeatState
|
|||
|
||||
public override function create():Void
|
||||
{
|
||||
conversation = ConversationDataParser.fetchConversation(conversationId);
|
||||
conversation = ConversationRegistry.instance.fetchEntry(conversationId);
|
||||
conversation.completeCallback = onConversationComplete;
|
||||
add(conversation);
|
||||
|
||||
|
@ -40,6 +49,12 @@ class ConversationDebugState extends MusicBeatState
|
|||
conversation = null;
|
||||
}
|
||||
|
||||
public override function dispatchEvent(event:ScriptEvent):Void
|
||||
{
|
||||
// Dispatch event to conversation script.
|
||||
ScriptEventDispatcher.callEvent(conversation, event);
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
|
@ -56,6 +56,31 @@ import lime.utils.Assets;
|
|||
|
||||
class FreeplayState extends MusicBeatSubState
|
||||
{
|
||||
/**
|
||||
* For the audio preview, the duration of the fade-in effect.
|
||||
*/
|
||||
public static final FADE_IN_DURATION:Float = 0.5;
|
||||
|
||||
/**
|
||||
* For the audio preview, the duration of the fade-out effect.
|
||||
*/
|
||||
public static final FADE_OUT_DURATION:Float = 0.25;
|
||||
|
||||
/**
|
||||
* For the audio preview, the volume at which the fade-in starts.
|
||||
*/
|
||||
public static final FADE_IN_START_VOLUME:Float = 0.25;
|
||||
|
||||
/**
|
||||
* For the audio preview, the volume at which the fade-in ends.
|
||||
*/
|
||||
public static final FADE_IN_END_VOLUME:Float = 1.0;
|
||||
|
||||
/**
|
||||
* For the audio preview, the volume at which the fade-out starts.
|
||||
*/
|
||||
public static final FADE_OUT_END_VOLUME:Float = 0.0;
|
||||
|
||||
var songs:Array<Null<FreeplaySongData>> = [];
|
||||
|
||||
var diffIdsCurrent:Array<String> = [];
|
||||
|
@ -139,9 +164,9 @@ class FreeplayState extends MusicBeatSubState
|
|||
isDebug = true;
|
||||
#end
|
||||
|
||||
if (FlxG.sound.music != null)
|
||||
if (FlxG.sound.music == null || (FlxG.sound.music != null && !FlxG.sound.music.playing))
|
||||
{
|
||||
if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
|
||||
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
|
||||
}
|
||||
|
||||
// Add a null entry that represents the RANDOM option
|
||||
|
|
17
source/funkin/ui/haxeui/components/WaveformPlayer.hx
Normal file
17
source/funkin/ui/haxeui/components/WaveformPlayer.hx
Normal file
|
@ -0,0 +1,17 @@
|
|||
package funkin.ui.haxeui.components;
|
||||
|
||||
import funkin.audio.waveform.WaveformSprite;
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
import haxe.ui.backend.flixel.components.SpriteWrapper;
|
||||
|
||||
class WaveformPlayer extends SpriteWrapper
|
||||
{
|
||||
public var waveform(default, null):WaveformSprite;
|
||||
|
||||
public function new(?waveformData:WaveformData)
|
||||
{
|
||||
super();
|
||||
this.waveform = new WaveformSprite(waveformData);
|
||||
this.sprite = waveform;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,8 @@ class DataAssets
|
|||
|
||||
public static function listDataFilesInPath(path:String, ?suffix:String = '.json'):Array<String>
|
||||
{
|
||||
var textAssets = openfl.utils.Assets.list();
|
||||
var textAssets = openfl.utils.Assets.list(TEXT);
|
||||
|
||||
var queryPath = buildDataPath(path);
|
||||
|
||||
var results:Array<String> = [];
|
||||
|
|
120
source/funkin/util/logging/AnsiTrace.hx
Normal file
120
source/funkin/util/logging/AnsiTrace.hx
Normal file
|
@ -0,0 +1,120 @@
|
|||
package funkin.util.logging;
|
||||
|
||||
class AnsiTrace
|
||||
{
|
||||
// mostly a copy of haxe.Log.trace()
|
||||
// but adds nice cute ANSI things
|
||||
public static function trace(v:Dynamic, ?info:haxe.PosInfos)
|
||||
{
|
||||
var str = formatOutput(v, info);
|
||||
#if js
|
||||
if (js.Syntax.typeof(untyped console) != "undefined" && (untyped console).log != null) (untyped console).log(str);
|
||||
#elseif lua
|
||||
untyped __define_feature__("use._hx_print", _hx_print(str));
|
||||
#elseif sys
|
||||
Sys.println(str);
|
||||
#else
|
||||
throw new haxe.exceptions.NotImplementedException()
|
||||
#end
|
||||
}
|
||||
|
||||
public static var colorSupported:Bool = #if sys (Sys.getEnv("TERM") == "xterm" || Sys.getEnv("ANSICON") != null) #else false #end;
|
||||
|
||||
// ansi stuff
|
||||
public static inline var RED = "\x1b[31m";
|
||||
public static inline var YELLOW = "\x1b[33m";
|
||||
public static inline var WHITE = "\x1b[37m";
|
||||
public static inline var NORMAL = "\x1b[0m";
|
||||
public static inline var BOLD = "\x1b[1m";
|
||||
public static inline var ITALIC = "\x1b[3m";
|
||||
|
||||
// where the real mf magic happens with ansi stuff!
|
||||
public static function formatOutput(v:Dynamic, infos:haxe.PosInfos):String
|
||||
{
|
||||
var str = Std.string(v);
|
||||
if (infos == null) return str;
|
||||
|
||||
if (colorSupported)
|
||||
{
|
||||
var dirs:Array<String> = infos.fileName.split("/");
|
||||
dirs[dirs.length - 1] = ansiWrap(dirs[dirs.length - 1], BOLD);
|
||||
|
||||
// rejoin the dirs
|
||||
infos.fileName = dirs.join("/");
|
||||
}
|
||||
|
||||
var pstr = infos.fileName + ":" + ansiWrap(infos.lineNumber, BOLD);
|
||||
if (infos.customParams != null) for (v in infos.customParams)
|
||||
str += ", " + Std.string(v);
|
||||
return pstr + ": " + str;
|
||||
}
|
||||
|
||||
public static function traceBF()
|
||||
{
|
||||
#if sys
|
||||
if (colorSupported) Sys.println(ansiBF.join("\n"));
|
||||
#end
|
||||
}
|
||||
|
||||
public static function ansiWrap(str:Dynamic, ansiCol:String)
|
||||
{
|
||||
return ansify(ansiCol) + str + ansify(NORMAL);
|
||||
}
|
||||
|
||||
public static function ansify(ansiCol:String)
|
||||
{
|
||||
return (colorSupported ? ansiCol : "");
|
||||
}
|
||||
|
||||
// generated using https://dom111.github.io/image-to-ansi/
|
||||
public static var ansiBF:Array<String> = [
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;184;46;83m \x1b[48;2;246;87;102m \x1b[48;2;239;83;100m \x1b[48;2;154;23;70m \x1b[48;2;154;23;69m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;191;52;87m \x1b[48;2;246;87;102m \x1b[48;2;241;84;100m \x1b[48;2;191;52;87m \x1b[48;2;153;23;69m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;234;94;114m \x1b[48;2;160;97;151m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;36;35;46m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;47;49;144m \x1b[48;2;246;87;102m \x1b[48;2;80;121;206m \x1b[48;2;193;167;177m \x1b[48;2;246;87;102m \x1b[48;2;184;46;83m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;47;49;144m \x1b[48;2;110;187;236m \x1b[48;2;109;66;125m \x1b[48;2;246;87;102m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;107;177;226m \x1b[48;2;234;94;114m \x1b[48;2;246;87;102m \x1b[48;2;237;81;99m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;74;106;196m \x1b[48;2;87;133;210m \x1b[48;2;64;105;174m \x1b[48;2;141;248;252m \x1b[48;2;126;219;244m \x1b[48;2;57;65;148m \x1b[48;2;47;49;144m \x1b[48;2;141;248;252m \x1b[48;2;129;225;245m \x1b[48;2;157;94;147m \x1b[48;2;246;87;102m \x1b[48;2;159;27;72m \x1b[48;2;205;63;93m \x1b[48;2;55;27;43m \x1b[48;2;21;21;31m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;125;216;244m \x1b[48;2;141;248;252m \x1b[48;2;126;219;244m \x1b[48;2;60;97;187m \x1b[48;2;141;248;252m \x1b[48;2;126;219;244m \x1b[48;2;104;173;229m \x1b[48;2;146;68;123m \x1b[48;2;246;87;102m \x1b[48;2;180;44;82m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;104;173;229m \x1b[48;2;146;68;123m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;78;118;190m \x1b[48;2;239;83;100m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;78;118;190m \x1b[48;2;239;83;100m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;110;187;236m \x1b[48;2;51;72;160m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;117;138;166m \x1b[48;2;20;19;31m \x1b[48;2;110;187;236m \x1b[48;2;55;134;228m \x1b[48;2;110;187;236m \x1b[48;2;139;244;251m \x1b[48;2;205;63;93m \x1b[48;2;123;4;53m \x1b[48;2;125;6;54m \x1b[48;2;146;23;68m \x1b[48;2;205;63;93m \x1b[48;2;123;4;53m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;103;130;185m \x1b[48;2;240;174;162m \x1b[48;2;74;107;200m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;107;177;226m \x1b[48;2;74;107;200m \x1b[48;2;20;19;31m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;58;123;219m \x1b[48;2;74;107;200m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;141;248;252m \x1b[48;2;135;237;249m \x1b[48;2;157;140;181m \x1b[48;2;44;30;46m \x1b[48;2;20;19;31m \x1b[48;2;205;63;93m \x1b[48;2;36;35;46m \x1b[48;2;153;78;112m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;74;107;200m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;93;37;66m \x1b[48;2;240;174;162m \x1b[48;2;74;107;200m \x1b[48;2;240;174;162m \x1b[48;2;130;96;96m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;205;63;93m \x1b[48;2;170;35;77m \x1b[48;2;196;126;137m \x1b[48;2;249;225;202m \x1b[48;2;132;70;100m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;138;242;250m \x1b[48;2;74;107;200m \x1b[49m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;20;19;31m \x1b[48;2;175;111;124m \x1b[48;2;235;169;159m \x1b[48;2;240;174;162m \x1b[48;2;227;160;155m \x1b[48;2;20;19;31m \x1b[48;2;232;165;158m \x1b[48;2;240;174;162m \x1b[48;2;85;109;196m \x1b[48;2;138;242;250m \x1b[48;2;112;191;237m \x1b[48;2;104;181;235m \x1b[48;2;110;187;236m \x1b[48;2;23;22;43m \x1b[48;2;26;23;37m \x1b[48;2;249;225;202m \x1b[48;2;248;220;198m \x1b[48;2;249;225;202m \x1b[48;2;137;90;124m \x1b[48;2;51;112;205m \x1b[48;2;53;128;224m \x1b[48;2;23;25;44m \x1b[48;2;18;18;28m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;109;182;229m \x1b[48;2;74;107;200m \x1b[49m \x1b[48;2;151;78;111m \x1b[48;2;194;126;136m \x1b[48;2;110;100;98m \x1b[48;2;244;194;178m \x1b[48;2;72;42;63m \x1b[48;2;103;76;81m \x1b[48;2;191;136;147m \x1b[48;2;240;174;162m \x1b[48;2;206;136;142m \x1b[48;2;20;19;31m \x1b[48;2;232;165;158m \x1b[48;2;240;174;162m \x1b[48;2;65;128;218m \x1b[48;2;141;248;252m \x1b[48;2;74;107;200m \x1b[48;2;85;133;200m \x1b[48;2;88;139;214m \x1b[48;2;84;69;150m \x1b[48;2;211;167;166m \x1b[48;2;187;116;132m \x1b[48;2;213;145;147m \x1b[48;2;245;205;186m \x1b[48;2;135;83;115m \x1b[48;2;43;66;124m \x1b[48;2;47;87;174m \x1b[48;2;51;130;227m \x1b[48;2;28;40;76m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;71;103;199m \x1b[48;2;73;105;197m \x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;246;209;189m \x1b[48;2;249;225;202m \x1b[48;2;226;159;154m \x1b[48;2;244;215;196m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;226;159;154m \x1b[48;2;142;54;93m \x1b[48;2;245;213;193m \x1b[48;2;249;225;202m \x1b[48;2;213;185;192m \x1b[48;2;85;132;211m \x1b[48;2;222;158;164m \x1b[48;2;183;111;129m \x1b[48;2;110;187;236m \x1b[48;2;171;158;211m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;240;174;162m \x1b[48;2;166;93;120m \x1b[48;2;130;70;98m \x1b[48;2;19;19;31m \x1b[48;2;29;34;56m \x1b[48;2;55;93;183m \x1b[48;2;68;101;193m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;249;225;202m \x1b[48;2;196;126;137m \x1b[48;2;218;150;149m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;196;126;137m \x1b[48;2;47;49;144m \x1b[48;2;196;126;137m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;20;19;31m \x1b[48;2;41;43;121m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;249;225;202m \x1b[48;2;244;215;196m \x1b[48;2;249;225;202m \x1b[48;2;145;49;90m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;249;225;202m \x1b[48;2;200;131;139m \x1b[48;2;249;225;202m \x1b[48;2;154;63;98m \x1b[48;2;145;49;90m \x1b[48;2;240;174;162m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;210;141;145m \x1b[48;2;241;181;168m \x1b[48;2;249;225;202m \x1b[48;2;196;126;137m \x1b[48;2;195;125;136m \x1b[48;2;170;94;119m \x1b[48;2;237;73;115m \x1b[48;2;244;75;120m \x1b[48;2;145;49;90m \x1b[48;2;249;225;202m \x1b[48;2;241;181;167m \x1b[48;2;181;121;161m \x1b[48;2;255;224;255m \x1b[48;2;178;117;159m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;136;72;102m \x1b[48;2;190;119;133m \x1b[48;2;171;99;123m \x1b[48;2;152;74;109m \x1b[48;2;244;75;120m \x1b[48;2;145;49;90m \x1b[48;2;190;119;133m \x1b[48;2;185;128;172m \x1b[48;2;180;121;164m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;147;80;107m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;36;35;46m \x1b[48;2;98;121;155m \x1b[48;2;50;68;111m \x1b[48;2;55;73;115m \x1b[48;2;36;35;46m \x1b[48;2;251;117;129m \x1b[48;2;205;63;93m \x1b[48;2;230;143;174m \x1b[48;2;255;224;255m \x1b[48;2;145;49;90m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;251;219;252m \x1b[48;2;105;60;85m \x1b[48;2;36;35;46m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;156;82;114m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;65;84;125m \x1b[48;2;98;121;155m \x1b[48;2;124;146;175m \x1b[48;2;194;215;238m \x1b[48;2;50;68;111m \x1b[48;2;124;146;175m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;254;224;245m \x1b[48;2;231;143;176m \x1b[48;2;255;224;255m \x1b[48;2;145;49;90m \x1b[48;2;255;224;255m \x1b[48;2;72;85;110m \x1b[48;2;240;174;162m \x1b[48;2;196;126;137m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;250;227;206m \x1b[48;2;50;68;111m \x1b[48;2;65;84;125m \x1b[48;2;50;68;111m \x1b[48;2;124;146;175m \x1b[48;2;78;99;137m \x1b[48;2;194;215;238m \x1b[48;2;124;146;175m \x1b[48;2;36;35;46m \x1b[48;2;254;224;245m \x1b[48;2;253;170;192m \x1b[48;2;255;224;255m \x1b[48;2;251;117;129m \x1b[48;2;255;224;255m \x1b[48;2;170;105;144m \x1b[48;2;240;174;162m \x1b[48;2;196;126;137m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;76;111m \x1b[48;2;165;91;118m \x1b[48;2;180;108;128m \x1b[48;2;250;227;206m \x1b[48;2;116;138;169m \x1b[48;2;124;146;175m \x1b[48;2;36;35;46m \x1b[48;2;116;138;169m \x1b[48;2;172;193;218m \x1b[48;2;168;206;237m \x1b[48;2;49;62;121m \x1b[48;2;73;92;131m \x1b[48;2;115;137;168m \x1b[48;2;116;138;169m \x1b[48;2;124;146;175m \x1b[48;2;50;40;54m \x1b[48;2;57;43;58m \x1b[48;2;251;170;183m \x1b[48;2;255;206;227m \x1b[48;2;251;117;129m \x1b[48;2;252;132;140m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[48;2;243;190;174m \x1b[48;2;249;226;203m \x1b[48;2;196;126;137m \x1b[48;2;44;70;156m \x1b[48;2;47;86;175m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;239;198;185m \x1b[48;2;250;227;206m \x1b[48;2;244;195;179m \x1b[48;2;250;227;206m \x1b[48;2;50;68;111m \x1b[48;2;166;188;213m \x1b[48;2;36;35;46m \x1b[48;2;46;58;91m \x1b[48;2;79;100;138m \x1b[48;2;71;79;97m \x1b[48;2;60;69;89m \x1b[48;2;136;158;188m \x1b[48;2;117;140;170m \x1b[48;2;36;35;46m \x1b[48;2;71;79;97m \x1b[48;2;79;100;138m \x1b[48;2;62;51;68m \x1b[48;2;246;192;224m \x1b[48;2;253;182;205m \x1b[48;2;255;224;255m \x1b[48;2;144;140;167m \x1b[48;2;82;52;72m \x1b[48;2;120;110;108m \x1b[48;2;250;227;206m \x1b[48;2;229;187;179m \x1b[48;2;169;166;186m \x1b[48;2;47;64;156m \x1b[48;2;46;87;175m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;220;168;164m \x1b[48;2;250;227;206m \x1b[48;2;50;68;111m \x1b[48;2;194;215;238m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;95;115;147m \x1b[48;2;93;115;150m \x1b[48;2;50;68;111m \x1b[48;2;37;37;48m \x1b[48;2;51;44;59m \x1b[48;2;66;116;206m \x1b[48;2;47;83;172m \x1b[48;2;68;134;227m \x1b[48;2;250;227;206m \x1b[48;2;61;91;174m \x1b[48;2;47;85;173m \x1b[48;2;47;87;175m \x1b[48;2;47;86;175m \x1b[49m \x1b[m",
|
||||
"\x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;217;165;163m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;82;52;72m \x1b[48;2;194;215;238m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;98;121;155m \x1b[48;2;50;68;111m \x1b[48;2;38;41;58m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;49;144m \x1b[48;2;47;87;175m \x1b[48;2;45;85;174m \x1b[49m \x1b[m",
|
||||
"\x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;242;185;171m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;217;165;163m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;36;35;46m \x1b[48;2;124;146;175m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;50;68;111m \x1b[48;2;36;35;46m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;38;41;58m \x1b[48;2;47;68;159m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;45;85;174m \x1b[49m \x1b[m",
|
||||
"\x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;242;185;171m \x1b[48;2;196;126;137m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;206;136;142m \x1b[48;2;227;160;155m \x1b[48;2;240;174;162m \x1b[48;2;82;52;72m \x1b[48;2;50;68;111m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;36;35;46m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;153;55;95m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;45;85;174m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;152;77;112m \x1b[48;2;228;161;155m \x1b[48;2;237;171;160m \x1b[48;2;196;126;137m \x1b[48;2;193;123;135m \x1b[48;2;177;105;126m \x1b[48;2;193;123;135m \x1b[48;2;37;37;50m \x1b[48;2;155;69;105m \x1b[48;2;36;35;46m \x1b[48;2;49;66;107m \x1b[48;2;36;35;46m \x1b[48;2;159;83;115m \x1b[48;2;155;67;103m \x1b[48;2;47;69;142m \x1b[48;2;47;87;175m \x1b[48;2;46;73;157m \x1b[48;2;47;85;173m \x1b[48;2;63;77;159m \x1b[48;2;247;97;126m \x1b[48;2;254;165;165m \x1b[48;2;253;160;161m \x1b[48;2;159;79;111m \x1b[48;2;72;106;198m \x1b[48;2;50;67;149m \x1b[48;2;53;69;151m \x1b[48;2;157;77;111m \x1b[48;2;151;22;68m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;147;80;107m \x1b[48;2;176;104;125m \x1b[48;2;240;174;162m \x1b[48;2;66;46;63m \x1b[48;2;36;35;46m \x1b[48;2;170;35;77m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;201;153;202m \x1b[48;2;246;87;102m \x1b[48;2;245;171;163m \x1b[48;2;253;156;159m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;47;87;175m \x1b[48;2;154;23;70m \x1b[48;2;246;170;163m \x1b[48;2;254;165;165m \x1b[48;2;246;87;102m \x1b[48;2;245;79;114m \x1b[48;2;118;0;50m \x1b[48;2;246;87;102m \x1b[48;2;219;68;93m \x1b[48;2;118;0;50m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;147;80;107m \x1b[48;2;187;116;132m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;255;224;255m \x1b[48;2;251;127;141m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;225;75;97m \x1b[48;2;154;23;70m \x1b[48;2;159;27;72m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;118;0;50m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;151;78;111m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;200;151;200m \x1b[48;2;216;175;226m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;246;85;105m \x1b[48;2;205;63;93m \x1b[48;2;118;0;50m \x1b[48;2;246;87;102m \x1b[48;2;159;27;72m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;144;16;64m \x1b[48;2;118;0;50m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;182;124;167m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[48;2;182;124;167m \x1b[48;2;255;224;255m \x1b[48;2;205;63;93m \x1b[48;2;159;27;72m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;118;0;50m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;168;74;106m \x1b[48;2;174;39;79m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;211;70;106m \x1b[48;2;233;196;238m \x1b[48;2;255;224;255m \x1b[48;2;178;42;81m \x1b[48;2;205;157;200m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;178;42;81m \x1b[48;2;154;23;70m \x1b[48;2;153;52;92m \x1b[48;2;136;40;82m \x1b[48;2;144;17;63m \x1b[48;2;225;75;97m \x1b[48;2;234;198;240m \x1b[48;2;255;224;255m \x1b[48;2;234;198;240m \x1b[48;2;233;196;238m \x1b[48;2;239;204;243m \x1b[48;2;212;165;204m \x1b[48;2;245;87;103m \x1b[48;2;180;44;82m \x1b[48;2;180;70;102m \x1b[48;2;152;77;112m \x1b[48;2;151;76;110m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;68;104m \x1b[48;2;213;67;95m \x1b[48;2;246;87;102m \x1b[48;2;213;67;95m \x1b[48;2;195;66;97m \x1b[48;2;173;61;105m \x1b[48;2;154;23;70m \x1b[48;2;195;55;89m \x1b[48;2;205;63;93m \x1b[48;2;125;4;54m \x1b[48;2;170;101;145m \x1b[48;2;171;102;146m \x1b[48;2;201;153;202m \x1b[48;2;182;124;167m \x1b[48;2;158;87;122m \x1b[48;2;138;36;79m \x1b[48;2;205;63;93m \x1b[48;2;196;143;183m \x1b[48;2;255;224;255m \x1b[48;2;234;94;114m \x1b[48;2;229;85;104m \x1b[48;2;236;96;117m \x1b[48;2;248;113;131m \x1b[48;2;246;88;103m \x1b[48;2;246;87;102m \x1b[48;2;195;66;97m \x1b[48;2;172;109;148m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;182;124;167m \x1b[48;2;197;147;195m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;212;170;223m \x1b[48;2;153;78;112m \x1b[48;2;142;44;86m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;255;224;255m \x1b[48;2;225;187;233m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;200;151;200m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;182;124;167m \x1b[48;2;147;64;101m \x1b[48;2;36;35;46m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;66;78;122m \x1b[48;2;200;100;119m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;200;151;200m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;36;35;46m \x1b[49m \x1b[48;2;66;78;122m \x1b[48;2;153;55;95m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;255;224;255m \x1b[48;2;176;115;156m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;185;128;172m \x1b[48;2;153;78;112m \x1b[48;2;36;35;46m \x1b[49m \x1b[48;2;66;78;122m \x1b[48;2;153;55;95m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;176;115;156m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;87;54;75m \x1b[48;2;207;163;214m \x1b[48;2;190;136;182m \x1b[48;2;202;151;191m \x1b[48;2;244;187;187m \x1b[48;2;241;206;245m \x1b[48;2;243;209;246m \x1b[48;2;212;170;223m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;56;42;57m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;243;208;246m \x1b[48;2;238;203;242m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;153;78;112m \x1b[48;2;153;77;111m \x1b[49m \x1b[m",
|
||||
"\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[49m \x1b[m"
|
||||
];
|
||||
}
|
|
@ -3,6 +3,7 @@ package funkin.util.logging;
|
|||
import openfl.Lib;
|
||||
import openfl.events.UncaughtErrorEvent;
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
import flixel.FlxG.FlxRenderMethod;
|
||||
|
||||
/**
|
||||
* A custom crash handler that writes to a log file and displays a message box.
|
||||
|
@ -118,6 +119,7 @@ class CrashHandler
|
|||
var driverInfo = FlxG?.stage?.context3D?.driverInfo ?? 'N/A';
|
||||
fullContents += 'Driver info: ${driverInfo}\n';
|
||||
fullContents += 'Platform: ${Sys.systemName()}\n';
|
||||
fullContents += 'Render method: ${renderMethod()}\n';
|
||||
|
||||
fullContents += '\n';
|
||||
|
||||
|
@ -196,4 +198,32 @@ class CrashHandler
|
|||
{
|
||||
throw "This is an example of an uncaught exception.";
|
||||
}
|
||||
|
||||
public static function induceNullObjectReference():Void
|
||||
{
|
||||
var obj:Dynamic = null;
|
||||
var value = obj.test;
|
||||
}
|
||||
|
||||
public static function induceNullObjectReference2():Void
|
||||
{
|
||||
var obj:Dynamic = null;
|
||||
var value = obj.test();
|
||||
}
|
||||
|
||||
public static function induceNullObjectReference3():Void
|
||||
{
|
||||
var obj:Dynamic = null;
|
||||
var value = obj();
|
||||
}
|
||||
|
||||
static function renderMethod():String
|
||||
{
|
||||
return switch (FlxG.renderMethod)
|
||||
{
|
||||
case FlxRenderMethod.DRAW_TILES: 'DRAW_TILES';
|
||||
case FlxRenderMethod.BLITTING: 'BLITTING';
|
||||
default: 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
<haxelib name="hxCodec" /> <!-- Video playback -->
|
||||
<haxelib name="thx.semver" /> <!-- Semantic version handling -->
|
||||
<haxelib name="json2object" /> <!-- JSON parsing -->
|
||||
<haxelib name="tink_json" /> <!-- JSON parsing -->
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<haxelib name="munit" /> <!-- Unit test execution -->
|
||||
|
|
Loading…
Reference in a new issue