Merge branch 'CodenameCrew:main' into patch-1
This commit is contained in:
@@ -0,0 +1,916 @@
|
||||
package flixel.sound;
|
||||
|
||||
import lime.media.AudioBuffer;
|
||||
import lime.media.AudioSource;
|
||||
import lime.media.AudioManager;
|
||||
|
||||
import openfl.Assets;
|
||||
import openfl.events.IEventDispatcher;
|
||||
import openfl.events.Event;
|
||||
import openfl.media.Sound;
|
||||
import openfl.media.SoundChannel;
|
||||
import openfl.media.SoundTransform;
|
||||
import openfl.media.SoundMixer;
|
||||
import openfl.net.URLRequest;
|
||||
import openfl.utils.AssetType;
|
||||
#if flash11
|
||||
import openfl.utils.ByteArray;
|
||||
#end
|
||||
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.system.FlxAssets.FlxSoundAsset;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.util.FlxSignal;
|
||||
import flixel.util.FlxStringUtil;
|
||||
import flixel.FlxBasic;
|
||||
import flixel.FlxObject;
|
||||
import flixel.FlxG;
|
||||
|
||||
@:access(flixel.FlxGame)
|
||||
class FlxSound extends FlxBasic {
|
||||
/**
|
||||
* The x position of this sound in world coordinates.
|
||||
* Only really matters if you are doing proximity/panning stuff.
|
||||
*/
|
||||
public var x:Float;
|
||||
|
||||
/**
|
||||
* The y position of this sound in world coordinates.
|
||||
* Only really matters if you are doing proximity/panning stuff.
|
||||
*/
|
||||
public var y:Float;
|
||||
|
||||
/**
|
||||
* Whether or not this sound should be automatically destroyed when you switch states.
|
||||
*/
|
||||
public var persist:Bool;
|
||||
|
||||
/**
|
||||
* Whether or not the sound is currently playing.
|
||||
*/
|
||||
public var playing(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Set volume to a value between 0 and 1* to change how this sound is.
|
||||
*/
|
||||
public var volume(get, set):Float;
|
||||
|
||||
/**
|
||||
* Whether to make this sound muted or not.
|
||||
*/
|
||||
public var muted(default, set):Bool;
|
||||
|
||||
#if FLX_PITCH
|
||||
/**
|
||||
* Set pitch, which also alters the playback speed. Default is 1.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public var pitch(get, set):Float;
|
||||
|
||||
/**
|
||||
* Alters the pitch of the sound depends on the current FlxG.timeScale. Default is true.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var timeScaleBased:Bool;
|
||||
#end
|
||||
|
||||
/**
|
||||
* Pan amount. -1 = full left, 1 = full right. Proximity based panning overrides this.
|
||||
*/
|
||||
public var pan(get, set):Float;
|
||||
|
||||
/**
|
||||
* The position in runtime of the music playback in milliseconds.
|
||||
* If set while paused, changes only come into effect after a `resume()` call.
|
||||
*/
|
||||
public var time(get, set):Float;
|
||||
|
||||
/**
|
||||
* The offset for this FlxSound.
|
||||
* Useful for just generally offsetting this sound without affecting time.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var offset(get, set):Float;
|
||||
|
||||
/**
|
||||
* The length of the sound in milliseconds.
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public var length(get, never):Float;
|
||||
|
||||
/**
|
||||
* The latency of the sound in milliseconds.
|
||||
* @since raltyMod
|
||||
*/
|
||||
//public var latency(get, never):Float;
|
||||
|
||||
/**
|
||||
* Whether or not this sound should loop.
|
||||
*/
|
||||
public var looped(default, set):Bool;
|
||||
|
||||
/**
|
||||
* In case of looping, the point (in milliseconds) from where to restart the sound when it loops back
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public var loopTime(default, set):Float;
|
||||
|
||||
/**
|
||||
* At which point to stop playing the sound, in milliseconds.
|
||||
* If not set / `null`, the sound completes normally.
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public var endTime(default, set):Null<Float>;
|
||||
|
||||
/**
|
||||
* The sound's "target" (for proximity and panning).
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var target:Null<FlxObject>;
|
||||
|
||||
/**
|
||||
* The maximum effective radius of this sound (for proximity and panning).
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var radius:Float;
|
||||
|
||||
/**
|
||||
* Whether the proximity alters the pan or not
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var proximityPan:Bool;
|
||||
|
||||
/**
|
||||
* Stores for how much channels are in the loaded sound.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var channels(get, never):Int;
|
||||
|
||||
/**
|
||||
* Whether or not this sound is stereo instead of mono.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var stereo(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Whether or not this sound is loaded yet.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var loaded(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Whether or not this sound is streamed, only vorbis though.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var streamed(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Stores the average wave amplitude of both stereo channels.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var amplitude(get, never):Float;
|
||||
|
||||
/**
|
||||
* Just the amplitude of the left stereo channel
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var amplitudeLeft(get, never):Float;
|
||||
|
||||
/**
|
||||
* Just the amplitude of the right stereo channel
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var amplitudeRight(get, never):Float;
|
||||
|
||||
/**
|
||||
* The ID3 song name. Defaults to null. Currently only works for MP3 streamed sounds.
|
||||
*/
|
||||
public var name(default, null):String;
|
||||
|
||||
/**
|
||||
* The ID3 artist name. Defaults to null. Currently only works for MP3 streamed sounds.
|
||||
*/
|
||||
public var artist(default, null):String;
|
||||
|
||||
/**
|
||||
* Whether to call `destroy()` when the sound has finished playing.
|
||||
*/
|
||||
public var autoDestroy:Bool;
|
||||
|
||||
/**
|
||||
* Signal that is dispatched on sound complete.
|
||||
* Seperates the looping from onComplete.
|
||||
*/
|
||||
public final onFinish:FlxSignal;
|
||||
|
||||
/**
|
||||
* Tracker for sound complete callback. If assigned, will be called
|
||||
* each time when sound reaches its end.
|
||||
*/
|
||||
//@:deprecated("`FlxSound.onComplete` is deprecated! Use `FlxSound.onFinish` instead.")
|
||||
public var onComplete:Void->Void;
|
||||
|
||||
/**
|
||||
* The sound group this sound belongs to
|
||||
*/
|
||||
public var group(default, set):FlxSoundGroup;
|
||||
|
||||
/**
|
||||
* Stores the sound lime AudioBuffer if exists.
|
||||
* @since raltyMod
|
||||
*/
|
||||
public var buffer(get, never):AudioBuffer;
|
||||
|
||||
/**
|
||||
* The tween used to fade this sound's volume in and out (set via `fadeIn()` and `fadeOut()`)
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public var fadeTween:FlxTween;
|
||||
|
||||
@:allow(flixel.system.frontEnds.SoundFrontEnd.load) var _sound:Sound;
|
||||
var _transform:SoundTransform;
|
||||
var _channel:SoundChannel;
|
||||
var _source:AudioSource;
|
||||
var _paused:Bool;
|
||||
var _volume:Float;
|
||||
var _volumeAdjust:Float;
|
||||
var _time:Float;
|
||||
var _offset:Float;
|
||||
var _timeInterpolation:Float;
|
||||
var _lastTime:Null<Float>; // FlxG.game.getTicks(), in MS
|
||||
var _length:Float;
|
||||
#if FLX_PITCH
|
||||
var _pitch:Float;
|
||||
var _timeScaleAdjust:Float;
|
||||
var _realPitch:Float;
|
||||
#end
|
||||
var _amplitudeLeft:Float;
|
||||
var _amplitudeRight:Float;
|
||||
var _amplitudeUpdate:Bool;
|
||||
var _alreadyPaused:Null<Bool>;
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
onFinish = new FlxSignal();
|
||||
revive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this FlxSound properties.
|
||||
*
|
||||
* @param clean Whether if this FlxSound also needs to be cleaned up too.
|
||||
*/
|
||||
public function reset():Void {
|
||||
if (_source != null) stop();
|
||||
onFinish.removeAll();
|
||||
|
||||
x = y = 0;
|
||||
muted = false;
|
||||
looped = false;
|
||||
loopTime = 0;
|
||||
endTime = null;
|
||||
autoDestroy = false;
|
||||
visible = false;
|
||||
target = null;
|
||||
radius = 0;
|
||||
proximityPan = true;
|
||||
onComplete = null;
|
||||
timeScaleBased = true;
|
||||
|
||||
_alreadyPaused = null;
|
||||
_cameras = null;
|
||||
_lastTime = null;
|
||||
_paused = false;
|
||||
_offset = _length = _time = 0;
|
||||
_volume = _volumeAdjust = 1;
|
||||
_amplitudeLeft = _amplitudeRight = 0;
|
||||
_amplitudeUpdate = true;
|
||||
#if FLX_PITCH _pitch = _realPitch = _timeScaleAdjust = 1; #end
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal cleanup function for cleaning up this FlxSound.
|
||||
* @since raltyMod
|
||||
*/
|
||||
function cleanup(destroySound:Bool, resetPosition:Bool = true) @:privateAccess {
|
||||
active = false;
|
||||
_lastTime = null;
|
||||
|
||||
if (destroySound) {
|
||||
if (group != null) group.remove(this);
|
||||
|
||||
if (_channel != null) {
|
||||
_channel.stop();
|
||||
_channel = null;
|
||||
}
|
||||
if (_source != null) {
|
||||
_source.onComplete.remove(stopped);
|
||||
_source.onLoop.remove(source_looped);
|
||||
_source.dispose();
|
||||
}
|
||||
_source = null;
|
||||
_sound = null;
|
||||
|
||||
reset();
|
||||
}
|
||||
else if (_channel != null && _channel.__isValid) {
|
||||
if (resetPosition) {
|
||||
_source.stop();
|
||||
|
||||
_time = 0;
|
||||
_paused = false;
|
||||
}
|
||||
else if (!_paused) {
|
||||
get_time();
|
||||
_source.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles fade out, fade in, panning, proximity, and amplitude operations each frame.
|
||||
*/
|
||||
override function update(elapsed:Float):Void {
|
||||
if (!playing) return;
|
||||
|
||||
var timeScaleTarget = timeScaleBased ? FlxG.timeScale : 1.0;
|
||||
if (_timeScaleAdjust != timeScaleTarget) {
|
||||
_timeScaleAdjust = timeScaleTarget;
|
||||
pitch = _pitch;
|
||||
if (_channel == null) return;
|
||||
}
|
||||
|
||||
_amplitudeUpdate = true;
|
||||
|
||||
var radialMultiplier = 1.0;
|
||||
|
||||
// Distance-based volume control (TODO for Ralty: REDO THIS)
|
||||
if (target != null) {
|
||||
var targetPosition = target.getPosition();
|
||||
radialMultiplier = targetPosition.distanceTo(FlxPoint.weak(x, y)) / radius;
|
||||
targetPosition.put();
|
||||
radialMultiplier = 1 - FlxMath.bound(radialMultiplier, 0, 1);
|
||||
|
||||
if (proximityPan && _transform != null) {
|
||||
var d:Float = (x - target.x) / radius;
|
||||
_transform.pan = FlxMath.bound(d, -1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
_volumeAdjust = radialMultiplier;
|
||||
updateTransform();
|
||||
}
|
||||
|
||||
override function revive() {
|
||||
reset();
|
||||
super.revive();
|
||||
}
|
||||
|
||||
override function kill() {
|
||||
super.kill();
|
||||
reset();
|
||||
}
|
||||
|
||||
override function destroy() {
|
||||
super.destroy();
|
||||
cleanup(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* One of the main setup functions for sounds, this function loads a sound from an embedded MP3.
|
||||
*
|
||||
* @param embeddedSound An embedded Class object representing an MP3 file.
|
||||
* @param looped Whether or not this sound should loop endlessly.
|
||||
* @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing.
|
||||
* Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default.
|
||||
* @param onComplete Called when the sound finished playing
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function loadEmbedded(embeddedSound:FlxSoundAsset, looped = false, autoDestroy = false, ?onComplete:Void->Void):FlxSound {
|
||||
if (!exists || embeddedSound == null) return this;
|
||||
cleanup(true);
|
||||
|
||||
if ((embeddedSound is Sound)) _sound = embeddedSound;
|
||||
else if ((embeddedSound is Class)) _sound = Type.createInstance(embeddedSound, []);
|
||||
else if ((embeddedSound is String)) {
|
||||
if (Assets.exists(embeddedSound, AssetType.MUSIC) && this == FlxG.sound.music)
|
||||
_sound = Assets.getMusic(embeddedSound);
|
||||
else if (Assets.exists(embeddedSound, AssetType.SOUND))
|
||||
_sound = Assets.getSound(embeddedSound);
|
||||
else
|
||||
FlxG.log.error('Could not find a Sound asset with an ID of \'$embeddedSound\'.');
|
||||
}
|
||||
|
||||
return init(looped, autoDestroy, onComplete);
|
||||
}
|
||||
|
||||
/**
|
||||
* One of the main setup functions for sounds, this function loads a sound from a URL.
|
||||
*
|
||||
* @param soundURL A string representing the URL of the MP3 file you want to play.
|
||||
* @param looped Whether or not this sound should loop endlessly.
|
||||
* @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing.
|
||||
* Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default.
|
||||
* @param onComplete Called when the sound finished playing
|
||||
* @param onLoad Called when the sound finished loading.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function loadStream(soundURL:String, looped = false, autoDestroy = false, ?onComplete:Void->Void, ?onLoad:Void->Void):FlxSound {
|
||||
if (!exists) return this;
|
||||
cleanup(true);
|
||||
|
||||
_sound = new Sound();
|
||||
_sound.addEventListener(Event.ID3, gotID3);
|
||||
var loadCallback:Event->Void = null;
|
||||
loadCallback = function(e:Event)
|
||||
{
|
||||
(e.target : IEventDispatcher).removeEventListener(e.type, loadCallback);
|
||||
// Check if the sound was destroyed before calling. Weak ref doesn't guarantee GC.
|
||||
if (_sound == e.target)
|
||||
{
|
||||
_length = _sound.length;
|
||||
if (onLoad != null)
|
||||
onLoad();
|
||||
}
|
||||
}
|
||||
// Use a weak reference so this can be garbage collected if destroyed before loading.
|
||||
_sound.addEventListener(Event.COMPLETE, loadCallback, false, 0, true);
|
||||
_sound.load(new URLRequest(soundURL));
|
||||
|
||||
return init(looped, autoDestroy, onComplete);
|
||||
}
|
||||
|
||||
#if flash11
|
||||
/**
|
||||
* One of the main setup functions for sounds, this function loads a sound from a ByteArray.
|
||||
*
|
||||
* @param bytes A ByteArray object.
|
||||
* @param looped Whether or not this sound should loop endlessly.
|
||||
* @param autoDestroy Whether or not this FlxSound instance should be destroyed when the sound finishes playing.
|
||||
* Default value is false, but `FlxG.sound.play()` and `FlxG.sound.stream()` will set it to true by default.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function loadByteArray(bytes:ByteArray, looped = false, autoDestroy = false, ?onComplete:Void->Void):FlxSound {
|
||||
if (!exists) return this;
|
||||
cleanup(true);
|
||||
|
||||
_sound = new Sound();
|
||||
_sound.addEventListener(Event.ID3, gotID3);
|
||||
_sound.loadCompressedDataFromByteArray(bytes, bytes.length);
|
||||
|
||||
return init(looped, autoDestroy, onComplete);
|
||||
}
|
||||
#end
|
||||
|
||||
function init(?looped:Null<Bool>, ?autoDestroy:Null<Bool>, ?onComplete:Null<Void->Void>):FlxSound {
|
||||
if (looped != null) this.looped = looped;
|
||||
if (autoDestroy != null) this.autoDestroy = autoDestroy;
|
||||
if (onComplete != null) this.onComplete = onComplete;
|
||||
|
||||
if (_sound != null) makeChannel();
|
||||
else _length = 0;
|
||||
|
||||
endTime = null;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call inbetween init function if a sound asset does exists.
|
||||
* @since raltyMod
|
||||
*/
|
||||
function makeChannel() @:privateAccess {
|
||||
if (_sound.__buffer == null) return;
|
||||
if (_source != null) _source.dispose();
|
||||
|
||||
if (_channel == null) (_source = (_channel = new SoundChannel(null)).__source = new AudioSource(_sound.__buffer)).gain = 0;
|
||||
else {
|
||||
(_source = new AudioSource(_sound.__buffer)).gain = 0;
|
||||
_channel.__dispose();
|
||||
_channel.__source = _source;
|
||||
SoundMixer.__registerSoundChannel(_channel);
|
||||
}
|
||||
if (_transform == null) _transform = new SoundTransform();
|
||||
_transform.pan = 0;
|
||||
_length = _source.length;
|
||||
|
||||
_source.onComplete.add(stopped);
|
||||
_source.onLoop.add(source_looped);
|
||||
_channel.__soundTransform = _transform;
|
||||
_channel.__isValid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call after adjusting the volume to update the sound channel's settings.
|
||||
*/
|
||||
@:allow(flixel.sound.FlxSoundGroup)
|
||||
function updateTransform() {
|
||||
if (_transform == null) {
|
||||
_transform = new SoundTransform();
|
||||
pan = pan;
|
||||
}
|
||||
_transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || muted) ? 0 : 1) * FlxG.sound.volume * #end
|
||||
(group != null ? group.volume : 1) * _volume * _volumeAdjust;
|
||||
|
||||
if (_channel != null) _channel.soundTransform = _transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this function if you want this sound's volume to change
|
||||
* based on distance from a particular FlxObject.
|
||||
*
|
||||
* @param X The X position of the sound.
|
||||
* @param Y The Y position of the sound.
|
||||
* @param TargetObject The object you want to track.
|
||||
* @param Radius The maximum distance this sound can travel.
|
||||
* @param Pan Whether panning should be used in addition to the volume changes.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function proximity(x = 0.0, y = 0.0, ?targetObject:FlxObject, ?radius:Float, pan = true):FlxSound {
|
||||
setPosition(x, y);
|
||||
if (targetObject != null) this.target = targetObject;
|
||||
if (radius != null) this.radius = radius;
|
||||
proximityPan = pan;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to set the coordinates of this object.
|
||||
* Sound positioning is used in conjunction with proximity/panning.
|
||||
*
|
||||
* @param x The new x position
|
||||
* @param y The new y position
|
||||
*/
|
||||
public inline function setPosition(x = 0.0, y = 0.0):Void {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this function to play the sound - also works on paused sounds.
|
||||
*
|
||||
* @param forceRestart Whether to start the sound over or not.
|
||||
* Default value is false, meaning if the sound is already playing or was
|
||||
* paused when you call play(), it will continue playing from its current
|
||||
* position, NOT start again from the beginning.
|
||||
* @param startTime At which point to start playing the sound, in milliseconds.
|
||||
* @param endTime At which point to stop playing the sound, in milliseconds.
|
||||
* If not set / `null`, the sound completes normally.
|
||||
*/
|
||||
public function play(forceRestart = false, startTime = 0.0, ?endTime:Null<Float>):FlxSound {
|
||||
if (!exists) return this;
|
||||
|
||||
if (forceRestart) cleanup(false, true);
|
||||
else if (playing) return this;
|
||||
|
||||
if (endTime != null) this.endTime = endTime;
|
||||
if (_paused) resume();
|
||||
else startSound(startTime);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpause a sound. Only works on sounds that have been paused.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function resume():FlxSound {
|
||||
if (_paused) startSound(_time);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this function to pause this sound.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function pause():FlxSound {
|
||||
if (!playing) return this;
|
||||
|
||||
cleanup(false, false);
|
||||
_paused = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this function to stop this sound.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function stop():FlxSound {
|
||||
cleanup(autoDestroy, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that tweens this sound's volume.
|
||||
*
|
||||
* @param duration The amount of time the fade-out operation should take.
|
||||
* @param to The volume to tween to, 0 by default.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function fadeOut(duration = 1.0, to = 0.0, ?onComplete:FlxTween->Void):FlxSound {
|
||||
if (fadeTween != null) fadeTween.cancel();
|
||||
fadeTween = FlxTween.num(volume, to, duration, {onComplete: onComplete}, volumeTween);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that tweens this sound's volume.
|
||||
* If the sound wasn't playing at all, it'll play before the tween starts.
|
||||
*
|
||||
* @param duration The amount of time the fade-in operation should take.
|
||||
* @param from The volume to tween from, 0 by default.
|
||||
* @param to The volume to tween to, 1 by default.
|
||||
* @return This FlxSound instance (nice for chaining stuff together, if you're into that).
|
||||
*/
|
||||
public function fadeIn(duration = 1.0, from = 0.0, to = 1.0, ?onComplete:FlxTween->Void):FlxSound {
|
||||
if (!playing) play();
|
||||
if (fadeTween != null) fadeTween.cancel();
|
||||
fadeTween = FlxTween.num(from, to, duration, {onComplete: onComplete}, volumeTween);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
function volumeTween(f:Float) volume = f;
|
||||
|
||||
/**
|
||||
* Returns the currently selected "real" volume of the sound (takes fades and proximity into account).
|
||||
*
|
||||
* @return The adjusted volume of the sound.
|
||||
*/
|
||||
public inline function getActualVolume():Float
|
||||
return _volume * _volumeAdjust;
|
||||
|
||||
#if FLX_PITCH
|
||||
/**
|
||||
* Returns the currently selected "real" pitch of the sound.
|
||||
*
|
||||
* @return The adjusted pitch of the sound.
|
||||
*/
|
||||
public inline function getActualPitch():Float
|
||||
return _realPitch;
|
||||
#end
|
||||
|
||||
/**
|
||||
* Returns the actual time coming from the internal, can be used for detecting sync error.
|
||||
*
|
||||
* @return The actual time of the sound.
|
||||
*/
|
||||
public inline function getActualTime():Float {
|
||||
get_time();
|
||||
return _time;
|
||||
}
|
||||
|
||||
/**
|
||||
* An internal helper function used to attempt to start playing
|
||||
* the sound and populate the _channel variable.
|
||||
*/
|
||||
function startSound(startTime:Float) @:privateAccess {
|
||||
if (_sound == null) return;
|
||||
|
||||
_paused = false;
|
||||
_time = startTime;
|
||||
_lastTime = FlxG.game.getTicks();
|
||||
if (_channel == null || !_channel.__isValid || _source == null #if lime_cffi || _source.__backend.disposed || _source.__backend.handle == null #end)
|
||||
makeChannel();
|
||||
|
||||
if (_channel != null) {
|
||||
#if FLX_PITCH
|
||||
_timeScaleAdjust = timeScaleBased ? FlxG.timeScale : 1.0;
|
||||
pitch = _pitch;
|
||||
#end
|
||||
|
||||
_channel.soundTransform = _transform;
|
||||
_channel.__lastPeakTime = -10;
|
||||
_channel.__leftPeak = 0;
|
||||
_channel.__rightPeak = 0;
|
||||
|
||||
#if lime_cffi _source.__backend.playing = true; #end
|
||||
_source.offset = 0;
|
||||
_source.currentTime = startTime + _offset;
|
||||
#if !lime_cffi _source.play(); #end
|
||||
|
||||
looped = looped;
|
||||
loopTime = loopTime;
|
||||
endTime = endTime;
|
||||
|
||||
active = true;
|
||||
}
|
||||
else {
|
||||
exists = false;
|
||||
active = false;
|
||||
}
|
||||
}
|
||||
|
||||
function stopped() {
|
||||
onFinish.dispatch();
|
||||
|
||||
if (onComplete != null) onComplete();
|
||||
|
||||
if (looped) {
|
||||
cleanup(false);
|
||||
play(false, loopTime, endTime);
|
||||
}
|
||||
else cleanup(autoDestroy);
|
||||
}
|
||||
|
||||
function source_looped() {
|
||||
if (onComplete != null) onComplete();
|
||||
|
||||
if (!looped) {
|
||||
cleanup(autoDestroy);
|
||||
_lastTime = FlxG.game.getTicks();
|
||||
_time = loopTime;
|
||||
}
|
||||
else _channel.loops = 999;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal event handler for ID3 info (i.e. fetching the song name).
|
||||
*/
|
||||
function gotID3(_) {
|
||||
name = _sound.id3.songName;
|
||||
artist = _sound.id3.artist;
|
||||
_sound.removeEventListener(Event.ID3, gotID3);
|
||||
}
|
||||
|
||||
#if FLX_SOUND_SYSTEM
|
||||
@:allow(flixel.system.frontEnds.SoundFrontEnd)
|
||||
function onFocus() if (!_alreadyPaused) {
|
||||
resume();
|
||||
_alreadyPaused = null;
|
||||
}
|
||||
|
||||
@:allow(flixel.system.frontEnds.SoundFrontEnd)
|
||||
function onFocusLost() if (_alreadyPaused == null && !(_alreadyPaused = _paused)) pause();
|
||||
#end
|
||||
|
||||
function set_group(value:FlxSoundGroup):FlxSoundGroup {
|
||||
if (value != null) value.add(this);
|
||||
else group.remove(this);
|
||||
return group;
|
||||
}
|
||||
|
||||
inline function get_playing():Bool @:privateAccess
|
||||
return _channel != null && _channel.__isValid && _source.playing;
|
||||
|
||||
inline function get_volume():Float
|
||||
return _volume;
|
||||
|
||||
inline function set_volume(v:Float):Float {
|
||||
_volume = FlxMath.bound(v, 0, 10);
|
||||
updateTransform();
|
||||
return _volume;
|
||||
}
|
||||
|
||||
inline function set_muted(v:Bool):Bool {
|
||||
muted = v;
|
||||
updateTransform();
|
||||
return muted;
|
||||
}
|
||||
|
||||
inline function get_loaded():Bool
|
||||
return buffer != null;
|
||||
|
||||
inline function get_streamed():Bool
|
||||
@:privateAccess return #if lime_vorbis _sound != null && _sound.__buffer.__srcVorbisFile != null #else false #end;
|
||||
|
||||
inline function get_buffer():AudioBuffer
|
||||
@:privateAccess return _sound != null ? _sound.__buffer : null;
|
||||
|
||||
function update_amplitude():Void @:privateAccess {
|
||||
if (_channel == null || !_channel.__updatePeaks(get_time()) || !_amplitudeUpdate) return;
|
||||
|
||||
_amplitudeUpdate = false;
|
||||
_amplitudeLeft = _channel.__leftPeak;
|
||||
_amplitudeRight = _channel.__rightPeak;
|
||||
}
|
||||
|
||||
inline function get_amplitudeLeft():Float {
|
||||
update_amplitude();
|
||||
return _amplitudeLeft;
|
||||
}
|
||||
|
||||
inline function get_amplitudeRight():Float {
|
||||
update_amplitude();
|
||||
return _amplitudeRight;
|
||||
}
|
||||
|
||||
inline function get_amplitude():Float {
|
||||
update_amplitude();
|
||||
return Math.max(_amplitudeLeft, _amplitudeRight);//if (stereo) (_amplitudeLeft + _amplitudeRight) * 0.5; else _amplitudeLeft;
|
||||
}
|
||||
|
||||
inline function get_channels():Int
|
||||
@:privateAccess return (buffer != null) ? buffer.channels : 0;
|
||||
|
||||
inline function get_stereo():Bool
|
||||
return channels > 1;
|
||||
|
||||
#if FLX_PITCH
|
||||
inline function get_pitch():Float
|
||||
return _pitch;
|
||||
|
||||
function set_pitch(v:Float):Float {
|
||||
_realPitch = (_pitch = v) * _timeScaleAdjust;
|
||||
if (_channel != null) _channel.pitch = _realPitch;
|
||||
return _pitch;
|
||||
}
|
||||
#end
|
||||
|
||||
function set_looped(v:Bool):Bool {
|
||||
if (playing) {
|
||||
if (v) _channel.loops = 999;
|
||||
else _channel.loops = 0;
|
||||
}
|
||||
return looped = v;
|
||||
}
|
||||
|
||||
function set_loopTime(v:Float):Float {
|
||||
if (playing) _channel.loopTime = v;
|
||||
return loopTime = v;
|
||||
}
|
||||
|
||||
function set_endTime(v:Null<Float>):Null<Float> {
|
||||
if (playing) {
|
||||
if (v != null && v > 0 && v < _length) _channel.endTime = v;
|
||||
else _channel.endTime = null;
|
||||
}
|
||||
return endTime = v;
|
||||
}
|
||||
|
||||
inline function get_pan():Float return _transform.pan;
|
||||
inline function set_pan(pan:Float):Float return _transform.pan = pan;
|
||||
|
||||
inline function getFakeTime():Float {
|
||||
if (_source.playing && _realPitch > 0 && _lastTime != null)
|
||||
return _time + (FlxG.game.getTicks() - _lastTime) * _realPitch * _timeInterpolation;
|
||||
else
|
||||
return _time;
|
||||
}
|
||||
function get_time():Float {
|
||||
if (_channel == null || @:privateAccess !_channel.__isValid || /*AudioManager.context == null*/funkin.backend.system.Main.audioDisconnected) return _time;
|
||||
|
||||
final sourceTime = _source.currentTime - _source.offset - _offset;
|
||||
if (!_source.playing || _realPitch <= 0) {
|
||||
_lastTime = null;
|
||||
return _time = sourceTime;
|
||||
}
|
||||
|
||||
final fakeTime = getFakeTime();
|
||||
if (sourceTime != _time) {
|
||||
_lastTime = FlxG.game.getTicks();
|
||||
if ((_timeInterpolation = 1 - Math.min(fakeTime - sourceTime, 1000) * 0.001) < 1 && _timeInterpolation > .9)
|
||||
return _time = fakeTime;
|
||||
else {
|
||||
_timeInterpolation = 1;
|
||||
return _time = sourceTime;
|
||||
}
|
||||
}
|
||||
else
|
||||
return fakeTime;
|
||||
}
|
||||
|
||||
function set_time(time:Float):Float @:privateAccess {
|
||||
time = FlxMath.bound(time, _offset, length - 1);
|
||||
if (_channel != null && _realPitch > 0) {
|
||||
if (!_channel.__isValid) {
|
||||
cleanup(false, true);
|
||||
startSound(time);
|
||||
}
|
||||
else {
|
||||
_source.offset = 0;
|
||||
_source.currentTime = time + _offset;
|
||||
}
|
||||
}
|
||||
|
||||
_lastTime = null;
|
||||
return _time = time;
|
||||
}
|
||||
|
||||
inline function get_offset():Float return _offset;
|
||||
inline function set_offset(offset:Float):Float {
|
||||
if (_offset == (_offset = offset)) return offset;
|
||||
//time = time + _offset;
|
||||
return offset;
|
||||
}
|
||||
|
||||
inline function get_length():Float return _length - _offset;
|
||||
|
||||
/*function get_latency():Float {
|
||||
if (_channel != null) return _source.latency;
|
||||
return 0;
|
||||
}*/
|
||||
|
||||
override function toString():String {
|
||||
return FlxStringUtil.getDebugString([
|
||||
LabelValuePair.weak("playing", playing),
|
||||
LabelValuePair.weak("time", _time),
|
||||
LabelValuePair.weak("length", length),
|
||||
LabelValuePair.weak("volume", volume),
|
||||
LabelValuePair.weak("pitch", pitch)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package funkin;
|
||||
|
||||
@:dox(hide)
|
||||
typedef SingleOrFloat = #if (java || hl || cpp) Single #else Float #end;
|
||||
@@ -85,25 +85,34 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
|
||||
moves = false;
|
||||
}
|
||||
|
||||
public static function copyFrom(source:FunkinSprite)
|
||||
/**
|
||||
* Gets the graphics and copies other properties from another sprite (Works both for `FlxSprite` and `FunkinSprite`!).
|
||||
*/
|
||||
public static function copyFrom(source:FlxSprite):FunkinSprite
|
||||
{
|
||||
var spr = new FunkinSprite();
|
||||
var casted:FunkinSprite = null;
|
||||
if (source is FunkinSprite)
|
||||
casted = cast source;
|
||||
|
||||
@:privateAccess {
|
||||
spr.setPosition(source.x, source.y);
|
||||
spr.frames = source.frames;
|
||||
if (source.animateAtlas != null && source.atlasPath != null)
|
||||
spr.loadSprite(source.atlasPath);
|
||||
if (casted != null && casted.animateAtlas != null && casted.atlasPath != null)
|
||||
spr.loadSprite(casted.atlasPath);
|
||||
spr.animation.copyFrom(source.animation);
|
||||
spr.visible = source.visible;
|
||||
spr.alpha = source.alpha;
|
||||
spr.antialiasing = source.antialiasing;
|
||||
spr.scale.set(source.scale.x, source.scale.y);
|
||||
spr.scrollFactor.set(source.scrollFactor.x, source.scrollFactor.y);
|
||||
spr.skew.set(source.skew.x, source.skew.y);
|
||||
spr.transformMatrix = source.transformMatrix;
|
||||
spr.matrixExposed = source.matrixExposed;
|
||||
spr.zoomFactor = source.zoomFactor;
|
||||
spr.animOffsets = source.animOffsets.copy();
|
||||
|
||||
if (casted != null) {
|
||||
spr.skew.set(casted.skew.x, casted.skew.y);
|
||||
spr.transformMatrix = casted.transformMatrix;
|
||||
spr.matrixExposed = casted.matrixExposed;
|
||||
spr.animOffsets = casted.animOffsets.copy();
|
||||
}
|
||||
}
|
||||
return spr;
|
||||
}
|
||||
@@ -148,7 +157,6 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
|
||||
if(!animEnabled) return;
|
||||
if (lastAnimContext != LOCK && beatAnims.length > 0 && (curBeat + beatOffset) % beatInterval == 0)
|
||||
{
|
||||
if(skipNegativeBeats && curBeat < 0) return;
|
||||
// TODO: find a solution without countedBeat
|
||||
var anim = beatAnims[FlxMath.wrap(countedBeat++, 0, beatAnims.length - 1)];
|
||||
if (anim.name != null && anim.name != "null" && anim.name != "none")
|
||||
|
||||
@@ -105,6 +105,8 @@ class MusicBeatState extends FlxState implements IBeatReceiver
|
||||
public static var skipTransOut:Bool = false;
|
||||
public static var skipTransIn:Bool = false;
|
||||
|
||||
public static var ALLOW_DEV_RELOAD:Bool = true;
|
||||
|
||||
inline function get_controls():Controls
|
||||
return PlayerSettings.solo.controls;
|
||||
inline function get_controlsP1():Controls
|
||||
@@ -153,16 +155,20 @@ class MusicBeatState extends FlxState implements IBeatReceiver
|
||||
call("postUpdate", [elapsed]);
|
||||
}
|
||||
|
||||
if (_requestSubStateReset)
|
||||
{
|
||||
if (_requestSubStateReset) {
|
||||
_requestSubStateReset = false;
|
||||
resetSubState();
|
||||
}
|
||||
if (subState != null)
|
||||
{
|
||||
subState.tryUpdate(elapsed);
|
||||
|
||||
if (/*subState == null && */(ALLOW_DEV_RELOAD && controls.DEV_RELOAD)) {
|
||||
Logs.trace("Reloading Current State...", INFO, YELLOW);
|
||||
FlxG.resetState();
|
||||
}
|
||||
|
||||
if (subState != null)
|
||||
subState.tryUpdate(elapsed);
|
||||
}
|
||||
|
||||
override function create()
|
||||
{
|
||||
loadScript();
|
||||
@@ -223,12 +229,6 @@ class MusicBeatState extends FlxState implements IBeatReceiver
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
// TODO: DEBUG MODE!!
|
||||
#if EXPERMENTAL_SCRIPT_RELOADING
|
||||
if (FlxG.keys.justPressed.F5) {
|
||||
loadScript();
|
||||
}
|
||||
#end
|
||||
call("update", [elapsed]);
|
||||
|
||||
super.update(elapsed);
|
||||
|
||||
@@ -136,15 +136,19 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver
|
||||
call("postUpdate", [elapsed]);
|
||||
}
|
||||
|
||||
if (_requestSubStateReset)
|
||||
{
|
||||
// if (subState == null && (MusicBeatState.ALLOW_DEV_RELOAD && controls.DEV_RELOAD)) {
|
||||
// Logs.trace("Reloading Current SubState...", INFO, YELLOW);
|
||||
// var test = Type.createInstance(Type.getClass(this), [this.scriptsAllowed, this.scriptName]);
|
||||
// parent.openSubState(test);
|
||||
// }
|
||||
|
||||
if (_requestSubStateReset) {
|
||||
_requestSubStateReset = false;
|
||||
resetSubState();
|
||||
}
|
||||
|
||||
if (subState != null)
|
||||
{
|
||||
subState.tryUpdate(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
override function close() {
|
||||
@@ -181,12 +185,6 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
#if EXPERMENTAL_SCRIPT_RELOADING
|
||||
// TODO: DEBUG MODE!!
|
||||
if (FlxG.keys.justPressed.F5) {
|
||||
loadScript();
|
||||
}
|
||||
#end
|
||||
call("update", [elapsed]);
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package funkin.backend.assets;
|
||||
|
||||
enum abstract AssetSource(Null<Int>)/* from Null<Bool>*/ from Null<Int> to Null<Int> {
|
||||
var SOURCE = 0;
|
||||
var MODS = 1;
|
||||
var BOTH = -1;
|
||||
|
||||
@:from public static function fromString(str:String):AssetSource
|
||||
{
|
||||
return switch (StringTools.trim(str).toLowerCase())
|
||||
{
|
||||
case "source": SOURCE;
|
||||
case "mods": MODS;
|
||||
case "both": BOTH;
|
||||
default: MODS;
|
||||
}
|
||||
}
|
||||
|
||||
/*@:from public static function fromBool(b:Null<Bool>):AssetSource
|
||||
{
|
||||
return switch (b)
|
||||
{
|
||||
case true: SOURCE;
|
||||
case false: MODS;
|
||||
case null: BOTH;
|
||||
default: MODS;
|
||||
}
|
||||
}*/
|
||||
|
||||
@:to public inline function toString():String
|
||||
{
|
||||
return switch (this)
|
||||
{
|
||||
case SOURCE: "source";
|
||||
case MODS: "mods";
|
||||
case BOTH: "both";
|
||||
default: "mods";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package funkin.backend.assets;
|
||||
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
import funkin.backend.assets.TranslatedAssetLibrary;
|
||||
#end
|
||||
import funkin.backend.assets.IModsAssetLibrary;
|
||||
import lime.utils.AssetLibrary;
|
||||
|
||||
@@ -11,18 +14,38 @@ class AssetsLibraryList extends AssetLibrary {
|
||||
private var __defaultLibraries:Array<AssetLibrary> = [];
|
||||
public var base:AssetLibrary;
|
||||
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
public var transLib:TranslatedAssetLibrary;
|
||||
#end
|
||||
|
||||
public function removeLibrary(lib:AssetLibrary) {
|
||||
if (lib == null) return lib;
|
||||
libraries.remove(lib);
|
||||
if (lib != null) {
|
||||
libraries.remove(lib);
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
// TODO: improve this code
|
||||
for(k=>l in libraries) {
|
||||
if(l == null) continue;
|
||||
if(l is TranslatedAssetLibrary) {
|
||||
var tlib = cast(l, TranslatedAssetLibrary);
|
||||
var lib:Dynamic = lib;
|
||||
if(tlib.forLibrary == lib) {
|
||||
libraries.remove(tlib);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
public function existsSpecific(id:String, type:String, source:AssetSource = BOTH) {
|
||||
if (!id.startsWith("assets/") && exists('assets/$id', type))
|
||||
if (!id.startsWith("assets/") && existsSpecific('assets/$id', type, source))
|
||||
return true;
|
||||
for(k=>e in libraries) {
|
||||
if (shouldSkipLib(k, source)) continue;
|
||||
if (e.exists(id, type))
|
||||
for(k=>l in libraries) {
|
||||
if (shouldSkipLib(l, source)) continue;
|
||||
if (l.exists(id, type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -31,7 +54,7 @@ class AssetsLibraryList extends AssetLibrary {
|
||||
|
||||
public function getSpecificPath(id:String, source:AssetSource = BOTH) {
|
||||
for(k=>e in libraries) {
|
||||
if (shouldSkipLib(k, source)) continue;
|
||||
if (shouldSkipLib(e, source)) continue;
|
||||
|
||||
@:privateAccess
|
||||
if (e.exists(id, e.types.get(id))) {
|
||||
@@ -48,22 +71,17 @@ class AssetsLibraryList extends AssetLibrary {
|
||||
|
||||
public function getFiles(folder:String, source:AssetSource = BOTH):Array<String> {
|
||||
var content:Array<String> = [];
|
||||
for(k=>e in libraries) {
|
||||
if (shouldSkipLib(k, source)) continue;
|
||||
for(k=>l in libraries) {
|
||||
if (shouldSkipLib(l, source)) continue;
|
||||
|
||||
var l = e;
|
||||
|
||||
if (l is openfl.utils.AssetLibrary) {
|
||||
@:privateAccess
|
||||
l = cast(l, openfl.utils.AssetLibrary).__proxy;
|
||||
}
|
||||
l = getCleanLibrary(l);
|
||||
|
||||
// TODO: do base folder scanning
|
||||
#if MOD_SUPPORT
|
||||
if (l is IModsAssetLibrary) {
|
||||
var lib = cast(l, IModsAssetLibrary);
|
||||
for(e in lib.getFiles(folder))
|
||||
content.push(e);
|
||||
content.pushOnce(e);
|
||||
}
|
||||
#end
|
||||
}
|
||||
@@ -72,15 +90,10 @@ class AssetsLibraryList extends AssetLibrary {
|
||||
|
||||
public function getFolders(folder:String, source:AssetSource = BOTH):Array<String> {
|
||||
var content:Array<String> = [];
|
||||
for(k=>e in libraries) {
|
||||
if (shouldSkipLib(k, source)) continue;
|
||||
for(k=>l in libraries) {
|
||||
if (shouldSkipLib(l, source)) continue;
|
||||
|
||||
var l = e;
|
||||
|
||||
if (l is openfl.utils.AssetLibrary) {
|
||||
@:privateAccess
|
||||
l = cast(l, openfl.utils.AssetLibrary).__proxy;
|
||||
}
|
||||
l = getCleanLibrary(l);
|
||||
|
||||
// TODO: do base folder scanning
|
||||
#if MOD_SUPPORT
|
||||
@@ -102,12 +115,12 @@ class AssetsLibraryList extends AssetLibrary {
|
||||
return ass;
|
||||
}
|
||||
}
|
||||
for(k=>e in libraries) {
|
||||
if (shouldSkipLib(k, source)) continue;
|
||||
for(k=>l in libraries) {
|
||||
if (shouldSkipLib(l, source)) continue;
|
||||
|
||||
@:privateAccess
|
||||
if (e.exists(id, e.types.get(id))) {
|
||||
var asset = e.getAsset(id, type);
|
||||
if (l.exists(id, l.types.get(id))) {
|
||||
var asset = l.getAsset(id, type);
|
||||
if (asset != null) {
|
||||
return asset;
|
||||
}
|
||||
@@ -115,17 +128,15 @@ class AssetsLibraryList extends AssetLibrary {
|
||||
}
|
||||
return null;
|
||||
} catch(e) {
|
||||
// TODO: trace the error
|
||||
throw e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function shouldSkipLib(k:Int, source:AssetSource) {
|
||||
return switch(source) {
|
||||
case BOTH: false;
|
||||
case SOURCE: k < libraries.length - __defaultLibraries.length;
|
||||
case MODS: k >= libraries.length - __defaultLibraries.length;
|
||||
};
|
||||
private function shouldSkipLib(lib:AssetLibrary, source:AssetSource) {
|
||||
if (source == BOTH || lib.tag == BOTH) return false;
|
||||
return source != lib.tag;
|
||||
}
|
||||
public override inline function getAsset(id:String, type:String):Dynamic
|
||||
return getSpecificAsset(id, type, BOTH);
|
||||
@@ -136,10 +147,7 @@ class AssetsLibraryList extends AssetLibrary {
|
||||
|
||||
public function new(?base:AssetLibrary) {
|
||||
super();
|
||||
if (base == null)
|
||||
base = Assets.getLibrary("default");
|
||||
addLibrary(this.base = base);
|
||||
__defaultLibraries.push(base);
|
||||
__defaultLibraries.push(addLibrary(this.base = (base == null ? Assets.getLibrary("default") : base), SOURCE));
|
||||
}
|
||||
|
||||
public function unloadLibraries() {
|
||||
@@ -158,14 +166,35 @@ class AssetsLibraryList extends AssetLibrary {
|
||||
addLibrary(d);
|
||||
}
|
||||
|
||||
public function addLibrary(lib:AssetLibrary) {
|
||||
public function addLibrary(lib:AssetLibrary, ?tag:AssetSource, ?addTransLib:Bool = true) {
|
||||
libraries.insert(0, lib);
|
||||
if(tag != null)
|
||||
lib.tag = tag;
|
||||
|
||||
if(lib.tag == null) { // if tag is null, set it to MODS, so it doesnt set the tag on the libs that exists
|
||||
lib.tag = MODS;
|
||||
//trace('AssetLibrary ${getCleanLibrary(lib)} tag not set, defaulting to MODS');
|
||||
}
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
if(addTransLib) {
|
||||
var cleanLib = getCleanLibrary(lib);
|
||||
if(cleanLib != null && (cleanLib is IModsAssetLibrary)) {
|
||||
var transLib = new TranslatedAssetLibrary(cast(cleanLib, IModsAssetLibrary));
|
||||
transLib.tag = cleanLib.tag;
|
||||
libraries.insert(0, transLib);
|
||||
}
|
||||
}
|
||||
#end
|
||||
return lib;
|
||||
}
|
||||
}
|
||||
|
||||
enum abstract AssetSource(Null<Bool>) from Bool from Null<Bool> to Null<Bool> {
|
||||
var SOURCE = true;
|
||||
var MODS = false;
|
||||
var BOTH = null;
|
||||
public static function getCleanLibrary(e:AssetLibrary):AssetLibrary {
|
||||
var l = e;
|
||||
if (l is openfl.utils.AssetLibrary) {
|
||||
var al = cast(l, openfl.utils.AssetLibrary);
|
||||
@:privateAccess
|
||||
if (al.__proxy != null) l = al.__proxy;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package funkin.backend.assets;
|
||||
|
||||
using StringTools;
|
||||
|
||||
interface IModsAssetLibrary {
|
||||
public var prefix:String;
|
||||
public var modName:String;
|
||||
public var libName:String;
|
||||
public var basePath:String;
|
||||
|
||||
#if MOD_SUPPORT
|
||||
public var _parsedAsset:String;
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package funkin.backend.assets;
|
||||
|
||||
import lime.utils.AssetLibrary;
|
||||
|
||||
#if sys
|
||||
/**
|
||||
* Used to prevent crashes
|
||||
*/
|
||||
class ModdableAssetsFolder extends ModsFolderLibrary {
|
||||
public var oldLibrary:AssetLibrary;
|
||||
|
||||
public override function exists(id:String, type:String) {
|
||||
if (!id.startsWith("assets/") && !exists('assets/$id', type))
|
||||
return oldLibrary.exists(id, type);
|
||||
return super.exists(id, type);
|
||||
}
|
||||
|
||||
public override function getAsset(id:String, type:String):Dynamic {
|
||||
if (!id.startsWith("assets/")) {
|
||||
var possibleReplacement = getAsset('assets/$id', type);
|
||||
if (possibleReplacement != null)
|
||||
return possibleReplacement;
|
||||
return oldLibrary.getAsset(id, type);
|
||||
}
|
||||
return super.getAsset(id, type);
|
||||
}
|
||||
|
||||
public function new(folder:String, libName:String, oldLib:AssetLibrary) {
|
||||
super(folder, libName);
|
||||
oldLibrary = oldLib;
|
||||
trace(Type.getClassName(Type.getClass(oldLibrary)));
|
||||
}
|
||||
|
||||
private override function __parseAsset(asset:String):Bool {
|
||||
var prefix = 'assets/';
|
||||
if (!asset.startsWith(prefix)) return false;
|
||||
_parsedAsset = asset.substr(prefix.length);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#end
|
||||
@@ -8,11 +8,11 @@ import openfl.text.Font as OpenFLFont;
|
||||
import openfl.utils.AssetLibrary;
|
||||
import openfl.utils.AssetManifest;
|
||||
|
||||
using StringTools;
|
||||
#if MOD_SUPPORT
|
||||
import sys.FileSystem;
|
||||
#end
|
||||
|
||||
using StringTools;
|
||||
|
||||
class ModsFolder {
|
||||
/**
|
||||
@@ -47,8 +47,11 @@ class ModsFolder {
|
||||
* Initializes `mods` folder.
|
||||
*/
|
||||
public static function init() {
|
||||
if(!getModsList().contains(Options.lastLoadedMod))
|
||||
if(!getModsList().contains(Options.lastLoadedMod)) {
|
||||
if(Options.lastLoadedMod != null)
|
||||
Logs.warn("Mod \"" + Options.lastLoadedMod + "\" not found in mods list, switching to base game!");
|
||||
Options.lastLoadedMod = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,6 +61,13 @@ class ModsFolder {
|
||||
public static function switchMod(mod:String) {
|
||||
Options.lastLoadedMod = currentModFolder = mod;
|
||||
reloadMods();
|
||||
if(mod == null) {
|
||||
mod = "(default)";
|
||||
}
|
||||
Logs.traceColored([
|
||||
Logs.logText('Switched to mod: '),
|
||||
Logs.logText(mod, GREEN)
|
||||
], VERBOSE);
|
||||
}
|
||||
|
||||
public static function reloadMods() {
|
||||
@@ -86,6 +96,11 @@ class ModsFolder {
|
||||
public static function getModsList():Array<String> {
|
||||
var mods:Array<String> = [];
|
||||
#if MOD_SUPPORT
|
||||
if (!FileSystem.exists(modsPath)) {
|
||||
// Mods directory does not exist yet, create it
|
||||
FileSystem.createDirectory(modsPath);
|
||||
}
|
||||
|
||||
final modsList:Array<String> = FileSystem.readDirectory(modsPath);
|
||||
|
||||
if (modsList == null || modsList.length <= 0)
|
||||
@@ -106,22 +121,20 @@ class ModsFolder {
|
||||
#end
|
||||
return mods;
|
||||
}
|
||||
public static function getLoadedMods():Array<String> {
|
||||
public static function getLoadedModsLibs(skipTranslated:Bool = false):Array<IModsAssetLibrary> {
|
||||
var libs = [];
|
||||
for (i in Paths.assetsTree.libraries) {
|
||||
var l = i;
|
||||
if (l is openfl.utils.AssetLibrary) {
|
||||
var al = cast(l, openfl.utils.AssetLibrary);
|
||||
@:privateAccess
|
||||
if (al.__proxy != null) l = al.__proxy;
|
||||
}
|
||||
var libString:String;
|
||||
if (l is ScriptedAssetLibrary || l is IModsAssetLibrary) libString = cast(l, IModsAssetLibrary).modName;
|
||||
else continue;
|
||||
libs.push(libString);
|
||||
var l = AssetsLibraryList.getCleanLibrary(i);
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
if(skipTranslated && (l is TranslatedAssetLibrary)) continue;
|
||||
#end
|
||||
if (l is ScriptedAssetLibrary || l is IModsAssetLibrary) libs.push(cast(l, IModsAssetLibrary));
|
||||
}
|
||||
return libs;
|
||||
}
|
||||
public static function getLoadedMods(skipTranslated:Bool = false):Array<String>
|
||||
return [for (modLib in getLoadedModsLibs(skipTranslated)) modLib.modName];
|
||||
|
||||
public static function prepareLibrary(libName:String, force:Bool = false) {
|
||||
var assets:AssetManifest = new AssetManifest();
|
||||
assets.name = libName;
|
||||
@@ -140,21 +153,25 @@ class ModsFolder {
|
||||
return font;
|
||||
}
|
||||
|
||||
public static function prepareModLibrary(libName:String, lib:IModsAssetLibrary, force:Bool = false) {
|
||||
public static function prepareModLibrary(libName:String, lib:IModsAssetLibrary, force:Bool = false, ?tag:AssetSource = MODS) {
|
||||
var openLib = prepareLibrary(libName, force);
|
||||
lib.prefix = 'assets/';
|
||||
@:privateAccess
|
||||
openLib.__proxy = cast(lib, lime.utils.AssetLibrary);
|
||||
if(tag != null) {
|
||||
openLib.tag = tag;
|
||||
cast(lib, lime.utils.AssetLibrary).tag = tag;
|
||||
}
|
||||
return openLib;
|
||||
}
|
||||
|
||||
#if MOD_SUPPORT
|
||||
public static function loadLibraryFromFolder(libName:String, folder:String, force:Bool = false, ?modName:String) {
|
||||
return prepareModLibrary(libName, new ModsFolderLibrary(folder, libName, modName), force);
|
||||
public static function loadLibraryFromFolder(libName:String, folder:String, force:Bool = false, ?modName:String, ?tag:AssetSource = MODS) {
|
||||
return prepareModLibrary(libName, new ModsFolderLibrary(folder, libName, modName), force, tag);
|
||||
}
|
||||
|
||||
public static function loadLibraryFromZip(libName:String, zipPath:String, force:Bool = false, ?modName:String) {
|
||||
return prepareModLibrary(libName, new ZipFolderLibrary(zipPath, libName, modName), force);
|
||||
public static function loadLibraryFromZip(libName:String, zipPath:String, force:Bool = false, ?modName:String, ?tag:AssetSource = MODS) {
|
||||
return prepareModLibrary(libName, new ZipFolderLibrary(zipPath, libName, modName), force, tag);
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package funkin.backend.assets;
|
||||
|
||||
import lime.graphics.Image;
|
||||
import openfl.utils.AssetLibrary;
|
||||
import lime.media.AudioBuffer;
|
||||
import lime.graphics.Image;
|
||||
import lime.text.Font;
|
||||
import lime.utils.AssetLibrary;
|
||||
import lime.utils.Assets as LimeAssets;
|
||||
import lime.utils.Bytes;
|
||||
|
||||
#if MOD_SUPPORT
|
||||
@@ -15,23 +14,24 @@ import sys.FileSystem;
|
||||
using StringTools;
|
||||
|
||||
class ModsFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
|
||||
public var folderPath:String;
|
||||
public var basePath:String;
|
||||
public var modName:String;
|
||||
public var libName:String;
|
||||
//public var useImageCache:Bool = true;
|
||||
public var prefix = 'assets/';
|
||||
|
||||
public function new(folderPath:String, libName:String, ?modName:String) {
|
||||
this.folderPath = folderPath;
|
||||
public function new(basePath:String, libName:String, ?modName:String) {
|
||||
this.basePath = basePath;
|
||||
this.libName = libName;
|
||||
this.prefix = 'assets/$libName/';
|
||||
if(modName == null)
|
||||
this.modName = libName;
|
||||
else
|
||||
this.modName = modName;
|
||||
this.modName = modName == null ? libName : modName;
|
||||
super();
|
||||
}
|
||||
|
||||
function toString():String {
|
||||
return '(ModsFolderLibrary: $modName)';
|
||||
}
|
||||
|
||||
#if MOD_SUPPORT
|
||||
private var editedTimes:Map<String, Float> = [];
|
||||
public var _parsedAsset:String = null;
|
||||
@@ -110,22 +110,23 @@ class ModsFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
|
||||
}
|
||||
|
||||
private function getAssetPath() {
|
||||
return '$folderPath/$_parsedAsset';
|
||||
return '$basePath/$_parsedAsset';
|
||||
}
|
||||
|
||||
private function __isCacheValid(cache:Map<String, Dynamic>, asset:String, isLocalCache:Bool = false) {
|
||||
if (!editedTimes.exists(asset))
|
||||
return false;
|
||||
if (editedTimes[asset] == null || editedTimes[asset] < FileSystem.stat(getPath(asset)).mtime.getTime()) {
|
||||
var editedTime = editedTimes[asset];
|
||||
if (editedTime == null || editedTime < FileSystem.stat(getPath(asset)).mtime.getTime()) {
|
||||
// destroy already existing to prevent memory leak!!!
|
||||
var asset = cache[asset];
|
||||
/*var asset = cache[asset];
|
||||
if (asset != null) {
|
||||
switch(Type.getClass(asset)) {
|
||||
case lime.graphics.Image:
|
||||
trace("getting rid of image cause replaced");
|
||||
cast(asset, lime.graphics.Image);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -149,4 +150,14 @@ class ModsFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
|
||||
return true;
|
||||
}
|
||||
#end
|
||||
|
||||
// Backwards compat
|
||||
|
||||
@:noCompletion public var folderPath(get, set):String;
|
||||
@:noCompletion private inline function get_folderPath():String {
|
||||
return basePath;
|
||||
}
|
||||
@:noCompletion private inline function set_folderPath(value:String):String {
|
||||
return basePath = value;
|
||||
}
|
||||
}
|
||||
@@ -32,37 +32,37 @@ class Paths
|
||||
public static inline function ndll(key:String)
|
||||
return getPath('ndlls/$key.ndll');
|
||||
|
||||
inline static public function file(file:String, ?library:String)
|
||||
public static inline function file(file:String, ?library:String)
|
||||
return getPath(file, library);
|
||||
|
||||
inline static public function txt(key:String, ?library:String)
|
||||
public static inline function txt(key:String, ?library:String)
|
||||
return getPath('data/$key.txt', library);
|
||||
|
||||
inline static public function pack(key:String, ?library:String)
|
||||
public static inline function pack(key:String, ?library:String)
|
||||
return getPath('data/$key.pack', library);
|
||||
|
||||
inline static public function ini(key:String, ?library:String)
|
||||
public static inline function ini(key:String, ?library:String)
|
||||
return getPath('data/$key.ini', library);
|
||||
|
||||
inline static public function fragShader(key:String, ?library:String)
|
||||
public static inline function fragShader(key:String, ?library:String)
|
||||
return getPath('shaders/$key.frag', library);
|
||||
|
||||
inline static public function vertShader(key:String, ?library:String)
|
||||
public static inline function vertShader(key:String, ?library:String)
|
||||
return getPath('shaders/$key.vert', library);
|
||||
|
||||
inline static public function xml(key:String, ?library:String)
|
||||
public static inline function xml(key:String, ?library:String)
|
||||
return getPath('data/$key.xml', library);
|
||||
|
||||
inline static public function json(key:String, ?library:String)
|
||||
public static inline function json(key:String, ?library:String)
|
||||
return getPath('data/$key.json', library);
|
||||
|
||||
inline static public function ps1(key:String, ?library:String)
|
||||
public static inline function ps1(key:String, ?library:String)
|
||||
return getPath('data/$key.ps1', library);
|
||||
|
||||
static public function sound(key:String, ?library:String, ?ext:String)
|
||||
return getPath('sounds/$key.${ext != null ? ext : Flags.SOUND_EXT}', library);
|
||||
|
||||
inline static public function soundRandom(key:String, min:Int, max:Int, ?library:String)
|
||||
public static inline function soundRandom(key:String, min:Int, max:Int, ?library:String)
|
||||
return sound(key + FlxG.random.int(min, max), library);
|
||||
|
||||
inline static public function music(key:String, ?library:String, ?ext:String)
|
||||
@@ -71,16 +71,14 @@ class Paths
|
||||
inline static public function voices(song:String, ?difficulty:String, ?prefix:String = "", ?ext:String) {
|
||||
if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY;
|
||||
if (ext == null) ext = Flags.SOUND_EXT;
|
||||
song = song.toLowerCase();
|
||||
var diff = getPath('songs/$song/song/Voices$prefix-${difficulty.toLowerCase()}.${ext}', null);
|
||||
var diff = getPath('songs/$song/song/Voices$prefix-${difficulty}.${ext}', null);
|
||||
return OpenFlAssets.exists(diff) ? diff : getPath('songs/$song/song/Voices$prefix.${ext}', null);
|
||||
}
|
||||
|
||||
inline static public function inst(song:String, ?difficulty:String, ?prefix:String = "", ?ext:String) {
|
||||
if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY;
|
||||
if (ext == null) ext = Flags.SOUND_EXT;
|
||||
song = song.toLowerCase();
|
||||
var diff = getPath('songs/$song/song/Inst$prefix-${difficulty.toLowerCase()}.${ext}', null);
|
||||
var diff = getPath('songs/$song/song/Inst$prefix-${difficulty}.${ext}', null);
|
||||
return OpenFlAssets.exists(diff) ? diff : getPath('songs/$song/song/Inst$prefix.${ext}', null);
|
||||
}
|
||||
|
||||
@@ -95,7 +93,7 @@ class Paths
|
||||
return getPath('images/$key.$ext', library);
|
||||
}
|
||||
|
||||
static public function script(key:String, ?library:String, isAssetsPath:Bool = false) {
|
||||
public static inline function script(key:String, ?library:String, isAssetsPath:Bool = false) {
|
||||
var scriptPath = isAssetsPath ? key : getPath(key, library);
|
||||
if (!OpenFlAssets.exists(scriptPath)) {
|
||||
var p:String;
|
||||
@@ -111,14 +109,12 @@ class Paths
|
||||
|
||||
static public function chart(song:String, ?difficulty:String):String
|
||||
{
|
||||
difficulty = (difficulty != null ? difficulty : Flags.DEFAULT_DIFFICULTY).toLowerCase();
|
||||
song = song.toLowerCase();
|
||||
difficulty = (difficulty != null ? difficulty : Flags.DEFAULT_DIFFICULTY);
|
||||
|
||||
return getPath('songs/$song/charts/$difficulty.json', null);
|
||||
}
|
||||
|
||||
inline static public function character(character:String):String
|
||||
{
|
||||
public static function character(character:String):String {
|
||||
return getPath('data/characters/$character.xml', null);
|
||||
}
|
||||
|
||||
@@ -126,33 +122,31 @@ class Paths
|
||||
* Gets the name of a registered font.
|
||||
* @param font The font's path (if it's already passed as a font name, the same name will be returned)
|
||||
*/
|
||||
inline static public function getFontName(font:String)
|
||||
{
|
||||
inline static public function getFontName(font:String) {
|
||||
return OpenFlAssets.exists(font, FONT) ? OpenFlAssets.getFont(font).fontName : font;
|
||||
}
|
||||
|
||||
inline static public function font(key:String)
|
||||
{
|
||||
public static inline function font(key:String) {
|
||||
return getPath('fonts/$key');
|
||||
}
|
||||
|
||||
inline static public function obj(key:String) {
|
||||
public static inline function obj(key:String) {
|
||||
return getPath('models/$key.obj');
|
||||
}
|
||||
|
||||
inline static public function dae(key:String) {
|
||||
public static inline function dae(key:String) {
|
||||
return getPath('models/$key.dae');
|
||||
}
|
||||
|
||||
inline static public function md2(key:String) {
|
||||
public static inline function md2(key:String) {
|
||||
return getPath('models/$key.md2');
|
||||
}
|
||||
|
||||
inline static public function md5(key:String) {
|
||||
public static inline function md5(key:String) {
|
||||
return getPath('models/$key.md5');
|
||||
}
|
||||
|
||||
inline static public function awd(key:String) {
|
||||
public static inline function awd(key:String) {
|
||||
return getPath('models/$key.awd');
|
||||
}
|
||||
|
||||
@@ -193,6 +187,29 @@ class Paths
|
||||
return tempFramesCache[key] = loadFrames(assetsPath ? key : Paths.image(key, library, true, ext), false, null, false, ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the images needed for using getFrames() exist.
|
||||
* @param key Path to the image
|
||||
* @param checkAtlas Whenever to check for the Animation.json file (used in FlxAnimate)
|
||||
* @param assetsPath Whenever to use the raw path or to pass it through Paths.image()
|
||||
* @param library (Additional) library to load the frames from.
|
||||
* @return True if the images exist, false otherwise.
|
||||
**/
|
||||
public static function framesExists(key:String, checkAtlas:Bool = false, checkMulti:Bool = true, assetsPath:Bool = false, ?library:String) {
|
||||
var path = assetsPath ? key : Paths.image(key, library, true);
|
||||
var noExt = Path.withoutExtension(path);
|
||||
if(checkAtlas && Assets.exists('$noExt/Animation.json'))
|
||||
return true;
|
||||
if(checkMulti && Assets.exists('$noExt/1.png'))
|
||||
return true;
|
||||
if(Assets.exists('$noExt.xml'))
|
||||
return true;
|
||||
if(Assets.exists('$noExt.txt'))
|
||||
return true;
|
||||
if(Assets.exists('$noExt.json'))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads frames from a specific image path. Supports Sparrow Atlases, Packer Atlases, and multiple spritesheets.
|
||||
@@ -238,7 +255,8 @@ class Paths
|
||||
return null;
|
||||
return graph.imageFrame;
|
||||
}
|
||||
static public function getFolderDirectories(key:String, addPath:Bool = false, source:AssetsLibraryList.AssetSource = BOTH):Array<String> {
|
||||
|
||||
public static function getFolderDirectories(key:String, addPath:Bool = false, source:AssetSource = BOTH):Array<String> {
|
||||
if (!key.endsWith("/")) key += "/";
|
||||
var content = assetsTree.getFolders('assets/$key', source);
|
||||
if (addPath) {
|
||||
@@ -247,13 +265,13 @@ class Paths
|
||||
}
|
||||
return content;
|
||||
}
|
||||
static public function getFolderContent(key:String, addPath:Bool = false, source:AssetsLibraryList.AssetSource = BOTH):Array<String> {
|
||||
static public function getFolderContent(key:String, addPath:Bool = false, source:AssetSource = BOTH, noExtension:Bool = false):Array<String> {
|
||||
// designed to work both on windows and web
|
||||
if (!key.endsWith("/")) key += "/";
|
||||
var content = assetsTree.getFiles('assets/$key', source);
|
||||
if (addPath) {
|
||||
for(k=>e in content)
|
||||
content[k] = '$key$e';
|
||||
for (k => e in content) {
|
||||
if (noExtension) e = Path.withoutExtension(e);
|
||||
content[k] = addPath ? '$key$e' : e;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ class ScriptedAssetLibrary extends ModsFolderLibrary {
|
||||
public var scriptName:String;
|
||||
private static var nullValue:Dynamic = {};
|
||||
|
||||
public function new(scriptName:String, args:Array<Dynamic> = null, folderPath:String="./assets/", libName:String="assets", ?modName:String) {
|
||||
public function new(scriptName:String, args:Array<Dynamic> = null, basePath:String="./assets/", libName:String="assets", ?modName:String) {
|
||||
if(modName == null) modName = scriptName;
|
||||
super(folderPath, libName, modName);
|
||||
super(basePath, libName, modName);
|
||||
this.scriptName = scriptName;
|
||||
script = Script.create(Paths.script("data/library/" + scriptName));
|
||||
script.setParent(this);
|
||||
@@ -30,7 +30,10 @@ class ScriptedAssetLibrary extends ModsFolderLibrary {
|
||||
script.load();
|
||||
if(args == null) args = [];
|
||||
script.call("create", args);
|
||||
trace(script);
|
||||
}
|
||||
|
||||
override function toString():String {
|
||||
return '(ScriptedAssetLibrary: $libName/$modName/$scriptName)';
|
||||
}
|
||||
|
||||
#if MOD_SUPPORT
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package funkin.backend.assets;
|
||||
|
||||
import lime.utils.AssetLibrary as LimeAssetLibrary;
|
||||
import openfl.utils.AssetLibrary;
|
||||
import lime.media.AudioBuffer;
|
||||
import lime.graphics.Image;
|
||||
import lime.text.Font;
|
||||
import lime.utils.Bytes;
|
||||
|
||||
class TranslatedAssetLibrary extends AssetLibrary implements IModsAssetLibrary {
|
||||
public var libName:String;
|
||||
public var modName:String;
|
||||
public var basePath:String;
|
||||
public var prefix:String = TranslationUtil.LANG_FOLDER + "/";
|
||||
|
||||
public var forLibrary:IModsAssetLibrary;
|
||||
|
||||
public var langFolder(get, set):String;
|
||||
@:noCompletion private inline function get_langFolder():String {
|
||||
return libName;
|
||||
}
|
||||
@:noCompletion private inline function set_langFolder(value:String):String {
|
||||
basePath = prefix + (libName = modName = (value != null ? value : Flags.DEFAULT_LANGUAGE)) + "/";
|
||||
return libName;
|
||||
}
|
||||
|
||||
public function new(lib:IModsAssetLibrary, ?langFolder:String) {
|
||||
super();
|
||||
this.forLibrary = lib;
|
||||
this.langFolder = langFolder;
|
||||
}
|
||||
|
||||
function toString():String
|
||||
return '(TranslatedAssetLibrary: Lang: $libName | For: ${forLibrary})';
|
||||
|
||||
private inline function getAssetPath():String // because of the IModsAssetLibrary - Nex
|
||||
return basePath;
|
||||
|
||||
public inline function formatPath(mainPath:String, ?asset:String):String {
|
||||
if(!mainPath.endsWith('/')) mainPath += '/';
|
||||
return mainPath + getAssetPath() + (asset == null ? "" : asset);
|
||||
}
|
||||
|
||||
public override function getAudioBuffer(id:String):AudioBuffer
|
||||
{
|
||||
// TODO: rewrite this, once it works
|
||||
var libs = [forLibrary];
|
||||
for(lib in libs) {
|
||||
if(!(lib is AssetLibrary)) continue;
|
||||
var val = cast(lib, AssetLibrary).getAudioBuffer(formatPath(lib.prefix, id));
|
||||
if(val != null) return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override function getBytes(id:String):Bytes
|
||||
{
|
||||
var libs = [forLibrary];
|
||||
for(lib in libs) {
|
||||
if(!(lib is AssetLibrary)) continue;
|
||||
var val = cast(lib, AssetLibrary).getBytes(formatPath(lib.prefix, id));
|
||||
if(val != null) return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override function getText(id:String):String
|
||||
{
|
||||
var libs = [forLibrary];
|
||||
for(lib in libs) {
|
||||
if(!(lib is AssetLibrary)) continue;
|
||||
var val = cast(lib, AssetLibrary).getText(formatPath(lib.prefix, id));
|
||||
if(val != null) return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override function getFont(id:String):Font
|
||||
{
|
||||
var libs = [forLibrary];
|
||||
for(lib in libs) {
|
||||
if(!(lib is AssetLibrary)) continue;
|
||||
var val = cast(lib, AssetLibrary).getFont(formatPath(lib.prefix, id));
|
||||
if(val != null) return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override function getImage(id:String):Image
|
||||
{
|
||||
var libs = [forLibrary];
|
||||
for(lib in libs) {
|
||||
if(!(lib is AssetLibrary)) continue;
|
||||
var val = cast(lib, AssetLibrary).getImage(formatPath(lib.prefix, id));
|
||||
if(val != null) return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override function getPath(id:String):String
|
||||
{
|
||||
var libs = [forLibrary];
|
||||
for(lib in libs) {
|
||||
if(!(lib is AssetLibrary)) continue;
|
||||
var val = cast(lib, AssetLibrary).getPath(formatPath(lib.prefix, id));
|
||||
if(val != null) return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#if MOD_SUPPORT
|
||||
public var _parsedAsset:String = null; // Theres no need to actually make this work - Nex
|
||||
|
||||
public function getFiles(folder:String):Array<String>
|
||||
{
|
||||
var libs = [forLibrary];
|
||||
for(lib in libs) {
|
||||
if(!(lib is AssetLibrary)) continue;
|
||||
var val = lib.getFiles(formatPath(lib.prefix, folder));
|
||||
if(val != null && val.length > 0) return val;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getFolders(folder:String):Array<String>
|
||||
{
|
||||
var libs = [forLibrary];
|
||||
for(lib in libs) {
|
||||
if(!(lib is AssetLibrary)) continue;
|
||||
var val = lib.getFolders(formatPath(lib.prefix, folder));
|
||||
if(val != null && val.length > 0) return val;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private function __isCacheValid(cache:Map<String, Dynamic>, asset:String, isLocal:Bool = false):Bool
|
||||
{
|
||||
var libs = [forLibrary];
|
||||
for(lib in libs) {
|
||||
if(!(lib is AssetLibrary)) continue;
|
||||
|
||||
// are you fucking serious (no the fucking switch doesnt work here) - Nex
|
||||
var _lib = cast(lib, AssetLibrary);
|
||||
var libCache = (cache == cachedAudioBuffers) ? _lib.cachedAudioBuffers :
|
||||
(cache == cachedBytes) ? _lib.cachedBytes :
|
||||
(cache == cachedFonts) ? _lib.cachedFonts :
|
||||
(cache == cachedImages) ? _lib.cachedImages :
|
||||
(cache == cachedText) ? _lib.cachedText : cache;
|
||||
|
||||
@:privateAccess if(lib.__isCacheValid(libCache, formatPath(lib.prefix, asset), isLocal)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function __parseAsset(asset:String):Bool
|
||||
{
|
||||
@:privateAccess
|
||||
var libs = [forLibrary];
|
||||
for(lib in libs) {
|
||||
if(!(lib is AssetLibrary)) continue;
|
||||
if(lib.__parseAsset(formatPath(lib.prefix, asset))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#end
|
||||
|
||||
public override function exists(id:String, type:String):Bool
|
||||
{
|
||||
for(lib in ModsFolder.getLoadedModsLibs(true)) if(lib is AssetLibrary && cast(lib, AssetLibrary).exists(formatPath(lib.prefix, id), type)) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,18 @@
|
||||
package funkin.backend.assets;
|
||||
|
||||
import haxe.io.Path;
|
||||
import lime.graphics.Image;
|
||||
import lime.media.AudioBuffer;
|
||||
import lime.text.Font;
|
||||
import lime.utils.Bytes;
|
||||
import openfl.utils.AssetLibrary;
|
||||
|
||||
#if MOD_SUPPORT
|
||||
import funkin.backend.utils.SysZip.SysZipEntry;
|
||||
import funkin.backend.utils.SysZip;
|
||||
import haxe.io.Path;
|
||||
import haxe.zip.Reader;
|
||||
import lime.app.Event;
|
||||
import lime.app.Future;
|
||||
import lime.app.Promise;
|
||||
import lime.graphics.Image;
|
||||
import lime.media.AudioBuffer;
|
||||
import lime.text.Font;
|
||||
import lime.utils.AssetLibrary;
|
||||
import lime.utils.AssetManifest;
|
||||
import lime.utils.AssetType;
|
||||
import lime.utils.Assets as LimeAssets;
|
||||
import lime.utils.Bytes;
|
||||
import lime.utils.Log;
|
||||
import openfl.text.Font as OpenFLFont;
|
||||
import sys.FileStat;
|
||||
import sys.FileSystem;
|
||||
import sys.io.File;
|
||||
|
||||
class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
|
||||
public var zipPath:String;
|
||||
public var basePath:String;
|
||||
public var modName:String;
|
||||
public var libName:String;
|
||||
public var useImageCache:Bool = false;
|
||||
@@ -34,16 +23,17 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
|
||||
public var lowerCaseAssets:Map<String, SysZipEntry> = [];
|
||||
public var nameMap:Map<String, String> = [];
|
||||
|
||||
public function new(zipPath:String, libName:String, ?modName:String) {
|
||||
this.zipPath = zipPath;
|
||||
public function new(basePath:String, libName:String, ?modName:String) {
|
||||
this.libName = libName;
|
||||
|
||||
this.basePath = basePath;
|
||||
|
||||
if(modName == null)
|
||||
this.modName = libName;
|
||||
else
|
||||
this.modName = modName;
|
||||
|
||||
zip = SysZip.openFromFile(zipPath);
|
||||
zip = SysZip.openFromFile(basePath);
|
||||
zip.read();
|
||||
for(entry in zip.entries) {
|
||||
lowerCaseAssets[entry.fileName.toLowerCase()] = assets[entry.fileName.toLowerCase()] = assets[entry.fileName] = entry;
|
||||
@@ -53,6 +43,10 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
|
||||
super();
|
||||
}
|
||||
|
||||
function toString():String {
|
||||
return '(ZipFolderLibrary: $libName/$modName)';
|
||||
}
|
||||
|
||||
public var _parsedAsset:String;
|
||||
|
||||
public override function getAudioBuffer(id:String):AudioBuffer {
|
||||
@@ -113,8 +107,8 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
|
||||
}
|
||||
|
||||
private function getAssetPath() {
|
||||
trace('[ZIP]$zipPath/$_parsedAsset');
|
||||
return '[ZIP]$zipPath/$_parsedAsset';
|
||||
trace('[ZIP]$basePath/$_parsedAsset');
|
||||
return '[ZIP]$basePath/$_parsedAsset';
|
||||
}
|
||||
|
||||
// TODO: rewrite this to 1 function, like ModsFolderLibrary
|
||||
@@ -162,5 +156,15 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// Backwards compat
|
||||
|
||||
@:noCompletion public var zipPath(get, set):String;
|
||||
@:noCompletion private inline function get_zipPath():String {
|
||||
return basePath;
|
||||
}
|
||||
@:noCompletion private inline function set_zipPath(value:String):String {
|
||||
return basePath = value;
|
||||
}
|
||||
}
|
||||
#end
|
||||
@@ -1,176 +0,0 @@
|
||||
package funkin.backend.chart;
|
||||
|
||||
import funkin.backend.chart.ChartData.ChartMetaData;
|
||||
|
||||
// These new structures are kinda a mess to port, i love and hate them at the same time; why the hell are every difficulty in the same file??? - Nex
|
||||
class BaseGameParser {
|
||||
public static function parseChart(data:Dynamic, metaData:Dynamic, result:ChartData) {
|
||||
// TO DO
|
||||
}
|
||||
|
||||
public static function parseMeta(data:Dynamic, result:ChartMetaData) {
|
||||
var data:SwagMetadata = data;
|
||||
result.name = data.songName;
|
||||
result.artist = data.artist;
|
||||
|
||||
result.customValues = {};
|
||||
result.customValues.timeChanges = data.timeChanges;
|
||||
var firstTimeChange:SwagTimeChange = data.timeChanges[0];
|
||||
result.bpm = firstTimeChange.bpm;
|
||||
result.beatsPerMeasure = firstTimeChange.b != null ? firstTimeChange.b : Flags.DEFAULT_BEATS_PER_MEASURE;
|
||||
|
||||
result.difficulties = data.playData.difficulties.concat(data.playData.songVariations);
|
||||
Reflect.deleteField(data.playData, "difficulties");
|
||||
|
||||
for (field in Reflect.fields(data.playData))
|
||||
Reflect.setProperty(result.customValues, field, Reflect.getProperty(data.playData, field));
|
||||
}
|
||||
|
||||
public static function encodeMeta(meta:ChartMetaData, ?chart:ChartData):SwagMetadata {
|
||||
var addVars:Dynamic = meta.customValues;
|
||||
var defStage:String = addVars.stage != null ? addVars.stage : Flags.DEFAULT_STAGE;
|
||||
var defChars:SwagCharactersList = addVars.characters != null ? addVars.characters : {player: Flags.DEFAULT_CHARACTER, girlfriend: Flags.DEFAULT_GIRLFRIEND, opponent: Flags.DEFAULT_OPPONENT};
|
||||
var defTimeCh:Array<SwagTimeChange>;
|
||||
|
||||
if (addVars.timeChanges != null && addVars.timeChanges.length > 0) {
|
||||
defTimeCh = addVars.timeChanges;
|
||||
defTimeCh[0].bpm = meta.bpm;
|
||||
}
|
||||
else defTimeCh = [{bpm: meta.bpm, t: -1}];
|
||||
|
||||
if (chart != null) {
|
||||
defStage = chart.stage;
|
||||
|
||||
var done:Array<Bool> = [false, false, false];
|
||||
for (strumLine in chart.strumLines) switch (strumLine.type) {
|
||||
case OPPONENT:
|
||||
if (!done[0]) {
|
||||
done[0] = true;
|
||||
defChars.opponent = strumLine.characters.getDefault([defChars.opponent])[0];
|
||||
}
|
||||
case PLAYER:
|
||||
if (!done[1]) {
|
||||
done[1] = true;
|
||||
defChars.player = strumLine.characters.getDefault([defChars.player])[0];
|
||||
}
|
||||
case ADDITIONAL:
|
||||
if (!done[3]) {
|
||||
done[3] = true;
|
||||
defChars.girlfriend = strumLine.characters.getDefault([defChars.girlfriend])[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result:SwagMetadata = {
|
||||
songName: meta.name,
|
||||
timeFormat: MILLISECONDS,
|
||||
artist: meta.artist,
|
||||
timeChanges: defTimeCh,
|
||||
looped: false,
|
||||
generatedBy: 'Basegame Chart Importer (Codename Engine)',
|
||||
version: Flags.BASEGAME_SONG_METADATA_VERSION,
|
||||
playData: {
|
||||
stage: defStage,
|
||||
characters: defChars,
|
||||
songVariations: addVars.songVariations != null ? [for (i in 0...addVars.songVariations.length) {meta.difficulties.remove(addVars.songVariations[i]); addVars.songVariations[i];}] : [],
|
||||
difficulties: meta.difficulties,
|
||||
noteStyle: addVars.noteStyle != null ? addVars.noteStyle : Flags.BASEGAME_DEFAULT_NOTE_STYLE,
|
||||
album: addVars.album != null ? addVars.album : Flags.BASEGAME_DEFAULT_ALBUM_ID,
|
||||
previewStart: addVars.previewStart != null ? addVars.previewStart : Flags.BASEGAME_DEFAULT_PREVIEW_START,
|
||||
previewEnd: addVars.previewEnd != null ? addVars.previewEnd : Flags.BASEGAME_DEFAULT_PREVIEW_END
|
||||
},
|
||||
};
|
||||
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function encodeChart(chart:ChartData):NewSwagSong {
|
||||
// TO DO
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// METADATA STRUCTURES
|
||||
typedef SwagMetadata =
|
||||
{
|
||||
var timeFormat:SwagTimeFormat;
|
||||
var artist:String;
|
||||
var songName:String;
|
||||
var playData:SwagPlayData;
|
||||
var timeChanges:Array<SwagTimeChange>;
|
||||
var generatedBy:String;
|
||||
var looped:Bool;
|
||||
var version:String;
|
||||
|
||||
var ?divisions:Int;
|
||||
var ?offsets:SwagSongOffsets;
|
||||
}
|
||||
|
||||
enum abstract SwagTimeFormat(String) from String to String
|
||||
{
|
||||
var TICKS = 'ticks';
|
||||
var FLOAT = 'float';
|
||||
var MILLISECONDS = 'ms';
|
||||
}
|
||||
|
||||
typedef SwagTimeChange =
|
||||
{
|
||||
var ?d:Int; // Time Signature Den
|
||||
var ?n:Int; // Time Signature Num
|
||||
var t:Int; // Time Stamp
|
||||
var ?b:Int; // Beat Time
|
||||
var ?bt:Array<Int>; // Beat Tuplets
|
||||
var bpm:Float;
|
||||
}
|
||||
|
||||
typedef SwagPlayData =
|
||||
{
|
||||
var album:String;
|
||||
var previewStart:Float;
|
||||
var previewEnd:Float;
|
||||
var stage:String;
|
||||
var characters:SwagCharactersList;
|
||||
var songVariations:Array<String>;
|
||||
var difficulties:Array<String>;
|
||||
var noteStyle:String;
|
||||
}
|
||||
|
||||
typedef SwagCharactersList =
|
||||
{
|
||||
var player:String;
|
||||
var girlfriend:String;
|
||||
var opponent:String;
|
||||
}
|
||||
|
||||
typedef SwagSongOffsets =
|
||||
{
|
||||
var ?instrumental:Float;
|
||||
var ?altInstrumentals:Dynamic;
|
||||
var ?vocals:Dynamic;
|
||||
}
|
||||
|
||||
// CHART STRUCTURE
|
||||
typedef NewSwagSong =
|
||||
{
|
||||
var version:String;
|
||||
var scrollSpeed:Dynamic; // Map<String, Float>
|
||||
var events:Array<SwagEvent>;
|
||||
var notes:Dynamic; // Map<String, Array<SwagNote>>
|
||||
var generatedBy:String;
|
||||
}
|
||||
|
||||
typedef SwagEvent =
|
||||
{
|
||||
var t:Float; // Time
|
||||
var e:String; // Event Kind
|
||||
var v:Dynamic; // Value (Map<String, Dynamic>)
|
||||
}
|
||||
|
||||
typedef SwagNote =
|
||||
{
|
||||
var t:Float; // Time
|
||||
var d:Int; // Data
|
||||
var l:Float; // Length
|
||||
var k:String; // Kind
|
||||
}
|
||||
@@ -72,7 +72,7 @@ class Chart {
|
||||
}
|
||||
|
||||
public static function loadEventsJson(songName:String) {
|
||||
var path = Paths.file('songs/${songName.toLowerCase()}/events.json');
|
||||
var path = Paths.file('songs/${songName}/events.json');
|
||||
var data:Array<ChartEvent> = null;
|
||||
if (Assets.exists(path)) {
|
||||
try {
|
||||
@@ -83,31 +83,29 @@ class Chart {
|
||||
return data;
|
||||
}
|
||||
|
||||
public static function loadChartMeta(songName:String, ?difficulty:String, fromMods:Bool = true) {
|
||||
public static function loadChartMeta(songName:String, ?difficulty:String, fromMods:Bool = true):ChartMetaData {
|
||||
if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY;
|
||||
|
||||
var songNameLower = songName.toLowerCase();
|
||||
var metaPath = Paths.file('songs/${songNameLower}/meta.json');
|
||||
var metaDiffPath = Paths.file('songs/${songNameLower}/meta-${difficulty.toLowerCase()}.json');
|
||||
var metaPath = Paths.file('songs/${songName}/meta.json');
|
||||
var metaDiffPath = Paths.file('songs/${songName}/meta-${difficulty}.json');
|
||||
|
||||
var data:ChartMetaData = null;
|
||||
var fromMods:Bool = fromMods;
|
||||
for(path in [metaDiffPath, metaPath]) {
|
||||
if (Assets.exists(path)) {
|
||||
fromMods = Paths.assetsTree.existsSpecific(path, "TEXT", MODS);
|
||||
try {
|
||||
data = Json.parse(Assets.getText(path));
|
||||
} catch(e) {
|
||||
Logs.trace('Failed to load song metadata for ${songName} ($path): ${Std.string(e)}', ERROR);
|
||||
}
|
||||
if (data != null) break;
|
||||
}
|
||||
for (path in [metaDiffPath, metaPath]) if (Assets.exists(path)) {
|
||||
fromMods = Paths.assetsTree.existsSpecific(path, "TEXT", MODS);
|
||||
try {
|
||||
var tempData = Json.parse(Assets.getText(path));
|
||||
tempData.color = CoolUtil.getColorFromDynamic(tempData.color).getDefault(Flags.DEFAULT_COLOR);
|
||||
data = tempData;
|
||||
} catch(e) Logs.trace('Failed to load song metadata for ${songName} ($path): ${Std.string(e)}', ERROR);
|
||||
if (data != null) break;
|
||||
}
|
||||
|
||||
if (data == null)
|
||||
data = {
|
||||
name: songName
|
||||
};
|
||||
if (data == null) data = {
|
||||
name: songName,
|
||||
bpm: 100
|
||||
};
|
||||
|
||||
data.setFieldDefault("name", songName);
|
||||
data.setFieldDefault("bpm", Flags.DEFAULT_BPM);
|
||||
data.setFieldDefault("beatsPerMeasure", Flags.DEFAULT_BEATS_PER_MEASURE);
|
||||
@@ -118,24 +116,19 @@ class Chart {
|
||||
data.setFieldDefault("coopAllowed", Flags.DEFAULT_COOP_ALLOWED);
|
||||
data.setFieldDefault("opponentModeAllowed", Flags.DEFAULT_OPPONENT_MODE_ALLOWED);
|
||||
data.setFieldDefault("displayName", data.name);
|
||||
data.setFieldDefault("parsedColor", data.color.getColorFromDynamic().getDefault(Flags.DEFAULT_COLOR));
|
||||
|
||||
if (data.difficulties.length <= 0) {
|
||||
data.difficulties = [for(f in Paths.getFolderContent('songs/${songNameLower}/charts/', false, fromMods ? MODS : SOURCE)) if (Path.extension(f = f.toUpperCase()) == "JSON") Path.withoutExtension(f)];
|
||||
data.difficulties = [for(f in Paths.getFolderContent('songs/${songName}/charts/', false, fromMods ? MODS : SOURCE)) if (Path.extension(f.toUpperCase()) == "JSON") Path.withoutExtension(f)];
|
||||
var tempDiffs = [];
|
||||
if (data.difficulties.length == 3) {
|
||||
var hasHard = false, hasNormal = false, hasEasy = false;
|
||||
for(d in data.difficulties) {
|
||||
switch(d) {
|
||||
case "EASY": hasEasy = true;
|
||||
case "NORMAL": hasNormal = true;
|
||||
case "HARD": hasHard = true;
|
||||
switch(d.toLowerCase()) {
|
||||
case "easy": tempDiffs.insert(0, d);
|
||||
case "normal": tempDiffs.insert(1, d);
|
||||
case "hard": tempDiffs.insert(2, d);
|
||||
}
|
||||
}
|
||||
if (hasHard && hasNormal && hasEasy) {
|
||||
data.difficulties[0] = "EASY";
|
||||
data.difficulties[1] = "NORMAL";
|
||||
data.difficulties[2] = "HARD";
|
||||
}
|
||||
data.difficulties = tempDiffs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,15 +154,13 @@ class Chart {
|
||||
|
||||
var valid:Bool = true;
|
||||
if (!Assets.exists(chartPath)) {
|
||||
Logs.trace('Chart for song ${songName} ($difficulty) at "$chartPath" was not found.', ERROR, RED);
|
||||
Logs.error('Chart for song ${songName} ($difficulty) at "$chartPath" was not found.');
|
||||
valid = false;
|
||||
}
|
||||
var data:Dynamic = null;
|
||||
try {
|
||||
if (valid)
|
||||
data = Json.parse(Assets.getText(chartPath));
|
||||
} catch(e) {
|
||||
Logs.trace('Could not parse chart for song ${songName} ($difficulty): ${Std.string(e)}', ERROR, RED);
|
||||
if (valid) {
|
||||
try data = Json.parse(Assets.getText(chartPath))
|
||||
catch(e) Logs.trace('Could not parse chart for song ${songName} ($difficulty): ${Std.string(e)}', ERROR, RED);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,7 +169,7 @@ class Chart {
|
||||
#if REGION
|
||||
if (data != null) switch (detectChartFormat(data)) {
|
||||
case CODENAME:
|
||||
// backward compat on events since its caused problems
|
||||
// backward compat on events since it caused problems
|
||||
var eventTypesToString:Map<Int, String> = [
|
||||
-1 => "HScript Call",
|
||||
0 => "Unknown",
|
||||
@@ -196,7 +187,7 @@ class Chart {
|
||||
|
||||
base = data;
|
||||
case PSYCH_NEW: PsychParser.parse(data, base);
|
||||
case VSLICE: // TODO
|
||||
case VSLICE: Logs.trace("Couldn't parse V-Slice chart because it's not supported on runtime, it MUST be imported and converted in the new song editor window.", ERROR, RED);
|
||||
case LEGACY: FNFLegacyParser.parse(data, base);
|
||||
}
|
||||
#end
|
||||
@@ -238,8 +229,7 @@ class Chart {
|
||||
return 0;
|
||||
default:
|
||||
var index = chart.noteTypes.indexOf(noteTypeName);
|
||||
if (index > -1)
|
||||
return index+1;
|
||||
if (index > -1) return index + 1;
|
||||
chart.noteTypes.push(noteTypeName);
|
||||
return chart.noteTypes.length;
|
||||
}
|
||||
@@ -275,9 +265,8 @@ class Chart {
|
||||
|
||||
CoolUtil.safeSaveFile(chartPath, Json.stringify(filteredChart, null, saveSettings.prettyPrint == true ? Flags.JSON_PRETTY_PRINT : null));
|
||||
|
||||
// idk how null reacts to it so better be sure
|
||||
if (saveSettings.overrideExistingMeta == true || !FileSystem.exists(metaPath))
|
||||
CoolUtil.safeSaveFile(metaPath, Json.stringify(meta, null, saveSettings.prettyPrint == true ? Flags.JSON_PRETTY_PRINT : null));
|
||||
CoolUtil.safeSaveFile(metaPath, makeMetaSaveable(meta));
|
||||
#end
|
||||
return filteredChart;
|
||||
}
|
||||
@@ -306,13 +295,19 @@ class Chart {
|
||||
data.fromMods = null;
|
||||
|
||||
var sortedData:Dynamic = {};
|
||||
for(f in Reflect.fields(data)) {
|
||||
for (f in Reflect.fields(data)) {
|
||||
var v = Reflect.field(data, f);
|
||||
if (v != null)
|
||||
Reflect.setField(sortedData, f, v);
|
||||
}
|
||||
return sortedData;
|
||||
}
|
||||
|
||||
public static inline function makeMetaSaveable(meta:ChartMetaData, prettyPrint:Bool = true):String {
|
||||
var data:Dynamic = Reflect.copy(meta);
|
||||
if (data.color != null) data.color = new FlxColor(data.color).toWebString(); // dont even ask me - Nex
|
||||
return Json.stringify(data, null, prettyPrint ? Flags.JSON_PRETTY_PRINT : null);
|
||||
}
|
||||
}
|
||||
|
||||
typedef ChartSaveSettings = {
|
||||
|
||||
@@ -10,6 +10,7 @@ typedef ChartData = {
|
||||
public var stage:String;
|
||||
public var scrollSpeed:Float;
|
||||
public var noteTypes:Array<String>;
|
||||
public var ?bookmarks:Array<ChartBookmark>;
|
||||
|
||||
public var ?chartVersion:String;
|
||||
public var ?fromMods:Bool;
|
||||
@@ -22,16 +23,12 @@ typedef ChartMetaData = {
|
||||
public var ?beatsPerMeasure:Float;
|
||||
public var ?stepsPerBeat:Int;
|
||||
public var ?icon:String;
|
||||
public var ?color:Dynamic;
|
||||
public var ?color:FlxColor;
|
||||
public var ?difficulties:Array<String>;
|
||||
public var ?coopAllowed:Bool;
|
||||
public var ?opponentModeAllowed:Bool;
|
||||
public var ?customValues:Dynamic;
|
||||
public var ?needsVoices:Bool;
|
||||
public var ?artist:String;
|
||||
|
||||
// NOT TO BE EXPORTED
|
||||
public var ?parsedColor:FlxColor;
|
||||
}
|
||||
|
||||
typedef ChartStrumLine = {
|
||||
@@ -57,6 +54,12 @@ typedef ChartNote = {
|
||||
var sLen:Float; // sustain length of the note (ms)
|
||||
}
|
||||
|
||||
typedef ChartBookmark = {
|
||||
var time:Float;
|
||||
var name:String;
|
||||
var color:String;
|
||||
}
|
||||
|
||||
typedef ChartEvent = {
|
||||
var name:String;
|
||||
var time:Float;
|
||||
|
||||
@@ -9,21 +9,76 @@ import openfl.Assets;
|
||||
|
||||
using StringTools;
|
||||
|
||||
final class EventsData {
|
||||
public static var defaultEventsList:Array<String> = ["HScript Call", "Camera Movement", "Add Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"];
|
||||
class EventsData {
|
||||
public static var defaultEventsList:Array<String> = ["HScript Call", "Camera Movement", "Camera Position", "Add Camera Zoom", "Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Continuous BPM Change", "Time Signature Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"];
|
||||
public static var defaultEventsParams:Map<String, Array<EventParamInfo>> = [
|
||||
"HScript Call" => [
|
||||
{name: "Function Name", type: TString, defValue: "myFunc"},
|
||||
{name: "Function Parameters (String split with commas)", type: TString, defValue: ""}
|
||||
],
|
||||
"Camera Movement" => [{name: "Camera Target", type: TStrumLine, defValue: 0}],
|
||||
"Camera Movement" => [
|
||||
{name: "Camera Target", type: TStrumLine, defValue: 0},
|
||||
{name: "Tween Movement?", type: TBool, defValue: true, saveIfDefault: false},
|
||||
{name: "Tween Time (Steps, IF NOT CLASSIC)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4, saveIfDefault: false},
|
||||
{ // since its the most used event even by default, we'll set saveIfDefault false to avoid filling up with unnecessary parameters the files - Nex
|
||||
name: "Tween Ease (ex: circ, quad, cube)",
|
||||
type: TDropDown(['CLASSIC', 'linear', 'back', 'bounce', 'circ', 'cube', 'elastic', 'expo', 'quad', 'quart', 'quint', 'sine', 'smoothStep', 'smootherStep']),
|
||||
defValue: "CLASSIC",
|
||||
saveIfDefault: false
|
||||
},
|
||||
{
|
||||
name: "Tween Type (excluded if CLASSIC or linear, ex: InOut)",
|
||||
type: TDropDown(['In', 'Out', 'InOut']),
|
||||
defValue: "In",
|
||||
saveIfDefault: false
|
||||
}
|
||||
],
|
||||
"Camera Position" => [
|
||||
{name: "X", type: TFloat(null, null, 10, 3), defValue: 0},
|
||||
{name: "Y", type: TFloat(null, null, 10, 3), defValue: 0},
|
||||
{name: "Tween Movement?", type: TBool, defValue: true, saveIfDefault: false},
|
||||
{name: "Tween Time (Steps, IF NOT CLASSIC)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4, saveIfDefault: false},
|
||||
{
|
||||
name: "Tween Ease (ex: circ, quad, cube)",
|
||||
type: TDropDown(['CLASSIC', 'linear', 'back', 'bounce', 'circ', 'cube', 'elastic', 'expo', 'quad', 'quart', 'quint', 'sine', 'smoothStep', 'smootherStep']),
|
||||
defValue: "CLASSIC",
|
||||
saveIfDefault: false
|
||||
},
|
||||
{
|
||||
name: "Tween Type (excluded if CLASSIC or linear, ex: InOut)",
|
||||
type: TDropDown(['In', 'Out', 'InOut']),
|
||||
defValue: "In",
|
||||
saveIfDefault: false
|
||||
},
|
||||
{name: "Is Offset?", type: TBool, defValue: false, saveIfDefault: false}
|
||||
],
|
||||
"Add Camera Zoom" => [
|
||||
{name: "Amount", type: TFloat(-10, 10, 0.01, 2), defValue: 0.05},
|
||||
{name: "Camera", type: TDropDown(['camGame', 'camHUD']), defValue: "camGame"}
|
||||
],
|
||||
"Camera Zoom" => [
|
||||
{name: "Tween Zoom?", type: TBool, defValue: true},
|
||||
{name: "New Zoom", type: TFloat(-10, 10, 0.01, 2), defValue: 1},
|
||||
{name: "Camera", type: TDropDown(['camGame', 'camHUD']), defValue: "camGame"},
|
||||
{name: "Tween Time (Steps)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4},
|
||||
{
|
||||
name: "Tween Ease (ex: circ, quad, cube)",
|
||||
type: TDropDown(['linear', 'back', 'bounce', 'circ', 'cube', 'elastic', 'expo', 'quad', 'quart', 'quint', 'sine', 'smoothStep', 'smootherStep']),
|
||||
defValue: "linear"
|
||||
},
|
||||
{
|
||||
name: "Tween Type (excluded if linear, ex: InOut)",
|
||||
type: TDropDown(['In', 'Out', 'InOut']),
|
||||
defValue: "In"
|
||||
},
|
||||
{name: "Mode", type: TDropDown(['direct', 'stage']), defValue: "direct"},
|
||||
{name: "Multiplicative?", type: TBool, defValue: true}
|
||||
],
|
||||
"Camera Modulo Change" => [
|
||||
{name: "Modulo Interval (Beats)", type: TInt(1, 9999999, 1), defValue: 4},
|
||||
{name: "Bump Strength", type: TFloat(0.1, 10, 0.01, 2), defValue: 1}
|
||||
{name: "Modulo Interval", type: TInt(1, 9999999, 1), defValue: 4},
|
||||
{name: "Bump Strength", type: TFloat(0.1, 10, 0.01, 2), defValue: 1},
|
||||
{name: "Every Beat Type", type: TDropDown(['BEAT', 'MEASURE', 'STEP']), defValue: 'BEAT'},
|
||||
{name: "Beat Offset", type: TFloat(-10, 10, 0.25, 2), defValue: 0}
|
||||
],
|
||||
"Camera Flash" => [
|
||||
{name: "Reversed?", type: TBool, defValue: false},
|
||||
@@ -31,7 +86,9 @@ final class EventsData {
|
||||
{name: "Time (Steps)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4},
|
||||
{name: "Camera", type: TDropDown(['camGame', 'camHUD']), defValue: "camHUD"}
|
||||
],
|
||||
"BPM Change" => [{name: "Target BPM", type: TFloat(1.00, null, 0.001, 3), defValue: 100}],
|
||||
"BPM Change" => [{name: "Target BPM", type: TFloat(1.00, 9999, 0.001, 3), defValue: 100}],
|
||||
"Continuous BPM Change" => [{name: "Target BPM", type: TFloat(1.00, 9999, 0.001, 3), defValue: 100}, {name: "Time (steps)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4}],
|
||||
"Time Signature Change" => [{name: "Target Numerator", type: TFloat(1), defValue: 4}, {name: "Target Denominator", type: TFloat(1), defValue: 4}, {name: "Denominator is Steps Per Beat", type: TBool, defValue: false}],
|
||||
"Scroll Speed Change" => [
|
||||
{name: "Tween Speed?", type: TBool, defValue: true},
|
||||
{name: "New Speed", type: TFloat(0.01, 99, 0.01, 2), defValue: 1.},
|
||||
@@ -42,13 +99,23 @@ final class EventsData {
|
||||
defValue: "linear"
|
||||
},
|
||||
{
|
||||
name: "Tween Type (ex: InOut)",
|
||||
name: "Tween Type (excluded if linear, ex: InOut)",
|
||||
type: TDropDown(['In', 'Out', 'InOut']),
|
||||
defValue: "In"
|
||||
}
|
||||
},
|
||||
{name: "Multiplicative?", type: TBool, defValue: false}
|
||||
],
|
||||
"Alt Animation Toggle" => [{name: "Enable On Sing Poses", type: TBool, defValue: true}, {name: "Enable On Idle", type: TBool, defValue: true}, {name: "Strumline", type: TStrumLine, defValue: 0}],
|
||||
"Play Animation" => [{name: "Character", type: TStrumLine, defValue: 0}, {name: "Animation", type: TString, defValue: "animation"}, {name: "Is forced?", type: TBool, defValue: true}],
|
||||
"Play Animation" => [
|
||||
{name: "Character", type: TStrumLine, defValue: 0},
|
||||
{name: "Animation", type: TString, defValue: "animation"},
|
||||
{name: "Is forced?", type: TBool, defValue: true},
|
||||
{
|
||||
name: "Animation Context",
|
||||
type: TDropDown(["NONE", "SING", "DANCE", "MISS", "LOCK"]),
|
||||
defValue: "NONE"
|
||||
}
|
||||
],
|
||||
];
|
||||
|
||||
public static var eventsList:Array<String> = defaultEventsList.copy();
|
||||
@@ -129,13 +196,13 @@ typedef EventInfoFile = {
|
||||
|
||||
typedef EventInfo = {
|
||||
var params:Array<EventParamInfo>;
|
||||
var paramValues:Array<Dynamic>;
|
||||
}
|
||||
|
||||
typedef EventParamInfo = {
|
||||
var name:String;
|
||||
var type:EventParamType;
|
||||
var defValue:Dynamic;
|
||||
var ?saveIfDefault:Bool;
|
||||
}
|
||||
|
||||
enum EventParamType {
|
||||
|
||||
@@ -55,6 +55,18 @@ class FNFLegacyParser {
|
||||
continue; // Yoshi Engine charts crash fix
|
||||
}
|
||||
|
||||
var newBeatsPerMeasure:Float = section.sectionBeats != null ? section.sectionBeats : data.beatsPerMeasure.getDefault(4); // Default to 4 if sectionBeats is null or undefined (oops :3)
|
||||
|
||||
if (newBeatsPerMeasure != beatsPerMeasure) {
|
||||
beatsPerMeasure = newBeatsPerMeasure;
|
||||
|
||||
result.events.push({
|
||||
time: curTime,
|
||||
name: "Time Signature Change",
|
||||
params: [newBeatsPerMeasure, 4]
|
||||
});
|
||||
}
|
||||
|
||||
if (camFocusedBF != (camFocusedBF = section.mustHitSection)) {
|
||||
result.events.push({
|
||||
time: curTime,
|
||||
@@ -178,7 +190,8 @@ class FNFLegacyParser {
|
||||
mustHitSection: notes[section-1] != null ? notes[section-1].mustHitSection : false,
|
||||
bpm: notes[section-1] != null ? notes[section-1].bpm : chart.meta.bpm,
|
||||
changeBPM: false,
|
||||
altAnim: notes[section-1] != null ? notes[section-1].altAnim : false
|
||||
altAnim: notes[section-1] != null ? notes[section-1].altAnim : false,
|
||||
sectionBeats: notes[section-1] != null ? notes[section-1].sectionBeats : chart.meta.beatsPerMeasure.getDefault(4)
|
||||
};
|
||||
|
||||
var sectionEndTime:Float = Conductor.getTimeForStep(Conductor.getMeasureLength() * (section+1));
|
||||
@@ -192,6 +205,8 @@ class FNFLegacyParser {
|
||||
case "BPM Change":
|
||||
baseSection.changeBPM = true;
|
||||
baseSection.bpm = event.params[0];
|
||||
case "Time Signature Change":
|
||||
baseSection.sectionBeats = event.params[0];
|
||||
}
|
||||
}
|
||||
notes[section] = baseSection;
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
package funkin.backend.chart;
|
||||
|
||||
import funkin.backend.chart.ChartData.ChartMetaData;
|
||||
|
||||
typedef ChartDataWithInfo = {
|
||||
var diffName:String;
|
||||
var chart:ChartData;
|
||||
}
|
||||
|
||||
// These new structures are kinda a mess to port, i love and hate them at the same time; why the hell are every difficulty in the same file??? - Nex
|
||||
class VSliceParser {
|
||||
public static function parse(metaData:Dynamic, chartData:Dynamic, resultMeta:ChartMetaData, resultCharts:Array<ChartDataWithInfo>, ?songID:String) {
|
||||
var metaData:SwagMetadata = metaData;
|
||||
var chartData:NewSwagSong = chartData;
|
||||
|
||||
parseMeta(metaData, resultMeta, songID);
|
||||
|
||||
for (diff in Reflect.fields(chartData.notes))
|
||||
{
|
||||
var base:ChartData = {
|
||||
strumLines: [],
|
||||
noteTypes: [],
|
||||
events: [],
|
||||
meta: {name: null},
|
||||
scrollSpeed: Reflect.field(chartData.scrollSpeed, diff),
|
||||
stage: Flags.DEFAULT_STAGE,
|
||||
codenameChart: true
|
||||
};
|
||||
parseChart(Reflect.field(chartData.notes, diff), metaData, chartData.events, base);
|
||||
resultCharts.push({diffName: diff, chart: base});
|
||||
}
|
||||
}
|
||||
|
||||
public static function parseChart(data:Dynamic, metaData:Dynamic, events:Dynamic, result:ChartData) {
|
||||
// vslice chart parsing
|
||||
var data:Array<SwagNote> = data;
|
||||
var metadata:SwagMetadata = metaData;
|
||||
var events:Array<SwagEvent> = events;
|
||||
|
||||
result.stage = metadata.playData.stage;
|
||||
|
||||
var p2isGF:Bool = false;
|
||||
result.strumLines.push({
|
||||
characters: [metadata.playData.characters.opponent],
|
||||
type: 0,
|
||||
position: (p2isGF = metadata.playData.characters.opponent.startsWith("gf")) ? "girlfriend" : "dad",
|
||||
notes: [],
|
||||
vocalsSuffix: "-Opponent"
|
||||
});
|
||||
result.strumLines.push({
|
||||
characters: [metadata.playData.characters.player],
|
||||
type: 1,
|
||||
position: "boyfriend",
|
||||
notes: [],
|
||||
vocalsSuffix: "-Player"
|
||||
});
|
||||
var gfName = metadata.playData.characters.girlfriend;
|
||||
if (!p2isGF && gfName != "none") {
|
||||
result.strumLines.push({
|
||||
characters: [gfName],
|
||||
type: 2,
|
||||
position: "girlfriend",
|
||||
notes: [],
|
||||
visible: false,
|
||||
});
|
||||
}
|
||||
|
||||
var timeChanges = metadata.timeChanges;
|
||||
result.meta.bpm = timeChanges[0].bpm;
|
||||
result.meta.needsVoices = true;
|
||||
|
||||
for (note in data)
|
||||
{
|
||||
var daNoteType:Null<Int> = null;
|
||||
if (note.k != null)
|
||||
daNoteType = Chart.addNoteType(result, note.k == "alt" ? "Alt Anim Note" : note.k); // they hardcoded "alt" for converting old charts BUT THEN WHY THE HELL WOULD YOU CALL "MOM" THE NOTE KIND IN WEEK5 GRAHHH - Nex
|
||||
|
||||
var daNoteData:Int = Std.int(note.d % 8);
|
||||
var isMustHit:Bool = Math.floor(daNoteData / 4) == 0;
|
||||
|
||||
result.strumLines[isMustHit ? 1 : 0].notes.push({
|
||||
time: note.t,
|
||||
id: daNoteData % 4,
|
||||
type: daNoteType,
|
||||
sLen: note.l
|
||||
});
|
||||
}
|
||||
|
||||
var curBPM = result.meta.bpm;
|
||||
for (i in 1...timeChanges.length) // starting from 1 on purpose - Nex
|
||||
{
|
||||
var curChange = timeChanges[i];
|
||||
if (curBPM != curChange.bpm)
|
||||
{
|
||||
curBPM = curChange.bpm;
|
||||
result.events.push({
|
||||
time: curChange.t,
|
||||
name: "BPM Change",
|
||||
params: [curBPM]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (event in events)
|
||||
{
|
||||
var values = event.v;
|
||||
switch (event.e)
|
||||
{
|
||||
case "FocusCamera":
|
||||
var isPosOnly = false;
|
||||
var arr:Array<Dynamic> = [switch(values.char) {
|
||||
// more cases here in the future if they add em? (i hope not) - Nex
|
||||
case 0: 1;
|
||||
case 1: 0;
|
||||
case -1:
|
||||
isPosOnly = true;
|
||||
-1;
|
||||
default: 2;
|
||||
}];
|
||||
if (values.ease != "CLASSIC" && values.ease != null) {
|
||||
if (values.ease == "INSTANT") arr = arr.concat([false]);
|
||||
else {
|
||||
var cneEase = parseEase(values.ease);
|
||||
arr = arr.concat([true, values.duration == null ? 4 : values.duration, cneEase[0], cneEase[1]]);
|
||||
}
|
||||
}
|
||||
if (isPosOnly || ((values.x != null && values.x != 0) || (values.y != null && values.y != 0))) {
|
||||
var useNull = arr.length <= 2;
|
||||
result.events.push({
|
||||
time: event.t,
|
||||
name: "Camera Position",
|
||||
params: [values.x, values.y, useNull ? null : arr[1], useNull ? null : arr[2], useNull ? null : arr[3], useNull ? null : arr[4], true]
|
||||
});
|
||||
}
|
||||
if (!isPosOnly) result.events.push({
|
||||
time: event.t,
|
||||
name: "Camera Movement",
|
||||
params: arr
|
||||
});
|
||||
case "PlayAnimation":
|
||||
result.events.push({
|
||||
time: event.t,
|
||||
name: "Play Animation",
|
||||
params: [switch(values.target) {
|
||||
case 'boyfriend' | 'bf' | 'player': 1;
|
||||
case 'dad' | 'opponent': 0;
|
||||
default /*case 'girlfriend' | 'gf'*/: 2; // usually the default should be the stage prop but we dont have that sooo - Nex
|
||||
}, values.anim, values.force == null ? false : values.force]
|
||||
});
|
||||
case "ScrollSpeed":
|
||||
var cneEase = values.ease == null || values.ease == "INSTANT" ? ["linear", null] : parseEase(values.ease);
|
||||
result.events.push({
|
||||
time: event.t,
|
||||
name: "Scroll Speed Change", // we dont support the strumline value and also i will put the whole ease name into a single parameter since it works anyways - Nex
|
||||
params: [values.ease != "INSTANT", values.scroll == null ? 1 : values.scroll, values.duration == null ? 4 : values.duration, cneEase[0], cneEase[1], values.absolute != true]
|
||||
});
|
||||
case "SetCameraBop":
|
||||
result.events.push({
|
||||
time: event.t,
|
||||
name: "Camera Modulo Change",
|
||||
params: [values.rate == null ? 4 : values.rate, values.intensity == null ? 1 : values.intensity]
|
||||
});
|
||||
case "ZoomCamera":
|
||||
var cneEase = values.ease == null || values.ease == "INSTANT" ? ["linear", null] : parseEase(values.ease);
|
||||
result.events.push({
|
||||
time: event.t,
|
||||
name: "Camera Zoom", // we dont support the direct mode since welp, its kind of useless here - Nex
|
||||
params: [values.ease != "INSTANT", values.zoom == null ? 1 : values.zoom, "camGame", values.duration == null ? 4 : values.duration, cneEase[0], cneEase[1], values.mode, false]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function parseMeta(data:Dynamic, result:ChartMetaData, ?songID:String) {
|
||||
var data:SwagMetadata = data;
|
||||
var songName = data.songName;
|
||||
var songID:String = songID == null ? songName.toLowerCase().replace(" ", "-") : songID;
|
||||
var firstTimeChange:SwagTimeChange = data.timeChanges[0];
|
||||
|
||||
result.name = songID;
|
||||
result.bpm = firstTimeChange.bpm.getDefault(Flags.DEFAULT_BPM);
|
||||
result.beatsPerMeasure = firstTimeChange.n.getDefault(Flags.DEFAULT_BEATS_PER_MEASURE);
|
||||
result.stepsPerBeat = firstTimeChange.d.getDefault(Flags.DEFAULT_STEPS_PER_BEAT);
|
||||
result.displayName = songName;
|
||||
result.icon = Flags.DEFAULT_HEALTH_ICON;
|
||||
result.color = 0xFFFFFF;
|
||||
result.opponentModeAllowed = true;
|
||||
result.coopAllowed = true;
|
||||
result.difficulties = data.playData.difficulties.concat(data.playData.songVariations.getDefault([]));
|
||||
|
||||
if (result.customValues == null) result.customValues = {};
|
||||
result.customValues.artist = data.artist;
|
||||
if (data.charter != null) result.customValues.charter = data.charter;
|
||||
for (field in Reflect.fields(data.playData)) if (field != "difficulties" && field != "songVariations" && field != "characters" && field != "stage")
|
||||
Reflect.setProperty(result.customValues, field, Reflect.getProperty(data.playData, field));
|
||||
}
|
||||
|
||||
public static function encodeMeta(meta:ChartMetaData, ?chart:ChartData):SwagMetadata {
|
||||
var addVars:Dynamic = meta.customValues;
|
||||
var defStage:String = addVars.stage != null ? addVars.stage : Flags.DEFAULT_STAGE;
|
||||
var defChars:SwagCharactersList = addVars.characters != null ? addVars.characters : {player: Flags.DEFAULT_CHARACTER, girlfriend: Flags.DEFAULT_GIRLFRIEND, opponent: Flags.DEFAULT_OPPONENT, playerVocals: [], opponentVocals: [], instrumental: '', altInstrumentals: []};
|
||||
var defTimeCh:Array<SwagTimeChange>;
|
||||
|
||||
if (addVars.timeChanges != null && addVars.timeChanges.length > 0) {
|
||||
defTimeCh = addVars.timeChanges;
|
||||
defTimeCh[0].bpm = meta.bpm;
|
||||
}
|
||||
else defTimeCh = [{bpm: meta.bpm, t: -1}];
|
||||
|
||||
if (chart != null) {
|
||||
defStage = chart.stage;
|
||||
|
||||
var done:Array<Bool> = [false, false, false];
|
||||
for (strumLine in chart.strumLines) switch (strumLine.type) {
|
||||
case OPPONENT:
|
||||
if (!done[0]) {
|
||||
done[0] = true;
|
||||
defChars.opponent = strumLine.characters.getDefault([defChars.opponent])[0];
|
||||
}
|
||||
case PLAYER:
|
||||
if (!done[1]) {
|
||||
done[1] = true;
|
||||
defChars.player = strumLine.characters.getDefault([defChars.player])[0];
|
||||
}
|
||||
case ADDITIONAL:
|
||||
if (!done[3]) {
|
||||
done[3] = true;
|
||||
defChars.girlfriend = strumLine.characters.getDefault([defChars.girlfriend])[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result:SwagMetadata = {
|
||||
songName: meta.name,
|
||||
timeFormat: MILLISECONDS,
|
||||
artist: "",
|
||||
timeChanges: defTimeCh,
|
||||
looped: false,
|
||||
generatedBy: 'V-Slice Chart Importer (Codename Engine)',
|
||||
version: Flags.VSLICE_SONG_METADATA_VERSION,
|
||||
playData: {
|
||||
stage: defStage,
|
||||
characters: defChars,
|
||||
songVariations: addVars.songVariations != null ? [for (i in 0...addVars.songVariations.length) {meta.difficulties.remove(addVars.songVariations[i]); addVars.songVariations[i];}] : [],
|
||||
difficulties: meta.difficulties,
|
||||
noteStyle: addVars.noteStyle != null ? addVars.noteStyle : Flags.VSLICE_DEFAULT_NOTE_STYLE,
|
||||
album: addVars.album != null ? addVars.album : Flags.VSLICE_DEFAULT_ALBUM_ID,
|
||||
previewStart: addVars.previewStart != null ? addVars.previewStart : Flags.VSLICE_DEFAULT_PREVIEW_START,
|
||||
previewEnd: addVars.previewEnd != null ? addVars.previewEnd : Flags.VSLICE_DEFAULT_PREVIEW_END
|
||||
},
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static function encodeChart(chart:ChartData):NewSwagSong {
|
||||
// TO DO
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function parseEase(vsliceEase:String):Array<String> {
|
||||
for (key in ['InOut', 'In', 'Out']) if (vsliceEase.endsWith(key)) return [vsliceEase.substr(0, vsliceEase.length - key.length), key];
|
||||
return [vsliceEase];
|
||||
}
|
||||
}
|
||||
|
||||
// METADATA STRUCTURES
|
||||
typedef SwagMetadata =
|
||||
{
|
||||
var version:String;
|
||||
var songName:String;
|
||||
var artist:String;
|
||||
var ?charter:String;
|
||||
var ?divisions:Int;
|
||||
var ?looped:Bool;
|
||||
var ?offsets:SwagSongOffsets;
|
||||
var playData:SwagPlayData;
|
||||
var generatedBy:String;
|
||||
var ?timeFormat:SwagTimeFormat;
|
||||
var timeChanges:Array<SwagTimeChange>;
|
||||
}
|
||||
|
||||
enum abstract SwagTimeFormat(String) from String to String
|
||||
{
|
||||
var TICKS = 'ticks';
|
||||
var FLOAT = 'float';
|
||||
var MILLISECONDS = 'ms';
|
||||
}
|
||||
|
||||
typedef SwagTimeChange =
|
||||
{
|
||||
var t:Int; // Time Stamp
|
||||
var ?b:Int; // Beat Time
|
||||
var bpm:Float;
|
||||
var ?n:Int; // Time Signature Num
|
||||
var ?d:Int; // Time Signature Den
|
||||
var ?bt:Array<Int>; // Beat Tuplets
|
||||
}
|
||||
|
||||
typedef SwagSongOffsets =
|
||||
{
|
||||
var ?instrumental:Float;
|
||||
var ?altInstrumentals:Dynamic;
|
||||
var ?vocals:Dynamic;
|
||||
var ?altVocals:Dynamic;
|
||||
}
|
||||
|
||||
typedef SwagPlayData =
|
||||
{
|
||||
var ?songVariations:Array<String>;
|
||||
var difficulties:Array<String>;
|
||||
var characters:SwagCharactersList;
|
||||
var stage:String;
|
||||
var noteStyle:String;
|
||||
var ?ratings:Dynamic;
|
||||
var ?album:String;
|
||||
var ?previewStart:Int;
|
||||
var ?previewEnd:Int;
|
||||
}
|
||||
|
||||
typedef SwagCharactersList =
|
||||
{
|
||||
var ?player:String;
|
||||
var ?girlfriend:String;
|
||||
var ?opponent:String;
|
||||
var ?instrumental:String;
|
||||
var ?altInstrumentals:Array<String>;
|
||||
var ?opponentVocals:Array<String>;
|
||||
var ?playerVocals:Array<String>;
|
||||
}
|
||||
|
||||
// CHART STRUCTURE
|
||||
typedef NewSwagSong =
|
||||
{
|
||||
var version:String;
|
||||
var scrollSpeed:Dynamic; // Map<String, Float>
|
||||
var events:Array<SwagEvent>;
|
||||
var notes:Dynamic; // Map<String, Array<SwagNote>>
|
||||
var generatedBy:String;
|
||||
}
|
||||
|
||||
typedef SwagEvent =
|
||||
{
|
||||
var t:Float; // Time
|
||||
var e:String; // Event Kind
|
||||
var ?v:Dynamic; // Value (Map<String, Dynamic>)
|
||||
}
|
||||
|
||||
typedef SwagNote =
|
||||
{
|
||||
var t:Float; // Time
|
||||
var d:Int; // Data
|
||||
var ?l:Float; // Length
|
||||
var ?k:String; // Kind
|
||||
var ?p:Array<SwagNoteParamsData>; // Params
|
||||
}
|
||||
|
||||
typedef SwagNoteParamsData =
|
||||
{
|
||||
var n:String; // Name
|
||||
var v:Dynamic; // Value
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import flixel.FlxState;
|
||||
import funkin.backend.assets.ModsFolder;
|
||||
import funkin.backend.scripting.events.CancellableEvent;
|
||||
import funkin.backend.system.Conductor;
|
||||
import funkin.options.PlayerSettings;
|
||||
|
||||
#if GLOBAL_SCRIPT
|
||||
/**
|
||||
* Class for THE Global Script, aka script that runs in the background at all times.
|
||||
@@ -12,6 +14,8 @@ class GlobalScript {
|
||||
public static var scripts:ScriptPack;
|
||||
|
||||
private static var initialized:Bool = false;
|
||||
private static var reloading:Bool = false;
|
||||
private static var _lastAllow_Reload:Bool = false;
|
||||
|
||||
public static function init() {
|
||||
if(initialized) return;
|
||||
@@ -46,19 +50,13 @@ class GlobalScript {
|
||||
});
|
||||
FlxG.signals.postUpdate.add(function() {
|
||||
call("postUpdate", [FlxG.elapsed]);
|
||||
#if EXPERMENTAL_SCRIPT_RELOADING
|
||||
if (FlxG.keys.justPressed.F5) {
|
||||
if (scripts.scripts.length > 0) {
|
||||
Logs.trace('Reloading global script...', WARNING, YELLOW);
|
||||
scripts.reload();
|
||||
Logs.trace('Global script successfully reloaded.', WARNING, GREEN);
|
||||
} else {
|
||||
Logs.trace('Loading global script...', WARNING, YELLOW);
|
||||
onModSwitch(#if MOD_SUPPORT ModsFolder.currentModFolder #else null #end);
|
||||
}
|
||||
|
||||
if (reloading) {
|
||||
reloading = false;
|
||||
MusicBeatState.ALLOW_DEV_RELOAD = _lastAllow_Reload;
|
||||
}
|
||||
#end
|
||||
if (FlxG.keys.justPressed.F2)
|
||||
|
||||
if (PlayerSettings.solo.controls.DEV_CONSOLE)
|
||||
NativeAPI.allocConsole();
|
||||
});
|
||||
FlxG.signals.preDraw.add(function() {
|
||||
@@ -76,13 +74,26 @@ class GlobalScript {
|
||||
FlxG.signals.preStateSwitch.add(function() {
|
||||
call("preStateSwitch", []);
|
||||
});
|
||||
|
||||
FlxG.signals.preUpdate.add(function() {
|
||||
call("preUpdate", [FlxG.elapsed]);
|
||||
call("update", [FlxG.elapsed]);
|
||||
});
|
||||
|
||||
// Commented this out, because it might be the cause why global runs twice - Neo
|
||||
//onModSwitch(#if MOD_SUPPORT ModsFolder.currentModFolder #else null #end);
|
||||
if (FlxG.keys.pressed.SHIFT) {
|
||||
// If we want, we could just make reseting GlobalScript it's own keybind, but for now this works.
|
||||
if (PlayerSettings.solo.controls.DEV_RELOAD) {
|
||||
reloading = true;
|
||||
Logs.trace("Reloading Global Scripts...", INFO, YELLOW);
|
||||
|
||||
// yeah its a bit messy, sorry. This just prevents actually reloading the actual state.
|
||||
_lastAllow_Reload = MusicBeatState.ALLOW_DEV_RELOAD;
|
||||
MusicBeatState.ALLOW_DEV_RELOAD = false;
|
||||
|
||||
// Would be better to just re-initalize GlobalScript so there aren't any lose ends.
|
||||
onModSwitch(#if MOD_SUPPORT ModsFolder.currentModFolder #else null #end);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function event<T:CancellableEvent>(name:String, event:T):T {
|
||||
@@ -95,6 +106,7 @@ class GlobalScript {
|
||||
if (scripts != null)
|
||||
scripts.call(name, args);
|
||||
}
|
||||
|
||||
public static function onModSwitch(newMod:String) {
|
||||
call("destroy");
|
||||
scripts = FlxDestroyUtil.destroy(scripts);
|
||||
|
||||
@@ -27,7 +27,7 @@ class HScript extends Script {
|
||||
|
||||
try {
|
||||
if(Assets.exists(rawPath)) code = Assets.getText(rawPath);
|
||||
} catch(e) Logs.trace('Error while reading $path: ${Std.string(e)}', ERROR);
|
||||
} catch(e) Logs.error('Error while reading $path: ${Std.string(e)}');
|
||||
|
||||
parser = initParser();
|
||||
//folderlessPath = Path.directory(path);
|
||||
@@ -128,6 +128,10 @@ class HScript extends Script {
|
||||
interp.execute(expr);
|
||||
call("new", []);
|
||||
}
|
||||
|
||||
#if GLOBAL_SCRIPT
|
||||
funkin.backend.scripting.GlobalScript.call("onScriptSetup", [this, "hscript"]);
|
||||
#end
|
||||
}
|
||||
|
||||
public override function reload() {
|
||||
|
||||
@@ -98,6 +98,11 @@ class Script extends FlxBasic implements IFlxDestroyable {
|
||||
"EngineUtil" => funkin.backend.utils.EngineUtil,
|
||||
"MemoryUtil" => funkin.backend.utils.MemoryUtil,
|
||||
"BitmapUtil" => funkin.backend.utils.BitmapUtil,
|
||||
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
"TranslationUtil" => funkin.backend.utils.TranslationUtil,
|
||||
"translate" => funkin.backend.utils.TranslationUtil.get,
|
||||
#end
|
||||
];
|
||||
}
|
||||
|
||||
@@ -208,7 +213,7 @@ class Script extends FlxBasic implements IFlxDestroyable {
|
||||
var arr = Assets.getText(path).split("________PACKSEP________");
|
||||
fromString(arr[1], arr[0]);
|
||||
case "lua":
|
||||
Logs.trace("Lua is not supported in this engine. Use HScript instead.", ERROR);
|
||||
Logs.error("Lua is not supported in this engine. Use HScript instead.");
|
||||
new DummyScript(path);
|
||||
default:
|
||||
new DummyScript(path);
|
||||
@@ -227,7 +232,7 @@ class Script extends FlxBasic implements IFlxDestroyable {
|
||||
case "hx" | "hscript" | "hsc" | "hxs":
|
||||
new HScript(path).loadFromString(code);
|
||||
case "lua":
|
||||
Logs.trace("Lua is not supported in this engine. Use HScript instead.", ERROR);
|
||||
Logs.error("Lua is not supported in this engine. Use HScript instead.");
|
||||
new DummyScript(path).loadFromString(code);
|
||||
default:
|
||||
new DummyScript(path).loadFromString(code);
|
||||
|
||||
@@ -83,7 +83,7 @@ class ScriptPack extends Script {
|
||||
**/
|
||||
public override function call(func:String, ?parameters:Array<Dynamic>):Dynamic {
|
||||
for(e in scripts)
|
||||
if(e.active)
|
||||
if (e.active)
|
||||
e.call(func, parameters);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ package funkin.backend.scripting.events.gameplay;
|
||||
|
||||
final class RatingUpdateEvent extends CancellableEvent {
|
||||
/**
|
||||
New combo
|
||||
New combo (may be null if no ratings were found)
|
||||
**/
|
||||
public var rating:ComboRating;
|
||||
public var rating:Null<ComboRating>;
|
||||
/**
|
||||
Old combo (may be null)
|
||||
**/
|
||||
public var oldRating:ComboRating;
|
||||
public var oldRating:Null<ComboRating>;
|
||||
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package funkin.backend.scripting.events.healthicon;
|
||||
|
||||
import flixel.util.typeLimit.OneOfTwo;
|
||||
|
||||
final class HealthIconChangeEvent extends CancellableEvent {
|
||||
/**
|
||||
* Amount
|
||||
* Animation State
|
||||
*/
|
||||
public var amount:Int;
|
||||
public var anim:OneOfTwo<String, Int>;
|
||||
|
||||
/**
|
||||
* The health icon
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package funkin.backend.scripting.events.menu.storymenu;
|
||||
|
||||
import funkin.menus.StoryMenuState.WeekData;
|
||||
import funkin.backend.week.WeekData;
|
||||
|
||||
final class WeekSelectEvent extends CancellableEvent {
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package funkin.backend.scripting.events.note;
|
||||
|
||||
import funkin.game.Character;
|
||||
import funkin.game.HealthIcon;
|
||||
import funkin.game.Note;
|
||||
|
||||
final class NoteHitEvent extends CancellableEvent {
|
||||
@@ -113,6 +114,10 @@ final class NoteHitEvent extends CancellableEvent {
|
||||
* Whenever the animation should be forced to play (if it's null it will be forced based on the sprite's data xml, if it has one).
|
||||
*/
|
||||
public var forceAnim:Null<Bool> = true;
|
||||
/**
|
||||
* The attached healthIcon used distinction for icons amongst others
|
||||
*/
|
||||
public var healthIcon:HealthIcon;
|
||||
|
||||
/**
|
||||
* Prevents the default sing animation from being played.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package funkin.backend.scripting.events.splash;
|
||||
|
||||
import funkin.game.Splash;
|
||||
import funkin.game.SplashGroup;
|
||||
import funkin.game.Strum;
|
||||
|
||||
@@ -11,7 +12,7 @@ final class SplashShowEvent extends CancellableEvent {
|
||||
/**
|
||||
Splash that is shown
|
||||
**/
|
||||
public var splash:FunkinSprite;
|
||||
public var splash:Splash;
|
||||
/**
|
||||
Strum that the splash is shown on
|
||||
**/
|
||||
|
||||
@@ -33,7 +33,7 @@ class CustomShader extends FunkinShader {
|
||||
path = fragShaderPath+vertShaderPath;
|
||||
|
||||
if (fragCode == null && vertCode == null)
|
||||
Logs.trace('Shader "$name" couldn\'t be found.', ERROR);
|
||||
Logs.error('Shader "$name" couldn\'t be found.');
|
||||
|
||||
super(fragCode, vertCode, glslVersion);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package funkin.backend.shaders;
|
||||
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.system.FlxAssets.FlxShader;
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
import haxe.Exception;
|
||||
@@ -555,34 +556,44 @@ class FunkinShader extends FlxShader implements IHScriptCustomBehaviour {
|
||||
return val;
|
||||
}
|
||||
|
||||
var field:Dynamic = Reflect.field(data, name);
|
||||
var cl:String = Type.getClassName(Type.getClass(field));
|
||||
var field = Reflect.field(data, name);
|
||||
var cl = Type.getClassName(Type.getClass(field));
|
||||
var isNotNull = val != null;
|
||||
// cant do "field is ShaderInput" for some reason
|
||||
if (cl.startsWith("openfl.display.ShaderParameter")) {
|
||||
if (field.__length <= 1) {
|
||||
if ((val is Array))
|
||||
return field.value = val;
|
||||
// that means we wait for a single number, instead of an array
|
||||
if (field.__isInt && !(val is Int))
|
||||
if (field.__isInt && isNotNull && !(val is Int)) {
|
||||
throw new ShaderTypeException(name, Type.getClass(val), 'Int');
|
||||
else if (field.__isBool && !(val is Bool))
|
||||
return null;
|
||||
} else
|
||||
if (field.__isBool && isNotNull && !(val is Bool)) {
|
||||
throw new ShaderTypeException(name, Type.getClass(val), 'Bool');
|
||||
else if (field.__isFloat && !(val is Float))
|
||||
return null;
|
||||
} else
|
||||
if (field.__isFloat && isNotNull && !(val is Float)) {
|
||||
throw new ShaderTypeException(name, Type.getClass(val), 'Float');
|
||||
|
||||
return field.value = [val];
|
||||
return null;
|
||||
}
|
||||
return field.value = isNotNull ? [val] : null;
|
||||
} else {
|
||||
if (!(val is Array))
|
||||
if (isNotNull && !(val is Array)) {
|
||||
throw new ShaderTypeException(name, Type.getClass(val), Array);
|
||||
|
||||
return null;
|
||||
}
|
||||
return field.value = val;
|
||||
}
|
||||
} else if (cl.startsWith("openfl.display.ShaderInput")) {
|
||||
// shader input!!
|
||||
if (!(val is BitmapData)) {
|
||||
var bitmap:BitmapData;
|
||||
if (!isNotNull) bitmap = null;
|
||||
else if (val is BitmapData) bitmap = val;
|
||||
else if (val is FlxGraphic) bitmap = val.bitmap;
|
||||
else {
|
||||
throw new ShaderTypeException(name, Type.getClass(val), BitmapData);
|
||||
return null;
|
||||
}
|
||||
field.input = cast val;
|
||||
field.input = bitmap;
|
||||
}
|
||||
|
||||
return val;
|
||||
|
||||
@@ -4,56 +4,58 @@ import flixel.FlxState;
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
import funkin.backend.chart.ChartData;
|
||||
import funkin.backend.system.interfaces.IBeatReceiver;
|
||||
import funkin.editors.charter.Charter;
|
||||
|
||||
typedef BPMChangeEvent =
|
||||
{
|
||||
var stepTime:Float;
|
||||
var songTime:Float;
|
||||
var bpm:Float;
|
||||
enum BeatType {
|
||||
BEAT;
|
||||
MEASURE;
|
||||
STEP;
|
||||
}
|
||||
|
||||
class Conductor
|
||||
@:structInit
|
||||
class BPMChangeEvent
|
||||
{
|
||||
public var songTime:Float;
|
||||
public var bpm:Float;
|
||||
public var beatsPerMeasure:Float = 4;
|
||||
public var stepsPerBeat:Int = 4;
|
||||
|
||||
public var endSongTime:Float = 0;
|
||||
public var endStepTime:Float = 0;
|
||||
public var continuous:Bool = false;
|
||||
|
||||
public var stepTime:Float;
|
||||
public var beatTime:Float;
|
||||
public var measureTime:Float;
|
||||
}
|
||||
|
||||
final class Conductor
|
||||
{
|
||||
public static function getBeats(?every:BeatType, interval:Float, offset:Float = 0):Float {
|
||||
final beat = switch(every) {
|
||||
case MEASURE: curMeasureFloat;
|
||||
case STEP: curStepFloat;
|
||||
default: curBeatFloat;
|
||||
}
|
||||
if (interval <= 0) return beat - offset;
|
||||
else return Math.floor((beat - offset) / interval) * interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* FlxSignals
|
||||
*/
|
||||
public static var onMeasureHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal();
|
||||
public static var onBeatHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal();
|
||||
public static var onStepHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal();
|
||||
public static var onBPMChange:FlxTypedSignal<Float->Void> = new FlxTypedSignal();
|
||||
|
||||
/**
|
||||
* Current BPM
|
||||
*/
|
||||
public static var bpm:Float = 100;
|
||||
|
||||
/**
|
||||
* Current Crochet (time per beat), in milliseconds.
|
||||
*/
|
||||
public static var crochet:Float = ((60 / bpm) * 1000); // beats in milliseconds
|
||||
|
||||
/**
|
||||
* Current StepCrochet (time per step), in milliseconds.
|
||||
*/
|
||||
public static var stepCrochet:Float = crochet / 4; // steps in milliseconds
|
||||
|
||||
/**
|
||||
* Number of beats per measure (top number in time signature). Defaults to 4.
|
||||
*/
|
||||
public static var beatsPerMeasure:Float = 4;
|
||||
|
||||
/**
|
||||
* Number of steps per beat (bottom number in time signature). Defaults to 4.
|
||||
*/
|
||||
public static var stepsPerBeat:Int = 4;
|
||||
|
||||
public static var onBPMChange:FlxTypedSignal<(Float,Float)->Void> = new FlxTypedSignal();
|
||||
public static var onTimeSignatureChange:FlxTypedSignal<(Float,Float)->Void> = new FlxTypedSignal();
|
||||
|
||||
/**
|
||||
* Current position of the song, in milliseconds.
|
||||
*/
|
||||
public static var songPosition(get, default):Float;
|
||||
private static function get_songPosition() {
|
||||
if (songOffset != Options.songOffset) trace(songOffset = Options.songOffset);
|
||||
if (songOffset != Options.songOffset) songOffset = Options.songOffset;
|
||||
return songPosition - songOffset;
|
||||
}
|
||||
|
||||
@@ -62,6 +64,88 @@ class Conductor
|
||||
*/
|
||||
public static var songOffset:Float = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Current bpmChangeMap index
|
||||
*/
|
||||
public static var curChangeIndex:Int = 0;
|
||||
|
||||
/**
|
||||
* Current bpmChangeMap
|
||||
*/
|
||||
public static var curChange(get, never):BPMChangeEvent;
|
||||
private static var dummyChange:BPMChangeEvent = {bpm: 100, beatsPerMeasure: 4, stepsPerBeat: 4, songTime: 0, stepTime: 0, beatTime: 0, measureTime: 0};
|
||||
private static function get_curChange()
|
||||
return bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[curChangeIndex];
|
||||
|
||||
/**
|
||||
* Current BPM
|
||||
*/
|
||||
public static var bpm(get, never):Float;
|
||||
private static function get_bpm()
|
||||
return (curChangeIndex == 0 || bpmChangeMap.length == 0) ? startingBPM : getTimeWithIndexInBPM(songPosition, curChangeIndex);
|
||||
|
||||
/**
|
||||
* Starting BPM
|
||||
*/
|
||||
public static var startingBPM(get, never):Float;
|
||||
private static function get_startingBPM()
|
||||
return (bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[0]).bpm;
|
||||
|
||||
/**
|
||||
* Current Crochet (time per beat), in milliseconds.
|
||||
* It should be crotchet but ehhh, now it's there for backward compatibility.
|
||||
*/
|
||||
public static var crochet(get, never):Float;
|
||||
private static function get_crochet() return 15000 * stepsPerBeat / bpm;
|
||||
|
||||
/**
|
||||
* Current StepCrochet (time per step), in milliseconds.
|
||||
*/
|
||||
public static var stepCrochet(get, never):Float;
|
||||
private static function get_stepCrochet() return 15000 / bpm;
|
||||
|
||||
/**
|
||||
* Number of beats per mesure (top number in time signature). Defaults to 4.
|
||||
*/
|
||||
public static var beatsPerMeasure(get, never):Float;
|
||||
private static function get_beatsPerMeasure()
|
||||
return (bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[curChangeIndex]).beatsPerMeasure;
|
||||
|
||||
/**
|
||||
* Number of steps per beat. Defaults to 4.
|
||||
* Not a divisor number for time signature, it does the complete opposite.
|
||||
* It's because CNE Conductor is based in sixteenth note instead of beat.
|
||||
*/
|
||||
public static var stepsPerBeat(get, never):Int;
|
||||
private static function get_stepsPerBeat()
|
||||
return (bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[curChangeIndex]).stepsPerBeat;
|
||||
|
||||
/**
|
||||
* How much value notes to divide for beat (bottom or divisor number in time signature).
|
||||
* Only for a convinient way to access divisor instead of multiply by steps per beat.
|
||||
*/
|
||||
public static var denominator(get, never):Float;
|
||||
private static function get_denominator() return FlxMath.roundDecimal(16 / stepsPerBeat, 2);
|
||||
|
||||
/**
|
||||
* Last step from BPM Change
|
||||
*/
|
||||
public static var lastStepChange(get, never):Float;
|
||||
private static function get_lastStepChange() return curChange.stepTime;
|
||||
|
||||
/**
|
||||
* Last beat from BPM Change
|
||||
*/
|
||||
public static var lastBeatChange(get, never):Float;
|
||||
private static function get_lastBeatChange() return curChange.beatTime;
|
||||
|
||||
/**
|
||||
* Last measure from BPM Change
|
||||
*/
|
||||
public static var lastMeasureChange(get, never):Float;
|
||||
private static function get_lastMeasureChange() return curChange.measureTime;
|
||||
|
||||
/**
|
||||
* Current step
|
||||
*/
|
||||
@@ -77,7 +161,6 @@ class Conductor
|
||||
*/
|
||||
public static var curMeasure:Int = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Current step, as a `Float` (ex: 4.94, instead of 4)
|
||||
*/
|
||||
@@ -100,54 +183,164 @@ class Conductor
|
||||
/**
|
||||
* Array of all BPM changes that have been mapped.
|
||||
*/
|
||||
public static var bpmChangeMap:Array<BPMChangeEvent> = [];
|
||||
public static var bpmChangeMap:Array<BPMChangeEvent>;
|
||||
|
||||
/**
|
||||
* Array of all events that have been rejected by the Conductor.
|
||||
*/
|
||||
public static var invalidEvents:Array<ChartEvent> = [];
|
||||
|
||||
private static var validEventNames:Array<String> = ["BPM Change", "Time Signature Change", "Continuous BPM Change"];
|
||||
|
||||
@:dox(hide) public function new() {}
|
||||
|
||||
public static function reset() {
|
||||
songPosition = lastSongPos = curBeatFloat = curStepFloat = curBeat = curStep = 0;
|
||||
bpmChangeMap = [];
|
||||
changeBPM(0);
|
||||
curChangeIndex = 0;
|
||||
changeBPM();
|
||||
}
|
||||
|
||||
public static function changeBPM(bpm:Float = 100, beatsPerMeasure:Float = 4, stepsPerBeat:Int = 4)
|
||||
bpmChangeMap = [{bpm: bpm, beatsPerMeasure: beatsPerMeasure, stepsPerBeat: stepsPerBeat, songTime: 0, stepTime: 0, beatTime: 0, measureTime: 0}];
|
||||
|
||||
public static function setupSong(SONG:ChartData) {
|
||||
reset();
|
||||
mapBPMChanges(SONG);
|
||||
changeBPM(SONG.meta.bpm, cast SONG.meta.beatsPerMeasure.getDefault(4), cast SONG.meta.stepsPerBeat.getDefault(4));
|
||||
}
|
||||
|
||||
private static function mapBPMChange(curChange:BPMChangeEvent, time:Float, bpm:Float):BPMChangeEvent {
|
||||
var beatTime:Float, measureTime:Float, stepTime:Float;
|
||||
stepTime = (curChange.continuous ? curChange.endStepTime : curChange.stepTime) + (time - (curChange.continuous ? curChange.endSongTime : curChange.songTime)) / (15000 / curChange.bpm);
|
||||
|
||||
beatTime = curChange.beatTime + (stepTime - curChange.stepTime) / curChange.stepsPerBeat;
|
||||
measureTime = curChange.measureTime + (beatTime - curChange.beatTime) / curChange.beatsPerMeasure;
|
||||
|
||||
bpmChangeMap.push(curChange = {
|
||||
songTime: time,
|
||||
stepTime: stepTime,
|
||||
beatTime: beatTime,
|
||||
measureTime: measureTime,
|
||||
bpm: bpm,
|
||||
beatsPerMeasure: curChange.beatsPerMeasure,
|
||||
stepsPerBeat: curChange.stepsPerBeat
|
||||
});
|
||||
return curChange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps BPM changes from a song.
|
||||
* @param song Song to map BPM changes from.
|
||||
*/
|
||||
public static function mapBPMChanges(song:ChartData)
|
||||
{
|
||||
bpmChangeMap = [
|
||||
{
|
||||
stepTime: 0,
|
||||
songTime: 0,
|
||||
bpm: song.meta.bpm
|
||||
}
|
||||
];
|
||||
|
||||
public static function mapBPMChanges(song:ChartData) {
|
||||
var curChange:BPMChangeEvent = {
|
||||
songTime: 0,
|
||||
stepTime: 0,
|
||||
beatTime: 0,
|
||||
measureTime: 0,
|
||||
bpm: song.meta.bpm,
|
||||
beatsPerMeasure: song.meta.beatsPerMeasure.getDefault(4),
|
||||
stepsPerBeat: CoolUtil.floorInt(song.meta.stepsPerBeat.getDefault(4))
|
||||
};
|
||||
curChangeIndex = 0;
|
||||
bpmChangeMap = [curChange];
|
||||
invalidEvents = [];
|
||||
if (song.events == null) return;
|
||||
|
||||
var curBPM:Float = song.meta.bpm;
|
||||
var songTime:Float = 0;
|
||||
var stepTime:Float = 0;
|
||||
// fix the sort first...
|
||||
var events:Array<ChartEvent> = [for (e in song.events) if (e.params != null && validEventNames.contains(e.name)) e];
|
||||
events.sort((a, b) -> MathUtil.equal(a.time, b.time) ? (a.name == "Time Signature Change" ? -1 : 1) : Std.int(a.time - b.time));
|
||||
|
||||
for(e in song.events) if (e.name == "BPM Change" && e.params != null && e.params[0] is Float) {
|
||||
if (e.params[0] == curBPM) continue;
|
||||
var steps = (e.time - songTime) / ((60 / curBPM) * 1000 / 4);
|
||||
stepTime += steps;
|
||||
songTime = e.time;
|
||||
curBPM = e.params[0];
|
||||
for (e in events) curChange = mapEvent(e, curChange);
|
||||
}
|
||||
|
||||
bpmChangeMap.push({
|
||||
stepTime: stepTime,
|
||||
songTime: songTime,
|
||||
bpm: curBPM
|
||||
});
|
||||
private static function mapEvent(e:ChartEvent, curChange:BPMChangeEvent) {
|
||||
var name = e.name, params = e.params, time = e.time;
|
||||
if (curChange.continuous && MathUtil.lessThan(time, curChange.endSongTime)) { //ensure that you cannot place any conductor events during a continuous change
|
||||
invalidEvents.push(e);
|
||||
Logs.trace('Invalid Conductor event "${e.name}" at ${e.time} (Intersecting continuous change!)', WARNING);
|
||||
return curChange;
|
||||
}
|
||||
|
||||
if (name == "BPM Change" && params[0] is Float && curChange.bpm != params[0])
|
||||
curChange = mapBPMChange(curChange, time, params[0]);
|
||||
else if (name == "Time Signature Change") {
|
||||
//if (beatsPerMeasure == curChange.beatsPerMeasure && stepsPerBeat == curChange.stepsPerBeat) continue;
|
||||
/* TODO: make so time sigs doesnt stop the bpm change if its in the duration of bpm change */
|
||||
|
||||
if (curChange.songTime != time) curChange = mapBPMChange(curChange, time, curChange.bpm);
|
||||
curChange.beatsPerMeasure = params[0];
|
||||
|
||||
if (params[2]) curChange.stepsPerBeat = params[1];
|
||||
else curChange.stepsPerBeat = Math.floor(16 / params[1]); // convert from denominator to stepsPerBeat
|
||||
|
||||
curChange.stepTime = CoolUtil.floorInt(curChange.stepTime + .99998);
|
||||
curChange.beatTime = CoolUtil.floorInt(curChange.beatTime + .99998);
|
||||
curChange.measureTime = CoolUtil.floorInt(curChange.measureTime + .99998);
|
||||
}
|
||||
else if (name == "Continuous BPM Change") {
|
||||
var prevBPM = curChange.bpm;
|
||||
if (curChange.bpm == params[0]) {
|
||||
invalidEvents.push(e);
|
||||
return curChange; //DO NOT!!!!
|
||||
}
|
||||
|
||||
curChange = mapBPMChange(curChange, time, params[0]);
|
||||
curChange.endSongTime = time + (params[1]) / (curChange.bpm - prevBPM) * Math.log(curChange.bpm / prevBPM) * 15000;
|
||||
curChange.endStepTime = curChange.stepTime + params[1];
|
||||
curChange.continuous = true;
|
||||
}
|
||||
return curChange;
|
||||
}
|
||||
|
||||
public static function mapCharterBPMChanges(song:ChartData) {
|
||||
var curChange:BPMChangeEvent = {
|
||||
songTime: 0,
|
||||
stepTime: 0,
|
||||
beatTime: 0,
|
||||
measureTime: 0,
|
||||
bpm: song.meta.bpm,
|
||||
beatsPerMeasure: song.meta.beatsPerMeasure.getDefault(4),
|
||||
stepsPerBeat: CoolUtil.floorInt(song.meta.stepsPerBeat.getDefault(4))
|
||||
};
|
||||
bpmChangeMap = [curChange];
|
||||
invalidEvents = [];
|
||||
|
||||
var grpEvents = Charter.instance.leftEventsGroup.members.concat(Charter.instance.rightEventsGroup.members);
|
||||
var n = grpEvents.length;
|
||||
|
||||
if (n != 0) {
|
||||
grpEvents.sort((a, b) -> a.step < b.step ? -1 : 1);
|
||||
curChangeIndex = 0;
|
||||
|
||||
var i = 0, i2 = 0, j = 0, x = 0, events = [], ce, e;
|
||||
while (i < n) {
|
||||
x += (ce = grpEvents[i]).events.length;
|
||||
if (++i < n && grpEvents[i].step == ce.step) continue;
|
||||
|
||||
var eventTime = Conductor.getStepsInTime(ce.step, curChangeIndex);
|
||||
if (Math.isNaN(eventTime)) eventTime = 0;
|
||||
|
||||
events.resize(x);
|
||||
while (i2 < i) {
|
||||
x = 0;
|
||||
ce = grpEvents[i2++];
|
||||
while (x < ce.events.length) if (validEventNames.contains((e = ce.events[x++]).name)) {
|
||||
events[j++] = e;
|
||||
if (eventTime != 0) e.time = eventTime;
|
||||
}
|
||||
}
|
||||
events.resize(x = j);
|
||||
events.sort((a, b) -> MathUtil.equal(a.time, b.time) ? (a.name == "Time Signature Change" ? -1 : 1) : 0);
|
||||
|
||||
j = 0;
|
||||
while (j < x) curChange = mapEvent(events[j++], curChange);
|
||||
curChangeIndex = bpmChangeMap.length - 1;
|
||||
|
||||
x = j = 0;
|
||||
}
|
||||
}
|
||||
|
||||
curChangeIndex = 0;
|
||||
}
|
||||
|
||||
private static var elapsed:Float;
|
||||
@@ -159,16 +352,9 @@ class Conductor
|
||||
}
|
||||
|
||||
private static function __updateSongPos(elapsed:Float) {
|
||||
if (FlxG.sound.music == null || !FlxG.sound.music.playing) {
|
||||
lastSongPos = FlxG.sound.music != null ? FlxG.sound.music.time - songOffset : -songOffset;
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset)) {
|
||||
// update conductor
|
||||
songPosition = lastSongPos;
|
||||
} else {
|
||||
songPosition += songOffset + elapsed * 1000;
|
||||
if (FlxG.sound.music != null) { // CNE FlxSound is Interpolated.
|
||||
lastSongPos = FlxG.sound.music.time - songOffset;
|
||||
if (FlxG.sound.music.playing) songPosition = FlxG.sound.music.time;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,137 +363,247 @@ class Conductor
|
||||
reset();
|
||||
}
|
||||
private static var __lastChange:BPMChangeEvent;
|
||||
private static var __updateStep:Bool;
|
||||
private static var __updateBeat:Bool;
|
||||
private static var __updateMeasure:Bool;
|
||||
|
||||
private static function update() {
|
||||
if (FlxG.state != null && FlxG.state is MusicBeatState) {
|
||||
var state:MusicBeatState = cast FlxG.state;
|
||||
if(state.cancelConductorUpdate)
|
||||
return;
|
||||
}
|
||||
if (FlxG.state != null && FlxG.state is MusicBeatState && cast(FlxG.state, MusicBeatState).cancelConductorUpdate) return;
|
||||
|
||||
__updateSongPos(FlxG.elapsed);
|
||||
|
||||
if (bpm > 0) {
|
||||
// updates curBeat and stuff
|
||||
__lastChange = {
|
||||
stepTime: 0,
|
||||
songTime: 0,
|
||||
bpm: 0
|
||||
};
|
||||
for (change in Conductor.bpmChangeMap)
|
||||
{
|
||||
if (Conductor.songPosition >= change.songTime)
|
||||
__lastChange = change;
|
||||
var oldStep = curStep, oldBeat = curBeat, oldMeasure = curMeasure, oldChangeIndex = curChangeIndex;
|
||||
|
||||
if ((curChangeIndex = getTimeInChangeIndex(songPosition, curChangeIndex)) > 0) {
|
||||
var change = curChange;
|
||||
curStepFloat = getTimeWithBPMInSteps(songPosition, curChangeIndex, getTimeWithIndexInBPM(songPosition, curChangeIndex));
|
||||
curBeatFloat = change.beatTime + (curStepFloat - change.stepTime) / change.stepsPerBeat;
|
||||
curMeasureFloat = change.measureTime + (curBeatFloat - change.beatTime) / change.beatsPerMeasure;
|
||||
}
|
||||
else
|
||||
curMeasureFloat = (curBeatFloat = (curStepFloat = songPosition / stepCrochet) / stepsPerBeat) / beatsPerMeasure;
|
||||
|
||||
if (curChangeIndex != oldChangeIndex) {
|
||||
var prev = bpmChangeMap[oldChangeIndex];
|
||||
if (prev != null) {
|
||||
if (beatsPerMeasure != prev.beatsPerMeasure || stepsPerBeat != prev.stepsPerBeat)
|
||||
onTimeSignatureChange.dispatch(beatsPerMeasure, stepsPerBeat);
|
||||
|
||||
if (curChange.bpm != prev.bpm) onBPMChange.dispatch(curChange.bpm, curChange.endSongTime);
|
||||
}
|
||||
else {
|
||||
onTimeSignatureChange.dispatch(beatsPerMeasure, stepsPerBeat);
|
||||
onBPMChange.dispatch(curChange.bpm, curChange.endSongTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (__lastChange.bpm > 0 && bpm != __lastChange.bpm) changeBPM(__lastChange.bpm);
|
||||
if (__updateStep = (curStep != (curStep = CoolUtil.floorInt(curStepFloat)))) {
|
||||
if (curStep > oldStep) for (i in oldStep...curStep) onStepHit.dispatch(i + 1);
|
||||
else onStepHit.dispatch(curStep);
|
||||
}
|
||||
|
||||
curStepFloat = __lastChange.stepTime + ((Conductor.songPosition - __lastChange.songTime) / Conductor.stepCrochet);
|
||||
curBeatFloat = curStepFloat / stepsPerBeat;
|
||||
curMeasureFloat = curBeatFloat / beatsPerMeasure;
|
||||
if (__updateBeat = (curBeat != (curBeat = CoolUtil.floorInt(curBeatFloat)))) {
|
||||
if (curBeat > oldBeat) for (i in oldBeat...curBeat) onBeatHit.dispatch(i + 1);
|
||||
else onBeatHit.dispatch(curBeat);
|
||||
}
|
||||
|
||||
var oldStep = curStep;
|
||||
var oldBeat = curBeat;
|
||||
var oldMeasure = curMeasure;
|
||||
if (curStep != (curStep = CoolUtil.floorInt(curStepFloat))) {
|
||||
if (curStep < oldStep && oldStep - curStep < 2) return;
|
||||
// updates step
|
||||
__updateBeat = curBeat != (curBeat = CoolUtil.floorInt(curBeatFloat));
|
||||
__updateMeasure = __updateBeat && (curMeasure != (curMeasure = CoolUtil.floorInt(curMeasureFloat)));
|
||||
if (__updateMeasure = (curMeasure != (curMeasure = CoolUtil.floorInt(curMeasureFloat)))) {
|
||||
if (curMeasure > oldMeasure) for (i in oldMeasure...curMeasure) onMeasureHit.dispatch(i + 1);
|
||||
else onMeasureHit.dispatch(curMeasure);
|
||||
}
|
||||
|
||||
if (curStep > oldStep) {
|
||||
for(i in oldStep...curStep) {
|
||||
onStepHit.dispatch(i+1);
|
||||
if (__updateStep || __updateBeat || __updateMeasure) {
|
||||
var state = FlxG.state;
|
||||
while (state != null) {
|
||||
if (state is IBeatReceiver && (state.subState == null || state.persistentUpdate)) {
|
||||
var st = cast(state, IBeatReceiver);
|
||||
|
||||
if (__updateStep) {
|
||||
if (curStep > oldStep) for (i in oldStep...curStep) st.stepHit(i + 1);
|
||||
else st.stepHit(curStep);
|
||||
}
|
||||
|
||||
if (__updateBeat) {
|
||||
if (curBeat > oldBeat) for (i in oldBeat...curBeat) st.beatHit(i + 1);
|
||||
else st.beatHit(curBeat);
|
||||
}
|
||||
|
||||
if (__updateMeasure) {
|
||||
if (curMeasure > oldMeasure) for (i in oldMeasure...curMeasure) st.measureHit(i + 1);
|
||||
else st.measureHit(curMeasure);
|
||||
}
|
||||
}
|
||||
if (__updateBeat && curBeat > oldBeat) {
|
||||
for(i in oldBeat...curBeat) {
|
||||
onBeatHit.dispatch(i+1);
|
||||
}
|
||||
}
|
||||
if (__updateMeasure && curMeasure > oldMeasure) {
|
||||
for(i in oldMeasure...curMeasure) {
|
||||
onMeasureHit.dispatch(i+1);
|
||||
}
|
||||
}
|
||||
|
||||
if (FlxG.state is IBeatReceiver) {
|
||||
var state = FlxG.state;
|
||||
while(state != null) {
|
||||
if (state is IBeatReceiver && (state.subState == null || state.persistentUpdate)) {
|
||||
var st:IBeatReceiver = cast state;
|
||||
if (curStep > oldStep) {
|
||||
for(i in oldStep...curStep) {
|
||||
st.stepHit(i+1);
|
||||
}
|
||||
}
|
||||
if (__updateBeat && curBeat > oldBeat) {
|
||||
for(i in oldBeat...curBeat) {
|
||||
st.beatHit(i+1);
|
||||
}
|
||||
}
|
||||
if (__updateMeasure && curMeasure > oldMeasure) {
|
||||
for(i in oldMeasure...curMeasure) {
|
||||
st.measureHit(i+1);
|
||||
}
|
||||
}
|
||||
}
|
||||
state = state.subState;
|
||||
}
|
||||
}
|
||||
|
||||
state = state.subState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function changeBPM(newBpm:Float, beatsPerMeasure:Float = 4, stepsPerBeat:Int = 4)
|
||||
{
|
||||
bpm = newBpm;
|
||||
|
||||
crochet = ((60 / bpm) * 1000);
|
||||
stepCrochet = crochet / stepsPerBeat;
|
||||
|
||||
Conductor.beatsPerMeasure = beatsPerMeasure;
|
||||
Conductor.stepsPerBeat = stepsPerBeat;
|
||||
|
||||
onBPMChange.dispatch(bpm);
|
||||
public static function getTimeInChangeIndex(time:Float, index:Int = 0):Int {
|
||||
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
|
||||
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].songTime > time) {
|
||||
while (--index > 0) if (time > bpmChangeMap[index].songTime) return index;
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].songTime > time) return i - 1;
|
||||
return bpmChangeMap.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getTimeForStep(step:Float) {
|
||||
var bpmChange:BPMChangeEvent = {
|
||||
stepTime: 0,
|
||||
songTime: 0,
|
||||
bpm: bpm
|
||||
};
|
||||
|
||||
for(change in bpmChangeMap)
|
||||
if (change.stepTime < step && change.stepTime >= bpmChange.stepTime)
|
||||
bpmChange = change;
|
||||
|
||||
return bpmChange.songTime + ((step - bpmChange.stepTime) * ((60 / bpmChange.bpm) * (1000/stepsPerBeat)));
|
||||
public static function getStepsInChangeIndex(stepTime:Float, index:Int = 0):Int {
|
||||
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
|
||||
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].stepTime > stepTime) {
|
||||
while (--index > 0) if (stepTime > bpmChangeMap[index].stepTime) return index;
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].stepTime > stepTime) return i - 1;
|
||||
return bpmChangeMap.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getStepForTime(time:Float) {
|
||||
var bpmChange:BPMChangeEvent = {
|
||||
stepTime: 0,
|
||||
songTime: 0,
|
||||
bpm: bpm
|
||||
};
|
||||
|
||||
for(change in bpmChangeMap)
|
||||
if (change.songTime < time && change.songTime >= bpmChange.songTime)
|
||||
bpmChange = change;
|
||||
|
||||
return bpmChange.stepTime + ((time - bpmChange.songTime) / ((60 / bpmChange.bpm) * (1000/stepsPerBeat)));
|
||||
public static function getBeatsInChangeIndex(beatTime:Float, index:Int = 0):Int {
|
||||
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
|
||||
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].beatTime > beatTime) {
|
||||
while (--index > 0) if (beatTime > bpmChangeMap[index].beatTime) return index;
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].beatTime > beatTime) return i - 1;
|
||||
return bpmChangeMap.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getMeasuresInChangeIndex(measureTime:Float, index:Int = 0):Int {
|
||||
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
|
||||
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].measureTime > measureTime) {
|
||||
while (--index > 0) if (measureTime > bpmChangeMap[index].measureTime) return index;
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].measureTime > measureTime) return i - 1;
|
||||
return bpmChangeMap.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getTimeWithIndexInBPM(time:Float, index:Int):Float {
|
||||
var bpmChange = bpmChangeMap[index];
|
||||
if (bpmChange.continuous && time < bpmChange.endSongTime && index > 0) {
|
||||
var prevBPM = bpmChangeMap[index - 1].bpm;
|
||||
if (time <= bpmChange.songTime) return prevBPM;
|
||||
|
||||
var ratio = (time - bpmChange.songTime) / (bpmChange.endSongTime - bpmChange.songTime);
|
||||
return Math.pow(prevBPM, 1 - ratio) * Math.pow(bpmChange.bpm, ratio);
|
||||
}
|
||||
return bpmChange.bpm;
|
||||
}
|
||||
|
||||
public static function getStepsWithIndexInBPM(stepTime:Float, index:Int):Float {
|
||||
var bpmChange = bpmChangeMap[index];
|
||||
if (bpmChange.continuous && index > 0) {
|
||||
var prevBPM = bpmChangeMap[index - 1].bpm;
|
||||
if (stepTime <= bpmChange.stepTime) return prevBPM;
|
||||
|
||||
var endStepTime = bpmChange.stepTime + (bpmChange.endSongTime - bpmChange.songTime) * (bpmChange.bpm - prevBPM) / Math.log(bpmChange.bpm / prevBPM) / 15000;
|
||||
if (stepTime < endStepTime) return FlxMath.remapToRange(stepTime, bpmChange.stepTime, endStepTime, prevBPM, bpmChange.bpm);
|
||||
}
|
||||
return bpmChange.bpm;
|
||||
}
|
||||
|
||||
public static function getTimeInBPM(time:Float):Float {
|
||||
if (bpmChangeMap.length == 0) return dummyChange.bpm;
|
||||
return getTimeWithIndexInBPM(time, getTimeInChangeIndex(time));
|
||||
}
|
||||
|
||||
public static function getTimeWithBPMInSteps(time:Float, index:Int, bpm:Float):Float {
|
||||
var bpmChange = bpmChangeMap[index];
|
||||
if (bpmChange.continuous && time > bpmChange.songTime && index > 0) {
|
||||
var prevBPM = bpmChangeMap[index - 1].bpm;
|
||||
if (time > bpmChange.endSongTime)
|
||||
return bpmChange.stepTime + (((bpmChange.endSongTime - bpmChange.songTime) * (bpmChange.bpm - prevBPM))
|
||||
/ Math.log(bpmChange.bpm / prevBPM) + (time - bpmChange.endSongTime) * bpm) / 15000;
|
||||
else
|
||||
return bpmChange.stepTime + (time - bpmChange.songTime) * (bpm - prevBPM) / Math.log(bpm / prevBPM) / 15000;
|
||||
}
|
||||
else {
|
||||
return bpmChange.stepTime + (time - bpmChange.songTime) / (15000 / bpm);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getTimeInBeats(time:Float, from:Int = 0):Float {
|
||||
var index = getTimeInChangeIndex(time, from);
|
||||
if (index == -1) return time / (60000 / dummyChange.bpm);
|
||||
else if (index == 0) return time / (15000 / bpmChangeMap[index].bpm) / bpmChangeMap[index].stepsPerBeat;
|
||||
else {
|
||||
var change = bpmChangeMap[index];
|
||||
return change.beatTime + (getTimeWithBPMInSteps(time, index, getTimeWithIndexInBPM(time, index)) - change.stepTime) / change.stepsPerBeat;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getTimeInSteps(time:Float, from:Int = 0):Float {
|
||||
var index = getTimeInChangeIndex(time, from);
|
||||
return index < 1 ? time / (15000 / getTimeInBPM(time)) : getTimeWithBPMInSteps(time, index, getTimeWithIndexInBPM(time, index));
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
@:haxe.warning("-WDeprecated")
|
||||
public static inline function getStepForTime(time:Float):Float return getTimeInSteps(time);
|
||||
|
||||
public static function getStepsWithBPMInTime(stepTime:Float, index:Int, bpm:Float):Float {
|
||||
var bpmChange = bpmChangeMap[index];
|
||||
if (bpmChange.continuous && stepTime > bpmChange.stepTime && index > 0) {
|
||||
var prevBPM = bpmChangeMap[index - 1].bpm;
|
||||
var time = bpmChange.songTime + (stepTime - bpmChange.stepTime) / (bpm - prevBPM) * Math.log(bpm / prevBPM) * 15000;
|
||||
if (time > bpmChange.endSongTime)
|
||||
return (15000 * (stepTime - bpmChange.stepTime) - ((bpmChange.endSongTime - bpmChange.songTime) * (bpm - prevBPM))
|
||||
/ Math.log(bpm / prevBPM)) / bpm + bpmChange.endSongTime;
|
||||
else
|
||||
return time;
|
||||
}
|
||||
else {
|
||||
return bpmChange.songTime + (stepTime - bpmChange.stepTime) * (15000 / bpm);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getMeasuresInTime(measureTime:Float, from:Int = 0):Float {
|
||||
var index = getMeasuresInChangeIndex(measureTime, from);
|
||||
if (index == -1) return measureTime * (15000 / dummyChange.bpm);
|
||||
else if (index == 0) return measureTime * (15000 / bpmChangeMap[index].bpm) * bpmChangeMap[index].stepsPerBeat * bpmChangeMap[index].beatsPerMeasure;
|
||||
else {
|
||||
var change = bpmChangeMap[index];
|
||||
var stepTime = change.stepTime + (measureTime - change.measureTime) * change.stepsPerBeat * change.beatsPerMeasure;
|
||||
return getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
|
||||
}
|
||||
}
|
||||
|
||||
public static function getBeatsInTime(beatTime:Float, from:Int = 0):Float {
|
||||
var index = getBeatsInChangeIndex(beatTime, from);
|
||||
if (index == -1) return beatTime * (15000 / dummyChange.bpm);
|
||||
else if (index == 0) return beatTime * (15000 / bpmChangeMap[index].bpm) * bpmChangeMap[index].stepsPerBeat;
|
||||
else {
|
||||
var change = bpmChangeMap[index];
|
||||
var stepTime = change.stepTime + (beatTime - change.beatTime) * change.stepsPerBeat;
|
||||
return getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
|
||||
}
|
||||
}
|
||||
|
||||
public static function getStepsInTime(stepTime:Float, from:Int = 0):Float {
|
||||
var index = getStepsInChangeIndex(stepTime, from);
|
||||
return index < 1 ? stepTime * (15000 / bpmChangeMap[index].bpm) : getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
@:haxe.warning("-WDeprecated")
|
||||
public static inline function getTimeForStep(steps:Float):Float return getStepsInTime(steps);
|
||||
|
||||
public static inline function getMeasureLength()
|
||||
return stepsPerBeat * beatsPerMeasure;
|
||||
|
||||
public static inline function getMeasuresLength() {
|
||||
if (FlxG.sound.music == null) return 0.0;
|
||||
return getStepForTime(FlxG.sound.music.length) / getMeasureLength();
|
||||
var length = FlxG.sound.music.length;
|
||||
var index = getTimeInChangeIndex(length, bpmChangeMap.length - 1);
|
||||
var change = bpmChangeMap[index];
|
||||
return change.measureTime + (getTimeInBeats(length, index) - change.beatTime) / change.beatsPerMeasure;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,11 @@ enum Control
|
||||
PAUSE;
|
||||
//CHEAT;
|
||||
SWITCHMOD;
|
||||
|
||||
// Debugs
|
||||
DEV_ACCESS;
|
||||
DEV_CONSOLE;
|
||||
DEV_RELOAD;
|
||||
}
|
||||
|
||||
enum KeyboardScheme
|
||||
@@ -150,6 +155,24 @@ class Controls extends FlxActionSet
|
||||
@:pressed("switchmod") public var SWITCHMOD_HOLD(get, set): Bool;
|
||||
@:justReleased("switchmod") public var SWITCHMOD_R(get, set): Bool;
|
||||
|
||||
@:devModeOnly
|
||||
@:gamepad([])
|
||||
@:justPressed("dev-access") public var DEV_ACCESS(get, set): Bool;
|
||||
@:pressed("dev-access") public var DEV_ACCESS_HOLD(get, set): Bool;
|
||||
@:justReleased("dev-access") public var DEV_ACCESS_R(get, set): Bool;
|
||||
|
||||
@:devModeOnly
|
||||
@:gamepad([])
|
||||
@:justPressed("dev-console") public var DEV_CONSOLE(get, set): Bool;
|
||||
@:pressed("dev-console") public var DEV_CONSOLE_HOLD(get, set): Bool;
|
||||
@:justReleased("dev-console") public var DEV_CONSOLE_R(get, set): Bool;
|
||||
|
||||
@:devModeOnly
|
||||
@:gamepad([])
|
||||
@:justPressed("dev-reload") public var DEV_RELOAD(get, set): Bool;
|
||||
@:pressed("dev-reload") public var DEV_RELOAD_HOLD(get, set): Bool;
|
||||
@:justReleased("dev-reload") public var DEV_RELOAD_R(get, set): Bool;
|
||||
|
||||
@:allow(funkin.backend.utils.ControlsUtil)
|
||||
var byName:Map<String, FlxActionDigital> = [];
|
||||
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
package funkin.backend.system;
|
||||
|
||||
import lime.utils.AssetLibrary as LimeAssetLibrary;
|
||||
import lime.utils.AssetType;
|
||||
import flixel.util.FlxColor;
|
||||
|
||||
import lime.app.Application;
|
||||
import funkin.backend.system.macros.GitCommitMacro;
|
||||
import funkin.backend.assets.ModsFolder;
|
||||
import funkin.backend.assets.IModsAssetLibrary;
|
||||
import funkin.backend.assets.ScriptedAssetLibrary;
|
||||
import funkin.backend.system.macros.GitCommitMacro;
|
||||
import funkin.backend.utils.IniUtil;
|
||||
import lime.app.Application;
|
||||
import lime.utils.AssetLibrary as LimeAssetLibrary;
|
||||
import lime.utils.AssetType;
|
||||
|
||||
/**
|
||||
* A class that reads the `flags.ini` file, allowing to read settable Flags (customs too).
|
||||
*/
|
||||
@:build(funkin.backend.system.macros.FlagMacro.build())
|
||||
class Flags {
|
||||
// -- Codename's Addon Config --
|
||||
@:bypass public static var addonFlags:Map<String, Dynamic> = [];
|
||||
|
||||
// -- Codename's Mod Config --
|
||||
public static var MOD_NAME:String = "";
|
||||
public static var MOD_DESCRIPTION:String = "";
|
||||
public static var MOD_AUTHOR:String = "";
|
||||
public static var MOD_API_VERSION:Int = 1;
|
||||
public static var MOD_DOWNLOAD_LINK:String = "";
|
||||
public static var MOD_DEPENDENCIES:Array<String> = [];
|
||||
|
||||
@:noCompletion public static var MOD_ICON64:String = "";
|
||||
@:noCompletion public static var MOD_ICON32:String = "";
|
||||
@:noCompletion public static var MOD_ICON16:String = "";
|
||||
public static var MOD_ICON:String = "";
|
||||
|
||||
public static var MOD_DISCORD_CLIENT_ID:String = "";
|
||||
public static var MOD_DISCORD_LOGO_KEY:String = "";
|
||||
public static var MOD_DISCORD_LOGO_TEXT:String = "";
|
||||
// -- Codename's Default Flags --
|
||||
public static var CURRENT_API_VERSION:Int = 1;
|
||||
public static var COMMIT_NUMBER:Int = GitCommitMacro.commitNumber;
|
||||
public static var COMMIT_HASH:String = GitCommitMacro.commitHash;
|
||||
public static var COMMIT_MESSAGE:String = 'Commit $COMMIT_NUMBER ($COMMIT_HASH)';
|
||||
@@ -28,10 +49,6 @@ class Flags {
|
||||
public static var REPO_OWNER:String = "CodenameCrew";
|
||||
public static var REPO_URL:String = 'https://github.com/$REPO_OWNER/$REPO_NAME';
|
||||
|
||||
// make this empty once you guys are done with the project.
|
||||
// good luck /gen <3 @crowplexus
|
||||
public static var RELEASE_CYCLE:String = "Beta";
|
||||
|
||||
/**
|
||||
* Preferred sound extension for the game's audio files.
|
||||
* Currently is set to `mp3` for web targets, and `ogg` for other targets.
|
||||
@@ -41,7 +58,7 @@ class Flags {
|
||||
public static var IMAGE_EXT:String = "png"; // we also support jpg
|
||||
|
||||
public static var DEFAULT_DISCORD_LOGO_KEY:String = "icon";
|
||||
public static var DEFAULT_DISCORD_CLIENT_ID:String = "1027994136193810442";
|
||||
public static var DEFAULT_DISCORD_CLIENT_ID:String = "1383853614589673472";
|
||||
public static var DEFAULT_DISCORD_LOGO_TEXT:String = "Codename Engine";
|
||||
|
||||
@:also(funkin.game.Character.FALLBACK_CHARACTER)
|
||||
@@ -58,24 +75,41 @@ class Flags {
|
||||
public static var SONGS_LIST_MOD_MODE:Allow<"prepend", "override", "append"> = "override";
|
||||
public static var WEEKS_LIST_MOD_MODE:Allow<"prepend", "override", "append"> = "override";
|
||||
|
||||
// Translations system //
|
||||
public static var DEFAULT_LANGUAGE:String = "en";
|
||||
public static var DEFAULT_LANGUAGE_NAME:String = "English";
|
||||
/**
|
||||
* **NOTICE:** This will only contain the id of the language, not the full name.
|
||||
* If you blacklist the default language, you will need to change DEFAULT_LANGUAGE and DEFAULT_LANGUAGE_NAME.
|
||||
*/
|
||||
public static var BLACKLISTED_LANGUAGES:Array<String> = [];
|
||||
/**
|
||||
* **NOTICE:** This will only contain the id of the language, not the full name.
|
||||
* If this list is not empty, the languages listed will be the only ones able to be used.
|
||||
*/
|
||||
public static var WHITELISTED_LANGUAGES:Array<String> = [];
|
||||
|
||||
// Internal stuff
|
||||
public static var DEFAULT_BPM:Float = 100.0;
|
||||
public static var DEFAULT_BEATS_PER_MEASURE:Float = 4;
|
||||
public static var DEFAULT_BEATS_PER_MEASURE:Int = 4;
|
||||
public static var DEFAULT_STEPS_PER_BEAT:Int = 4;
|
||||
public static var DEFAULT_LOOP_TIME:Float = 0.0;
|
||||
|
||||
public static var SUPPORTED_CHART_RUNTIME_FORMATS:Array<String> = ["Legacy", "Psych Engine"];
|
||||
public static var SUPPORTED_CHART_FORMATS:Array<String> = ["BaseGame"];
|
||||
|
||||
public static var BASEGAME_SONG_METADATA_VERSION:String = "2.2.2";
|
||||
public static var BASEGAME_SONG_CHART_DATA_VERSION:String = "2.0.0";
|
||||
public static var BASEGAME_DEFAULT_NOTE_STYLE:String = 'funkin';
|
||||
public static var BASEGAME_DEFAULT_ALBUM_ID:String = 'volume1';
|
||||
public static var BASEGAME_DEFAULT_PREVIEW_START:Float = 0;
|
||||
public static var BASEGAME_DEFAULT_PREVIEW_END:Float = 15000;
|
||||
public static var VSLICE_SONG_METADATA_VERSION:String = "2.2.2";
|
||||
public static var VSLICE_SONG_CHART_DATA_VERSION:String = "2.0.0";
|
||||
public static var VSLICE_DEFAULT_NOTE_STYLE:String = 'funkin';
|
||||
public static var VSLICE_DEFAULT_ALBUM_ID:String = 'volume1';
|
||||
public static var VSLICE_DEFAULT_PREVIEW_START:Int = 0;
|
||||
public static var VSLICE_DEFAULT_PREVIEW_END:Int = 15000;
|
||||
|
||||
/**
|
||||
* Default background colors for songs without bg color
|
||||
* Default background colors for songs or more without bg color
|
||||
*/
|
||||
public static var DEFAULT_COLOR:FlxColor = 0xFF9271FD;
|
||||
public static var DEFAULT_WEEK_COLOR:FlxColor = 0xFFF9CF51;
|
||||
public static var DEFAULT_COOP_ALLOWED:Bool = false;
|
||||
public static var DEFAULT_OPPONENT_MODE_ALLOWED:Bool = false;
|
||||
|
||||
@@ -93,12 +127,24 @@ class Flags {
|
||||
public static var DEFAULT_GAMEOVER_LOSS_SFX:String = "gameOverSFX";
|
||||
public static var DEFAULT_GAMEOVER_RETRY_SFX:String = "gameOverEnd";
|
||||
|
||||
public static var DEFAULT_CAM_ZOOM_INTERVAL:Int = 4;
|
||||
public static var DEFAULT_CAM_ZOOM_INTERVAL:Int = 1;
|
||||
public static var DEFAULT_CAM_ZOOM_OFFSET:Float = 0;
|
||||
//public static var DEFAULT_CAM_ZOOM_EVERY:BeatType = MEASURE;
|
||||
public static var DEFAULT_CAM_ZOOM_STRENGTH:Int = 1;
|
||||
public static var DEFAULT_CAM_ZOOM:Float = 1.05; // what zoom level it defaults to
|
||||
public static var DEFAULT_HUD_ZOOM:Float = 1.0;
|
||||
public static var MAX_CAMERA_ZOOM:Float = 1.35;
|
||||
public static var MAX_CAMERA_ZOOM_MULT:Float = 1.35;
|
||||
|
||||
// to translate these you need to convert them into ids
|
||||
// Resume -> pause.resume
|
||||
// Restart Song -> pause.restart
|
||||
// Change Controls -> pause.changeControls
|
||||
// Change Options -> pause.changeOptions
|
||||
// Exit to menu -> pause.exitToMenu
|
||||
// Exit to charter -> pause.exitToCharter
|
||||
// Resume Cutscene -> pause.resumeCutscene
|
||||
// Skip Cutscene -> pause.skipCutscene
|
||||
// Restart Cutscene -> pause.restartCutscene
|
||||
public static var DEFAULT_PAUSE_ITEMS:Array<String> = ['Resume', 'Restart Song', 'Change Controls', 'Change Options', 'Exit to menu', "Exit to charter"];
|
||||
public static var DEFAULT_CUTSCENE_PAUSE_ITEMS:Array<String> = ['Resume Cutscene', 'Skip Cutscene', 'Restart Cutscene', 'Exit to menu'];
|
||||
public static var DEFAULT_GITAROO:Bool = true;
|
||||
@@ -137,7 +183,7 @@ class Flags {
|
||||
public static var JSON_PRETTY_PRINT:String = "\t";
|
||||
|
||||
public static var DISABLE_EDITORS:Bool = false;
|
||||
public static var DISABLE_BETA_WARNING_SCREEN:Bool = false;
|
||||
public static var DISABLE_WARNING_SCREEN:Bool = true;
|
||||
public static var DISABLE_TRANSITIONS:Bool = false;
|
||||
|
||||
@:also(funkin.backend.MusicBeatTransition.script)
|
||||
@@ -151,46 +197,57 @@ class Flags {
|
||||
public static var URL_EDITOR_FALLBACK:String = "https://www.youtube.com/watch?v=9Youam7GYdQ";
|
||||
public static var URL_FNF_ITCH:String = "https://ninja-muffin24.itch.io/funkin";
|
||||
|
||||
/**
|
||||
* Default editor sound paths
|
||||
*/
|
||||
public static var DEFAULT_EDITOR_AUTOSAVE_SOUND = "editors/autosave";
|
||||
public static var DEFAULT_EDITOR_BUTTONCLICK_SOUND = "editors/buttonClick";
|
||||
public static var DEFAULT_EDITOR_CLICK_SOUND = "editors/click";
|
||||
public static var DEFAULT_EDITOR_COPY_SOUND = "editors/copy";
|
||||
public static var DEFAULT_EDITOR_CUT_SOUND = "editors/cut";
|
||||
public static var DEFAULT_EDITOR_DELETE_SOUND = "editors/delete";
|
||||
public static var DEFAULT_EDITOR_OFFSETDRAG_SOUND = "editors/offsetDrag";
|
||||
public static var DEFAULT_EDITOR_PASTE_SOUND = "editors/paste";
|
||||
public static var DEFAULT_EDITOR_REDO_SOUND = "editors/redo";
|
||||
public static var DEFAULT_EDITOR_SAVE_SOUND = "editors/save";
|
||||
public static var DEFAULT_EDITOR_TEXTREMOVE_SOUND = "editors/textRemove";
|
||||
public static var DEFAULT_EDITOR_TEXTTYPE_SOUND = "editors/textType";
|
||||
public static var DEFAULT_EDITOR_UNDO_SOUND = "editors/undo";
|
||||
public static var DEFAULT_EDITOR_WINDOWAPPEAR_SOUND = "editors/windowAppear";
|
||||
public static var DEFAULT_EDITOR_WINDOWCLOSE_SOUND = "editors/windowClose";
|
||||
public static var DEFAULT_EDITOR_DROPDOWNAPPEAR_SOUND = "editors/dropdownAppear";
|
||||
public static var DEFAULT_CHARTER_HITSOUND_SOUND = "editors/charter/hitsound";
|
||||
public static var DEFAULT_CHARTER_METRONOME_SOUND = "editors/charter/metronome";
|
||||
public static var DEFAULT_CHARTER_NOTEDELETE_SOUND = "editors/charter/noteDelete";
|
||||
public static var DEFAULT_CHARTER_NOTEPLACE_SOUND = "editors/charter/notePlace";
|
||||
public static var DEFAULT_CHARTER_SCROLL_SOUND = "editors/charter/scroll";
|
||||
public static var DEFAULT_CHARTER_SNAPPINGCHANGE_SOUND = "editors/charter/snappingChange";
|
||||
public static var DEFAULT_CHARTER_STRUMLOCK_SOUND = "editors/charter/strumLock";
|
||||
public static var DEFAULT_CHARTER_STRUMUNLOCK_SOUND = "editors/charter/strumUnlock";
|
||||
public static var DEFAULT_CHARTER_SUSTAINADD_SOUND = "editors/charter/sustainAdd";
|
||||
public static var DEFAULT_CHARTER_SUSTAINDELETE_SOUND = "editors/charter/sustainDelete";
|
||||
public static var DEFAULT_CHARACTER_GHOSTDISABLE_SOUND = "editors/character/ghostDisable";
|
||||
public static var DEFAULT_CHARACTER_GHOSTENABLE_SOUND = "editors/character/ghostEnable";
|
||||
|
||||
public static var DEFAULT_GLSL_VERSION:String = "120";
|
||||
@:also(funkin.backend.utils.HttpUtil.userAgent)
|
||||
public static var USER_AGENT:String = 'request';
|
||||
// -- End of Codename's Default Flags --
|
||||
|
||||
/**
|
||||
* Flags that Codename couldn't recognize as it's own defaults (they can only be `string`!).
|
||||
* Flags that Codename couldn't recognize as it's own defaults (they can only be `string`! due to them being unparsed).
|
||||
*/
|
||||
@:bypass public static var customFlags:Map<String, String> = [];
|
||||
|
||||
public static function loadFromData(flags:Map<String, String>, data:String) {
|
||||
var trimmed:String;
|
||||
var splitContent = [for(e in data.split("\n")) if ((trimmed = e.trim()) != "") trimmed];
|
||||
var res = IniUtil.parseString(data);
|
||||
|
||||
for(line in splitContent) {
|
||||
if(line.startsWith(";")) continue;
|
||||
if(line.startsWith("#")) continue;
|
||||
if(line.startsWith("//")) continue;
|
||||
if(line.length == 0) continue;
|
||||
if(line.charAt(0) == "[" && line.charAt(line.length-1) == "]") continue;
|
||||
|
||||
var index = line.indexOf("=");
|
||||
if(index == -1) continue;
|
||||
var name = line.substr(0, index).trim();
|
||||
var value = line.substr(index+1).trim();
|
||||
|
||||
var wasQuoted = value.length > 1 && value.charCodeAt(0) == '"'.code && value.charCodeAt(value.length-1) == '"'.code;
|
||||
if(wasQuoted) value = value.substr(1, value.length - 2);
|
||||
if((!wasQuoted && value.length == 0) || name.length == 0)
|
||||
continue;
|
||||
|
||||
if(!flags.exists(name))
|
||||
flags[name] = value;
|
||||
}
|
||||
for (section in res) for (key => value in section) flags[key] = value;
|
||||
}
|
||||
|
||||
public static function loadFromDatas(files:Array<String>) {
|
||||
public static function loadFromDatas(datas:Array<String>) {
|
||||
var flags:Map<String, String> = [];
|
||||
for(file in files) {
|
||||
var data:String = Assets.getText(file);
|
||||
for(data in datas) {
|
||||
if(data != null)
|
||||
loadFromData(flags, data);
|
||||
}
|
||||
@@ -209,16 +266,46 @@ class Flags {
|
||||
* Loads the flags from the assets.
|
||||
**/
|
||||
public static function load(?libs:Array<LimeAssetLibrary> = null) {
|
||||
if (libs == null)
|
||||
libs = Paths.assetsTree.libraries;
|
||||
final flagsPath = Paths.getPath("flags.ini");
|
||||
var datas:Array<String> = [
|
||||
for(lib in libs)
|
||||
if(lib.exists(flagsPath, AssetType.TEXT))
|
||||
lib.getAsset(flagsPath, AssetType.TEXT)
|
||||
];
|
||||
if (libs == null) {
|
||||
libs = Paths.assetsTree.libraries.copy();
|
||||
libs.reverse();
|
||||
}
|
||||
for(lib in libs) {
|
||||
var l = lib;
|
||||
if (l is openfl.utils.AssetLibrary) {
|
||||
@:privateAccess
|
||||
l = cast(l, openfl.utils.AssetLibrary).__proxy;
|
||||
}
|
||||
if(lib is funkin.backend.assets.TranslatedAssetLibrary) {
|
||||
// skip translations since it would be useless, if you wanna modify it set the flags inside of global.hx
|
||||
continue;
|
||||
}
|
||||
|
||||
var flags:Map<String, String> = loadFromDatas(datas);
|
||||
parseFlags(flags);
|
||||
if (l is IModsAssetLibrary) {
|
||||
var flagsTxt = "";
|
||||
if (l.exists(Paths.ini("config/modpack"), AssetType.TEXT))
|
||||
flagsTxt = l.getAsset(Paths.ini("config/modpack"), AssetType.TEXT);
|
||||
if (cast(l, IModsAssetLibrary).modName == "assets") continue;
|
||||
|
||||
if (cast(l, IModsAssetLibrary).modName == ModsFolder.currentModFolder) {
|
||||
var flags:Map<String, String> = [];
|
||||
loadFromData(flags, flagsTxt);
|
||||
parseFlags(flags);
|
||||
}
|
||||
else {
|
||||
var flags:Map<String, String> = [];
|
||||
loadFromData(flags, flagsTxt);
|
||||
addonFlags.set(cast(l, IModsAssetLibrary).modName.toLowerCase().replace(" ", "").trim(), flags);
|
||||
}
|
||||
}
|
||||
else {
|
||||
var flagsTxt = "";
|
||||
if (l.exists(Paths.getPath("data/config/flags.ini"), AssetType.TEXT))
|
||||
flagsTxt = l.getAsset(Paths.getPath("data/config/flags.ini"), AssetType.TEXT);
|
||||
var flags:Map<String, String> = [];
|
||||
loadFromData(flags, flagsTxt);
|
||||
parseFlags(flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,16 +82,13 @@ final class Logs {
|
||||
logText(' |'),
|
||||
switch (level)
|
||||
{
|
||||
case WARNING:
|
||||
logText(' WARNING ', DARKYELLOW);
|
||||
case ERROR:
|
||||
logText(' ERROR ', DARKRED);
|
||||
case TRACE:
|
||||
logText(' TRACE ', GRAY);
|
||||
case VERBOSE:
|
||||
logText(' VERBOSE ', DARKMAGENTA);
|
||||
default:
|
||||
logText(' INFORMATION ', CYAN);
|
||||
case WARNING: logText(' WARNING ', DARKYELLOW);
|
||||
case ERROR: logText(' ERROR ', DARKRED);
|
||||
case TRACE: logText(' TRACE ', GRAY);
|
||||
case VERBOSE: logText(' VERBOSE ', DARKMAGENTA);
|
||||
case SUCCESS: logText(' SUCCESS ', GREEN);
|
||||
case FAILURE: logText(' FAILURE ', RED);
|
||||
default: logText(' INFORMATION ', CYAN);
|
||||
},
|
||||
logText('] ')
|
||||
];
|
||||
@@ -165,6 +162,8 @@ enum abstract Level(Int) {
|
||||
var ERROR = 2;
|
||||
var TRACE = 3;
|
||||
var VERBOSE = 4;
|
||||
var SUCCESS = 5;
|
||||
var FAILURE = 6;
|
||||
}
|
||||
|
||||
typedef LogText = {
|
||||
|
||||
@@ -61,6 +61,12 @@ class Main extends Sprite
|
||||
public static var gameThreads:Array<Thread> = [];
|
||||
#end
|
||||
|
||||
public static function preInit() {
|
||||
funkin.backend.utils.NativeAPI.registerAsDPICompatible();
|
||||
funkin.backend.system.CommandLineHandler.parseCommandLine(Sys.args());
|
||||
funkin.backend.system.Main.fixWorkingDirectory();
|
||||
}
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
@@ -81,12 +87,12 @@ class Main extends Sprite
|
||||
public static var audioDisconnected:Bool = false;
|
||||
|
||||
public static var changeID:Int = 0;
|
||||
public static var pathBack = #if windows
|
||||
public static var pathBack = #if (windows || linux)
|
||||
"../../../../"
|
||||
#elseif mac
|
||||
"../../../../../../../"
|
||||
#else
|
||||
""
|
||||
"../../../../"
|
||||
#end;
|
||||
public static var startedFromSource:Bool = #if TEST_BUILD true #else false #end;
|
||||
|
||||
@@ -132,17 +138,16 @@ class Main extends Sprite
|
||||
#end
|
||||
|
||||
#if (sys && TEST_BUILD)
|
||||
trace("Used cne test / cne build. Switching into source assets.");
|
||||
Logs.infos("Used cne test / cne build. Switching into source assets.");
|
||||
#if MOD_SUPPORT
|
||||
ModsFolder.modsPath = './${pathBack}mods/';
|
||||
ModsFolder.addonsPath = './${pathBack}addons/';
|
||||
#end
|
||||
Paths.assetsTree.__defaultLibraries.push(ModsFolder.loadLibraryFromFolder('assets', './${pathBack}assets/', true));
|
||||
Paths.assetsTree.__defaultLibraries.push(ModsFolder.loadLibraryFromFolder('assets', './${pathBack}assets/', true, SOURCE));
|
||||
#elseif USE_ADAPTED_ASSETS
|
||||
Paths.assetsTree.__defaultLibraries.push(ModsFolder.loadLibraryFromFolder('assets', './assets/', true));
|
||||
Paths.assetsTree.__defaultLibraries.push(ModsFolder.loadLibraryFromFolder('assets', './assets/', true, SOURCE));
|
||||
#end
|
||||
|
||||
|
||||
var lib = new AssetLibrary();
|
||||
@:privateAccess
|
||||
lib.__proxy = Paths.assetsTree;
|
||||
@@ -180,6 +185,8 @@ class Main extends Sprite
|
||||
}
|
||||
|
||||
public static function refreshAssets() @:privateAccess {
|
||||
FunkinCache.instance.clearSecondLayer();
|
||||
|
||||
var game = FlxG.game;
|
||||
var daSndTray = Type.createInstance(game._customSoundTray = funkin.menus.ui.FunkinSoundTray, []);
|
||||
var index:Int = game.numChildren - 1;
|
||||
@@ -238,9 +245,9 @@ class Main extends Sprite
|
||||
Sys.setCwd(haxe.io.Path.directory(Sys.programPath()));
|
||||
}
|
||||
#elseif android
|
||||
Sys.setCwd(Path.addTrailingSlash(VERSION.SDK_INT > 30 ? Context.getObbDir() : Context.getExternalFilesDir()));
|
||||
Sys.setCwd(haxe.io.Path.addTrailingSlash(VERSION.SDK_INT > 30 ? Context.getObbDir() : Context.getExternalFilesDir()));
|
||||
#elseif (ios || switch)
|
||||
Sys.setCwd(Path.addTrailingSlash(openfl.filesystem.File.applicationStorageDirectory.nativePath));
|
||||
Sys.setCwd(haxe.io.Path.addTrailingSlash(openfl.filesystem.File.applicationStorageDirectory.nativePath));
|
||||
#end
|
||||
}
|
||||
|
||||
|
||||
@@ -5,17 +5,24 @@ import sys.FileSystem;
|
||||
#end
|
||||
import flixel.FlxState;
|
||||
import funkin.backend.assets.ModsFolder;
|
||||
import funkin.backend.assets.ModsFolderLibrary;
|
||||
import funkin.backend.chart.EventsData;
|
||||
import funkin.menus.BetaWarningState;
|
||||
import funkin.menus.TitleState;
|
||||
import funkin.backend.system.framerate.Framerate;
|
||||
import funkin.editors.ModConfigWarning;
|
||||
import funkin.menus.TitleState;
|
||||
import haxe.io.Path;
|
||||
|
||||
@dox(hide)
|
||||
typedef AddonInfo = {
|
||||
var name:String;
|
||||
var path:String;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple state used for loading the game
|
||||
*/
|
||||
class MainState extends FlxState {
|
||||
public static var initiated:Bool = false;
|
||||
public static var betaWarningShown:Bool = false;
|
||||
public override function create() {
|
||||
super.create();
|
||||
if (!initiated) {
|
||||
@@ -36,28 +43,69 @@ class MainState extends FlxState {
|
||||
Paths.assetsTree.reset();
|
||||
|
||||
#if MOD_SUPPORT
|
||||
var _lowPriorityAddons:Array<String> = [];
|
||||
var _highPriorityAddons:Array<String> = [];
|
||||
var _noPriorityAddons:Array<String> = [];
|
||||
if (FileSystem.exists(ModsFolder.addonsPath) && FileSystem.isDirectory(ModsFolder.addonsPath)) {
|
||||
for(i=>addon in [for(dir in FileSystem.readDirectory(ModsFolder.addonsPath)) if (FileSystem.isDirectory(ModsFolder.addonsPath + dir)) dir]) {
|
||||
if (addon.startsWith("[LOW]")) _lowPriorityAddons.insert(0, addon);
|
||||
else if (addon.startsWith("[HIGH]")) _highPriorityAddons.insert(0, addon);
|
||||
else _noPriorityAddons.insert(0, addon);
|
||||
}
|
||||
for (addon in _lowPriorityAddons)
|
||||
Paths.assetsTree.addLibrary(ModsFolder.loadModLib(ModsFolder.addonsPath + addon, StringTools.ltrim(addon.substr("[LOW]".length))));
|
||||
}
|
||||
if (ModsFolder.currentModFolder != null)
|
||||
Paths.assetsTree.addLibrary(ModsFolder.loadModLib(ModsFolder.modsPath + ModsFolder.currentModFolder, ModsFolder.currentModFolder));
|
||||
inline function isDirectory(path:String):Bool
|
||||
return FileSystem.exists(path) && FileSystem.isDirectory(path);
|
||||
|
||||
if (FileSystem.exists(ModsFolder.addonsPath) && FileSystem.isDirectory(ModsFolder.addonsPath)){
|
||||
for (addon in _noPriorityAddons) Paths.assetsTree.addLibrary(ModsFolder.loadModLib(ModsFolder.addonsPath + addon, addon));
|
||||
for (addon in _highPriorityAddons) Paths.assetsTree.addLibrary(ModsFolder.loadModLib(ModsFolder.addonsPath + addon, StringTools.ltrim(addon.substr("[HIGH]".length))));
|
||||
inline function ltrim(str:String, prefix:String):String
|
||||
return str.substr(prefix.length).ltrim();
|
||||
|
||||
inline function loadLib(path:String, name:String)
|
||||
Paths.assetsTree.addLibrary(ModsFolder.loadModLib(path, name));
|
||||
|
||||
var _lowPriorityAddons:Array<AddonInfo> = [];
|
||||
var _highPriorityAddons:Array<AddonInfo> = [];
|
||||
var _noPriorityAddons:Array<AddonInfo> = [];
|
||||
|
||||
var addonPaths = [
|
||||
ModsFolder.addonsPath,
|
||||
(
|
||||
ModsFolder.currentModFolder != null ?
|
||||
ModsFolder.modsPath + ModsFolder.currentModFolder + "/addons/" :
|
||||
null
|
||||
)
|
||||
];
|
||||
|
||||
for(path in addonPaths) {
|
||||
if (path == null) continue;
|
||||
if (!isDirectory(path)) continue;
|
||||
|
||||
for(addon in FileSystem.readDirectory(path)) {
|
||||
if(!FileSystem.isDirectory(path + addon)) {
|
||||
switch(Path.extension(addon).toLowerCase()) {
|
||||
case 'zip':
|
||||
addon = Path.withoutExtension(addon);
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var data:AddonInfo = {
|
||||
name: addon,
|
||||
path: path + addon
|
||||
};
|
||||
|
||||
if (addon.startsWith("[LOW]")) _lowPriorityAddons.insert(0, data);
|
||||
else if (addon.startsWith("[HIGH]")) _highPriorityAddons.insert(0, data);
|
||||
else _noPriorityAddons.insert(0, data);
|
||||
}
|
||||
}
|
||||
|
||||
for (addon in _lowPriorityAddons)
|
||||
loadLib(addon.path, ltrim(addon.name, "[LOW]"));
|
||||
|
||||
if (ModsFolder.currentModFolder != null)
|
||||
loadLib(ModsFolder.modsPath + ModsFolder.currentModFolder, ModsFolder.currentModFolder);
|
||||
|
||||
for (addon in _noPriorityAddons)
|
||||
loadLib(addon.path, addon.name);
|
||||
|
||||
for (addon in _highPriorityAddons)
|
||||
loadLib(addon.path, ltrim(addon.name, "[HIGH]"));
|
||||
#end
|
||||
|
||||
Flags.load();
|
||||
TranslationUtil.findAllLanguages();
|
||||
TranslationUtil.setLanguage();
|
||||
ModsFolder.onModSwitch.dispatch(ModsFolder.currentModFolder); // Loads global.hx
|
||||
MusicBeatTransition.script = Flags.DEFAULT_TRANSITION_SCRIPT;
|
||||
WindowUtils.resetTitle();
|
||||
@@ -67,17 +115,27 @@ class MainState extends FlxState {
|
||||
ControlsUtil.loadCustomControls();
|
||||
TitleState.initialized = false;
|
||||
|
||||
if(Framerate.isLoaded)
|
||||
if (Framerate.isLoaded)
|
||||
Framerate.instance.reload();
|
||||
|
||||
if (Flags.DISABLE_BETA_WARNING_SCREEN || betaWarningShown)
|
||||
FlxG.switchState(new TitleState());
|
||||
else
|
||||
FlxG.switchState(new BetaWarningState());
|
||||
betaWarningShown = true;
|
||||
|
||||
#if sys
|
||||
CoolUtil.safeAddAttributes('./.temp/', NativeAPI.FileAttribute.HIDDEN);
|
||||
#end
|
||||
|
||||
if (Options.devMode) {
|
||||
var lib:ModsFolderLibrary;
|
||||
for (e in Paths.assetsTree.libraries) {
|
||||
@:privateAccess if (!(e is openfl.utils.AssetLibrary) || !((lib = cast cast(e, openfl.utils.AssetLibrary).__proxy) is ModsFolderLibrary)) continue;
|
||||
if (lib.modName == ModsFolder.currentModFolder) {
|
||||
if (lib.exists(Paths.ini("config/modpack"), lime.utils.AssetType.TEXT)) break;
|
||||
|
||||
FlxG.switchState(new ModConfigWarning(lib));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Flags.DISABLE_WARNING_SCREEN) FlxG.switchState(new funkin.menus.WarningState());
|
||||
else FlxG.switchState(new TitleState());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
package funkin.backend.system.framerate;
|
||||
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
import funkin.backend.assets.TranslatedAssetLibrary;
|
||||
#end
|
||||
import funkin.backend.assets.AssetsLibraryList;
|
||||
import funkin.backend.assets.IModsAssetLibrary;
|
||||
import funkin.backend.assets.ScriptedAssetLibrary;
|
||||
|
||||
@@ -13,20 +17,25 @@ class AssetTreeInfo extends FramerateCategory {
|
||||
var text = 'Not initialized yet\n';
|
||||
if (Paths.assetsTree != null){
|
||||
text = "";
|
||||
for(e in Paths.assetsTree.libraries) {
|
||||
var l = e;
|
||||
if (l is openfl.utils.AssetLibrary) {
|
||||
var al = cast(l, openfl.utils.AssetLibrary);
|
||||
@:privateAccess
|
||||
if (al.__proxy != null) l = al.__proxy;
|
||||
}
|
||||
for(l in Paths.assetsTree.libraries) {
|
||||
var l = AssetsLibraryList.getCleanLibrary(l);
|
||||
|
||||
if (l is ScriptedAssetLibrary)
|
||||
text += '${Type.getClassName(Type.getClass(l))} - ${cast(l, ScriptedAssetLibrary).scriptName} (${cast(l, ScriptedAssetLibrary).modName} | ${cast(l, ScriptedAssetLibrary).libName} | ${cast(l, ScriptedAssetLibrary).prefix})\n';
|
||||
var tag = l.tag.toString().toUpperCase();
|
||||
|
||||
text += '[$tag] ';
|
||||
|
||||
var className = Type.getClassName(Type.getClass(l));
|
||||
className = className.substr(className.lastIndexOf(".") + 1);
|
||||
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
if (l is TranslatedAssetLibrary)
|
||||
text += '${className} - ${cast(l, TranslatedAssetLibrary).langFolder} for (${cast(l, TranslatedAssetLibrary).forLibrary.modName})\n';
|
||||
else #end if (l is ScriptedAssetLibrary)
|
||||
text += '${className} - ${cast(l, ScriptedAssetLibrary).scriptName} (${cast(l, ScriptedAssetLibrary).modName} | ${cast(l, ScriptedAssetLibrary).libName} | ${cast(l, ScriptedAssetLibrary).prefix})\n';
|
||||
else if (l is IModsAssetLibrary)
|
||||
text += '${Type.getClassName(Type.getClass(l))} - ${cast(l, IModsAssetLibrary).modName} - ${cast(l, IModsAssetLibrary).libName} (${cast(l, IModsAssetLibrary).prefix})\n';
|
||||
text += '${className} - ${cast(l, IModsAssetLibrary).modName} - ${cast(l, IModsAssetLibrary).libName} (${cast(l, IModsAssetLibrary).prefix})\n';
|
||||
else
|
||||
text += Std.string(e) + "\n";
|
||||
text += Std.string(l) + '\n';
|
||||
}
|
||||
}
|
||||
if (text != "")
|
||||
|
||||
@@ -12,6 +12,10 @@ class CodenameBuildField extends TextField {
|
||||
reload();
|
||||
}
|
||||
|
||||
public function reload()
|
||||
text = 'Codename Engine ${Flags.RELEASE_CYCLE}\n${Flags.COMMIT_MESSAGE}';
|
||||
public function reload() {
|
||||
text = '${Flags.VERSION_MESSAGE}';
|
||||
#if debug
|
||||
text += '\n${Flags.COMMIT_MESSAGE}';
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class ConductorInfo extends FramerateCategory {
|
||||
_text += '\n - ${Conductor.curStep} steps';
|
||||
_text += '\n - ${Conductor.curMeasure} measures';
|
||||
_text += '\nCurrent BPM: ${Conductor.bpm}';
|
||||
_text += '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.stepsPerBeat}';
|
||||
_text += '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.denominator}';
|
||||
|
||||
this.text.text = _text;
|
||||
super.__enterFrame(t);
|
||||
|
||||
@@ -12,9 +12,7 @@ class FlixelInfo extends FramerateCategory {
|
||||
|
||||
|
||||
@:privateAccess {
|
||||
var c:Int = 0;
|
||||
for(_ in FlxG.bitmap._cache.keys())
|
||||
c++;
|
||||
var c:Int = Lambda.count(FlxG.bitmap._cache);
|
||||
|
||||
if((FlxG.state is ModState)) {
|
||||
var state:ModState = cast FlxG.state;
|
||||
@@ -27,6 +25,7 @@ class FlixelInfo extends FramerateCategory {
|
||||
_text += '\nBitmaps Count: ${c}';
|
||||
_text += '\nSounds Count: ${FlxG.sound.list.length}';
|
||||
_text += '\nFlxG.game Childs Count: ${FlxG.game.numChildren}';
|
||||
if(FlxG.renderBlit) _text += '\nBlitting Render: true';
|
||||
// _text += '\nCached objects count: ${cachedObjects}';
|
||||
#if FLX_POINT_POOL
|
||||
//var points = flixel.math.FlxPoint.FlxBasePoint.pool;
|
||||
|
||||
@@ -20,7 +20,7 @@ class Framerate extends Sprite {
|
||||
public static var codenameBuildField:CodenameBuildField;
|
||||
#end
|
||||
|
||||
public static var fontName:String = #if windows '${Sys.getEnv("windir")}\\Fonts\\consola.ttf' #else "_sans" #end;
|
||||
public static var fontName:String = #if windows '${Sys.getEnv("windir")}\\Fonts\\consola.ttf' #else "_typewriter" #end;
|
||||
|
||||
/**
|
||||
* 0: FPS INVISIBLE
|
||||
@@ -46,7 +46,7 @@ class Framerate extends Sprite {
|
||||
super();
|
||||
if (instance != null) throw "Cannot create another instance";
|
||||
instance = this;
|
||||
textFormat = new TextFormat("Consolas", 12, -1);
|
||||
textFormat = new TextFormat(fontName, 12, -1);
|
||||
|
||||
isLoaded = true;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class SystemInfo extends FramerateCategory {
|
||||
public static function init() {
|
||||
#if linux
|
||||
var process = new HiddenProcess("cat", ["/etc/os-release"]);
|
||||
if (process.exitCode() != 0) Logs.trace('Unable to grab OS Label', ERROR, RED);
|
||||
if (process.exitCode() != 0) Logs.error('Unable to grab OS Label');
|
||||
else {
|
||||
var osName = "";
|
||||
var osVersion = "";
|
||||
@@ -53,19 +53,35 @@ class SystemInfo extends FramerateCategory {
|
||||
if (osName != "")
|
||||
osInfo = '${osName} ${osVersion}'.trim();
|
||||
}
|
||||
#elseif windows
|
||||
var windowsCurrentVersionPath = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
|
||||
var buildNumber = Std.parseInt(RegistryUtil.get(HKEY_LOCAL_MACHINE, windowsCurrentVersionPath, "CurrentBuildNumber"));
|
||||
var edition = RegistryUtil.get(HKEY_LOCAL_MACHINE, windowsCurrentVersionPath, "ProductName");
|
||||
|
||||
var lcuKey = "WinREVersion"; // Last Cumulative Update Key On Older Windows Versions
|
||||
if (buildNumber >= 22000) { // Windows 11 Initial Release Build Number
|
||||
edition = edition.replace("Windows 10", "Windows 11");
|
||||
lcuKey = "LCUVer"; // Last Cumulative Update Key On Windows 11
|
||||
}
|
||||
|
||||
var lcuVersion = RegistryUtil.get(HKEY_LOCAL_MACHINE, windowsCurrentVersionPath, lcuKey);
|
||||
|
||||
osInfo = edition;
|
||||
|
||||
if (lcuVersion != null && lcuVersion != "")
|
||||
osInfo += ' ${lcuVersion}';
|
||||
else if (lime.system.System.platformVersion != null && lime.system.System.platformVersion != "")
|
||||
osInfo += ' ${lime.system.System.platformVersion}';
|
||||
#else
|
||||
if (lime.system.System.platformLabel != null && lime.system.System.platformLabel != "" && lime.system.System.platformVersion != null && lime.system.System.platformVersion != "")
|
||||
osInfo = '${lime.system.System.platformLabel.replace(lime.system.System.platformVersion, "").trim()} ${lime.system.System.platformVersion}';
|
||||
else
|
||||
Logs.trace('Unable to grab OS Label', ERROR, RED);
|
||||
Logs.error('Unable to grab OS Label');
|
||||
#end
|
||||
|
||||
try {
|
||||
#if windows
|
||||
var process = new HiddenProcess("wmic", ["cpu", "get", "name"]);
|
||||
if (process.exitCode() != 0) throw 'Could not fetch CPU information';
|
||||
|
||||
cpuName = process.stdout.readAll().toString().trim().split("\n")[1].trim();
|
||||
cpuName = RegistryUtil.get(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "ProcessorNameString");
|
||||
#elseif mac
|
||||
var process = new HiddenProcess("sysctl -a | grep brand_string"); // Somehow this isn't able to use the args but it still works
|
||||
if (process.exitCode() != 0) throw 'Could not fetch CPU information';
|
||||
@@ -83,10 +99,10 @@ class SystemInfo extends FramerateCategory {
|
||||
}
|
||||
#end
|
||||
} catch (e) {
|
||||
Logs.trace('Unable to grab CPU Name: $e', ERROR, RED);
|
||||
Logs.error('Unable to grab CPU Name: $e');
|
||||
}
|
||||
|
||||
@:privateAccess {
|
||||
@:privateAccess if(FlxG.renderTile) { // Blit doesn't enable the gpu. Idk if we should fix this
|
||||
if (flixel.FlxG.stage.context3D != null && flixel.FlxG.stage.context3D.gl != null) {
|
||||
gpuName = Std.string(flixel.FlxG.stage.context3D.gl.getParameter(flixel.FlxG.stage.context3D.gl.RENDERER)).split("/")[0].trim();
|
||||
#if !flash
|
||||
@@ -104,19 +120,19 @@ class SystemInfo extends FramerateCategory {
|
||||
}
|
||||
}
|
||||
} else
|
||||
Logs.trace('Unable to grab GPU Info', ERROR, RED);
|
||||
Logs.error('Unable to grab GPU Info');
|
||||
}
|
||||
|
||||
#if cpp
|
||||
totalMem = Std.string(MemoryUtil.getTotalMem() / 1024) + " GB";
|
||||
#else
|
||||
Logs.trace('Unable to grab RAM Amount', ERROR, RED);
|
||||
Logs.error('Unable to grab RAM Amount');
|
||||
#end
|
||||
|
||||
try {
|
||||
memType = MemoryUtil.getMemType();
|
||||
} catch (e) {
|
||||
Logs.trace('Unable to grab RAM Type: $e', ERROR, RED);
|
||||
Logs.error('Unable to grab RAM Type: $e');
|
||||
}
|
||||
formatSysInfo();
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ using StringTools;
|
||||
#if macro
|
||||
class BuildInfo {
|
||||
public static function printBuildInfo() {
|
||||
if(haxe.macro.Context.defined("display")) return;
|
||||
if(haxe.macro.Context.defined("display") || haxe.macro.Context.defined("display-details")) return;
|
||||
var haxeVersion = haxe.macro.Context.definedValue("haxe").trim();
|
||||
var sleepChangeVersion = false;
|
||||
|
||||
Sys.println('[ BUILD INFO ]');
|
||||
Sys.println('Haxe Version: ${haxeVersion}');
|
||||
try {
|
||||
@@ -33,8 +35,10 @@ class BuildInfo {
|
||||
}
|
||||
}
|
||||
|
||||
if(lastBuiltWith != null && lastBuiltWith.length > 0 && lastBuiltWith != haxeVersion)
|
||||
if(lastBuiltWith != null && lastBuiltWith.length > 0 && lastBuiltWith != haxeVersion) {
|
||||
Sys.println('Last Built With Haxe: ${lastBuiltWith} [!!!! MAKE SURE IF YOU SWITCHED VERSIONS YOU DELETE EXPORT FOLDERS !!!!]');
|
||||
sleepChangeVersion = true;
|
||||
}
|
||||
} catch(e) {}
|
||||
var targetPlatform = #if html5 "Web (HTML5)"
|
||||
#elseif ios "iOS"
|
||||
@@ -51,6 +55,14 @@ class BuildInfo {
|
||||
Sys.println('Target Platform: ' + targetPlatform);
|
||||
Sys.println('Build Date: ${Date.now().toString()}');
|
||||
Sys.println('');
|
||||
|
||||
if(sleepChangeVersion) {
|
||||
Sys.println('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
Sys.println('!!!! MAKE SURE IF YOU SWITCHED VERSIONS YOU DELETE EXPORT FOLDERS !!!!');
|
||||
Sys.println('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
Sys.println('');
|
||||
Sys.sleep(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
@@ -14,6 +14,7 @@ class ControlsMacro
|
||||
{
|
||||
static var _allControls: Array<String> = null;
|
||||
static var _allInternalControls: Array<String> = null;
|
||||
static var _allDevModeOnlyControls: Array<String> = null;
|
||||
static var _currentControls: Map<String, Array<Expr>> = null;
|
||||
static var _currentGamepadControls: Map<String, Expr> = null;
|
||||
static var _keySet: Map<String, String> = null;
|
||||
@@ -29,6 +30,7 @@ class ControlsMacro
|
||||
|
||||
_allControls = [];
|
||||
_allInternalControls = [];
|
||||
_allDevModeOnlyControls = [];
|
||||
_currentControls = [];
|
||||
_currentGamepadControls = [];
|
||||
_keySet = [];
|
||||
@@ -154,6 +156,7 @@ class ControlsMacro
|
||||
|
||||
_allControls = null;
|
||||
_allInternalControls = null;
|
||||
_allDevModeOnlyControls = null;
|
||||
_currentControls = null;
|
||||
_currentGamepadControls = null;
|
||||
_keySet = null;
|
||||
@@ -289,6 +292,9 @@ class ControlsMacro
|
||||
case ":justReleased":
|
||||
keyset = extractString(meta.params[0]);
|
||||
expr = macro func($i{internalName}, JUST_RELEASED);
|
||||
case ":devModeOnly":
|
||||
if (!_allDevModeOnlyControls.contains(shortName))
|
||||
_allDevModeOnlyControls.push(shortName);
|
||||
default:
|
||||
shouldRemove = false;
|
||||
}
|
||||
@@ -326,17 +332,16 @@ class ControlsMacro
|
||||
|
||||
// Generated Code:
|
||||
// inline function get_UI_UP(): Bool
|
||||
// return _uiUp.check();
|
||||
// return _uiUp.check(); or return Options.devMode && _uiUp.check(); depending if its dev mode
|
||||
var getField: Field = {
|
||||
name: "get_" + name,
|
||||
access: [APrivate, AInline],
|
||||
kind: FFun({
|
||||
ret: macro : Bool,
|
||||
params: [],
|
||||
expr: macro
|
||||
{
|
||||
return $i{internalName}.check();
|
||||
},
|
||||
expr: _allDevModeOnlyControls.contains(shortName) ?
|
||||
(macro return Options.devMode && $i{internalName}.check()) :
|
||||
(macro return $i{internalName}.check()),
|
||||
args: []
|
||||
}),
|
||||
pos: field.pos,
|
||||
|
||||
@@ -5,147 +5,69 @@ import haxe.macro.Expr;
|
||||
import haxe.macro.*;
|
||||
|
||||
using StringTools;
|
||||
using haxe.macro.PositionTools;
|
||||
|
||||
class HashLinkFixer {
|
||||
public static var buildMacroString = '@:build(funkin.backend.system.macros.HashLinkFixer.build())';
|
||||
|
||||
public static var applyOn:Array<String> = [
|
||||
"lime",
|
||||
"std",
|
||||
"Math",
|
||||
"",
|
||||
];
|
||||
|
||||
public static var modifiedClasses:Array<String> = [];
|
||||
|
||||
public static function init() {
|
||||
#if !display
|
||||
if(Context.defined("hl")) {
|
||||
for(apply in applyOn) {
|
||||
compile(apply);
|
||||
}
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
public static function compile(name:String) {
|
||||
#if !display
|
||||
Compiler.addGlobalMetadata(name, buildMacroString);
|
||||
#end
|
||||
}
|
||||
|
||||
public static function build():Array<Field> {
|
||||
var fields = Context.getBuildFields();
|
||||
var clRef = Context.getLocalClass();
|
||||
final fields = Context.getBuildFields(), clRef = Context.getLocalClass();
|
||||
|
||||
if (clRef == null) return fields;
|
||||
var cl = clRef.get();
|
||||
final cl = clRef.get();
|
||||
|
||||
if (cl.isAbstract || cl.isExtern || cl.isInterface) return fields;
|
||||
if (!cl.name.endsWith("_Impl_") && !cl.name.endsWith("_HSX") && !cl.name.endsWith("_HSC")) {
|
||||
if(cl.params.length > 0)
|
||||
return fields;
|
||||
if (
|
||||
cl.isAbstract || cl.isExtern || cl.isInterface || cl.params.length > 0 ||
|
||||
cl.name.endsWith("_Impl_") || cl.name.endsWith("_HSX") || cl.name.endsWith("_HSC")
|
||||
) return fields;
|
||||
|
||||
var definedFields = [];
|
||||
final pos = Context.currentPos();
|
||||
for (i in 0...fields.length) {
|
||||
final f = fields[i];
|
||||
if (f == null || f.name == 'new') continue;
|
||||
|
||||
for(f in fields.copy()) {
|
||||
if (f == null)
|
||||
continue;
|
||||
if (f.name == "new")
|
||||
continue;
|
||||
switch (f.kind) {
|
||||
case FFun(func):
|
||||
if (func == null) continue;
|
||||
|
||||
if(definedFields.contains(f.name)) continue; // no duplicate fields
|
||||
var hlNativeMeta = null;
|
||||
final cleanMeta = f.meta.filter((m) -> {
|
||||
if (m.name != ':hlNative') return true;
|
||||
else {
|
||||
hlNativeMeta = m;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (hlNativeMeta == null) continue;
|
||||
|
||||
var hlNativeMeta = null;
|
||||
var hasHlNative = false;
|
||||
for(m in f.meta)
|
||||
if (m.name == ":hlNative") {
|
||||
hasHlNative = true;
|
||||
hlNativeMeta = m;
|
||||
break;
|
||||
switch (hlNativeMeta.params) {
|
||||
case []: hlNativeMeta.params = [macro "std", macro $v{f.name}];
|
||||
case [_.expr => EConst(CString(name))]: hlNativeMeta.params = [macro "std", macro $v{name}];
|
||||
case [_.expr => EConst(CFloat(version))]:
|
||||
final curVersion = Context.definedValue("hl_ver");
|
||||
if (curVersion != null && version > curVersion) {
|
||||
if (f.meta.filter((m) -> return m.name == ":noExpr").length > 0)
|
||||
Context.error("Missing function body", hlNativeMeta.pos);
|
||||
}
|
||||
else
|
||||
hlNativeMeta.params = [macro "std", macro $v{f.name}];
|
||||
default:
|
||||
}
|
||||
|
||||
if(!hasHlNative) continue;
|
||||
final name = 'hlf_${f.name}';
|
||||
fields.push({
|
||||
name: name,
|
||||
pos: pos,
|
||||
kind: FFun({ret: func.ret, params: func.params, expr: func.expr, args: func.args}),
|
||||
access: f.access.filter((a) -> return a != APublic && a != APrivate).concat([APrivate]),
|
||||
meta: [hlNativeMeta]
|
||||
});
|
||||
|
||||
switch(f.kind) {
|
||||
case FFun(fun):
|
||||
if (fun == null)
|
||||
continue;
|
||||
if (fun.params != null && fun.params.length > 0) // TODO: Support for this maybe?
|
||||
continue;
|
||||
final args = func.args == null ? [] : [for (a in func.args) macro $i{a.name}];
|
||||
|
||||
if(fun.params == null)
|
||||
fun.params = [];
|
||||
|
||||
var overrideExpr:Expr;
|
||||
var returns:Bool = !fun.ret.match(TPath({name: "Void"}));
|
||||
|
||||
var printer = new haxe.macro.Printer();
|
||||
if(cl.module == "hl.Gc" && fun.ret == null) returns = false; // fix since they don't explicitly set :Void
|
||||
if(cl.module == "hl.Format" && fun.ret == null) returns = false; // fix since they don't explicitly set :Void
|
||||
|
||||
var name = 'hlf_${f.name}';
|
||||
|
||||
var arguments = fun.args == null ? [] : [for(a in fun.args) macro $i{a.name}];
|
||||
|
||||
var funcExpr:Expr = macro @:privateAccess $i{name}($a{arguments});
|
||||
if(returns) funcExpr = macro return $funcExpr;
|
||||
|
||||
var cleanMeta = f.meta.copy().filter(function(m) return m.name != ":hlNative");
|
||||
var hasBareMeta = hlNativeMeta.params.length == 0;
|
||||
|
||||
var meta = f.meta.copy();
|
||||
switch hlNativeMeta {
|
||||
case {params: []}:
|
||||
meta = [{name: ":hlNative", params: [macro "std", macro $v{f.name}], pos: Context.currentPos()}].concat(cleanMeta);
|
||||
case {params: [_.expr => EConst(CString(name))]}:
|
||||
meta = [{name: ":hlNative", params: [macro "std", macro $v{name}], pos: Context.currentPos()}].concat(cleanMeta);
|
||||
case {params: [_.expr => EConst(CFloat(version))]}:
|
||||
var curVersion = Context.definedValue("hl_ver");
|
||||
if(curVersion == null) curVersion = "";
|
||||
if(version > curVersion) {
|
||||
meta = cleanMeta;
|
||||
if(f.meta.filter((m) -> m.name == ":noExpr").length > 0)
|
||||
Context.error("Missing function body", f.pos);
|
||||
funcExpr = fun.expr; // restore to default behaviour
|
||||
} else {
|
||||
meta = [{name: ":hlNative", params: [macro "std", macro $v{f.name}], pos: Context.currentPos()}].concat(cleanMeta);
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
var field:Field = {
|
||||
name: name,
|
||||
pos: Context.currentPos(),
|
||||
kind: FFun({
|
||||
ret: fun.ret,
|
||||
params: fun.params.copy(),
|
||||
expr: funcExpr,
|
||||
args: fun.args.copy()
|
||||
}),
|
||||
access: f.access.copy().filter(function(a) return a != APublic && a != APrivate).concat([APrivate]),
|
||||
meta: meta
|
||||
};
|
||||
fields.push(field);
|
||||
definedFields.push(f.name);
|
||||
|
||||
// Remove meta from original function
|
||||
for(m in f.meta.copy())
|
||||
if (m.name == ":hlNative") {
|
||||
f.meta.remove(m);
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
f.meta = cleanMeta;
|
||||
func.expr = args.length == 0 ? macro $i{name}() : macro $i{name}($a{args});
|
||||
if (func.ret != null && !func.ret.match(TPath({name: 'Void'})))
|
||||
func.expr = macro return ${func.expr};
|
||||
default:
|
||||
}
|
||||
|
||||
/*if(definedFields.length > 0) {
|
||||
trace(cl.module);
|
||||
|
||||
var printer = new haxe.macro.Printer();
|
||||
for(field in fields) if(field.name.startsWith("hlf_"))
|
||||
Sys.println(printer.printField(field));
|
||||
}*/
|
||||
}
|
||||
|
||||
return fields;
|
||||
|
||||
@@ -39,9 +39,9 @@ class Macros {
|
||||
|
||||
if(Context.defined("sys")) {
|
||||
for(inc in ["sys", "openfl.net", "funkin.backend.system.net"]) {
|
||||
if(!isHl)
|
||||
Compiler.include(inc, compathx4);
|
||||
if(!isHl) Compiler.include(inc, compathx4);
|
||||
else {
|
||||
|
||||
// TODO: Hashlink
|
||||
//Compiler.include(inc, compathx4.concat(["sys.net.UdpSocket", "openfl.net.DatagramSocket"]); // fixes FATAL ERROR : Failed to load function std@socket_set_broadcast
|
||||
}
|
||||
@@ -52,8 +52,20 @@ class Macros {
|
||||
}
|
||||
|
||||
public static function initMacros() {
|
||||
if(Context.defined("hl"))
|
||||
HashLinkFixer.init();
|
||||
if (Context.defined("hl")) {
|
||||
for (c in ["lime", "std", "Math", ""]) Compiler.addGlobalMetadata(c, "@:build(funkin.backend.system.macros.HashLinkFixer.build())");
|
||||
}
|
||||
|
||||
final macroPath = 'funkin.backend.system.macros.Macros';
|
||||
Compiler.addMetadata('@:build($macroPath.buildLimeAssetLibrary())', 'lime.utils.AssetLibrary');
|
||||
}
|
||||
|
||||
public static function buildLimeAssetLibrary():Array<Field> {
|
||||
final fields:Array<Field> = Context.getBuildFields(), pos:Position = Context.currentPos();
|
||||
|
||||
fields.push({name: 'tag', access: [APublic], pos: pos, kind: FVar(macro :funkin.backend.assets.AssetSource)});
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
#end
|
||||
@@ -4,6 +4,8 @@ import flixel.FlxState;
|
||||
import flixel.sound.FlxSound;
|
||||
import funkin.backend.utils.NativeAPI;
|
||||
import lime.media.AudioManager;
|
||||
import lime.media.AudioSource;
|
||||
import haxe.Timer;
|
||||
|
||||
/**
|
||||
* if you are stealing this keep this comment at least please lol
|
||||
@@ -11,59 +13,57 @@ import lime.media.AudioManager;
|
||||
* hi gray itsa me yoshicrafter29 i fixed it hehe
|
||||
*/
|
||||
@:dox(hide)
|
||||
final class AudioSwitchFix {
|
||||
@:noCompletion
|
||||
private static function onStateSwitch(state:FlxState):Void {
|
||||
#if windows
|
||||
if (Main.audioDisconnected) {
|
||||
class AudioSwitchFix {
|
||||
public static function onAudioDisconnected() @:privateAccess {
|
||||
var sources:Array<{source:AudioSource, playing:Bool, time:Float, gain:Float, pitch:Float, position:lime.math.Vector4}> = [];
|
||||
for (source in AudioSource.activeSources) {
|
||||
var wasPlaying = source.playing;
|
||||
sources.push({
|
||||
source: source,
|
||||
playing: wasPlaying,
|
||||
time: source.currentTime,
|
||||
gain: source.gain,
|
||||
pitch: source.pitch,
|
||||
position: source.position
|
||||
});
|
||||
|
||||
var playingList:Array<PlayingSound> = [];
|
||||
for(e in FlxG.sound.list) {
|
||||
if (e.playing) {
|
||||
playingList.push({
|
||||
sound: e,
|
||||
time: e.time
|
||||
});
|
||||
e.stop();
|
||||
}
|
||||
}
|
||||
if (FlxG.sound.music != null)
|
||||
FlxG.sound.music.stop();
|
||||
source.__backend.dispose();
|
||||
if (wasPlaying) source.__backend.playing = true;
|
||||
}
|
||||
|
||||
AudioManager.shutdown();
|
||||
AudioManager.init();
|
||||
// #if !lime_doc_gen
|
||||
// if (AudioManager.context.type == OPENAL)
|
||||
// {
|
||||
// var alc = AudioManager.context.openal;
|
||||
AudioManager.shutdown();
|
||||
AudioManager.init();
|
||||
// #if !lime_doc_gen
|
||||
// if (AudioManager.context.type == OPENAL)
|
||||
// {
|
||||
// var alc = AudioManager.context.openal;
|
||||
|
||||
// var device = alc.openDevice();
|
||||
// var ctx = alc.createContext(device);
|
||||
// alc.makeContextCurrent(ctx);
|
||||
// alc.processContext(ctx);
|
||||
// }
|
||||
// #end
|
||||
Main.changeID++;
|
||||
// var device = alc.openDevice();
|
||||
// var ctx = alc.createContext(device);
|
||||
// alc.makeContextCurrent(ctx);
|
||||
// alc.processContext(ctx);
|
||||
// }
|
||||
// #end
|
||||
|
||||
for (d in sources) {
|
||||
d.source.__backend.init();
|
||||
d.source.currentTime = d.time;
|
||||
d.source.gain = d.gain;
|
||||
d.source.pitch = d.pitch;
|
||||
d.source.position = d.position;
|
||||
|
||||
for(e in playingList) {
|
||||
e.sound.play(e.time);
|
||||
}
|
||||
if (d.playing) d.source.play();
|
||||
}
|
||||
|
||||
Main.audioDisconnected = false;
|
||||
}
|
||||
#end
|
||||
Main.changeID++;
|
||||
Main.audioDisconnected = false;
|
||||
}
|
||||
|
||||
private static var timer:Timer;
|
||||
|
||||
private static function onRun() if (Main.audioDisconnected) onAudioDisconnected();
|
||||
public static function init() {
|
||||
#if windows
|
||||
NativeAPI.registerAudio();
|
||||
FlxG.signals.preStateCreate.add(onStateSwitch);
|
||||
#end
|
||||
if (timer == null) (timer = new Timer(1000)).run = onRun;
|
||||
}
|
||||
}
|
||||
|
||||
@:dox(hide)
|
||||
typedef PlayingSound = {
|
||||
var sound:FlxSound;
|
||||
var time:Float;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ class Socket implements IFlxDestroyable {
|
||||
|
||||
public function read():String {
|
||||
try {
|
||||
return this.socket.input.readUntil(('\n').charCodeAt(0)).replace("\\n", "\n");
|
||||
return this.socket.input.readUntil('\n'.code).replace("\\n", "\n");
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
|
||||
@@ -41,7 +41,9 @@ class UpdateAvailableScreen extends MusicBeatState {
|
||||
bg.screenCenter();
|
||||
add(bg);
|
||||
|
||||
title = new Alphabet(0, 10, "NEW UPDATE", true);
|
||||
// which file should i put this in?
|
||||
// Misc.xml?
|
||||
title = new Alphabet(0, 10, TU.translate("updateAvailable.title"), true);
|
||||
title.screenCenter(X);
|
||||
title.scrollFactor.set();
|
||||
|
||||
@@ -61,8 +63,8 @@ class UpdateAvailableScreen extends MusicBeatState {
|
||||
changeLogText.borderColor = 0xFF000000;
|
||||
MarkdownUtil.applyMarkdownText(changeLogText, check.updates.last().body);
|
||||
|
||||
installButton = new FunkinText(0, FlxG.height - 25, Std.int(FlxG.width / 2), "> INSTALL <", 32);
|
||||
skipButton = new FunkinText(Std.int(FlxG.width / 2), FlxG.height - 25, Std.int(FlxG.width / 2), "SKIP", 32);
|
||||
installButton = new FunkinText(0, FlxG.height - 25, Std.int(FlxG.width / 2), "PLACEHOLDER", 32);
|
||||
skipButton = new FunkinText(Std.int(FlxG.width / 2), FlxG.height - 25, Std.int(FlxG.width / 2), "PLACEHOLDER", 32);
|
||||
|
||||
skipButton.y -= skipButton.height;
|
||||
installButton.y -= installButton.height;
|
||||
@@ -91,6 +93,8 @@ class UpdateAvailableScreen extends MusicBeatState {
|
||||
|
||||
oldPos = FlxG.mouse.getScreenPosition();
|
||||
|
||||
changeSelection(false);
|
||||
|
||||
DiscordUtil.call("onMenuLoaded", ["Update Available Screen"]);
|
||||
}
|
||||
|
||||
@@ -132,16 +136,18 @@ class UpdateAvailableScreen extends MusicBeatState {
|
||||
}
|
||||
}
|
||||
|
||||
var installText = TU.translate("updateAvailable.install");
|
||||
var skipText = TU.translate("updateAvailable.skip");
|
||||
|
||||
public function changeSelection() {
|
||||
public function changeSelection(playSFX:Bool = true) {
|
||||
CoolUtil.playMenuSFX(SCROLL, 0.7);
|
||||
if (installSelected) {
|
||||
installButton.text = "> INSTALL <";
|
||||
skipButton.text = "SKIP";
|
||||
} else {
|
||||
installButton.text = "INSTALL";
|
||||
skipButton.text = "> SKIP <";
|
||||
}
|
||||
|
||||
// keeping this local just incase we wanna change this later
|
||||
inline function wrap(str:String, shouldWrap:Bool)
|
||||
return shouldWrap ? "> " + str + " <" : str;
|
||||
|
||||
installButton.text = wrap(installText, installSelected);
|
||||
skipButton.text = wrap(skipText, !installSelected);
|
||||
}
|
||||
|
||||
public override function destroy() {
|
||||
|
||||
@@ -82,20 +82,20 @@ class UpdateScreen extends MusicBeatState {
|
||||
switch(prog.step) {
|
||||
case PREPARING:
|
||||
progressBar.value = 0;
|
||||
generalProgress.text = "Preparing update installation... (1/4)";
|
||||
partProgress.text = "Creating installation folder and cleaning old update files...";
|
||||
generalProgress.text = TU.translate("updateScreen.general-1") + " (1/4)";
|
||||
partProgress.text = TU.translate("updateScreen.part-1");
|
||||
case DOWNLOADING_ASSETS:
|
||||
progressBar.value = 1 + ((prog.curFile-1+(prog.bytesLoaded/prog.bytesTotal)) / prog.files);
|
||||
generalProgress.text = "Downloading update assets... (2/4)";
|
||||
partProgress.text = 'Downloading file ${prog.curFileName}\n(${prog.curFile+1}/${prog.files} | ${CoolUtil.getSizeString(prog.bytesLoaded)} / ${CoolUtil.getSizeString(prog.bytesTotal)} | ${CoolUtil.getSizeString(lerpSpeed)}/s)';
|
||||
generalProgress.text = TU.translate("updateScreen.general-2") + " (2/4)";
|
||||
partProgress.text = TU.translate("updateScreen.part-2", [prog.curFileName]) + '\n(${prog.curFile+1}/${prog.files} | ${CoolUtil.getSizeString(prog.bytesLoaded)} / ${CoolUtil.getSizeString(prog.bytesTotal)} | ${CoolUtil.getSizeString(lerpSpeed)}/s)';
|
||||
case DOWNLOADING_EXECUTABLE:
|
||||
progressBar.value = 2 + (prog.bytesLoaded/prog.bytesTotal);
|
||||
generalProgress.text = "Downloading new engine executable... (3/4)";
|
||||
partProgress.text = 'Downloading ${prog.curFileName}\n(${CoolUtil.getSizeString(prog.bytesLoaded)} / ${CoolUtil.getSizeString(prog.bytesTotal)} | ${CoolUtil.getSizeString(lerpSpeed)}/s)';
|
||||
generalProgress.text = TU.translate("updateScreen.general-3") + " (3/4)";
|
||||
partProgress.text = TU.translate("updateScreen.part-3", [prog.curFileName]) + '\n(${CoolUtil.getSizeString(prog.bytesLoaded)} / ${CoolUtil.getSizeString(prog.bytesTotal)} | ${CoolUtil.getSizeString(lerpSpeed)}/s)';
|
||||
case INSTALLING:
|
||||
progressBar.value = 3 + ((prog.curFile-1+(prog.curZipProgress.curFile/prog.curZipProgress.fileCount))/prog.files);
|
||||
generalProgress.text = "Installing new files... (4/4)";
|
||||
partProgress.text = 'Installing ${prog.curFileName}\n(${prog.curFile}/${prog.files})';
|
||||
generalProgress.text = TU.translate("updateScreen.general-4") + " (4/4)";
|
||||
partProgress.text = TU.translate("updateScreen.part-4", [prog.curFileName]) + '\n(${prog.curFile}/${prog.files})';
|
||||
}
|
||||
var rect = new FlxRect(0, (1 - (progressBar.value / 4)) * bf.frameHeight, bf.frameWidth, 0);
|
||||
rect.height = bf.frameHeight - rect.y;
|
||||
|
||||
@@ -3,43 +3,184 @@ package funkin.backend.utils;
|
||||
import flixel.sound.FlxSound;
|
||||
import lime.media.AudioBuffer;
|
||||
|
||||
// Thank you yosh :DDD -lunar
|
||||
// (https://github.com/CodenameCrew/YoshiCrafterEngine/blob/main/source/WaveformSprite.hx)
|
||||
#if lime_vorbis
|
||||
import lime.media.vorbis.Vorbis;
|
||||
import lime.media.vorbis.VorbisFile;
|
||||
import lime.utils.ArrayBuffer;
|
||||
#end
|
||||
|
||||
// ORIGINAL CODES FROM YOSH & LUNAR https://github.com/CodenameCrew/YoshiCrafterEngine/blob/main/source/WaveformSprite.hx
|
||||
// REWRITTEN BY RALTYRO
|
||||
class AudioAnalyzer {
|
||||
public var sound:FlxSound;
|
||||
public var buffer:AudioBuffer;
|
||||
var __peakByte:Float = 0;
|
||||
var __timeMulti:Float = 0;
|
||||
var __toBits:Float;
|
||||
var __wordSize:Int;
|
||||
var __bitUnsignedSize:Int;
|
||||
var __bitSize:Int;
|
||||
|
||||
#if lime_vorbis
|
||||
var __vorbis:VorbisFile;
|
||||
#end
|
||||
|
||||
public function new(sound:FlxSound) {
|
||||
@:privateAccess buffer = sound._sound.__buffer;
|
||||
|
||||
__peakByte = Math.pow(2, buffer.bitsPerSample-1)-1;
|
||||
|
||||
__timeMulti = 1 / (1 / buffer.sampleRate);
|
||||
__timeMulti *= buffer.bitsPerSample;
|
||||
__timeMulti -= __timeMulti % buffer.bitsPerSample;
|
||||
this.sound = sound;
|
||||
__check();
|
||||
}
|
||||
|
||||
public function analyze(startPos:Float, endPos:Float):Float {
|
||||
var bitsPerSample = buffer.bitsPerSample;
|
||||
var multi = __timeMulti / 4000 / bitsPerSample;
|
||||
var bytesStartPos:Int = Math.floor(startPos * multi) * bitsPerSample;
|
||||
var bytesEndPos:Int = Math.floor(endPos * multi) * bitsPerSample;
|
||||
inline function __check() if (sound.buffer != buffer) {
|
||||
__bitSize = 1 << ((buffer = sound.buffer).bitsPerSample - 1);
|
||||
__bitUnsignedSize = 1 << buffer.bitsPerSample;
|
||||
__toBits = buffer.sampleRate / 1000 * buffer.channels * (__wordSize = buffer.bitsPerSample >> 3);
|
||||
__vorbis = null;
|
||||
}
|
||||
|
||||
bytesStartPos -= bytesStartPos % bitsPerSample;
|
||||
bytesEndPos -= bytesEndPos % bitsPerSample;
|
||||
public function analyze(startPos:Float, endPos:Float, ?outSeperate:Array<Float>):Float {
|
||||
__check();
|
||||
if (buffer.data != null) return __analyze(startPos, endPos, outSeperate);
|
||||
#if lime_vorbis
|
||||
else if (__prepareVorbis()) return __analyzeVorbis(startPos, endPos, outSeperate);
|
||||
#end
|
||||
return 0;
|
||||
}
|
||||
|
||||
var dataBuffer = buffer.data.buffer;
|
||||
|
||||
var maxByte:Int = 0;
|
||||
for(i in 0...Math.floor((bytesEndPos - bytesStartPos) / bitsPerSample)) {
|
||||
var bytePos = bytesStartPos + (i * bitsPerSample);
|
||||
// What if bitsPerSample is 8? or 32? or 64? -Ne_Eo
|
||||
var byte:Int = dataBuffer.get(bytePos) | (dataBuffer.get(bytePos + 1) << 8);
|
||||
if (byte > 256 * 128) byte -= 256 * 256;
|
||||
if (maxByte < byte) maxByte = byte;
|
||||
inline function __analyze(startPos:Float, endPos:Float, ?outSeperate:Array<Float>):Float {
|
||||
var pos = Math.floor(startPos * __toBits), end = Math.floor(endPos * __toBits), buf = buffer.data #if !js .buffer #end;
|
||||
var max = 0, b = 0, w = 0, c = 0, useSeperate = outSeperate != null;
|
||||
if (useSeperate) {
|
||||
while (c < outSeperate.length) outSeperate[c++] *= __bitSize;
|
||||
while (c++ < buffer.channels) outSeperate.push(0);
|
||||
c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize);
|
||||
}
|
||||
|
||||
return maxByte/__peakByte;
|
||||
pos -= pos % __wordSize;
|
||||
end -= end % __wordSize;
|
||||
|
||||
while (pos < end) {
|
||||
// 8-bit audio data is unsigned (0 is 128, 128 is 256, -128 is 0)
|
||||
if (__wordSize == 1) b = #if js buf[pos++] #else buf.get(pos++) #end - __bitSize;
|
||||
else {
|
||||
while (w < buffer.bitsPerSample) {
|
||||
b |= #if js buf[pos] #else buf.get(pos) #end << w;
|
||||
w += 8;
|
||||
pos++;
|
||||
}
|
||||
if (b > __bitSize) b -= __bitUnsignedSize;
|
||||
}
|
||||
if (max < b) max = b;
|
||||
if (useSeperate) {
|
||||
if (outSeperate[c] < b) outSeperate[c] = b;
|
||||
if (++c > buffer.channels) c = 0;
|
||||
}
|
||||
w = b = 0;
|
||||
}
|
||||
|
||||
return max / __bitSize;
|
||||
}
|
||||
|
||||
#if lime_vorbis // As far i know, only native supports vorbis
|
||||
var __buffer:ArrayBuffer;
|
||||
var __bufferSize:Int;
|
||||
function __prepareVorbis():Bool @:privateAccess {
|
||||
if (buffer.__srcVorbisFile == null) return __vorbis != null;
|
||||
if (__vorbis != null) return true;
|
||||
if ((__vorbis = buffer.__srcVorbisFile.clone()) != null) { // IM HOPING IT HAVE A GC CLOSURE.
|
||||
__buffer = new ArrayBuffer(__bufferSize = 0x400 * buffer.channels * (buffer.bitsPerSample >> 3));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline function __analyzeVorbis(startPos:Float, endPos:Float, ?outSeperate:Array<Float>):Float @:privateAccess {
|
||||
var n = Math.floor((endPos - startPos) * __toBits);
|
||||
if ((n -= n % __wordSize) < __wordSize) return 0;
|
||||
|
||||
var pos = 0, max = 0, b = 0, w = 0, c = 0, useSeperate = outSeperate != null;
|
||||
if (useSeperate) {
|
||||
while (c < outSeperate.length) outSeperate[c++] *= __bitSize;
|
||||
while (c++ < buffer.channels) outSeperate.push(0);
|
||||
}
|
||||
|
||||
// IT SHOULD BE ALWAYS BACKEND.STREAMED IF THIS GETS CALLED.
|
||||
var backend = sound._source != null ? sound._source.__backend : null;
|
||||
if (backend != null && backend.streamTimer != null) {
|
||||
var i = backend.bufferSizes.length - backend.queuedBuffers;
|
||||
var time = backend.bufferTimes[i] * 1000;
|
||||
|
||||
if (startPos >= time && startPos < backend.bufferTimes[backend.bufferSizes.length - 1] * 1000) {
|
||||
var buf = backend.bufferDatas[i].buffer, size = backend.bufferSizes[i];
|
||||
pos = Math.floor((startPos - time) * __toBits);
|
||||
c = Math.floor((pos % (__wordSize * buffer.channels)) / __wordSize);
|
||||
pos -= pos % __wordSize;
|
||||
|
||||
while (n > 0) {
|
||||
if (__wordSize == 1) {
|
||||
b = buf.get(pos++) - __bitSize;
|
||||
n--;
|
||||
}
|
||||
else {
|
||||
while (w < buffer.bitsPerSample) {
|
||||
b |= buf.get(pos) << w;
|
||||
w += 8;
|
||||
pos++;
|
||||
n--;
|
||||
}
|
||||
if (b > __bitSize) b -= __bitUnsignedSize;
|
||||
}
|
||||
if (max < b) max = b;
|
||||
if (useSeperate) {
|
||||
if (outSeperate[c] < b) outSeperate[c] = b;
|
||||
if (++c > buffer.channels) c = 0;
|
||||
}
|
||||
if (pos >= size) {
|
||||
if (++i >= backend.bufferDatas.length) break;
|
||||
size = backend.bufferSizes[i];
|
||||
buf = backend.bufferDatas[i].buffer;
|
||||
pos = 0;
|
||||
}
|
||||
w = b = 0;
|
||||
}
|
||||
|
||||
startPos = pos / __toBits + time;
|
||||
// Cannot inline a not final return
|
||||
}
|
||||
}
|
||||
if (n == 0) return max / __bitSize;
|
||||
|
||||
if (Math.abs(startPos - __vorbis.timeTell() * 1000) > 4) {
|
||||
if (startPos < 1) __vorbis.rawSeek(0);
|
||||
else __vorbis.timeSeek(startPos / 1000);
|
||||
}
|
||||
|
||||
pos = c = 0;
|
||||
var isBigEndian = lime.system.System.endianness == lime.system.Endian.BIG_ENDIAN, result;
|
||||
while (n > 0) {
|
||||
n -= (result = __vorbis.read(__buffer, 0, n < __bufferSize ? n : __bufferSize, isBigEndian, __wordSize, true));
|
||||
|
||||
if (result == Vorbis.HOLE) continue;
|
||||
else if (result <= 0) break;
|
||||
|
||||
while (pos < result) {
|
||||
if (__wordSize == 1) b = __buffer.get(pos++) - __bitSize;
|
||||
else {
|
||||
while (w < buffer.bitsPerSample) {
|
||||
b |= __buffer.get(pos) << w;
|
||||
w += 8;
|
||||
pos++;
|
||||
}
|
||||
if (b > __bitSize) b -= __bitUnsignedSize;
|
||||
}
|
||||
if (max < b) max = b;
|
||||
if (useSeperate) {
|
||||
if (outSeperate[c] < b) outSeperate[c] = b;
|
||||
if (++c > buffer.channels) c = 0;
|
||||
}
|
||||
w = b = 0;
|
||||
}
|
||||
pos = 0;
|
||||
}
|
||||
|
||||
return max / __bitSize;
|
||||
}
|
||||
#end
|
||||
}
|
||||
@@ -97,7 +97,7 @@ class ControlsUtil {
|
||||
|
||||
public static function loadCustomControls() {
|
||||
var xmlPath = Paths.xml("config/controls");
|
||||
for(source in [funkin.backend.assets.AssetsLibraryList.AssetSource.SOURCE, funkin.backend.assets.AssetsLibraryList.AssetSource.MODS]) {
|
||||
for(source in [funkin.backend.assets.AssetSource.SOURCE, funkin.backend.assets.AssetSource.MODS]) {
|
||||
if (Paths.assetsTree.existsSpecific(xmlPath, "TEXT", source)) {
|
||||
var access:Access = null;
|
||||
try {
|
||||
|
||||
@@ -83,6 +83,38 @@ final class CoolUtil
|
||||
return (v == null || isNaN(v)) ? defaultValue : v;
|
||||
}
|
||||
|
||||
/**
|
||||
* For use when using Std.parseFloat, if not using that then use `getDefault`.
|
||||
* @param v The value
|
||||
* @param defaultValue The default value
|
||||
* @return The return value
|
||||
*/
|
||||
public static inline function getDefaultFloat(v:Float, defaultValue:Float):Float {
|
||||
return (Math.isNaN(v)) ? defaultValue : v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the % operator, but without any negatives.
|
||||
*
|
||||
* @param dividend The intial value. The left side of the equation.
|
||||
* @param divisor What to modulo by. The right side of the equation.
|
||||
* @return A positive reminder of `dividend / divisor`.
|
||||
*/
|
||||
public static inline function positiveModulo(dividend:Float, divisor:Float) {
|
||||
return ((dividend % divisor) + divisor) % divisor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the % operator, but without any negatives. But it's an int.
|
||||
*
|
||||
* @param dividend The intial value. The left side of the equation.
|
||||
* @param divisor What to modulo by. The right side of the equation.
|
||||
* @return A positive reminder of `dividend / divisor`.
|
||||
*/
|
||||
public static inline function positiveModuloInt(dividend:Int, divisor:Int) {
|
||||
return ((dividend % divisor) + divisor) % divisor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to parse JSON from an Asset path
|
||||
* @param assetPath Path to the JSON asset.
|
||||
@@ -105,7 +137,7 @@ final class CoolUtil
|
||||
FileSystem.deleteDirectory(delete + "/" + file);
|
||||
} else {
|
||||
try FileSystem.deleteFile(delete + "/" + file)
|
||||
catch(e) Logs.trace("Could not delete " + delete + "/" + file, WARNING);
|
||||
catch(e) Logs.warn("Could not delete " + delete + "/" + file);
|
||||
}
|
||||
}
|
||||
#end
|
||||
@@ -124,7 +156,7 @@ final class CoolUtil
|
||||
else sys.io.File.saveContent(path, content);
|
||||
} catch(e) {
|
||||
var errMsg:String = 'Error while trying to save the file: ${Std.string(e).replace('\n', ' ')}';
|
||||
Logs.traceColored([Logs.logText(errMsg, RED)], ERROR);
|
||||
Logs.error(errMsg);
|
||||
if(showErrorBox) funkin.backend.utils.NativeAPI.showMessageBox("Codename Engine Warning", errMsg, MSG_WARNING);
|
||||
}
|
||||
#end
|
||||
@@ -145,7 +177,7 @@ final class CoolUtil
|
||||
if(result.isNothing) Logs.trace('The file where it has been tried to get the attributes from, might be corrupted or inexistent (code: ${result.getValue()})', WARNING);
|
||||
return result;
|
||||
#else
|
||||
return 0;
|
||||
return new FileAttributeWrapper(0);
|
||||
#end
|
||||
}
|
||||
|
||||
@@ -183,7 +215,7 @@ final class CoolUtil
|
||||
addMissingFolders(Path.directory(path));
|
||||
|
||||
var result = NativeAPI.addFileAttributes(path, attrib, useAbsolute);
|
||||
if(result == 0) Logs.trace('Failed to add attributes to $path with a code of: $result', WARNING);
|
||||
if (result == 0) Logs.trace('Failed to add attributes to $path with a code of: $result', WARNING);
|
||||
return result;
|
||||
#else
|
||||
return 0;
|
||||
@@ -249,7 +281,16 @@ final class CoolUtil
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last of an Array
|
||||
* Returns the first element of an Array
|
||||
* @param array Array
|
||||
* @return T Last element
|
||||
*/
|
||||
public static inline function first<T>(array:Array<T>):T {
|
||||
return array[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last element of an Array
|
||||
* @param array Array
|
||||
* @return T Last element
|
||||
*/
|
||||
@@ -367,7 +408,7 @@ final class CoolUtil
|
||||
* @return FPS-Modified Ratio
|
||||
*/
|
||||
@:noUsing public static inline function getFPSRatio(ratio:Float):Float {
|
||||
return CoolUtil.bound(ratio * 60 * FlxG.elapsed, 0.0, 1.0);
|
||||
return 1.0 - Math.pow(1.0 - ratio, FlxG.elapsed * 60);
|
||||
}
|
||||
/**
|
||||
* Tries to get a color from a `Dynamic` variable.
|
||||
@@ -414,7 +455,6 @@ final class CoolUtil
|
||||
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
|
||||
{
|
||||
playMusic(Paths.music('freakyMenu'), true, fadeIn ? 0 : 1, true, 102);
|
||||
FlxG.sound.music.persist = true;
|
||||
if (fadeIn)
|
||||
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
||||
}
|
||||
@@ -444,32 +484,34 @@ final class CoolUtil
|
||||
*/
|
||||
@:noUsing public static function playMusic(path:String, Persist:Bool = false, Volume:Float = 1, Looped:Bool = true, DefaultBPM:Float = 102, ?Group:FlxSoundGroup) {
|
||||
Conductor.reset();
|
||||
FlxG.sound.playMusic(path, Volume, Looped, Group);
|
||||
if (FlxG.sound.music != null) {
|
||||
FlxG.sound.music.persist = Persist;
|
||||
}
|
||||
if (FlxG.sound.music == null || !FlxG.sound.music.exists) FlxG.sound.music = new FlxSound();
|
||||
else if (FlxG.sound.music.active) FlxG.sound.music.stop();
|
||||
FlxG.sound.music.loadEmbedded(path, Looped);
|
||||
FlxG.sound.music.volume = Volume;
|
||||
FlxG.sound.music.persist = Persist;
|
||||
FlxG.sound.defaultMusicGroup.add(FlxG.sound.music);
|
||||
|
||||
var infoPath = '${Path.withoutExtension(path)}.ini';
|
||||
if (Assets.exists(infoPath)) {
|
||||
var musicInfo = IniUtil.parseAsset(infoPath, [
|
||||
"BPM" => null,
|
||||
"TimeSignature" => Flags.DEFAULT_BEATS_PER_MEASURE + "/" + Flags.DEFAULT_STEPS_PER_BEAT
|
||||
]);
|
||||
var iniPath = '${Path.withoutExtension(path)}.ini';
|
||||
var musicData = Assets.exists(iniPath) ? IniUtil.parseAsset(iniPath)["Global"] : null;
|
||||
if (musicData != null) {
|
||||
if (musicData["LoopTime"] != null) FlxG.sound.music.loopTime = Std.parseFloat(musicData["LoopTime"]) * 1000;
|
||||
if (musicData["EndTime"] != null) FlxG.sound.music.endTime = Std.parseFloat(musicData["EndTime"]) * 1000;
|
||||
if (musicData["Offset"] != null) FlxG.sound.music.offset = Std.parseFloat(musicData["Offset"]) * 1000;
|
||||
|
||||
var timeSignParsed:Array<Null<Float>> = musicInfo["TimeSignature"] == null ? [] : [for(s in musicInfo["TimeSignature"].split("/")) Std.parseFloat(s)];
|
||||
var beatsPerMeasure:Float = Flags.DEFAULT_BEATS_PER_MEASURE;
|
||||
var stepsPerBeat:Int = Flags.DEFAULT_STEPS_PER_BEAT;
|
||||
|
||||
// Check later, i don't think timeSignParsed can contain null, only nan
|
||||
if (timeSignParsed.length == 2 && !timeSignParsed.contains(null)) {
|
||||
beatsPerMeasure = timeSignParsed[0] == null || timeSignParsed[0] <= 0 ? Flags.DEFAULT_BEATS_PER_MEASURE : cast timeSignParsed[0];
|
||||
stepsPerBeat = timeSignParsed[1] == null || timeSignParsed[1] <= 0 ? Flags.DEFAULT_STEPS_PER_BEAT : cast timeSignParsed[1];
|
||||
var timeSignParsed:Array<Null<Float>> = musicData["TimeSignature"] == null ? [] : [for(s in musicData["TimeSignature"].split("/")) Std.parseFloat(s)];
|
||||
var beatsPerMeasure:Float = Flags.DEFAULT_BEATS_PER_MEASURE, stepsPerBeat:Float = Flags.DEFAULT_STEPS_PER_BEAT;
|
||||
if (timeSignParsed.length == 2) {
|
||||
beatsPerMeasure = Math.isNaN(timeSignParsed[0]) ? Flags.DEFAULT_BEATS_PER_MEASURE : timeSignParsed[0];
|
||||
stepsPerBeat = Math.isNaN(timeSignParsed[1]) ? Flags.DEFAULT_STEPS_PER_BEAT : (16 / timeSignParsed[1]); // from denominator
|
||||
}
|
||||
|
||||
var bpm:Null<Float> = Std.parseFloat(musicInfo["BPM"]).getDefault(DefaultBPM);
|
||||
Conductor.changeBPM(bpm, beatsPerMeasure, stepsPerBeat);
|
||||
} else
|
||||
var bpm:Float = Std.parseFloat(musicData["BPM"]).getDefault(DefaultBPM);
|
||||
Conductor.changeBPM(bpm, beatsPerMeasure, Math.floor(stepsPerBeat));
|
||||
}
|
||||
else
|
||||
Conductor.changeBPM(DefaultBPM);
|
||||
|
||||
FlxG.sound.music.play();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -478,14 +520,14 @@ final class CoolUtil
|
||||
* @param volume At which volume it should play
|
||||
*/
|
||||
@:noUsing public static inline function playMenuSFX(menuSFX:CoolSfx = SCROLL, volume:Float = 1) {
|
||||
FlxG.sound.play(Paths.sound(switch(menuSFX) {
|
||||
case CONFIRM: 'menu/confirm';
|
||||
case CANCEL: 'menu/cancel';
|
||||
case SCROLL: 'menu/scroll';
|
||||
case CHECKED: 'menu/checkboxChecked';
|
||||
case UNCHECKED: 'menu/checkboxUnchecked';
|
||||
case WARNING: 'menu/warningMenu';
|
||||
default: 'menu/scroll';
|
||||
FlxG.sound.play(Paths.sound('menu/' + switch(menuSFX) {
|
||||
case CONFIRM: 'confirm';
|
||||
case CANCEL: 'cancel';
|
||||
case SCROLL: 'scroll';
|
||||
case CHECKED: 'checkboxChecked';
|
||||
case UNCHECKED: 'checkboxUnchecked';
|
||||
case WARNING: 'warningMenu';
|
||||
default: 'scroll';
|
||||
}), volume);
|
||||
}
|
||||
|
||||
@@ -712,11 +754,11 @@ final class CoolUtil
|
||||
* @param spr Sprite to load the graphic for
|
||||
* @param path Path to the graphic
|
||||
*/
|
||||
public static function loadAnimatedGraphic(spr:FlxSprite, path:String) {
|
||||
public static function loadAnimatedGraphic(spr:FlxSprite, path:String, fps:Float = 24.0) {
|
||||
spr.frames = Paths.getFrames(path, true);
|
||||
|
||||
if (spr.frames != null && spr.frames.frames != null) {
|
||||
spr.animation.add("idle", [for(i in 0...spr.frames.frames.length) i], 24, true);
|
||||
spr.animation.add("idle", [for(i in 0...spr.frames.frames.length) i], fps, true);
|
||||
spr.animation.play("idle");
|
||||
}
|
||||
|
||||
@@ -849,11 +891,7 @@ final class CoolUtil
|
||||
* Stops a sound, set its time to 0 then play it again.
|
||||
* @param sound Sound to replay.
|
||||
*/
|
||||
public static inline function replay(sound:FlxSound) {
|
||||
sound.stop();
|
||||
sound.time = 0;
|
||||
sound.play();
|
||||
}
|
||||
public static inline function replay(sound:FlxSound) sound.play(true, 0);
|
||||
|
||||
/**
|
||||
* Equivalent of `Math.max`, except doesn't require a Int -> Float -> Int conversion.
|
||||
@@ -904,10 +942,11 @@ final class CoolUtil
|
||||
* @param music Music
|
||||
*/
|
||||
public static inline function setMusic(frontEnd:SoundFrontEnd, music:FlxSound) {
|
||||
if (frontEnd.music != null)
|
||||
@:privateAccess frontEnd.destroySound(frontEnd.music);
|
||||
if (frontEnd.music == music) return;
|
||||
|
||||
if (frontEnd.music != null) @:privateAccess frontEnd.destroySound(frontEnd.music);
|
||||
frontEnd.list.remove(music);
|
||||
frontEnd.music = music;
|
||||
frontEnd.defaultMusicGroup.add(frontEnd.music = music);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -915,8 +954,8 @@ final class CoolUtil
|
||||
* @param mainEase Main ease
|
||||
* @param suffix Suffix (Ignored if `mainEase` is `linear`)
|
||||
*/
|
||||
@:noUsing public static inline function flxeaseFromString(mainEase:String, suffix:String)
|
||||
return Reflect.field(FlxEase, mainEase + (mainEase == "linear" ? "" : suffix));
|
||||
@:noUsing public static inline function flxeaseFromString(mainEase:String, ?suffix:String)
|
||||
return Reflect.field(FlxEase, mainEase + (mainEase == "linear" || suffix == null ? "" : suffix));
|
||||
|
||||
/*
|
||||
* Returns the filename of a path, without the extension.
|
||||
@@ -1021,6 +1060,13 @@ final class CoolUtil
|
||||
}
|
||||
#end
|
||||
|
||||
public static inline function repeat(str:String, times:Int) {
|
||||
var r = new StringBuf();
|
||||
for(i in 0...times)
|
||||
r.add(str);
|
||||
return r.toString();
|
||||
}
|
||||
|
||||
public static inline function bound(Value:Float, Min:Float, Max:Float):Float {
|
||||
#if cpp
|
||||
var _hx_tmp1:Float = Value;
|
||||
@@ -1156,6 +1202,15 @@ final class CoolUtil
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the luminance of the given color
|
||||
* @param color Color to use
|
||||
* @return Number between 0 and 1
|
||||
*/
|
||||
public static function getLuminance(color:FlxColor):Float {
|
||||
return (0.2126*color.redFloat + 0.7152*color.greenFloat + 0.0722*color.blueFloat);
|
||||
}
|
||||
|
||||
/**
|
||||
* ! REQUIRES FULL PATH!!!
|
||||
* @param path
|
||||
@@ -1323,6 +1378,124 @@ final class CoolUtil
|
||||
public static inline function isMapEmpty<K, V>(map: Map<K, V>): Bool {
|
||||
return !map.keys().hasNext();
|
||||
}
|
||||
|
||||
public inline static function parsePropertyString(fieldPath:String):Array<OneOfTwo<String, Int>> {
|
||||
return FlxTween.parseFieldString(fieldPath);
|
||||
}
|
||||
|
||||
public static function stringifyFieldsPath(fields:Array<OneOfTwo<String, Int>>):String {
|
||||
var str = new StringBuf();
|
||||
var first = true;
|
||||
for (field in fields) {
|
||||
if (Type.typeof(field) == TInt) {
|
||||
str.add('[${field}]');
|
||||
} else {
|
||||
if (!first)
|
||||
str.add('.');
|
||||
str.add(field);
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
public static function parseProperty(target:Dynamic, fields:OneOfTwo<String, Array<OneOfTwo<String, Int>>>):Dynamic {
|
||||
var fields:Array<OneOfTwo<String, Int>> = {
|
||||
if((fields is String)) CoolUtil.parsePropertyString(fields);
|
||||
else fields;
|
||||
}
|
||||
|
||||
var field = CoolUtil.last(fields);
|
||||
for (i in 0...fields.length - 1) {
|
||||
var component = fields[i];
|
||||
if (Type.typeof(component) == TInt) {
|
||||
if ((target is Array)) {
|
||||
var index:Int = cast component;
|
||||
var arr:Array<Dynamic> = cast target;
|
||||
target = arr[index];
|
||||
}
|
||||
} else { // TClass(String)
|
||||
target = Reflect.getProperty(target, component);
|
||||
}
|
||||
if (!Reflect.isObject(target) && !(target is Array))
|
||||
throw 'The object does not have the property "$component" in "${stringifyFieldsPath(fields)}"';
|
||||
}
|
||||
return new PropertyInfo(target, field);
|
||||
}
|
||||
|
||||
public static function cloneProperty(toTarget:Dynamic, fields:OneOfTwo<String, Array<OneOfTwo<String, Int>>>, fromTarget:Dynamic):Dynamic {
|
||||
var fields:Array<OneOfTwo<String, Int>> = {
|
||||
if((fields is String)) CoolUtil.parsePropertyString(fields);
|
||||
else fields;
|
||||
}
|
||||
|
||||
var toProperty = CoolUtil.parseProperty(toTarget, fields);
|
||||
var fromProperty = CoolUtil.parseProperty(fromTarget, fields);
|
||||
|
||||
return toProperty.setValue(fromProperty.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
class PropertyInfo {
|
||||
public var object:Dynamic;
|
||||
public var field:OneOfTwo<String, Int>;
|
||||
public var typeOfField:Type.ValueType;
|
||||
#if hscript_improved
|
||||
public var isCustom:Bool = false;
|
||||
public var custom:hscript.IHScriptCustomBehaviour;
|
||||
#end
|
||||
|
||||
public function new(object:Dynamic, field:OneOfTwo<String, Int>) {
|
||||
this.object = object;
|
||||
this.field = field;
|
||||
#if hscript_improved
|
||||
if (object is hscript.IHScriptCustomBehaviour)
|
||||
{
|
||||
isCustom = true;
|
||||
custom = cast object;
|
||||
}
|
||||
#end
|
||||
|
||||
typeOfField = Type.typeof(field);
|
||||
}
|
||||
|
||||
public function getValue():Dynamic
|
||||
{
|
||||
if (typeOfField == TInt)
|
||||
{
|
||||
var index:Int = cast field;
|
||||
var arr:Array<Dynamic> = cast object;
|
||||
return arr[index];
|
||||
}
|
||||
else
|
||||
{
|
||||
#if hscript_improved
|
||||
if (isCustom)
|
||||
return custom.hget(field);
|
||||
else
|
||||
#end
|
||||
return Reflect.getProperty(object, field);
|
||||
}
|
||||
}
|
||||
|
||||
public function setValue(value:Dynamic):Void
|
||||
{
|
||||
if (typeOfField == TInt)
|
||||
{
|
||||
var index:Int = cast field;
|
||||
var arr:Array<Dynamic> = cast object;
|
||||
arr[index] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
#if hscript_improved
|
||||
if (isCustom)
|
||||
custom.hset(field, value);
|
||||
else
|
||||
#end
|
||||
Reflect.setProperty(object, field, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -80,7 +80,7 @@ final class DiscordUtil
|
||||
try
|
||||
config = Json.parse(Assets.getText(jsonPath))
|
||||
catch (e)
|
||||
Logs.trace('Couldn\'t load Discord RPC configuration: ${e.toString()}', ERROR);
|
||||
Logs.error('Couldn\'t load Discord RPC configuration: ${e.toString()}');
|
||||
}
|
||||
|
||||
if (config == null)
|
||||
@@ -89,6 +89,10 @@ final class DiscordUtil
|
||||
config.logoKey = config.logoKey.getDefault(Flags.DEFAULT_DISCORD_LOGO_KEY);
|
||||
config.logoText = config.logoText.getDefault(Flags.DEFAULT_DISCORD_LOGO_TEXT);
|
||||
config.clientID = config.clientID.getDefault(Flags.DEFAULT_DISCORD_CLIENT_ID);
|
||||
|
||||
if (Flags.MOD_DISCORD_CLIENT_ID.length > 0) config.clientID = Flags.MOD_DISCORD_CLIENT_ID;
|
||||
if (Flags.MOD_DISCORD_LOGO_KEY.length > 0) config.logoKey = Flags.MOD_DISCORD_LOGO_KEY;
|
||||
if (Flags.MOD_DISCORD_LOGO_TEXT.length > 0) config.logoText = Flags.MOD_DISCORD_LOGO_TEXT;
|
||||
currentID = config.clientID;
|
||||
#end
|
||||
}
|
||||
@@ -364,7 +368,7 @@ final class DiscordUtil
|
||||
Logs.traceColored([
|
||||
Logs.getPrefix("Discord"),
|
||||
Logs.logText("Someone has just requested to join", YELLOW)
|
||||
], WARNING);
|
||||
], INFO);
|
||||
|
||||
var req:DUser = DUser.initRaw(request);
|
||||
call("onJoinRequest", [req]);
|
||||
|
||||
@@ -63,7 +63,7 @@ final class HttpUtil
|
||||
{
|
||||
// 301: Moved Permanently, 302: Found (Moved Temporarily), 307: Temporary Redirect, 308: Permanent Redirect - Nex
|
||||
case 301 | 302 | 307 | 308:
|
||||
Logs.traceColored([Logs.logText('[Connection Status] ', BLUE), Logs.logText('Redirected with status code: ', YELLOW), Logs.logText('$status', GREEN)], WARNING);
|
||||
Logs.traceColored([Logs.logText('[Connection Status] ', BLUE), Logs.logText('Redirected with status code: ', YELLOW), Logs.logText('$status', GREEN)], VERBOSE);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -1,39 +1,33 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
/**
|
||||
* DOESN'T SUPPORT CATEGORIES YET!!
|
||||
*/
|
||||
final class IniUtil {
|
||||
public static inline function parseAsset(assetPath:String, ?defaultVariables:Map<String, String>)
|
||||
return parseString(Assets.getText(assetPath), defaultVariables);
|
||||
class IniUtil {
|
||||
public static inline function parseAsset(assetPath:String)
|
||||
return parseString(Assets.getText(assetPath));
|
||||
|
||||
public static function parseString(data:String, ?defaultVariables:Map<String, String>) {
|
||||
var trimmed:String;
|
||||
var splitContent = [for(e in data.split("\n")) if ((trimmed = e.trim()) != "") trimmed];
|
||||
public static function parseString(data:String):Map<String, Map<String, String>> {
|
||||
var res:Map<String, Map<String, String>> = [];
|
||||
parseStringToMap(res, data);
|
||||
return res;
|
||||
}
|
||||
|
||||
var finalMap:Map<String, String> = [];
|
||||
if (defaultVariables != null)
|
||||
for(k=>e in defaultVariables)
|
||||
finalMap[k] = e;
|
||||
public static function parseStringToMap(map:Map<String, Map<String, String>>, data:String) {
|
||||
var section:Map<String, String> = null;
|
||||
|
||||
for(line in splitContent) {
|
||||
// comment
|
||||
if (line.startsWith(";")) continue;
|
||||
// categories; not supported yet
|
||||
if (line.startsWith("[") && line.endsWith("]")) continue;
|
||||
var regexSec = ~/\\[(.+)\\]/g, regexVal = ~/(.+)=(.+)/g, quote = ~/[\\'"](.+)[\\'"]/g, iln = 0;
|
||||
do {
|
||||
var line = data.substring(iln, ((iln = data.indexOf('\n', iln) + 1) == 0 ? data.length : iln - 1));
|
||||
if (regexSec.match(line)) {
|
||||
if ((section = map.get(regexSec.matched(1))) == null) map.set(regexSec.matched(1), section = []);
|
||||
}
|
||||
else if (regexVal.match(line)) {
|
||||
var s = regexVal.matched(2).trim();
|
||||
if (quote.match(s)) s = quote.matched(1);
|
||||
|
||||
var index = line.indexOf("=");
|
||||
var name = line.substr(0, index).trim();
|
||||
var value = line.substr(index+1).trim();
|
||||
|
||||
if (value.length > 1 && value.charCodeAt(0) == '"'.code && value.charCodeAt(value.length-1) == '"'.code)
|
||||
value = value.substr(1, value.length - 2);
|
||||
|
||||
if (value.length == 0 || name.length == 0)
|
||||
continue;
|
||||
|
||||
finalMap[name] = value;
|
||||
}
|
||||
return finalMap;
|
||||
if (section == null) {
|
||||
if ((section = map.get("Global")) == null) map.set("Global", section = []);
|
||||
}
|
||||
section.set(regexVal.matched(1).trim(), s);
|
||||
}
|
||||
} while (iln != 0);
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,84 @@ final class MathUtil {
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a is less than b with considering a margin of error.
|
||||
*
|
||||
* @param a Float
|
||||
* @param b Float
|
||||
* @param margin Float (Default: EPSILON)
|
||||
*
|
||||
* @return Bool
|
||||
**/
|
||||
public static function lessThan(a:Float, b:Float, margin:Float = 0.0000001):Bool {
|
||||
return a < b - margin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a is less than or equally b with considering a margin of error.
|
||||
*
|
||||
* @param a Float
|
||||
* @param b Float
|
||||
* @param margin Float (Default: EPSILON)
|
||||
*
|
||||
* @return Bool
|
||||
**/
|
||||
public static function lessThanEqual(a:Float, b:Float, margin:Float = 0.0000001):Bool {
|
||||
return a <= b - margin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a is greater than b with considering a margin of error.
|
||||
*
|
||||
* @param a Float
|
||||
* @param b Float
|
||||
* @param margin Float (Default: EPSILON)
|
||||
*
|
||||
* @return Bool
|
||||
**/
|
||||
public static function greaterThan(a:Float, b:Float, margin:Float = 0.0000001):Bool {
|
||||
return a > b + margin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a is greater than or equally b with considering a margin of error.
|
||||
*
|
||||
* @param a Float
|
||||
* @param b Float
|
||||
* @param margin Float (Default: EPSILON)
|
||||
*
|
||||
* @return Bool
|
||||
**/
|
||||
public static function greaterThanEqual(a:Float, b:Float, margin:Float = 0.0000001):Bool {
|
||||
return a >= b + margin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a is approximately equal to b.
|
||||
*
|
||||
* @param a Float
|
||||
* @param b Float
|
||||
* @param margin Float (Default: EPSILON)
|
||||
*
|
||||
* @return Bool
|
||||
**/
|
||||
public static function equal(a:Float, b:Float, margin:Float = 0.0000001):Bool {
|
||||
return Math.abs(a - b) <= margin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a are not approximately equal to b.
|
||||
*
|
||||
* @param a Float
|
||||
* @param b Float
|
||||
* @param margin Float (Default: EPSILON)
|
||||
*
|
||||
* @return Bool
|
||||
**/
|
||||
public static function notEqual(a:Float, b:Float, margin:Float = 0.0000001):Bool {
|
||||
return Math.abs(a - b) > margin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to `Math.max` but with infinite amount of arguments
|
||||
*
|
||||
|
||||
@@ -127,7 +127,7 @@ final class MemoryUtil {
|
||||
public static function getMemType():String {
|
||||
#if windows
|
||||
var memoryMap:Map<Int, String> = [
|
||||
0 => "Unknown",
|
||||
0 => null,
|
||||
1 => "Other",
|
||||
2 => "DRAM",
|
||||
3 => "Synchronous DRAM",
|
||||
@@ -166,9 +166,9 @@ final class MemoryUtil {
|
||||
];
|
||||
var memoryOutput:Int = -1;
|
||||
|
||||
var process = new HiddenProcess("wmic", ["memorychip", "get", "SMBIOSMemoryType"]);
|
||||
var process = new HiddenProcess("powershell", ["-Command", "Get-CimInstance Win32_PhysicalMemory | Select-Object -ExpandProperty SMBIOSMemoryType" ]);
|
||||
if (process.exitCode() == 0) memoryOutput = Std.int(Std.parseFloat(process.stdout.readAll().toString().trim().split("\n")[1]));
|
||||
if (memoryOutput != -1) return memoryMap[memoryOutput];
|
||||
if (memoryOutput != -1) return memoryMap[memoryOutput] == null ? 'Unknown ($memoryOutput)' : memoryMap[memoryOutput];
|
||||
#elseif mac
|
||||
var process = new HiddenProcess("system_profiler", ["SPMemoryDataType"]);
|
||||
var reg = ~/Type: (.+)/;
|
||||
|
||||
@@ -41,7 +41,7 @@ final class NdllUtil {
|
||||
//return Reflect.callMethod(null, func, a); // wouldn't work for some reason, maybe cause like c++ functions doesn't have reflection enabled
|
||||
});
|
||||
#else
|
||||
Logs.trace('NDLLs are not supported on this platform.', WARNING);
|
||||
Logs.warn('NDLLs are not supported on this platform.');
|
||||
return noop;
|
||||
#end
|
||||
}
|
||||
@@ -56,20 +56,20 @@ final class NdllUtil {
|
||||
public static function getFunctionFromPath(ndll:String, name:String, args:Int):Dynamic {
|
||||
#if NDLLS_SUPPORTED
|
||||
if (!Assets.exists(ndll)) {
|
||||
Logs.trace('Couldn\'t find ndll at ${ndll}.', WARNING);
|
||||
Logs.warn('Couldn\'t find ndll at ${ndll}.');
|
||||
return noop;
|
||||
}
|
||||
var func = lime.system.CFFI.load(Assets.getPath(ndll), name, args);
|
||||
|
||||
if (func == null) {
|
||||
Logs.trace('Method ${name} in ndll ${ndll} with ${args} args was not found.', ERROR);
|
||||
Logs.error('Method ${name} in ndll ${ndll} with ${args} args was not found.');
|
||||
return noop;
|
||||
}
|
||||
return func;
|
||||
#else
|
||||
Logs.trace('NDLLs are not supported on this platform.', WARNING);
|
||||
#end
|
||||
Logs.warn('NDLLs are not supported on this platform.');
|
||||
return noop;
|
||||
#end
|
||||
}
|
||||
|
||||
@:dox(hide) @:noCompletion static function noop() {}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
enum abstract RegistryHive(Int) {
|
||||
var HKEY_CLASSES_ROOT = 0x80000000;
|
||||
var HKEY_CURRENT_USER = 0x80000001;
|
||||
var HKEY_LOCAL_MACHINE = 0x80000002;
|
||||
var HKEY_USERS = 0x80000003;
|
||||
var HKEY_CURRENT_CONFIG = 0x80000005;
|
||||
}
|
||||
#if windows
|
||||
@:cppFileCode('
|
||||
#include <windows.h>
|
||||
#include <tchar.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
')
|
||||
#end
|
||||
class RegistryUtil {
|
||||
#if windows
|
||||
@:functionCode('
|
||||
HKEY hKey;
|
||||
LONG result;
|
||||
DWORD dataSize = 0;
|
||||
DWORD dataType = 0;
|
||||
|
||||
std::wstring subkey = std::wstring(key.wchar_str());
|
||||
std::wstring valname = std::wstring(string.wchar_str());
|
||||
|
||||
result = RegOpenKeyExW((HKEY)reinterpret_cast<HKEY>(static_cast<uintptr_t>(hive)), subkey.c_str(), 0, KEY_READ, &hKey);
|
||||
if (result != ERROR_SUCCESS) return null();
|
||||
|
||||
result = RegQueryValueExW(hKey, valname.c_str(), NULL, &dataType, NULL, &dataSize);
|
||||
if (result != ERROR_SUCCESS || dataSize == 0) {
|
||||
RegCloseKey(hKey);
|
||||
return null();
|
||||
}
|
||||
|
||||
std::vector<wchar_t> buffer(dataSize / sizeof(wchar_t));
|
||||
result = RegQueryValueExW(hKey, valname.c_str(), NULL, NULL, (LPBYTE)buffer.data(), &dataSize);
|
||||
RegCloseKey(hKey);
|
||||
|
||||
if (result == ERROR_SUCCESS) {
|
||||
return ::String(buffer.data());
|
||||
}
|
||||
return null();
|
||||
')
|
||||
#end
|
||||
public static function get(hive:RegistryHive, key:String, string:String):Null<String>
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
#if windows
|
||||
@:functionCode('
|
||||
HKEY hKey;
|
||||
LONG result;
|
||||
|
||||
std::wstring subkey = std::wstring(key.wchar_str());
|
||||
std::wstring valname = std::wstring(string.wchar_str());
|
||||
std::wstring data = std::wstring(value.wchar_str());
|
||||
|
||||
result = RegCreateKeyExW((HKEY)reinterpret_cast<HKEY>(static_cast<uintptr_t>(hive)), subkey.c_str(), 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL);
|
||||
if (result != ERROR_SUCCESS) return false;
|
||||
|
||||
result = RegSetValueExW(hKey, valname.c_str(), 0, REG_SZ, (const BYTE*)data.c_str(), (DWORD)((data.length() + 1) * sizeof(wchar_t)));
|
||||
RegCloseKey(hKey);
|
||||
|
||||
return result == ERROR_SUCCESS;
|
||||
')
|
||||
#end
|
||||
public static function set(hive:RegistryHive, key:String, string:String, value:String):Bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#if windows
|
||||
@:functionCode('
|
||||
HKEY hKey;
|
||||
LONG result;
|
||||
|
||||
std::wstring subkey = std::wstring(key.wchar_str());
|
||||
std::wstring valname = std::wstring(string.wchar_str());
|
||||
|
||||
result = RegOpenKeyExW((HKEY)reinterpret_cast<HKEY>(static_cast<uintptr_t>(hive)), subkey.c_str(), 0, KEY_READ, &hKey);
|
||||
if (result != ERROR_SUCCESS) return false;
|
||||
|
||||
DWORD dataType = 0;
|
||||
result = RegQueryValueExW(hKey, valname.c_str(), NULL, &dataType, NULL, NULL);
|
||||
|
||||
RegCloseKey(hKey);
|
||||
|
||||
return result == ERROR_SUCCESS;
|
||||
')
|
||||
#end
|
||||
public static function exists(hive:RegistryHive, key:String, string:String):Bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#if windows
|
||||
@:functionCode('
|
||||
HKEY hKey;
|
||||
LONG result;
|
||||
|
||||
std::wstring subkey = std::wstring(key.wchar_str());
|
||||
std::wstring valname = std::wstring(string.wchar_str());
|
||||
|
||||
result = RegOpenKeyExW((HKEY)reinterpret_cast<HKEY>(static_cast<uintptr_t>(hive)), subkey.c_str(), 0, KEY_SET_VALUE, &hKey);
|
||||
if (result != ERROR_SUCCESS) return false;
|
||||
|
||||
result = RegDeleteValueW(hKey, valname.c_str());
|
||||
RegCloseKey(hKey);
|
||||
|
||||
return result == ERROR_SUCCESS;
|
||||
')
|
||||
#end
|
||||
public static function delete(hive:RegistryHive, key:String, string:String):Bool {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ final class SortedArrayUtil {
|
||||
* @param getVal Function that returns the position value of T
|
||||
* @return The index of the element
|
||||
*/
|
||||
public static inline function binarySearch<T>(array:Array<T>, val:Float, getVal:T->Float):Int {
|
||||
public static function binarySearch<T>(array:Array<T>, val:Float, getVal:T->Float):Int {
|
||||
if (array.length <= 0) return 0; // if the array is empty, it should be equal to zero (the beginning)
|
||||
if (getVal(array[0]) > val) return 0; // in case its the minimum
|
||||
if (getVal(array[array.length-1]) < val) return array.length; // in case its the maximum
|
||||
@@ -56,7 +56,8 @@ final class SortedArrayUtil {
|
||||
* @param getVal Function that returns the value that needs to be sorted
|
||||
*/
|
||||
public static inline function removeSorted<T>(array:Array<T>, val:T, getVal:T->Float) {
|
||||
if (val != null)
|
||||
array.splice(binarySearch(array, getVal(val), getVal), 1);
|
||||
var index = binarySearch(array, getVal(val), getVal);
|
||||
if (index != -1)
|
||||
array.splice(index, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
import funkin.backend.utils.IniUtil;
|
||||
import funkin.backend.assets.TranslatedAssetLibrary;
|
||||
import funkin.backend.assets.ModsFolder;
|
||||
import openfl.utils.Assets;
|
||||
import haxe.io.Path;
|
||||
import haxe.xml.Access;
|
||||
import haxe.Exception;
|
||||
import funkin.backend.utils.translations.FormatUtil;
|
||||
|
||||
/**
|
||||
* The class used for translations based on the XMLs inside the translations folders.
|
||||
*
|
||||
* Made by @NexIsDumb originally for the Poldhub mod and improved thanks to him and @NeeEoo.
|
||||
*/
|
||||
@:allow(funkin.backend.assets.TranslatedAssetLibrary)
|
||||
final class TranslationUtil
|
||||
{
|
||||
/**
|
||||
* The current language selected translation map.
|
||||
*
|
||||
* It'll never be `null`.
|
||||
*/
|
||||
public static var stringMap(default, set):Map<String, IFormatInfo> = [];
|
||||
|
||||
/**
|
||||
* The alternative language selected translation map.
|
||||
* It is filled with the default language.
|
||||
*
|
||||
* This is filled in if the current language is not the same as the default language.
|
||||
* Its used when showMissingIds in the config is false (or is not present).
|
||||
*
|
||||
* It'll never be `null`.
|
||||
**/
|
||||
public static var alternativeStringMap(default, set):Map<String, IFormatInfo> = [];
|
||||
|
||||
/**
|
||||
* Returns the current language config.
|
||||
*/
|
||||
public static var config:Map<String, String> = [];
|
||||
/**
|
||||
* Returns the current language.
|
||||
*/
|
||||
public static var curLanguage(get, set):String;
|
||||
/**
|
||||
* Returns the current language name.
|
||||
*/
|
||||
public static var curLanguageName(get, set):String;
|
||||
|
||||
/**
|
||||
* Returns if the current language is the default one (`DEFAULT_LANGUAGE`).
|
||||
*/
|
||||
public static var isDefaultLanguage(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Returns if any translation are loaded.
|
||||
*/
|
||||
public static var isLanguageLoaded(get, never):Bool;
|
||||
|
||||
/**
|
||||
* Returns an array has a list of the languages that were found.
|
||||
*/
|
||||
public static var foundLanguages:Array<String> = [];
|
||||
|
||||
// Private
|
||||
private static inline var LANG_FOLDER:String = "languages";
|
||||
private static var langConfigs:Map<String, Map<String, String>> = [];
|
||||
private static var nameMap:Map<String, String> = [];
|
||||
private static inline function getDefaultNameMap():Map<String, String> {
|
||||
return [Flags.DEFAULT_LANGUAGE => Flags.DEFAULT_LANGUAGE_NAME];
|
||||
}
|
||||
private static inline function getDefaultLangConfigs():Map<String, Map<String, String>> {
|
||||
return [Flags.DEFAULT_LANGUAGE => getDefaultConfig(Flags.DEFAULT_LANGUAGE)];
|
||||
}
|
||||
@:noUsing private static inline function getDefaultConfig(name:String):Map<String, String> {
|
||||
return ["name" => getLanguageName(name), "credits" => "", "version" => "1.0.0"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the language.
|
||||
* Also changes the translations map.
|
||||
*
|
||||
* If `name` is `null`, its gonna use the current language.
|
||||
* If `name` is not `null`, it will load the translations for the given language.
|
||||
*/
|
||||
public static function setLanguage(?name:String):Void {
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
if(name == null) name = curLanguage;
|
||||
if(!langConfigs.exists(name)) name = Flags.DEFAULT_LANGUAGE;
|
||||
if(!langConfigs.exists(name)) name = foundLanguages[0];
|
||||
|
||||
if(curLanguage != name) {
|
||||
Logs.traceColored([
|
||||
Logs.getPrefix("Language"),
|
||||
Logs.logText("Changing saved language to: "),
|
||||
Logs.logText('${getLanguageName(name)} ($name)', GREEN)
|
||||
], INFO);
|
||||
// todo: make this into a notification system for the user
|
||||
// notification would be in previous language
|
||||
// but only if the user didnt manually change it.
|
||||
curLanguage = name;
|
||||
} else {
|
||||
Logs.traceColored([
|
||||
Logs.getPrefix("Language"),
|
||||
Logs.logText("Setting Language To: "),
|
||||
Logs.logText('${getLanguageName(name)} ($name)', GREEN)
|
||||
], VERBOSE);
|
||||
}
|
||||
|
||||
for(mod in ModsFolder.getLoadedModsLibs(false))
|
||||
if(mod is TranslatedAssetLibrary)
|
||||
cast(mod, TranslatedAssetLibrary).langFolder = name;
|
||||
|
||||
config = getConfig(name);
|
||||
stringMap = loadLanguage(name);
|
||||
alternativeStringMap = name == Flags.DEFAULT_LANGUAGE || config.get("showMissingIds").getDefault("false") == "true" ? [] : loadLanguage(Flags.DEFAULT_LANGUAGE);
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* This is for checking and getting a translation, `defString` it's just the string that gets returned just in case it won't find the translation OR the current language selected is ``DEFAULT_LANGUAGE``.
|
||||
*
|
||||
* If `id` is `null` then it's gonna search using `defString`.
|
||||
*/
|
||||
public static inline function get(?id:String, ?params:Array<Dynamic>, ?def:String):String
|
||||
return getRaw(id, def).format(params);
|
||||
|
||||
public static inline function translate(?id:String, ?params:Array<Dynamic>, ?def:String):String
|
||||
return get(id, params, def);
|
||||
|
||||
public static inline function translateDiff(?id:String, ?params:Array<Dynamic>):String
|
||||
return get("diff." + id.toLowerCase(), params, id);
|
||||
|
||||
public static function exists(id:String):Bool {
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
for (map in [stringMap, alternativeStringMap]) if (map.exists(id)) return true;
|
||||
#end
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getRaw(id:String, ?def:String):IFormatInfo
|
||||
{
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
for (map in [stringMap, alternativeStringMap]) if (map.exists(id))
|
||||
return map.get(id);
|
||||
#end
|
||||
|
||||
if(def != null)
|
||||
return FormatUtil.get(def);
|
||||
|
||||
return FormatUtil.getStr("{" + id + "}");
|
||||
|
||||
/*if(curLanguage == Flags.DEFAULT_LANGUAGE) {
|
||||
return FormatUtil.getStr("{" + id + "}");
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
return FormatUtil.get(showMissingIds ? "{" + id + "}" : id);*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a normal string into an ID for translations.
|
||||
*
|
||||
* Example: `Resume Song` => `resumeSong`
|
||||
*/
|
||||
public static function raw2Id(str:String):String
|
||||
{
|
||||
str = str.trim().toLowerCase();
|
||||
return [for(i => s in str.split(" "))
|
||||
i != 0 ? s.charAt(0).toUpperCase() + s.substr(1) : s
|
||||
].join("");
|
||||
}
|
||||
|
||||
public static inline function isBlacklisted(lang:String):Bool
|
||||
return Flags.BLACKLISTED_LANGUAGES.contains(lang);
|
||||
|
||||
public static inline function isWhitelisted(lang:String):Bool
|
||||
return Flags.WHITELISTED_LANGUAGES.length == 0 || Flags.WHITELISTED_LANGUAGES.contains(lang);
|
||||
|
||||
public static inline function isAllowed(lang:String):Bool
|
||||
return isWhitelisted(lang) && !isBlacklisted(lang);
|
||||
|
||||
/**
|
||||
* Returns an array that specifies which languages were found.
|
||||
*/
|
||||
public static function findAllLanguages():Void
|
||||
{
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
foundLanguages = [];
|
||||
nameMap = getDefaultNameMap();
|
||||
langConfigs = getDefaultLangConfigs();
|
||||
var mainPath:String = translationsMain("");
|
||||
var langName:String = null;
|
||||
for (lang in Paths.assetsTree.getFolders("assets/" + mainPath)) {
|
||||
if (!isAllowed(lang)) continue;
|
||||
|
||||
var path:String = Path.join([mainPath, lang, "config.ini"]);
|
||||
var config = getDefaultConfig(lang);
|
||||
|
||||
if(Assets.exists(path)) {
|
||||
var c = IniUtil.parseAsset(path);
|
||||
for (i => v in c)
|
||||
for (key => value in v)
|
||||
config[key] = value;
|
||||
} else { // if there was no config.ini, use the file name as the language name
|
||||
for(file in Paths.getFolderContent(mainPath + lang).sortAlphabetically()) {
|
||||
if(Path.extension(file) == "xml") {
|
||||
config["name"] = Path.withoutExtension(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
langName = config["name"];
|
||||
nameMap.set(lang, langName);
|
||||
langConfigs.set(lang, config);
|
||||
foundLanguages.push('$lang/$langName');
|
||||
}
|
||||
|
||||
// Ensure that the default language is always first
|
||||
var defaultName = Flags.DEFAULT_LANGUAGE + "/" + getLanguageName(Flags.DEFAULT_LANGUAGE);
|
||||
if(foundLanguages.contains(defaultName)) foundLanguages.remove(defaultName);
|
||||
foundLanguages.insert(0, defaultName);
|
||||
|
||||
if(!nameMap.exists(curLanguage)) curLanguage = Flags.DEFAULT_LANGUAGE;
|
||||
Logs.trace("Found languages: " + foundLanguages.join(", "), "Language");
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of translations based on its XML.
|
||||
*/
|
||||
public static function loadLanguage(lang:String):Map<String, IFormatInfo>
|
||||
{
|
||||
#if TRANSLATIONS_SUPPORT
|
||||
FormatUtil.clear(); // Clean up the format cache
|
||||
var mainPath:String = translationsMain(lang);
|
||||
var leMap:Map<String, IFormatInfo> = [];
|
||||
var translations:Array<TranslationPair> = [];
|
||||
|
||||
final NODE_NAMES = ["text", "trans", "lang", "string", "str"];
|
||||
function parseXml(xml:Access, prefix:String = "") {
|
||||
for(node in xml.elements) {
|
||||
if (node.name == "group") // Cosmetic name
|
||||
parseXml(node, prefix + (node.has.prefix ? node.att.prefix : ""));
|
||||
else if(NODE_NAMES.contains(node.name))
|
||||
translations.push({
|
||||
prefix: prefix,
|
||||
node: node
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// todo make it load the default languages in a second string map
|
||||
|
||||
for(mod in ModsFolder.getLoadedModsLibs(true)) for(file in mod.getFiles("assets/" + mainPath).sortAlphabetically().map((v)->'$mainPath/$v')) {
|
||||
if(Path.extension(file).toLowerCase() != "xml") continue;
|
||||
|
||||
// Parse the XML
|
||||
var xml:Access = null;
|
||||
try xml = new Access(Xml.parse(Assets.getText("assets/" + file)))
|
||||
catch(e) Logs.error('Error while parsing $file: ${Std.string(e)}', "Language");
|
||||
|
||||
if (xml == null) continue;
|
||||
if (!xml.hasNode.language) {
|
||||
Logs.warn('File $file requires a <language> root element.', "Language");
|
||||
continue;
|
||||
}
|
||||
|
||||
var langNode = xml.node.language;
|
||||
var prefix = langNode.getAtt("prefix").getDefault("");
|
||||
|
||||
//if (langNode.has.name) {
|
||||
// getConfig(lang).set("name", langNode.att.name);
|
||||
//}
|
||||
|
||||
parseXml(langNode, prefix);
|
||||
}
|
||||
|
||||
for(pair in translations) {
|
||||
var node = pair.node;
|
||||
if (!node.has.id) {
|
||||
Logs.warn('A <${node.name}> node requires an ID attribute.', "Language");
|
||||
continue;
|
||||
}
|
||||
var prefix = pair.prefix;
|
||||
|
||||
var id = prefix + node.att.id;
|
||||
|
||||
if(leMap.exists(id)) continue;
|
||||
var value:String = node.has.string ? node.att.string : node.innerData;
|
||||
if(node.getAtt("notrim").getDefault("true") != "true") value = value.trim();
|
||||
// make it so you can escape the backslash
|
||||
value = value.replace("\\n", "\n").replace("\r", ""); // remove stupid windows line breaks and convert newline literals to newlines
|
||||
leMap.set(id, FormatUtil.get(value));
|
||||
//leMap.set(id, FormatUtil.getStr("{" + id + "}"));
|
||||
//Logs.trace("Added " + id + " -> `" + value + "`", "Language");
|
||||
}
|
||||
|
||||
return leMap;
|
||||
#else
|
||||
return [];
|
||||
#end
|
||||
}
|
||||
|
||||
// Utils
|
||||
|
||||
public static function getLanguageName(lang:String):String {
|
||||
return nameMap.exists(lang) ? nameMap.get(lang) : lang;
|
||||
}
|
||||
|
||||
public static function getLanguageFromName(name:String):String {
|
||||
var reverseMap = new Map<String, String>();
|
||||
for(key => val in nameMap) reverseMap.set(val, key);
|
||||
return reverseMap.exists(name) ? reverseMap.get(name) : name;
|
||||
}
|
||||
|
||||
public static function getConfig(lang:String):Map<String, String> {
|
||||
return langConfigs.exists(lang) ? langConfigs.get(lang) : getDefaultConfig(lang);
|
||||
}
|
||||
|
||||
public static inline function isShowingMissingIds():Bool {
|
||||
return Lambda.count(alternativeStringMap) > 0;
|
||||
}
|
||||
|
||||
public static inline function translationsMain(key:String):String
|
||||
return '$LANG_FOLDER/$key';
|
||||
|
||||
public static inline function translations(key:String):String
|
||||
return translationsMain('$curLanguage/$key');
|
||||
|
||||
// getters & setters
|
||||
|
||||
@:noCompletion private static function set_stringMap(value:Map<String, IFormatInfo>):Map<String, IFormatInfo> {
|
||||
if (value == null) value = [];
|
||||
return stringMap = value;
|
||||
}
|
||||
|
||||
@:noCompletion private static function set_alternativeStringMap(value:Map<String, IFormatInfo>):Map<String, IFormatInfo> {
|
||||
if (value == null) value = [];
|
||||
return alternativeStringMap = value;
|
||||
}
|
||||
|
||||
@:noCompletion private static function get_curLanguage():String {
|
||||
return Options.language;
|
||||
}
|
||||
@:noCompletion private static function set_curLanguage(value:String):String {
|
||||
return Options.language = value;
|
||||
}
|
||||
|
||||
@:noCompletion private static function get_curLanguageName():String {
|
||||
return getLanguageName(Options.language);
|
||||
}
|
||||
@:noCompletion private static function set_curLanguageName(value:String):String {
|
||||
return Options.language = getLanguageFromName(value);
|
||||
}
|
||||
|
||||
@:noCompletion private static function get_isDefaultLanguage():Bool
|
||||
return Options.language == Flags.DEFAULT_LANGUAGE;
|
||||
|
||||
@:noCompletion private static function get_isLanguageLoaded():Bool
|
||||
return Lambda.count(stringMap) > 0 || isShowingMissingIds();
|
||||
}
|
||||
|
||||
@:structInit
|
||||
class TranslationPair {
|
||||
public var prefix:String;
|
||||
public var node:Access;
|
||||
}
|
||||
@@ -57,6 +57,17 @@ final class WindowUtils {
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the window title and icon.
|
||||
* @param title The title to set.
|
||||
* @param image The image to set as the icon.
|
||||
**/
|
||||
public static inline function setWindow(?name:String, ?image:String)
|
||||
{
|
||||
Lib.application.window.setIcon(lime.graphics.Image.fromBytes(Assets.getBytes(Flags.MOD_ICON != null ? Flags.MOD_ICON : image)));
|
||||
title = Flags.MOD_NAME != null ? Flags.MOD_NAME : title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the window title to have the current title and prefix/suffix.
|
||||
**/
|
||||
|
||||
@@ -67,7 +67,7 @@ final class XMLUtil {
|
||||
*/
|
||||
public static function applyXMLProperty(object:Dynamic, property:Access):ErrorCode {
|
||||
if (!property.has.name || !property.has.type || !property.has.value) {
|
||||
Logs.trace('Failed to apply XML property: XML Element is missing name, type, or value attributes.', WARNING);
|
||||
Logs.warn('Failed to apply XML property: XML Element is missing name, type, or value attributes.');
|
||||
return MISSING_PROPERTY;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ final class XMLUtil {
|
||||
if(isPath) {
|
||||
str += ' (Path: ${property.att.name})';
|
||||
}
|
||||
Logs.trace(str, WARNING);
|
||||
Logs.warn(str);
|
||||
return REFLECT_ERROR;
|
||||
}
|
||||
return OK;
|
||||
@@ -114,12 +114,13 @@ final class XMLUtil {
|
||||
* @param parentFolder The parent folder
|
||||
* @param defaultAnimType The default animation type
|
||||
*/
|
||||
public static function loadSpriteFromXML(spr:FunkinSprite, node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT):FunkinSprite {
|
||||
public static function loadSpriteFromXML(spr:FunkinSprite, node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT, loadGraphic:Bool = true):FunkinSprite {
|
||||
if (parentFolder == null) parentFolder = "";
|
||||
|
||||
spr.name = node.getAtt("name");
|
||||
spr.antialiasing = true;
|
||||
spr.loadSprite(Paths.image('$parentFolder${node.getAtt("sprite").getDefault(spr.name)}', null, true));
|
||||
if (loadGraphic)
|
||||
spr.loadSprite(Paths.image('$parentFolder${node.getAtt("sprite").getDefault(spr.name)}', null, true));
|
||||
|
||||
spr.spriteAnimType = defaultAnimType;
|
||||
if (node.has.type) {
|
||||
@@ -192,10 +193,10 @@ final class XMLUtil {
|
||||
if (node.has.updateHitbox && node.att.updateHitbox == "true") spr.updateHitbox();
|
||||
|
||||
if (node.has.zoomfactor)
|
||||
spr.zoomFactor = Std.parseFloat(node.getAtt("zoomfactor")).getDefault(spr.zoomFactor);
|
||||
spr.zoomFactor = Std.parseFloat(node.getAtt("zoomfactor")).getDefaultFloat(spr.zoomFactor);
|
||||
|
||||
if (node.has.alpha)
|
||||
spr.alpha = Std.parseFloat(node.getAtt("alpha")).getDefault(spr.alpha);
|
||||
spr.alpha = Std.parseFloat(node.getAtt("alpha")).getDefaultFloat(spr.alpha);
|
||||
|
||||
if(node.has.color)
|
||||
spr.color = FlxColor.fromString(node.getAtt("color")).getDefault(0xFFFFFFFF);
|
||||
@@ -239,10 +240,10 @@ final class XMLUtil {
|
||||
* @param cl The class to create (advanced)
|
||||
* @param args The arguments to pass to the class (advanced)
|
||||
*/
|
||||
public static inline function createSpriteFromXML(node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT, ?cl:Class<FunkinSprite>, ?args:Array<Dynamic>):FunkinSprite {
|
||||
public static inline function createSpriteFromXML(node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT, ?cl:Class<FunkinSprite>, ?args:Array<Dynamic>, loadGraphic:Bool = true):FunkinSprite {
|
||||
if(cl == null) cl = FunkinSprite;
|
||||
if(args == null) args = [];
|
||||
return loadSpriteFromXML(Type.createInstance(cl, args), node, parentFolder, defaultAnimType);
|
||||
return loadSpriteFromXML(Type.createInstance(cl, args), node, parentFolder, defaultAnimType, loadGraphic);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,9 +267,9 @@ final class XMLUtil {
|
||||
if (anim.has.name) animData.name = anim.att.name;
|
||||
if (anim.has.type) animData.animType = XMLAnimType.fromString(anim.att.type, animData.animType);
|
||||
if (anim.has.anim) animData.anim = anim.att.anim;
|
||||
if (anim.has.fps) animData.fps = Std.parseFloat(anim.att.fps).getDefault(animData.fps);
|
||||
if (anim.has.x) animData.x = Std.parseFloat(anim.att.x).getDefault(animData.x);
|
||||
if (anim.has.y) animData.y = Std.parseFloat(anim.att.y).getDefault(animData.y);
|
||||
if (anim.has.fps) animData.fps = Std.parseFloat(anim.att.fps).getDefaultFloat(animData.fps);
|
||||
if (anim.has.x) animData.x = Std.parseFloat(anim.att.x).getDefaultFloat(animData.x);
|
||||
if (anim.has.y) animData.y = Std.parseFloat(anim.att.y).getDefaultFloat(animData.y);
|
||||
if (anim.has.loop) animData.loop = anim.att.loop == "true";
|
||||
if (anim.has.forced) animData.forced = anim.att.forced == "true";
|
||||
if (anim.has.indices) animData.indices = CoolUtil.parseNumberRange(anim.att.indices);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
import haxe.zip.Entry;
|
||||
import haxe.zip.Reader;
|
||||
|
||||
#if sys
|
||||
#if (!macro && sys)
|
||||
import openfl.display.BitmapData;
|
||||
@@ -9,8 +12,6 @@ import haxe.Exception;
|
||||
import haxe.Json;
|
||||
import haxe.crypto.Crc32;
|
||||
import haxe.zip.Compress;
|
||||
import haxe.zip.Entry;
|
||||
import haxe.zip.Reader;
|
||||
import haxe.zip.Tools;
|
||||
import haxe.zip.Uncompress;
|
||||
import haxe.zip.Writer;
|
||||
@@ -135,12 +136,12 @@ final class ZipUtil {
|
||||
if (prog == null) prog = new ZipProgress();
|
||||
|
||||
try {
|
||||
var curPath:Array<String> = ['$path'];
|
||||
var curPath:Array<String> = [path];
|
||||
var destPath:Array<String> = [];
|
||||
if (prefix != "") {
|
||||
prefix = prefix.replace("\\", "/");
|
||||
while(prefix.charAt(0) == "/") prefix = prefix.substr(1);
|
||||
while(prefix.charAt(prefix.length-1) == "/") prefix = prefix.substr(0, prefix.length-1);
|
||||
while(prefix.charCodeAt(0) == "/".code) prefix = prefix.substr(1);
|
||||
while(prefix.charCodeAt(prefix.length-1) == "/".code) prefix = prefix.substr(0, prefix.length-1);
|
||||
destPath.push(prefix);
|
||||
}
|
||||
|
||||
@@ -160,7 +161,7 @@ final class ZipUtil {
|
||||
} else {
|
||||
// is file, put it in the list
|
||||
var zipPath = '$zipPath/$e';
|
||||
while(zipPath.charAt(0) == "/") zipPath = zipPath.substr(1);
|
||||
while(zipPath.charCodeAt(0) == "/".code) zipPath = zipPath.substr(1);
|
||||
files.push(new StrNameLabel('$path/$e', zipPath));
|
||||
}
|
||||
}
|
||||
@@ -227,20 +228,6 @@ class ZipProgress {
|
||||
public function new() {}
|
||||
}
|
||||
|
||||
class ZipReader extends Reader {
|
||||
public var files:List<Entry>;
|
||||
|
||||
public override function read() {
|
||||
if (files != null) return files;
|
||||
try {
|
||||
var files = super.read();
|
||||
return this.files = files;
|
||||
} catch(e) {
|
||||
}
|
||||
return new List<Entry>();
|
||||
}
|
||||
}
|
||||
|
||||
class ZipWriter extends Writer {
|
||||
public function flush() {
|
||||
o.flush();
|
||||
@@ -265,4 +252,18 @@ class StrNameLabel {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
#end
|
||||
#end
|
||||
|
||||
class ZipReader extends Reader {
|
||||
public var files:List<Entry>;
|
||||
|
||||
public override function read() {
|
||||
if (files != null) return files;
|
||||
try {
|
||||
var files = super.read();
|
||||
return this.files = files;
|
||||
} catch(e) {
|
||||
}
|
||||
return new List<Entry>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package funkin.backend.utils.translations;
|
||||
|
||||
/**
|
||||
* The class used to format strings based on parameters.
|
||||
*
|
||||
* For example if the parameter list is just an `Int` which is `9`, `You have been blue balled {0} times` becomes `You have been blue balled 9 times`.
|
||||
*/
|
||||
final class FormatUtil {
|
||||
private static var cache:Map<String, IFormatInfo> = new Map();
|
||||
private static var cacheStr:Map<String, IFormatInfo> = new Map();
|
||||
|
||||
public static function get(id:String):IFormatInfo {
|
||||
if (cache.exists(id))
|
||||
return cache.get(id);
|
||||
|
||||
var fi:IFormatInfo = ParamFormatInfo.returnOnlyIfValid(id);
|
||||
if(fi == null) fi = new StrFormatInfo(id);
|
||||
cache.set(id, fi);
|
||||
return fi;
|
||||
}
|
||||
|
||||
public static function getStr(id:String):IFormatInfo {
|
||||
if (cacheStr.exists(id))
|
||||
return cacheStr.get(id);
|
||||
|
||||
var fi:IFormatInfo = new StrFormatInfo(id);
|
||||
cacheStr.set(id, fi);
|
||||
return fi;
|
||||
}
|
||||
|
||||
public inline static function clear():Void {
|
||||
cache.clear();
|
||||
cacheStr.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class StrFormatInfo implements IFormatInfo {
|
||||
public var string:String;
|
||||
|
||||
public function new(str:String) {
|
||||
this.string = str;
|
||||
}
|
||||
|
||||
public function format(params:Array<Dynamic>):String {
|
||||
return string;
|
||||
}
|
||||
|
||||
public function toString():String {
|
||||
return "StrFormatInfo(" + string + ")";
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add support for @:({0}==1?(Hello):(World))
|
||||
class ParamFormatInfo implements IFormatInfo {
|
||||
public var strings:Array<String> = [];
|
||||
public var indexes:Array<Int> = [];
|
||||
|
||||
public function new(str:String) {
|
||||
var i = 0;
|
||||
|
||||
while (i < str.length) {
|
||||
var fi = str.indexOf("{", i); // search from the start of i
|
||||
|
||||
if (fi == -1) {
|
||||
// if there are no more parameters, just add the rest of the string
|
||||
this.strings.push(str.substring(i));
|
||||
break;
|
||||
}
|
||||
|
||||
var fe = str.indexOf("}", fi);
|
||||
|
||||
this.strings.push(str.substring(i, fi));
|
||||
this.indexes.push(Std.parseInt(str.substring(fi+1, fe)));
|
||||
i = fe + 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static function isValid(str:String):Bool {
|
||||
var fi = new ParamFormatInfo(str);
|
||||
return fi.indexes.length > 0;
|
||||
}
|
||||
public static function returnOnlyIfValid(str:String):IFormatInfo {
|
||||
var fi = new ParamFormatInfo(str);
|
||||
return fi.indexes.length > 0 ? fi : null;
|
||||
}
|
||||
|
||||
public function format(params:Array<Dynamic>):String {
|
||||
if (params == null) params = [];
|
||||
|
||||
var lenStr = strings.length;
|
||||
var lenInd = indexes.length;
|
||||
|
||||
var arr = new haxe.ds.Vector<String>(lenStr + ((lenStr < lenInd) ? lenStr : lenInd));
|
||||
var i = 0;
|
||||
for (idx=>s in strings) {
|
||||
arr[i++] = s;
|
||||
if (idx < lenInd)
|
||||
arr[i++] = Std.string(params[indexes[idx]]);
|
||||
}
|
||||
return arr.join("");
|
||||
}
|
||||
|
||||
public function toString():String {
|
||||
return 'ParamFormatInfo([${strings.join(", ")}] [${indexes.join(", ")}])';
|
||||
}
|
||||
}
|
||||
|
||||
interface IFormatInfo {
|
||||
public function format(params:Array<Dynamic>):String;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package funkin.backend.week;
|
||||
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.backend.week.WeekData.WeekCharacter;
|
||||
import funkin.backend.week.WeekData;
|
||||
import haxe.xml.Access;
|
||||
|
||||
class Week {
|
||||
public static function loadWeek(weekName:String, loadCharactersData:Bool = true):WeekData {
|
||||
var week:Access = null;
|
||||
try week = new Access(Xml.parse(Assets.getText(Paths.xml('weeks/weeks/$weekName'))).firstElement())
|
||||
catch(e) Logs.trace('Cannot parse week "$weekName.xml": ${Std.string(e)}', ERROR);
|
||||
|
||||
if (week == null) return null;
|
||||
if (!week.has.name) {
|
||||
Logs.trace('Week "${weekName}" has no name attribute', WARNING);
|
||||
return null;
|
||||
}
|
||||
|
||||
var weekObj:WeekData = {
|
||||
xml: week,
|
||||
name: week.att.name,
|
||||
id: weekName,
|
||||
sprite: week.getAtt('sprite').getDefault(weekName),
|
||||
chars: [null, null, null],
|
||||
songs: [],
|
||||
difficulties: ['easy', 'normal', 'hard'],
|
||||
bgColor: week.has.bgColor ? FlxColor.fromString(week.getAtt("bgColor")) : Flags.DEFAULT_WEEK_COLOR
|
||||
};
|
||||
|
||||
var weekName = weekObj.name;
|
||||
for (k => song in week.nodes.song) {
|
||||
if (song == null) continue;
|
||||
try {
|
||||
var name = song.innerData.trim();
|
||||
if (name == "") {
|
||||
Logs.trace('Song at index ${k} in week $weekName has no name. Skipping...', WARNING);
|
||||
continue;
|
||||
}
|
||||
weekObj.songs.push({
|
||||
name: name,
|
||||
hide: song.getAtt('hide').getDefault('false') == "true",
|
||||
displayName: song.getAtt('displayName')
|
||||
});
|
||||
} catch(e) {
|
||||
Logs.trace('Song at index ${k} in week $weekName cannot contain any other XML nodes in its name.', WARNING);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (weekObj.songs.length <= 0) {
|
||||
Logs.trace('Week $weekName has no songs.', WARNING);
|
||||
return null;
|
||||
}
|
||||
|
||||
var diffNodes = week.nodes.difficulty;
|
||||
if (diffNodes.length > 0) {
|
||||
var diffs:Array<String> = [for (e in diffNodes) if (e.has.name) e.att.name];
|
||||
if (diffs.length > 0) weekObj.difficulties = diffs;
|
||||
}
|
||||
|
||||
if (week.has.chars) for (k => e in week.att.chars.split(",")) {
|
||||
var trim = e.trim();
|
||||
weekObj.chars[k] = (trim == "" || e == "none" || e == "null") ? null : (loadCharactersData ? loadWeekCharacter(trim) : {name: trim});
|
||||
}
|
||||
|
||||
return weekObj;
|
||||
}
|
||||
|
||||
public static function loadWeekCharacter(charName:String):WeekCharacter {
|
||||
var char:Access = null;
|
||||
try char = new Access(Xml.parse(Assets.getText(Paths.xml('weeks/characters/$charName'))).firstElement())
|
||||
catch(e) Logs.trace('Cannot parse character "$charName.xml": ${Std.string(e)}', ERROR);
|
||||
if (char == null) return null;
|
||||
|
||||
if (!char.has.name) char.x.set("name", charName);
|
||||
if (!char.has.sprite) char.x.set("sprite", 'menus/storymenu/characters/${char.att.name}');
|
||||
if (!char.has.updateHitbox) char.x.set("updateHitbox", "true");
|
||||
|
||||
return {
|
||||
xml: char,
|
||||
name: char.att.name,
|
||||
spritePath: char.att.sprite,
|
||||
scale: Std.parseFloat(char.getAtt('scale')).getDefault(1),
|
||||
offset: FlxPoint.get(
|
||||
Std.parseFloat(char.getAtt('x')).getDefault(0),
|
||||
Std.parseFloat(char.getAtt('y')).getDefault(0)
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package funkin.backend.week;
|
||||
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.util.FlxColor;
|
||||
import haxe.xml.Access;
|
||||
|
||||
typedef WeekData = {
|
||||
var ?xml:Access;
|
||||
var name:String; // name SHOULD NOT be used for loading week highscores, its just the name on the right side of the week, remember that next time!! - Nex
|
||||
var id:String; // id IS instead for saving and loading!! - Nex
|
||||
var sprite:String;
|
||||
var chars:Array<WeekCharacter>;
|
||||
var songs:Array<WeekSong>;
|
||||
var difficulties:Array<String>;
|
||||
var bgColor:FlxColor;
|
||||
}
|
||||
|
||||
typedef WeekCharacter = {
|
||||
var ?xml:Access;
|
||||
var name:String;
|
||||
var ?spritePath:String;
|
||||
var ?scale:Float;
|
||||
var ?offset:FlxPoint;
|
||||
// var frames:FlxFramesCollection;
|
||||
}
|
||||
|
||||
typedef WeekSong = {
|
||||
var name:String;
|
||||
var hide:Bool;
|
||||
var ?displayName:String;
|
||||
}
|
||||
@@ -26,50 +26,55 @@ class DebugOptions extends TreeMenu {
|
||||
|
||||
class DebugOptionsScreen extends OptionsScreen {
|
||||
public override function new() {
|
||||
super("Debug Options", "Use this menu to change debug options.");
|
||||
prefix = "DebugOptions.";
|
||||
super(translate("title"), translate("desc"), prefix);
|
||||
#if windows
|
||||
add(new TextOption(
|
||||
"Show Console",
|
||||
"Select this to show the debug console, which contains log information about the game.",
|
||||
getName("showConsole"),
|
||||
getDesc("showConsole"),
|
||||
function() {
|
||||
NativeAPI.allocConsole();
|
||||
}));
|
||||
#end
|
||||
add(new Checkbox(
|
||||
"Resizable Editors",
|
||||
"If checked, this will allow the editors to render beyond the base 1280x720 resolution of FNF (allowing for more detail and space...)",
|
||||
getName("editorsResizable"),
|
||||
getDesc("editorsResizable"),
|
||||
"editorsResizable"));
|
||||
add(new Checkbox(
|
||||
"Editor SFXs",
|
||||
"If checked, will play sound effects when working on editors (ex: will play SFXs when checking checkboxes...)",
|
||||
getName("bypassEditorsResize"),
|
||||
getDesc("bypassEditorsResize"),
|
||||
"bypassEditorsResize"));
|
||||
add(new Checkbox(
|
||||
getName("editorSFX"),
|
||||
getDesc("editorSFX"),
|
||||
"editorSFX"));
|
||||
add(new Checkbox(
|
||||
"Pretty Print",
|
||||
"If checked, the saved files from the editor will be formatted to be easily viewable (does not apply to XMLs...)",
|
||||
getName("editorPrettyPrint"),
|
||||
getDesc("editorPrettyPrint"),
|
||||
"editorPrettyPrint"));
|
||||
add(new Checkbox(
|
||||
"Intensive Blur",
|
||||
"If checked, will use more intensive blur that may be laggier but look better.",
|
||||
getName("intensiveBlur"),
|
||||
getDesc("intensiveBlur"),
|
||||
"intensiveBlur"));
|
||||
add(new Checkbox(
|
||||
"Editor Autosaves",
|
||||
"If checked, this will autosave your files in the editor, with the settings listed below.",
|
||||
getName("charterAutoSaves"),
|
||||
getDesc("charterAutoSaves"),
|
||||
"charterAutoSaves"));
|
||||
add(new NumOption(
|
||||
"Autosaving Time",
|
||||
"This controls how often the editor will autosave your file (in seconds...)",
|
||||
getName("charterAutoSaveTime"),
|
||||
getDesc("charterAutoSaveTime"),
|
||||
60, 60*10, 30,
|
||||
"charterAutoSaveTime"
|
||||
));
|
||||
add(new NumOption(
|
||||
"Save Warning Time",
|
||||
"This controls how long the editor will warn you before it autosaves (in seconds..., 0 to disable)",
|
||||
getName("charterAutoSaveWarningTime"),
|
||||
getDesc("charterAutoSaveWarningTime"),
|
||||
0, 15, 1,
|
||||
"charterAutoSaveWarningTime"
|
||||
));
|
||||
add(new Checkbox(
|
||||
"Autosaves Folder",
|
||||
"If checked, this will autosave your file in a separate folder with a time stamp instead of overriding your current file. (song/autosaves/)",
|
||||
getName("charterAutoSavesSeparateFolder"),
|
||||
getDesc("charterAutoSavesSeparateFolder"),
|
||||
"charterAutoSavesSeparateFolder"));
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,13 @@ import flixel.math.FlxPoint;
|
||||
class EditorPicker extends MusicBeatSubstate {
|
||||
public var bg:FlxSprite;
|
||||
|
||||
// Name is for backwards compatibility, don't use it, use id instead
|
||||
public var options:Array<Editor> = [
|
||||
{
|
||||
name: "Alphabet Editor",
|
||||
id: "alphabet",
|
||||
state: funkin.editors.alphabet.AlphabetSelection
|
||||
},
|
||||
{
|
||||
name: "Chart Editor",
|
||||
id: "chart",
|
||||
@@ -22,7 +28,7 @@ class EditorPicker extends MusicBeatSubstate {
|
||||
id: "stage",
|
||||
state: funkin.editors.stage.StageSelection
|
||||
},
|
||||
#if debug
|
||||
#if (debug || debug_ui)
|
||||
{
|
||||
name: "UI Debug State",
|
||||
id: "uiDebug",
|
||||
@@ -74,7 +80,8 @@ class EditorPicker extends MusicBeatSubstate {
|
||||
|
||||
optionHeight = FlxG.height / options.length;
|
||||
for(k=>o in options) {
|
||||
var spr = new EditorPickerOption(o.name, o.id, optionHeight);
|
||||
var visualName = (o.id != null) ? TU.translate("editor." + o.id + ".name") : o.name;
|
||||
var spr = new EditorPickerOption(visualName, o.id, optionHeight);
|
||||
spr.y = k * optionHeight;
|
||||
add(spr);
|
||||
sprites.push(spr);
|
||||
@@ -176,13 +183,16 @@ class EditorPickerOption extends FlxTypedSpriteGroup<FlxSprite> {
|
||||
|
||||
FlxG.mouse.visible = true;
|
||||
iconSpr = new FlxSprite();
|
||||
iconSpr.loadGraphic(Paths.image('editors/icons/$iconID'));
|
||||
if(iconID != null)
|
||||
iconSpr.loadGraphic(Paths.image('editors/icons/$iconID'));
|
||||
else
|
||||
iconSpr.exists = false;
|
||||
iconSpr.antialiasing = true;
|
||||
iconSpr.setUnstretchedGraphicSize(110, 110, false);
|
||||
iconSpr.x = 25 + ((height - iconSpr.width) / 2);
|
||||
iconSpr.y = (height - iconSpr.height) / 2;
|
||||
|
||||
label = new Alphabet(25 + iconSpr.width + 25, 0, name, true);
|
||||
label = new Alphabet(25 + iconSpr.width + 25, 0, name, "bold");
|
||||
label.y = (height - label.height) / 2;
|
||||
|
||||
selectionBG = new FlxSprite().makeGraphic(1, 1, -1);
|
||||
|
||||
@@ -7,6 +7,7 @@ import funkin.options.*;
|
||||
class EditorTreeMenu extends TreeMenu {
|
||||
public var bg:FlxBackdrop;
|
||||
public var bgType:String = "default";
|
||||
public var bgMovement:FlxPoint = new FlxPoint();
|
||||
|
||||
public override function create() {
|
||||
super.create();
|
||||
@@ -18,16 +19,16 @@ class EditorTreeMenu extends TreeMenu {
|
||||
bg = new FlxBackdrop();
|
||||
bg.loadGraphic(Paths.image('editors/bgs/${bgType}'));
|
||||
bg.antialiasing = true;
|
||||
setBackgroundRotation(-5);
|
||||
add(bg);
|
||||
}
|
||||
|
||||
public inline function setBackgroundRotation(rotation:Float) {
|
||||
bg.rotation = rotation;
|
||||
bg.velocity.set(85, 0).degrees = bg.rotation;
|
||||
}
|
||||
|
||||
public override function exit() {
|
||||
FlxG.switchState(new MainMenuState());
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float) {
|
||||
super.update(elapsed);
|
||||
bg.x += elapsed * 125;
|
||||
bg.y += elapsed * 125;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package funkin.editors;
|
||||
|
||||
import funkin.backend.assets.ModsFolderLibrary;
|
||||
import flixel.FlxState;
|
||||
|
||||
class ModConfigWarning extends UIState {
|
||||
public static var hadPopup:Bool = false;
|
||||
|
||||
var library:ModsFolderLibrary = null;
|
||||
var goToState:Class<FlxState>;
|
||||
|
||||
public static inline var defaultModConfigText =
|
||||
'[Common]
|
||||
MOD_NAME="YOUR MOD NAME HERE"
|
||||
MOD_DESCRIPTION="YOUR MOD DESCRIPTION HERE"
|
||||
MOD_AUTHOR="YOU/YOUR TEAM HERE"
|
||||
MOD_VERSION="YOUR MOD\'S VERSION HERE"
|
||||
|
||||
# DO NOT EDIT!! this is used to check for version compatibility!
|
||||
MOD_API_VERSION=1
|
||||
|
||||
MOD_DOWNLOAD_LINK="YOUR MOD PAGE LINK HERE"
|
||||
|
||||
# Not supported yet
|
||||
;MOD_ICON64="path/to/icon64.png"
|
||||
;MOD_ICON32="path/to/icon32.png"
|
||||
;MOD_ICON16="path/to/icon16.png"
|
||||
MOD_ICON="path/to/icon.png"
|
||||
|
||||
[Flags]
|
||||
DISABLE_WARNING_SCREEN=true
|
||||
# Set this to false if you want to bring back the warning state (prior to 1.0.0)
|
||||
# NOTE: Beta warning state has been renamed from BetaWarningState.hx to WarningState.hx
|
||||
|
||||
[Discord]
|
||||
MOD_DISCORD_CLIENT_ID=""
|
||||
MOD_DISCORD_LOGO_KEY=""
|
||||
MOD_DISCORD_LOGO_TEXT=""';
|
||||
|
||||
public function new(library:ModsFolderLibrary, ?goToState:Class<FlxState>) {
|
||||
super();
|
||||
this.library = library;
|
||||
this.goToState = goToState != null ? goToState : funkin.menus.TitleState;
|
||||
}
|
||||
|
||||
override function createPost() {
|
||||
super.createPost();
|
||||
hadPopup = true;
|
||||
|
||||
var substate = new UIWarningSubstate("Missing mod folder configuration!", "Your mod is currently missing a mod config file!\n\nWould you like to automatically generate one?\n\n(PS: If this is not your mod please disable Developer mode to stop this popup from appearing.)", [
|
||||
{
|
||||
label: "Not Now",
|
||||
color: 0x969533,
|
||||
onClick: function (_) {
|
||||
MusicBeatState.skipTransOut = MusicBeatState.skipTransIn = false;
|
||||
FlxG.switchState(cast Type.createInstance(goToState, []));
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Yes",
|
||||
onClick: function(_) {
|
||||
var path = '${library.folderPath}/data/config/modpack.ini';
|
||||
CoolUtil.safeSaveFile(path, defaultModConfigText);
|
||||
openSubState(new UIWarningSubstate("Created mod config!", "Your mod config file has been created at " + path + "!", [
|
||||
{
|
||||
label: "Ok",
|
||||
onClick: function (_) {
|
||||
MusicBeatState.skipTransOut = MusicBeatState.skipTransIn = false;
|
||||
FlxG.switchState(cast Type.createInstance(goToState, []));
|
||||
}
|
||||
},
|
||||
], false));
|
||||
}
|
||||
}
|
||||
], false);
|
||||
openSubState(substate);
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,15 @@ class SaveWarning {
|
||||
if (warningFunc != null) warningFunc(closingWindow);
|
||||
|
||||
if (FlxG.state != null && FlxG.state is UIState) {
|
||||
FlxG.state.openSubState(new UIWarningSubstate("Unsaved Changes!", "Your changes will be lost if you don't save them. (Can't be recovered)\n\n\nWould you like to Cancel?",
|
||||
FlxG.state.openSubState(new UIWarningSubstate(TU.translate("saveWarning.warningTitle"), TU.translate("saveWarning.warningDesc"),
|
||||
[
|
||||
{
|
||||
label: "Cancel",
|
||||
label: TU.translate("saveWarning.cancel"),
|
||||
color: 0x969533,
|
||||
onClick: function (_) {if (closingWindow) WindowUtils.resetClosing();}
|
||||
},
|
||||
{
|
||||
label: closingWindow ? "Exit Game" : "Exit To Menu",
|
||||
label: closingWindow ? TU.translate("saveWarning.exitGame") : TU.translate("saveWarning.exitToMenu"),
|
||||
color: 0x969533,
|
||||
onClick: function(_) {
|
||||
if (!closingWindow) {
|
||||
@@ -37,12 +37,12 @@ class SaveWarning {
|
||||
if (closingWindow) WindowUtils.resetClosing();
|
||||
} else {
|
||||
WindowUtils.preventClosing = false; WindowUtils.resetClosing();
|
||||
Sys.exit(0);
|
||||
openfl.system.System.exit(0);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: closingWindow ? "Save & Exit Game" : "Save & Exit To Menu",
|
||||
label: closingWindow ? TU.translate("saveWarning.saveAndExitGame") : TU.translate("saveWarning.saveAndExitToMenu"),
|
||||
color: 0x969533,
|
||||
onClick: function(_) {
|
||||
if (saveFunc != null) saveFunc();
|
||||
@@ -51,7 +51,7 @@ class SaveWarning {
|
||||
if (closingWindow) WindowUtils.resetClosing();
|
||||
} else {
|
||||
WindowUtils.preventClosing = false; WindowUtils.resetClosing();
|
||||
Sys.exit(0);
|
||||
openfl.system.System.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
package funkin.editors.alphabet;
|
||||
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.backend.system.framerate.Framerate;
|
||||
import funkin.backend.utils.XMLUtil.AnimData;
|
||||
import funkin.editors.ui.UIContextMenu.UIContextMenuOption;
|
||||
import funkin.game.Character;
|
||||
import haxe.xml.Access;
|
||||
import haxe.xml.Printer;
|
||||
|
||||
@:access(funkin.menus.ui.Alphabet)
|
||||
class AlphabetEditor extends UIState {
|
||||
static var __typeface:String;
|
||||
|
||||
public static var instance(get, null):AlphabetEditor;
|
||||
|
||||
private static inline function get_instance()
|
||||
return FlxG.state is AlphabetEditor ? cast FlxG.state : null;
|
||||
|
||||
public var topMenu:Array<UIContextMenuOption>;
|
||||
public var topMenuSpr:UITopMenu;
|
||||
|
||||
public var uiGroup:FlxTypedGroup<FlxSprite> = new FlxTypedGroup<FlxSprite>();
|
||||
|
||||
var editorCamera:FlxCamera;
|
||||
var uiCamera:FlxCamera;
|
||||
|
||||
public function new(typeface:String) {
|
||||
super();
|
||||
if (typeface != null) __typeface = typeface;
|
||||
}
|
||||
|
||||
inline function translate(id:String, ?args:Array<Dynamic>)
|
||||
return TU.translate("editor.alphabet." + id, args);
|
||||
|
||||
public var brokenWarning:UIText;
|
||||
public var tape:Alphabet;
|
||||
public var bigLetter:Alphabet;
|
||||
public var curLetter:Int = 0;
|
||||
public var targetX:Float = 0;
|
||||
|
||||
public var queueReorder:Bool = false;
|
||||
public var componentList:UIButtonList<ComponentButton>;
|
||||
|
||||
public var infoWindow:GlyphInfoWindow;
|
||||
public var curSelectedComponent:AlphabetComponent = null;
|
||||
public var curSelectedData:AlphabetLetterData = null;
|
||||
public var outlineIdx:Int = -1;
|
||||
|
||||
public var defaultTmr:Float = 0.0;
|
||||
public var charsForDefault:Array<Array<String>> = [];
|
||||
|
||||
public override function create() {
|
||||
super.create();
|
||||
|
||||
WindowUtils.suffix = " (" + translate("name") + ")";
|
||||
SaveWarning.selectionClass = AlphabetSelection;
|
||||
SaveWarning.saveFunc = () -> {_file_save(null);};
|
||||
|
||||
topMenu = [
|
||||
{
|
||||
label: translate("topBar.file"),
|
||||
childs: [
|
||||
{
|
||||
label: translate("file.save"),
|
||||
keybind: [CONTROL, S],
|
||||
onSelect: _file_save
|
||||
},
|
||||
{
|
||||
label: translate("file.saveAs"),
|
||||
keybind: [CONTROL, SHIFT, S],
|
||||
onSelect: _file_saveas
|
||||
},
|
||||
null,
|
||||
{
|
||||
label: translate("file.exit"),
|
||||
onSelect: _file_exit
|
||||
}
|
||||
]
|
||||
// TODO: add more options
|
||||
},
|
||||
{
|
||||
label: translate("topBar.edit"),
|
||||
childs: [
|
||||
{
|
||||
label: translate("edit.undo"),
|
||||
// TODO: add undo
|
||||
},
|
||||
{
|
||||
label: translate("edit.redo"),
|
||||
// TODO: add redo
|
||||
},
|
||||
{
|
||||
label: "Edit Main Data", // TODO: add translations
|
||||
onSelect: _edit_main
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: translate("topBar.glyph"),
|
||||
childs: [
|
||||
{
|
||||
label: translate("glyph.newGlyph"),
|
||||
//onSelect: _glyph_new
|
||||
},
|
||||
{
|
||||
label: translate("glyph.editGlyph"),
|
||||
//onSelect: _glyph_edit
|
||||
},
|
||||
{
|
||||
label: translate("glyph.deleteGlyph"),
|
||||
//onSelect: _glyph_delete
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: translate("topBar.view"),
|
||||
childs: [
|
||||
{
|
||||
label: translate("view.zoomIn"),
|
||||
keybind: [CONTROL, NUMPADPLUS],
|
||||
//onSelect: _view_zoomin
|
||||
},
|
||||
{
|
||||
label: translate("view.zoomOut"),
|
||||
keybind: [CONTROL, NUMPADMINUS],
|
||||
//onSelect: _view_zoomout
|
||||
},
|
||||
{
|
||||
label: translate("view.resetZoom"),
|
||||
keybind: [CONTROL, NUMPADZERO],
|
||||
//onSelect: _view_zoomreset
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Tape",
|
||||
childs: [
|
||||
{
|
||||
label: "Move Tape Left",
|
||||
keybind: [LEFT],
|
||||
onSelect: _tape_left
|
||||
},
|
||||
{
|
||||
label: "Move Tape Right",
|
||||
keybind: [RIGHT],
|
||||
onSelect: _tape_right
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
editorCamera = FlxG.camera;
|
||||
|
||||
uiCamera = new FlxCamera();
|
||||
uiCamera.bgColor = 0;
|
||||
|
||||
FlxG.cameras.add(uiCamera, false);
|
||||
|
||||
var bg = new FlxSprite(0, 0).makeSolid(Std.int(FlxG.width + 100), Std.int(FlxG.height + 100), 0xFF7f7f7f);
|
||||
bg.cameras = [editorCamera];
|
||||
bg.scrollFactor.set();
|
||||
bg.screenCenter();
|
||||
add(bg);
|
||||
|
||||
topMenuSpr = new UITopMenu(topMenu);
|
||||
topMenuSpr.cameras = uiGroup.cameras = [uiCamera];
|
||||
|
||||
tape = new Alphabet(0, 70, "", __typeface);
|
||||
tape.alignment = CENTER;
|
||||
tape.renderMode = MONOSPACE;
|
||||
add(tape);
|
||||
|
||||
bigLetter = new Alphabet(0, 0, "", "<SKIP>");
|
||||
bigLetter.copyData(tape);
|
||||
bigLetter.alignment = CENTER;
|
||||
bigLetter.scale.set(4, 4);
|
||||
// TODO: fix the offset issues, when not using updateHitbox
|
||||
bigLetter.updateHitbox();
|
||||
bigLetter.screenCenter();
|
||||
add(bigLetter);
|
||||
|
||||
tape.x = targetX;
|
||||
|
||||
brokenWarning = new UIText(0, 550, FlxG.width, "", 32);
|
||||
brokenWarning.alignment = CENTER;
|
||||
brokenWarning.color = 0xFFFF6969;
|
||||
add(brokenWarning);
|
||||
|
||||
infoWindow = new GlyphInfoWindow();
|
||||
uiGroup.add(infoWindow);
|
||||
|
||||
componentList = new UIButtonList<ComponentButton>(0, 720 - 170 - 30, 230, 170, "Components:", FlxPoint.get(230, 50), FlxPoint.get(0, 0), 0);
|
||||
componentList.dragCallback = (button, oldID, newID) -> {
|
||||
queueReorder = true; // not do it for every button reordered
|
||||
}
|
||||
componentList.addButton.callback = () -> {
|
||||
curSelectedComponent = {
|
||||
anim: "",
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
||||
shouldRotate: false,
|
||||
angle: 0,
|
||||
sin: 0,
|
||||
cos: 1,
|
||||
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
|
||||
hasColorMode: false,
|
||||
colorMode: bigLetter.colorMode
|
||||
};
|
||||
curSelectedData.components.push(curSelectedComponent);
|
||||
var newButton = new ComponentButton(curSelectedComponent);
|
||||
componentList.add(newButton);
|
||||
newButton.ID = componentList.buttons.members.length - 1; // readjust it because UIButtonList doesnt generate the id properly
|
||||
findOutline();
|
||||
infoWindow.updateInfo();
|
||||
}
|
||||
|
||||
updateTape();
|
||||
uiGroup.add(componentList);
|
||||
|
||||
add(topMenuSpr);
|
||||
add(uiGroup);
|
||||
|
||||
if(Framerate.isLoaded) {
|
||||
Framerate.fpsCounter.alpha = 0.4;
|
||||
Framerate.memoryCounter.alpha = 0.4;
|
||||
Framerate.codenameBuildField.alpha = 0.4;
|
||||
}
|
||||
|
||||
DiscordUtil.call("onEditorLoaded", ["Alphabet Editor", __typeface]);
|
||||
}
|
||||
|
||||
override function destroy() {
|
||||
super.destroy();
|
||||
if(Framerate.isLoaded) {
|
||||
Framerate.fpsCounter.alpha = 1;
|
||||
Framerate.memoryCounter.alpha = 1;
|
||||
Framerate.codenameBuildField.alpha = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float) {
|
||||
super.update(elapsed);
|
||||
if (queueReorder) {
|
||||
queueReorder = false;
|
||||
var outlines = [];
|
||||
var newList = [];
|
||||
for (button in componentList.buttons.members) {
|
||||
var com = button.component;
|
||||
if (com.outIndex != null) {
|
||||
var out = curSelectedData.components[com.outIndex];
|
||||
out.refIndex = newList.length;
|
||||
com.outIndex = outlines.length;
|
||||
outlines.push(out);
|
||||
}
|
||||
newList.push(com);
|
||||
}
|
||||
curSelectedData.components = outlines.concat(newList);
|
||||
|
||||
findOutline();
|
||||
for (i in 0...curSelectedData.components.length) {
|
||||
var anim = bigLetter.text + i;
|
||||
bigLetter.animation.remove(anim);
|
||||
tape.animation.remove(anim);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentFocus == null) {
|
||||
if(FlxG.keys.justPressed.ANY)
|
||||
UIUtil.processShortcuts(topMenu);
|
||||
|
||||
if(curSelectedComponent != null) {
|
||||
if(FlxG.keys.pressed.K) {
|
||||
curSelectedComponent.y -= 100 * elapsed;
|
||||
}
|
||||
if(FlxG.keys.pressed.I) {
|
||||
curSelectedComponent.y += 100 * elapsed;
|
||||
}
|
||||
if(FlxG.keys.pressed.L) {
|
||||
curSelectedComponent.x -= 100 * elapsed;
|
||||
}
|
||||
if(FlxG.keys.pressed.J) {
|
||||
curSelectedComponent.x += 100 * elapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var lastIdx = Math.floor(defaultTmr);
|
||||
defaultTmr += elapsed * 0.5;
|
||||
if (lastIdx != (lastIdx = Math.floor(defaultTmr))) {
|
||||
tape.text = "";
|
||||
for (def in charsForDefault)
|
||||
tape.text += def[Std.int(lastIdx % def.length)] + " ";
|
||||
tape.text += tape.manualLetters.join(" ");
|
||||
}
|
||||
tape.x = lerp(tape.x, targetX, 0.25);
|
||||
}
|
||||
|
||||
function updateTape() {
|
||||
charsForDefault = [[], [], []];
|
||||
|
||||
for (i in 33...127) {
|
||||
var letter = String.fromCharCode(i);
|
||||
if (tape.manualLetters.contains(letter)) continue;
|
||||
|
||||
var data = tape.fastGetData(letter);
|
||||
for (i => com in data.components) {
|
||||
if (tape.fastGetLetterAnim(letter, data, com, i) == null)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i >= 'A'.code && i <= 'Z'.code && tape.defaults[CaseMode.UPPER] != null)
|
||||
charsForDefault[CaseMode.UPPER].push(letter);
|
||||
else if (i >= 'a'.code && i <= 'z'.code && tape.defaults[CaseMode.LOWER] != null)
|
||||
charsForDefault[CaseMode.LOWER].push(letter);
|
||||
else if (tape.defaults[CaseMode.NONE] != null)
|
||||
charsForDefault[CaseMode.NONE].push(letter);
|
||||
}
|
||||
|
||||
var i = charsForDefault.length - 1;
|
||||
while (i >= 0) {
|
||||
if (charsForDefault[i].length <= 0)
|
||||
charsForDefault.splice(i, 1);
|
||||
--i;
|
||||
}
|
||||
|
||||
tape.text = "";
|
||||
for (def in charsForDefault)
|
||||
tape.text += def[Math.floor(defaultTmr % def.length)] + " ";
|
||||
tape.text += tape.manualLetters.join(" ");
|
||||
changeLetter(0);
|
||||
}
|
||||
|
||||
function changeLetter(inc:Int) {
|
||||
curLetter = CoolUtil.positiveModuloInt(curLetter + inc, tape.manualLetters.length + charsForDefault.length);
|
||||
targetX = FlxG.width * 0.5 - tape.defaultAdvance * (0.5 + curLetter * 2);
|
||||
bigLetter.text = (curLetter < charsForDefault.length) ? charsForDefault[curLetter][0] : tape.manualLetters[curLetter - charsForDefault.length];
|
||||
bigLetter.updateHitbox();
|
||||
bigLetter.screenCenter();
|
||||
|
||||
checkForFailed();
|
||||
|
||||
while (componentList.buttons.members.length > 0)
|
||||
componentList.remove(componentList.buttons.members[0]);
|
||||
for (i in curSelectedData.startIndex...curSelectedData.components.length) {
|
||||
var newButton = new ComponentButton(curSelectedData.components[i]);
|
||||
componentList.add(newButton);
|
||||
newButton.ID = componentList.buttons.members.length - 1; // readjust it because UIButtonList doesnt generate the id properly
|
||||
}
|
||||
curSelectedComponent = (curSelectedData.components.length > 0) ? curSelectedData.components[curSelectedData.startIndex] : null;
|
||||
findOutline();
|
||||
infoWindow.updateInfo();
|
||||
}
|
||||
|
||||
public function findOutline() {
|
||||
outlineIdx = -1;
|
||||
for (i in curSelectedData.startIndex...curSelectedData.components.length) {
|
||||
var com = curSelectedData.components[i];
|
||||
if (com.outIndex != null)
|
||||
outlineIdx = com.outIndex;
|
||||
if (com == curSelectedComponent)
|
||||
break;
|
||||
}
|
||||
++outlineIdx;
|
||||
}
|
||||
|
||||
public function checkForFailed() {
|
||||
bigLetter.failedLetters.remove(bigLetter.text);
|
||||
curSelectedData = bigLetter.fastGetData(bigLetter.text);
|
||||
brokenWarning.text = "";
|
||||
bigLetter.draw();
|
||||
if (!bigLetter.failedLetters.contains(bigLetter.text))
|
||||
return false;
|
||||
|
||||
for (i in 0...curSelectedData.components.length) {
|
||||
var com = curSelectedData.components[i];
|
||||
if (!bigLetter.animation.exists(bigLetter.text + Std.string(i)))
|
||||
brokenWarning.text += " Unable to find: " + com.anim + "\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function _tape_left(_) {
|
||||
changeLetter(-1);
|
||||
}
|
||||
function _tape_right(_) {
|
||||
changeLetter(1);
|
||||
}
|
||||
|
||||
function _file_save(_) {
|
||||
#if sys
|
||||
CoolUtil.safeSaveFile(
|
||||
'${Paths.getAssetsRoot()}/data/alphabet/${__typeface}.xml',
|
||||
""//alphabet.buildXML()
|
||||
);
|
||||
#else
|
||||
_file_saveas(_);
|
||||
#end
|
||||
}
|
||||
|
||||
function _file_saveas(_) {
|
||||
openSubState(new SaveSubstate(""/*alphabet.buildXML()*/, {
|
||||
defaultSaveFile: '${__typeface}.xml'
|
||||
}));
|
||||
}
|
||||
|
||||
function _file_exit(_) {
|
||||
/*if (undos.unsaved) SaveWarning.triggerWarning();
|
||||
else */FlxG.switchState(new AlphabetSelection());
|
||||
}
|
||||
|
||||
function _edit_main(_) {
|
||||
FlxG.state.openSubState(new AlphabetMainDataScreen());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
/===============\
|
||||
| Components |
|
||||
|===============|
|
||||
|[ Component 1 ]|
|
||||
|[ Component 2 ]|
|
||||
|[ Component 3 ]|
|
||||
|[ Component 4 ]|
|
||||
|[ Component 5 ]|
|
||||
|[ Add component]|
|
||||
\===============/
|
||||
|
||||
*/
|
||||
|
||||
class ComponentButton extends UIButton {
|
||||
public var component:AlphabetComponent;
|
||||
|
||||
public var selected:Bool = false;
|
||||
|
||||
public var deleteButton:UIButton;
|
||||
public var deleteIcon:FlxSprite;
|
||||
|
||||
public function new(component:AlphabetComponent) {
|
||||
super(0, 0, component.anim, function() {
|
||||
AlphabetEditor.instance.curSelectedComponent = component;
|
||||
AlphabetEditor.instance.findOutline();
|
||||
AlphabetEditor.instance.infoWindow.updateInfo();
|
||||
}, 230, 50);
|
||||
this.component = component;
|
||||
|
||||
deleteButton = new UIButton(bWidth - 32 - 10, 0, "", function() {
|
||||
var data = AlphabetEditor.instance.curSelectedData;
|
||||
|
||||
var outlineDrop = (component.outIndex != null) ? 1 : 0;
|
||||
var componentIndex = data.components.indexOf(component);
|
||||
for (i in 0...data.components.length) {
|
||||
var nextCompon = data.components[i];
|
||||
var anim = AlphabetEditor.instance.bigLetter.text + i;
|
||||
AlphabetEditor.instance.bigLetter.animation.remove(anim);
|
||||
AlphabetEditor.instance.tape.animation.remove(anim);
|
||||
|
||||
if (i > componentIndex && nextCompon.outIndex != null) {
|
||||
--data.components[nextCompon.outIndex].refIndex;
|
||||
nextCompon.outIndex -= outlineDrop;
|
||||
}
|
||||
}
|
||||
|
||||
if (component.outIndex != null) {
|
||||
data.components.splice(component.outIndex, 1);
|
||||
--data.startIndex;
|
||||
}
|
||||
|
||||
data.components.remove(component);
|
||||
AlphabetEditor.instance.curSelectedComponent = (AlphabetEditor.instance.curSelectedComponent == component) ? null : AlphabetEditor.instance.curSelectedComponent;
|
||||
AlphabetEditor.instance.findOutline();
|
||||
AlphabetEditor.instance.infoWindow.updateInfo();
|
||||
|
||||
AlphabetEditor.instance.componentList.remove(this);
|
||||
}, 32);
|
||||
deleteButton.color = FlxColor.RED;
|
||||
deleteButton.autoAlpha = false;
|
||||
members.push(deleteButton);
|
||||
|
||||
deleteIcon = new FlxSprite(deleteButton.x + (15/2), deleteButton.y + 8).loadGraphic(Paths.image('editors/delete-button'));
|
||||
deleteIcon.antialiasing = false;
|
||||
members.push(deleteIcon);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float) {
|
||||
super.update(elapsed);
|
||||
|
||||
deleteButton.y = y + 10;
|
||||
deleteIcon.x = deleteButton.x + (15/2);
|
||||
deleteIcon.y = deleteButton.y + 8;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package funkin.editors.alphabet;
|
||||
|
||||
import flixel.math.FlxRect;
|
||||
import funkin.editors.alphabet.AlphabetEditor;
|
||||
import funkin.menus.ui.Alphabet;
|
||||
|
||||
using StringTools;
|
||||
|
||||
// TODO: add translations
|
||||
class AlphabetMainDataScreen extends UISubstateWindow {
|
||||
var bigLetter:Alphabet;
|
||||
var tape:Alphabet;
|
||||
|
||||
public var saveButton:UIButton;
|
||||
public var closeButton:UIButton;
|
||||
public var helpButton:UIButton;
|
||||
|
||||
public var fpsStepper:UINumericStepper;
|
||||
public var advanceStepper:UINumericStepper;
|
||||
public var lineGapStepper:UINumericStepper;
|
||||
|
||||
public var caseDropdown:UIDropDown;
|
||||
public var colorDropdown:UIDropDown;
|
||||
|
||||
public var regTextBox:UITextBox;
|
||||
public var upperTextBox:UITextBox;
|
||||
public var lowerTextBox:UITextBox;
|
||||
|
||||
public var targetPercent:Float = 0.0;
|
||||
public var helpBG:UISliceSprite;
|
||||
public var helpTxt:UIText;
|
||||
public var helpScroll:UIScrollBar;
|
||||
|
||||
public override function create() {
|
||||
bigLetter = AlphabetEditor.instance.bigLetter;
|
||||
tape = AlphabetEditor.instance.tape;
|
||||
|
||||
// temporary until translated
|
||||
var helpDesc =
|
||||
"FPS - Frames per second. You get it.
|
||||
Advance - Actually a backup advance value for missing characters and spaces.
|
||||
Line Gap - How many pixels to move down when a new line is created.
|
||||
|
||||
Force Casing - Whether to force the text casing to uppercase, lowercase, or neither.
|
||||
Color Mode - How to color the alphabet.
|
||||
TINT muliplies the channels, perfect for white letters.
|
||||
OFFSETS adds to the channels, perfect for black letters.
|
||||
NONE simply disables coloring.
|
||||
|
||||
Backup Prefixes - What prefix to use if a glyph is not predefined.
|
||||
Uppercase and Lowercase will default to regular if it is left blank.
|
||||
LETTER will be substituted with the provided glyph.
|
||||
UPPERLETTER will be substituted with the uppercase glyph.
|
||||
LOWERLETTER will be substituted with the lowercase glyph.
|
||||
UPPERLETTER and LOWERLETTTER can be used in any backup prefix.";
|
||||
|
||||
winTitle = "Alphabet Properties";
|
||||
winWidth = 420; winHeight = 520;
|
||||
|
||||
super.create();
|
||||
|
||||
function addLabelOn(ui:UISprite, text:String)
|
||||
add(new UIText(ui.x, ui.y - 24, 0, text));
|
||||
|
||||
var title:UIText;
|
||||
add(title = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Edit Main Alphabet Data", 28));
|
||||
|
||||
fpsStepper = new UINumericStepper(title.x, title.y + title.height + 38, AlphabetEditor.instance.bigLetter.fps, 1, 2, 0.01, null, 120);
|
||||
add(fpsStepper);
|
||||
addLabelOn(fpsStepper, "FPS");
|
||||
|
||||
advanceStepper = new UINumericStepper(fpsStepper.x + 140, fpsStepper.y, AlphabetEditor.instance.bigLetter.defaultAdvance, 1, 2, 0, null, 120);
|
||||
add(advanceStepper);
|
||||
addLabelOn(advanceStepper, "Advance");
|
||||
|
||||
lineGapStepper = new UINumericStepper(advanceStepper.x + 140, advanceStepper.y, AlphabetEditor.instance.bigLetter.lineGap, 1, 2, 0, null, 120);
|
||||
add(lineGapStepper);
|
||||
addLabelOn(lineGapStepper, "Line Gap");
|
||||
|
||||
caseDropdown = new UIDropDown(fpsStepper.x, fpsStepper.y + fpsStepper.bHeight + 50, 206, 32, ["NONE", "UPPER", "LOWER"], tape.forceCase);
|
||||
add(caseDropdown);
|
||||
addLabelOn(caseDropdown, "Force Casing");
|
||||
|
||||
colorDropdown = new UIDropDown(caseDropdown.x + caseDropdown.bWidth + 20, caseDropdown.y, 206, 32, ["TINT", "OFFSETS", "NONE"], tape.colorMode);
|
||||
add(colorDropdown);
|
||||
addLabelOn(colorDropdown, "Color Mode");
|
||||
|
||||
var regPrefix = (tape.defaults[0] != null) ? tape.defaults[0].components[0].anim : "";
|
||||
regTextBox = new UITextBox(caseDropdown.x, caseDropdown.y + caseDropdown.bHeight + 50, regPrefix, 368);
|
||||
add(regTextBox);
|
||||
addLabelOn(regTextBox, "Backup Prefix (Regular)");
|
||||
|
||||
var upperPrefix = (tape.defaults[1] != null) ? tape.defaults[1].components[0].anim : "";
|
||||
upperTextBox = new UITextBox(regTextBox.x, regTextBox.y + regTextBox.bHeight + 35, upperPrefix, 368);
|
||||
add(upperTextBox);
|
||||
addLabelOn(upperTextBox, "Backup Prefix (Uppercase)");
|
||||
|
||||
var lowerPrefix = (tape.defaults[2] != null) ? tape.defaults[2].components[0].anim : "";
|
||||
lowerTextBox = new UITextBox(upperTextBox.x, upperTextBox.y + upperTextBox.bHeight + 35, lowerPrefix, 368);
|
||||
add(lowerTextBox);
|
||||
addLabelOn(lowerTextBox, "Backup Prefix (Lowercase)");
|
||||
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() {
|
||||
saveInfo();
|
||||
close();
|
||||
}, 125);
|
||||
add(saveButton);
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 20, saveButton.y, TU.translate("editor.close"), function() {
|
||||
close();
|
||||
}, 125);
|
||||
add(closeButton);
|
||||
closeButton.color = 0xFFFF0000;
|
||||
closeButton.x -= closeButton.bWidth;
|
||||
|
||||
helpButton = new UIButton(closeButton.x - 20, closeButton.y, "?", function() {
|
||||
targetPercent = 1.0 - targetPercent;
|
||||
}, 32);
|
||||
add(helpButton);
|
||||
helpButton.x -= helpButton.bWidth;
|
||||
|
||||
helpBG = new UISliceSprite(windowSpr.x + windowSpr.bWidth - 20, windowSpr.y, 340, windowSpr.bHeight, 'editors/ui/context-bg');
|
||||
helpBG.alpha = 0.0;
|
||||
add(helpBG);
|
||||
|
||||
helpTxt = new UIText(helpBG.x + 20, helpBG.y + 20, helpBG.bWidth - 60, helpDesc);
|
||||
helpTxt.clipRect = new FlxRect(0, 0, helpTxt.width, helpBG.bHeight - 40);
|
||||
helpTxt.alpha = 0.0;
|
||||
add(helpTxt);
|
||||
|
||||
helpScroll = new UIScrollBar(helpBG.x + helpBG.bWidth - 30, helpBG.y + 20, (helpTxt.height - helpTxt.clipRect.height), 0, (helpTxt.clipRect.height / helpTxt.height), 20, helpBG.bHeight - 40);
|
||||
helpScroll.size *= helpScroll.height;
|
||||
helpScroll.start -= helpScroll.size * 0.5;
|
||||
helpScroll.alpha = 0.0;
|
||||
helpScroll.onChange = function(v) {
|
||||
helpTxt.clipRect.y = v;
|
||||
helpTxt.offset.y = v;
|
||||
helpTxt.clipRect = helpTxt.clipRect;
|
||||
helpScroll.start = v - helpScroll.size * 0.5;
|
||||
}
|
||||
add(helpScroll);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float) {
|
||||
super.update(elapsed);
|
||||
|
||||
helpBG.alpha = FlxMath.lerp(helpBG.alpha, targetPercent, elapsed * 15);
|
||||
helpBG.x = windowSpr.x + windowSpr.bWidth - 20 + 40 * helpBG.alpha;
|
||||
helpTxt.alpha = helpBG.alpha;
|
||||
helpTxt.x = helpBG.x + 20;
|
||||
helpScroll.alpha = helpScroll.thumb.alpha = helpScroll.thumbIcon.alpha = helpBG.alpha;
|
||||
helpScroll.x = helpBG.x + helpBG.bWidth - 30;
|
||||
}
|
||||
|
||||
public function saveInfo() {
|
||||
@:privateAccess fpsStepper.__onChange(fpsStepper.label.text);
|
||||
@:privateAccess advanceStepper.__onChange(advanceStepper.label.text);
|
||||
@:privateAccess lineGapStepper.__onChange(lineGapStepper.label.text);
|
||||
|
||||
bigLetter.fps = tape.fps = fpsStepper.value;
|
||||
bigLetter.defaultAdvance = tape.defaultAdvance = advanceStepper.value;
|
||||
bigLetter.lineGap = tape.lineGap = lineGapStepper.value;
|
||||
|
||||
bigLetter.forceCase = tape.forceCase = caseDropdown.index;
|
||||
bigLetter.colorMode = tape.colorMode = colorDropdown.index;
|
||||
|
||||
// give the letter loading another chance
|
||||
tape.animation.destroyAnimations();
|
||||
bigLetter.animation.destroyAnimations();
|
||||
tape.failedLetters.splice(1, tape.failedLetters.length - 1);
|
||||
|
||||
setDefault(0, regTextBox.label.text);
|
||||
setDefault(1, upperTextBox.label.text);
|
||||
setDefault(2, lowerTextBox.label.text);
|
||||
}
|
||||
|
||||
function setDefault(index:Int, name:String) {
|
||||
tape.loaded[index].splice(0, tape.loaded[index].length);
|
||||
if (name.trim() == "") {
|
||||
tape.defaults[index] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (tape.defaults[index] != null) {
|
||||
tape.defaults[index].components[0].anim = name;
|
||||
trace(tape.defaults[index].components[0].anim);
|
||||
return;
|
||||
}
|
||||
|
||||
final newData:AlphabetLetterData = {
|
||||
isDefault: true,
|
||||
advance: 0.0,
|
||||
advanceEmpty: true,
|
||||
components: [{
|
||||
anim: name,
|
||||
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
scaleX: 1.0,
|
||||
scaleY: 1.0,
|
||||
|
||||
shouldRotate: false,
|
||||
angle: 0.0,
|
||||
cos: 1.0,
|
||||
sin: 0.0,
|
||||
|
||||
hasColorMode: false,
|
||||
colorMode: 0
|
||||
}],
|
||||
startIndex: 0
|
||||
};
|
||||
tape.defaults[index] = newData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package funkin.editors.alphabet;
|
||||
|
||||
import haxe.xml.Access;
|
||||
import funkin.game.Character;
|
||||
import funkin.options.OptionsScreen;
|
||||
import funkin.options.type.IconOption;
|
||||
import funkin.options.type.NewOption;
|
||||
import funkin.options.type.TextOption;
|
||||
import funkin.options.type.OptionType;
|
||||
import haxe.io.Path;
|
||||
|
||||
class AlphabetSelection extends EditorTreeMenu
|
||||
{
|
||||
inline function translate(id:String, ?args:Array<Dynamic>)
|
||||
return TU.translate("editor.alphabet.selection." + id, args);
|
||||
|
||||
public override function create()
|
||||
{
|
||||
bgType = "charter";
|
||||
super.create();
|
||||
|
||||
var modsList:Array<String> = [];
|
||||
for(file in Paths.getFolderContent('data/alphabet/', true, BOTH)) // mods ? MODS : BOTH
|
||||
if (Path.extension(file) == "xml") modsList.push(CoolUtil.getFilename(file));
|
||||
|
||||
var list:Array<OptionType> = [
|
||||
for (typeface in modsList)
|
||||
new AlphabetIconOption(typeface, translate("acceptTypeface"), typeface, function() {
|
||||
FlxG.switchState(new AlphabetEditor(typeface));
|
||||
})
|
||||
];
|
||||
|
||||
var newChar = translate("newTypeface");
|
||||
list.insert(0, new NewOption(newChar, newChar, function() {
|
||||
openSubState(new UIWarningSubstate(translate("warnings.notImplemented-title"), translate("warnings.notImplemented-body"), [
|
||||
{label: TU.translate("editor.ok"), color: 0xFFFF0000, onClick: function(t) {}}
|
||||
]));
|
||||
}));
|
||||
|
||||
main = new OptionsScreen(TU.translate("editor.alphabet.name"), translate("desc"), list);
|
||||
|
||||
DiscordUtil.call("onEditorTreeLoaded", ["Alphabet Editor"]);
|
||||
}
|
||||
|
||||
override function createPost() {
|
||||
super.createPost();
|
||||
|
||||
main.changeSelection(1);
|
||||
}
|
||||
}
|
||||
|
||||
class AlphabetIconOption extends TextOption {
|
||||
public var iconSpr:FlxSprite;
|
||||
|
||||
public function new(name:String, desc:String, typeface:String, callback:Void->Void) {
|
||||
super(name, desc, callback);
|
||||
|
||||
var xml = Xml.parse(Assets.getText(Paths.xml('alphabet/$typeface'))).firstElement();
|
||||
var spritesheet = null;
|
||||
for(node in xml.elements()) {
|
||||
if (node.nodeName == "spritesheet") {
|
||||
spritesheet = node.firstChild().nodeValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var useColorOffsets = xml.get("useColorOffsets").getDefault("false") == "true";
|
||||
|
||||
|
||||
// todo fix crash if invalid spritesheet;
|
||||
|
||||
iconSpr = new FlxSprite();
|
||||
iconSpr.frames = Paths.getFrames(spritesheet);
|
||||
iconSpr.antialiasing = true;
|
||||
var frameToUse = iconSpr.frames.frames[0];
|
||||
for(frame in iconSpr.frames.frames) {
|
||||
if (frame.name.toUpperCase().startsWith("A")) {
|
||||
frameToUse = frame;
|
||||
break;
|
||||
}
|
||||
}
|
||||
iconSpr.frame = frameToUse;
|
||||
if(useColorOffsets) {
|
||||
iconSpr.colorTransform.color = -1;
|
||||
}
|
||||
iconSpr.setPosition(90 - iconSpr.width - 20, (__text.height - iconSpr.height) / 2);
|
||||
add(iconSpr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package funkin.editors.alphabet;
|
||||
|
||||
import flixel.math.FlxAngle;
|
||||
|
||||
class GlyphInfoWindow extends UIWindow {
|
||||
public var prefixBox:UITextBox;
|
||||
public var xBox:UINumericStepper; // LIVE
|
||||
public var yBox:UINumericStepper; // MJWF
|
||||
public var scaleXBox:UINumericStepper;
|
||||
public var scaleYBox:UINumericStepper;
|
||||
public var angleBox:UINumericStepper;
|
||||
public var colorModeDrop:UIDropDown;
|
||||
|
||||
public var outlineTitle:UIText;
|
||||
public var outlineSep:UISprite;
|
||||
public var outlineCheck:UICheckbox;
|
||||
public var outlineBoxTitle:UIText;
|
||||
public var outlineBox:UITextBox;
|
||||
public var outlineOffTitle:UIText;
|
||||
public var outlineXBox:UINumericStepper;
|
||||
public var outlineYBox:UINumericStepper;
|
||||
|
||||
public var targetPercent:Float = 0;
|
||||
public var outlinePercent:Float = 0;
|
||||
public var outlineAlphaMults:Array<Float>;
|
||||
public var outlineItems:Array<FlxSprite>;
|
||||
public var initialY:Array<Float> = [];
|
||||
|
||||
var compon(get, never):AlphabetComponent;
|
||||
var data(get, never):AlphabetLetterData;
|
||||
|
||||
public function new() {
|
||||
var width = 360;
|
||||
var height = 255;
|
||||
var margin = 30;
|
||||
var itemMargin = 15; // there was already a buncha variables so i thought why not follow
|
||||
var labelOffset = 24;
|
||||
super(FlxG.width - width - margin, FlxG.height - height - margin, width, height, "Glyph Info");
|
||||
|
||||
function addLabelOn(ui:UISprite, text:String) {
|
||||
var title = new UIText(ui.x, ui.y - labelOffset, 0, text);
|
||||
members.push(title);
|
||||
return title;
|
||||
}
|
||||
|
||||
prefixBox = new UITextBox(x + itemMargin, y + itemMargin + labelOffset * 2, "", 330);
|
||||
prefixBox.onChange = function(val) {
|
||||
var index = data.components.indexOf(compon);
|
||||
compon.anim = val;
|
||||
var anim = AlphabetEditor.instance.bigLetter.text + Std.string(index);
|
||||
AlphabetEditor.instance.bigLetter.animation.remove(anim);
|
||||
AlphabetEditor.instance.tape.animation.remove(anim);
|
||||
AlphabetEditor.instance.checkForFailed();
|
||||
}
|
||||
members.push(prefixBox);
|
||||
addLabelOn(prefixBox, "Animation Prefix");
|
||||
|
||||
xBox = new UINumericStepper(prefixBox.x, prefixBox.y + prefixBox.bHeight + itemMargin + labelOffset, 0, 1, 2, null, null, 80);
|
||||
xBox.onChange = valueSet.bind(xBox, function(val) { // kinda dumb but blame cne ui
|
||||
compon.x = val;
|
||||
});
|
||||
members.push(xBox);
|
||||
|
||||
members.push(new UIText(xBox.x + xBox.bWidth - 2, xBox.y + 9, 0, ",", 22));
|
||||
|
||||
yBox = new UINumericStepper(xBox.x + xBox.bWidth + itemMargin, xBox.y, 0, 1, 2, null, null, 80);
|
||||
yBox.onChange = valueSet.bind(yBox, function(val) { // kinda dumb but blame cne ui
|
||||
compon.y = val;
|
||||
});
|
||||
members.push(yBox);
|
||||
addLabelOn(xBox, "Offset (x,y)");
|
||||
|
||||
scaleXBox = new UINumericStepper(yBox.x + yBox.bWidth + itemMargin * 2, yBox.y, 1, 0.1, 2, null, null, 80);
|
||||
scaleXBox.onChange = valueSet.bind(scaleXBox, function(val) { // kinda dumb but blame cne ui
|
||||
compon.scaleX = val;
|
||||
if (outlineCheck.checked)
|
||||
data.components[compon.outIndex].scaleX = val;
|
||||
});
|
||||
members.push(scaleXBox);
|
||||
|
||||
members.push(new UIText(scaleXBox.x + scaleXBox.bWidth - 2, scaleXBox.y + 9, 0, ",", 22));
|
||||
|
||||
scaleYBox = new UINumericStepper(scaleXBox.x + scaleXBox.bWidth + itemMargin, scaleXBox.y, 1, 0.1, 2, null, null, 80);
|
||||
scaleYBox.onChange = valueSet.bind(scaleYBox, function(val) { // kinda dumb but blame cne ui
|
||||
compon.scaleY = val;
|
||||
if (outlineCheck.checked)
|
||||
data.components[compon.outIndex].scaleY = val;
|
||||
});
|
||||
members.push(scaleYBox);
|
||||
addLabelOn(scaleXBox, "Scale (x,y)");
|
||||
|
||||
angleBox = new UINumericStepper(scaleYBox.x + scaleYBox.bWidth + itemMargin * 2, scaleYBox.y, 0, 1, 2, null, null, 80);
|
||||
angleBox.onChange = valueSet.bind(angleBox, function(val) { // kinda dumb but blame cne ui
|
||||
compon.shouldRotate = val != 0;
|
||||
compon.angle = val;
|
||||
compon.cos = Math.cos(val * FlxAngle.TO_RAD);
|
||||
compon.sin = Math.sin(val * FlxAngle.TO_RAD);
|
||||
|
||||
if (outlineCheck.checked) {
|
||||
data.components[compon.outIndex].shouldRotate = compon.shouldRotate;
|
||||
data.components[compon.outIndex].angle = compon.angle;
|
||||
data.components[compon.outIndex].cos = compon.cos;
|
||||
data.components[compon.outIndex].sin = compon.sin;
|
||||
}
|
||||
});
|
||||
members.push(angleBox);
|
||||
addLabelOn(angleBox, "Angle");
|
||||
|
||||
colorModeDrop = new UIDropDown(xBox.x, xBox.y + xBox.bHeight + itemMargin + labelOffset, 145, 32, ["TINT", "OFFSETS", "NONE"]);
|
||||
colorModeDrop.onChange = function(mode) {
|
||||
compon.colorMode = mode;
|
||||
@:privateAccess compon.hasColorMode = mode != AlphabetEditor.instance.tape.colorMode;
|
||||
if (outlineCheck.checked) {
|
||||
data.components[compon.outIndex].colorMode = mode;
|
||||
@:privateAccess data.components[compon.outIndex].hasColorMode = compon.hasColorMode;
|
||||
}
|
||||
};
|
||||
members.push(colorModeDrop);
|
||||
addLabelOn(colorModeDrop, "Color Mode");
|
||||
|
||||
outlineCheck = new UICheckbox(colorModeDrop.x + colorModeDrop.bWidth + itemMargin * 2, colorModeDrop.y + 4, "Use Outline?");
|
||||
outlineCheck.onChecked = function(val) {
|
||||
targetPercent = val ? 1 : 0;
|
||||
|
||||
for (i in 0...data.components.length) {
|
||||
var anim = AlphabetEditor.instance.bigLetter.text + i;
|
||||
AlphabetEditor.instance.bigLetter.animation.remove(anim);
|
||||
AlphabetEditor.instance.tape.animation.remove(anim);
|
||||
}
|
||||
|
||||
if (val) {
|
||||
compon.outIndex = AlphabetEditor.instance.outlineIdx;
|
||||
|
||||
var newOutline:AlphabetComponent = {
|
||||
refIndex: data.components.indexOf(compon),
|
||||
anim: outlineBox.label.text,
|
||||
|
||||
x: outlineXBox.value,
|
||||
y: outlineYBox.value,
|
||||
scaleX: compon.scaleX,
|
||||
scaleY: compon.scaleY,
|
||||
|
||||
shouldRotate: compon.shouldRotate,
|
||||
angle: compon.angle,
|
||||
cos: compon.cos,
|
||||
sin: compon.sin,
|
||||
|
||||
hasColorMode: compon.hasColorMode,
|
||||
colorMode: compon.colorMode
|
||||
};
|
||||
|
||||
data.components.insert(AlphabetEditor.instance.outlineIdx, newOutline);
|
||||
++data.startIndex;
|
||||
} else {
|
||||
compon.outIndex = null;
|
||||
data.components.splice(AlphabetEditor.instance.outlineIdx - 1, 1);
|
||||
--data.startIndex;
|
||||
}
|
||||
}
|
||||
members.push(outlineCheck);
|
||||
|
||||
outlineTitle = new UIText(colorModeDrop.x, colorModeDrop.y + colorModeDrop.bHeight + itemMargin, 0, "Outline Data");
|
||||
members.push(outlineTitle);
|
||||
outlineSep = new UISprite(x + itemMargin, colorModeDrop.y + colorModeDrop.bHeight + itemMargin + 19);
|
||||
outlineSep.makeSolid(width - itemMargin * 2, 1, 0x40FFFFFF);
|
||||
members.push(outlineSep);
|
||||
|
||||
outlineBox = new UITextBox(colorModeDrop.x, colorModeDrop.y + colorModeDrop.bHeight + itemMargin + labelOffset * 2 + 3, "", 190);
|
||||
outlineBox.onChange = function(val) {
|
||||
data.components[compon.outIndex].anim = val;
|
||||
var anim = AlphabetEditor.instance.bigLetter.text + Std.string(compon.outIndex);
|
||||
AlphabetEditor.instance.bigLetter.animation.remove(anim);
|
||||
AlphabetEditor.instance.tape.animation.remove(anim);
|
||||
AlphabetEditor.instance.checkForFailed();
|
||||
}
|
||||
members.push(outlineBox);
|
||||
outlineBoxTitle = addLabelOn(outlineBox, "Prefix");
|
||||
|
||||
outlineXBox = new UINumericStepper(outlineBox.x + outlineBox.bWidth + itemMargin * 2, outlineBox.y, 0, 1, 2, null, null, 80);
|
||||
outlineXBox.onChange = valueSet.bind(outlineXBox, function(val) { // kinda dumb but blame cne ui
|
||||
if (outlineCheck.checked)
|
||||
data.components[compon.outIndex].x = val;
|
||||
});
|
||||
members.push(outlineXBox);
|
||||
|
||||
members.push(new UIText(outlineXBox.x + outlineXBox.bWidth - 2, outlineXBox.y + 9, 0, ",", 22));
|
||||
|
||||
outlineYBox = new UINumericStepper(outlineXBox.x + outlineXBox.bWidth + itemMargin, outlineXBox.y, 0, 1, 2, null, null, 80);
|
||||
outlineYBox.onChange = valueSet.bind(outlineYBox, function(val) { // kinda dumb but blame cne ui
|
||||
if (outlineCheck.checked)
|
||||
data.components[compon.outIndex].y = val;
|
||||
});
|
||||
members.push(outlineYBox);
|
||||
outlineOffTitle = addLabelOn(outlineXBox, "Offset (x,y)");
|
||||
|
||||
for (mem in members) {
|
||||
if (mem is FlxObject)
|
||||
initialY.push(cast (mem, FlxObject).y);
|
||||
}
|
||||
outlineAlphaMults = [1.0, 0.9, 0.5, 0.15, 0.15, 0.5, 0.15, 0.15, 0.15, 0.15];
|
||||
outlineItems = [outlineTitle, outlineSep, outlineBoxTitle, outlineBox, outlineBox.label, outlineOffTitle, outlineXBox, outlineXBox.label, outlineYBox, outlineYBox.label];
|
||||
}
|
||||
|
||||
override public function update(elapsed:Float) {
|
||||
super.update(elapsed);
|
||||
|
||||
outlinePercent = FlxMath.lerp(outlinePercent, targetPercent, elapsed * 10);
|
||||
bHeight = Std.int(255 + 105 * outlinePercent);
|
||||
y = FlxG.height - bHeight - 30;
|
||||
|
||||
for (i in 0...outlineItems.length)
|
||||
outlineItems[i].alpha = outlinePercent * FlxMath.lerp(outlineAlphaMults[i], 1, outlinePercent * outlinePercent) * alpha;
|
||||
|
||||
var yIdxOff = 0;
|
||||
for (i in 0...members.length) {
|
||||
if (members[i] is FlxObject)
|
||||
cast (members[i], FlxObject).y = initialY[i - yIdxOff] - (bHeight - 255);
|
||||
else
|
||||
++yIdxOff;
|
||||
}
|
||||
}
|
||||
|
||||
public function updateInfo() {
|
||||
var com = compon;
|
||||
if (com == null) {
|
||||
for (item in members) {
|
||||
if (item is UISprite)
|
||||
cast(item, UISprite).selectable = false;
|
||||
}
|
||||
colorModeDrop.dropButton.selectable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
prefixBox.label.text = com.anim;
|
||||
xBox.value = com.x;
|
||||
yBox.value = com.y;
|
||||
scaleXBox.value = com.scaleX;
|
||||
scaleYBox.value = com.scaleY;
|
||||
angleBox.value = com.angle;
|
||||
@:privateAccess colorModeDrop.index = com.hasColorMode ? com.colorMode : AlphabetEditor.instance.tape.colorMode;
|
||||
@:privateAccess colorModeDrop.label.text = colorModeDrop.options[colorModeDrop.index];
|
||||
|
||||
outlineCheck.checked = com.outIndex != null;
|
||||
outlineBox.label.text = "";
|
||||
outlineXBox.value = 0;
|
||||
outlineYBox.value = 0;
|
||||
targetPercent = 0;
|
||||
if (com.outIndex != null) {
|
||||
var out = data.components[com.outIndex];
|
||||
outlineBox.label.text = out.anim;
|
||||
outlineXBox.value = out.x;
|
||||
outlineYBox.value = out.y;
|
||||
targetPercent = 1;
|
||||
}
|
||||
|
||||
for (item in members) {
|
||||
if (item is UISprite)
|
||||
cast(item, UISprite).selectable = true;
|
||||
}
|
||||
colorModeDrop.dropButton.selectable = true;
|
||||
}
|
||||
|
||||
function valueSet(item:UINumericStepper, func:Dynamic, text:String) {
|
||||
@:privateAccess item.__onChange(text);
|
||||
func(item.value);
|
||||
}
|
||||
|
||||
function get_compon() {
|
||||
return AlphabetEditor.instance.curSelectedComponent;
|
||||
}
|
||||
function get_data() {
|
||||
return AlphabetEditor.instance.curSelectedData;
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,9 @@ class CharacterAnimButton extends UIButton {
|
||||
public var foldableButtons:Array<FlxSprite> = [];
|
||||
public var closed:Bool = true;
|
||||
|
||||
inline function translate(id:String, ?args:Array<Dynamic>)
|
||||
return TU.translate("characterEditor.characterAnim." + id, args);
|
||||
|
||||
public function new(x:Float, y:Float, animData:AnimData, parent:CharacterAnimsWindow) {
|
||||
this.anim = animData.name;
|
||||
this.data = animData;
|
||||
@@ -76,13 +79,13 @@ class CharacterAnimButton extends UIButton {
|
||||
nameTextBox.onChange = (newName:String) -> {this.changeName(newName);};
|
||||
members.push(nameTextBox);
|
||||
foldableButtons.push(nameTextBox);
|
||||
addLabelOn(nameTextBox, "Name", 12);
|
||||
addLabelOn(nameTextBox, translate("name"), 12);
|
||||
|
||||
animTextBox = new UIAutoCompleteTextBox(nameTextBox.x + 100, nameTextBox.y, animData.anim, 156, 22, false, true);
|
||||
animTextBox.onChange = (newAnim:String) -> {this.changeAnim(newAnim);};
|
||||
members.push(animTextBox);
|
||||
foldableButtons.push(animTextBox);
|
||||
addLabelOn(animTextBox, "Animation", 12);
|
||||
addLabelOn(animTextBox, translate("anim"), 12);
|
||||
|
||||
positionXStepper = new UINumericStepper(animTextBox.x, animTextBox.y+32+18, animData.x, 0.001, 2, null, null, 64, 22, true);
|
||||
positionXStepper.onChange = (text:String) -> {
|
||||
@@ -91,7 +94,7 @@ class CharacterAnimButton extends UIButton {
|
||||
};
|
||||
members.push(positionXStepper);
|
||||
foldableButtons.push(positionXStepper);
|
||||
addLabelOn(positionXStepper, "Position (X,Y)", 12);
|
||||
addLabelOn(positionXStepper, translate("position"), 12);
|
||||
|
||||
members.push(XYComma = new UIText(positionXStepper.x+104-32+0, positionXStepper.y + 9, 0, ",", 18));
|
||||
foldableButtons.push(XYComma);
|
||||
@@ -111,7 +114,7 @@ class CharacterAnimButton extends UIButton {
|
||||
};
|
||||
members.push(fpsStepper);
|
||||
foldableButtons.push(fpsStepper);
|
||||
addLabelOn(fpsStepper, "FPS", 12);
|
||||
addLabelOn(fpsStepper, translate("fps"), 12);
|
||||
|
||||
loopedCheckbox = new UICheckbox(fpsStepper.x + 82 - 32 + 26, fpsStepper.y, "Looping?", animData.loop, 0, true);
|
||||
loopedCheckbox.onChecked = (newLooping:Bool) -> {this.changeLooping(newLooping);};
|
||||
@@ -126,7 +129,7 @@ class CharacterAnimButton extends UIButton {
|
||||
}
|
||||
members.push(indicesTextBox);
|
||||
foldableButtons.push(indicesTextBox);
|
||||
addLabelOn(indicesTextBox, "Indices (frames)", 12);
|
||||
addLabelOn(indicesTextBox, translate("indices"), 12);
|
||||
|
||||
animIcon = new FlxSprite(x-(10+16), y+8).loadGraphic(Paths.image("editors/character/anim-icons"), true, 16, 12);
|
||||
animIcon.animation.add("play", [0]);
|
||||
@@ -421,10 +424,12 @@ class CharacterAnimButton extends UIButton {
|
||||
|
||||
public function toggleGhost() {
|
||||
if (valid && parent.ghosts.indexOf(anim) == -1) {
|
||||
FlxG.sound.play(Paths.sound(Flags.DEFAULT_CHARACTER_GHOSTENABLE_SOUND));
|
||||
parent.ghosts.push(anim);
|
||||
ghostIcon.animation.play("alive", true);
|
||||
ghostIcon.color = 0xFFFFFFFF;
|
||||
} else {
|
||||
FlxG.sound.play(Paths.sound(Flags.DEFAULT_CHARACTER_GHOSTDISABLE_SOUND));
|
||||
parent.ghosts.remove(anim);
|
||||
ghostIcon.animation.play("dead", true);
|
||||
ghostIcon.color = 0xFFADADAD;
|
||||
|
||||
@@ -99,6 +99,7 @@ class CharacterAnimsWindow extends UIButtonList<CharacterAnimButton> {
|
||||
displayAnimsFramesList.remove(name);
|
||||
|
||||
public function deleteAnimation(button:CharacterAnimButton, addToUndo:Bool = true) {
|
||||
FlxG.sound.play(Paths.sound(Flags.DEFAULT_EDITOR_DELETE_SOUND));
|
||||
if (buttons.members.length <= 1) return;
|
||||
if (character.getAnimName() == button.anim)
|
||||
@:privateAccess CharacterEditor.instance._animation_down(null);
|
||||
@@ -112,13 +113,14 @@ class CharacterAnimsWindow extends UIButtonList<CharacterAnimButton> {
|
||||
}
|
||||
|
||||
public function generateAnimation() {
|
||||
var animName:String = "New Anim";
|
||||
var newAnim = TU.translate("characterEditor.characterAnim.defaultAnimName");
|
||||
var animName:String = newAnim;
|
||||
var animNames:Array<String> = character.getNameList();
|
||||
|
||||
var newAnimCount:Int = 0;
|
||||
while (animNames.indexOf(animName) != -1) {
|
||||
newAnimCount++;
|
||||
animName = 'New Anim - $newAnimCount';
|
||||
animName = '$newAnim - $newAnimCount';
|
||||
}
|
||||
|
||||
if (__autoCompleteAnims.length <= 0)
|
||||
|
||||
@@ -15,13 +15,16 @@ class CharacterCreationScreen extends UISubstateWindow {
|
||||
public var saveButton:UIButton;
|
||||
public var closeButton:UIButton;
|
||||
|
||||
inline function translate(id:String, ?args:Array<Dynamic>)
|
||||
return TU.translate("characterCreationScreen." + id, args);
|
||||
|
||||
public function new(?onSave:(String, ImageSaveData, Xml)->Void) {
|
||||
super();
|
||||
if (onSave != null) this.onSave = onSave;
|
||||
}
|
||||
|
||||
public override function create() {
|
||||
winTitle = "Creating New Character";
|
||||
winTitle = translate("win-title");
|
||||
|
||||
winWidth = 360;
|
||||
winHeight = 520;
|
||||
@@ -37,24 +40,24 @@ class CharacterCreationScreen extends UISubstateWindow {
|
||||
characterNameTextBox = new UITextBox(windowSpr.x + 20, windowSpr.y + 30 + 16 + 20, "character", 320);
|
||||
characterNameTextBox.onChange = (_) -> {checkRequired();};
|
||||
add(characterNameTextBox);
|
||||
addLabelOn(characterNameTextBox, "Character File Name").applyMarkup(
|
||||
"Character File Name $* Required$",
|
||||
addLabelOn(characterNameTextBox, "").applyMarkup(
|
||||
translate("charName"),
|
||||
[new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFAD1212), "$")]);
|
||||
|
||||
imageExplorer = new UIImageExplorer(characterNameTextBox.x, characterNameTextBox.y + 30 + 16 + 20, null, 320, 58, (_, _) -> {onLoadImage();}, "images/characters");
|
||||
add(imageExplorer);
|
||||
addLabelOn(imageExplorer, "Character Image File").applyMarkup(
|
||||
"Character Image File $* Required$",
|
||||
addLabelOn(imageExplorer, "").applyMarkup(
|
||||
translate("charImgName"),
|
||||
[new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFAD1212), "$")]);
|
||||
imageExplorer.maxSize.y -= 100;
|
||||
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, "Save & Close", function() {
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() {
|
||||
close();
|
||||
createCharacter();
|
||||
}, 125);
|
||||
add(saveButton);
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 20 - saveButton.bWidth, saveButton.y, "Cancel", function() {
|
||||
closeButton = new UIButton(saveButton.x - 20 - saveButton.bWidth, saveButton.y, TU.translate("editor.cancel"), function() {
|
||||
close();
|
||||
}, 125);
|
||||
add(closeButton);
|
||||
|
||||
@@ -65,74 +65,77 @@ class CharacterEditor extends UIState {
|
||||
if (character != null) __character = character;
|
||||
}
|
||||
|
||||
inline function translate(id:String, ?args:Array<Dynamic>)
|
||||
return TU.translate("characterEditor." + id, args);
|
||||
|
||||
public override function create() {
|
||||
super.create();
|
||||
|
||||
undos = new UndoList<CharacterEditorChange>();
|
||||
|
||||
WindowUtils.suffix = " (Character Editor)";
|
||||
WindowUtils.suffix = " (" + TU.translate("editor.character.name") + ")";
|
||||
SaveWarning.selectionClass = CharacterSelection;
|
||||
SaveWarning.saveFunc = () -> {_file_save(null);};
|
||||
|
||||
topMenu = [
|
||||
{
|
||||
label: "File",
|
||||
label: translate("topBar.file"),
|
||||
childs: [
|
||||
{
|
||||
label: "New",
|
||||
label: translate("file.new"),
|
||||
onSelect: _file_new,
|
||||
},
|
||||
null,
|
||||
{
|
||||
label: "Save",
|
||||
label: translate("file.save"),
|
||||
keybind: [CONTROL, S],
|
||||
onSelect: _file_save,
|
||||
},
|
||||
{
|
||||
label: "Save As...",
|
||||
label: translate("file.saveAs"),
|
||||
keybind: [CONTROL, SHIFT, S],
|
||||
onSelect: _file_saveas,
|
||||
},
|
||||
null,
|
||||
{
|
||||
label: "Exit",
|
||||
label: translate("file.exit"),
|
||||
onSelect: _file_exit
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
label: translate("topBar.edit"),
|
||||
childs: [
|
||||
{
|
||||
label: "Copy Offset",
|
||||
label: translate("edit.copyOffset"),
|
||||
keybind: [CONTROL, C],
|
||||
onSelect: _edit_copy_offset
|
||||
},
|
||||
{
|
||||
label: "Paste Offset",
|
||||
label: translate("edit.pasteOffset"),
|
||||
keybind: [CONTROL, V],
|
||||
onSelect: _edit_paste_offset
|
||||
},
|
||||
null,
|
||||
{
|
||||
label: "Undo",
|
||||
label: translate("edit.undo"),
|
||||
keybind: [CONTROL, Z],
|
||||
onSelect: _edit_undo
|
||||
},
|
||||
{
|
||||
label: "Redo",
|
||||
label: translate("edit.redo"),
|
||||
keybinds: [[CONTROL, Y], [CONTROL, SHIFT, Z]],
|
||||
onSelect: _edit_redo
|
||||
},
|
||||
null,
|
||||
{
|
||||
label: "Edit information",
|
||||
label: translate("edit.editInfo"),
|
||||
color: 0xFF959829, icon: 4,
|
||||
onCreate: function (button:UIContextMenuOptionSpr) {button.label.offset.x = button.icon.offset.x = -2;},
|
||||
onSelect: _edit_info
|
||||
},
|
||||
{
|
||||
label: "Edit sprite",
|
||||
label: translate("edit.editSprite"),
|
||||
color: 0xFF959829, icon: 4,
|
||||
onCreate: function (button:UIContextMenuOptionSpr) {button.label.offset.x = button.icon.offset.x = -2;},
|
||||
onSelect: _edit_sprite
|
||||
@@ -140,128 +143,128 @@ class CharacterEditor extends UIState {
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Offsets",
|
||||
label: translate("topBar.offset"),
|
||||
childs: [
|
||||
{
|
||||
label: "Move Left",
|
||||
label: translate("offset.moveLeft"),
|
||||
keybind: [LEFT],
|
||||
onSelect: _offsets_left,
|
||||
},
|
||||
{
|
||||
label: "Move Up",
|
||||
label: translate("offset.moveUp"),
|
||||
keybind: [UP],
|
||||
onSelect: _offsets_up,
|
||||
},
|
||||
{
|
||||
label: "Move Down",
|
||||
label: translate("offset.moveDown"),
|
||||
keybind: [DOWN],
|
||||
onSelect: _offsets_down,
|
||||
},
|
||||
{
|
||||
label: "Move Right",
|
||||
label: translate("offset.moveRight"),
|
||||
keybind: [RIGHT],
|
||||
onSelect: _offsets_right,
|
||||
},
|
||||
null,
|
||||
{
|
||||
label: "Move Extra Left",
|
||||
label: translate("offset.moveExtraLeft"),
|
||||
keybind: [SHIFT, LEFT],
|
||||
onSelect: _offsets_extra_left,
|
||||
},
|
||||
{
|
||||
label: "Move Extra Up",
|
||||
label: translate("offset.moveExtraUp"),
|
||||
keybind: [SHIFT, UP],
|
||||
onSelect: _offsets_extra_up,
|
||||
},
|
||||
{
|
||||
label: "Move Extra Down",
|
||||
label: translate("offset.moveExtraDown"),
|
||||
keybind: [SHIFT, DOWN],
|
||||
onSelect: _offsets_extra_down,
|
||||
},
|
||||
{
|
||||
label: "Move Extra Right",
|
||||
label: translate("offset.moveExtraRight"),
|
||||
keybind: [SHIFT, RIGHT],
|
||||
onSelect: _offsets_extra_right,
|
||||
},
|
||||
null,
|
||||
{
|
||||
label: "Drag Offsets With Mouse?",
|
||||
label: translate("offset.dragOffsetsMouse"),
|
||||
onSelect: _offsets_drag_offsets_mouse,
|
||||
icon: Options.characterDragging ? 1 : 0
|
||||
},
|
||||
{
|
||||
label: "Clear Offsets",
|
||||
label: translate("offset.clearOffsets"),
|
||||
keybind: [CONTROL, R],
|
||||
onSelect: _offsets_clear,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Stage",
|
||||
label: translate("topBar.stage"),
|
||||
childs: buildStagesUI()
|
||||
},
|
||||
{
|
||||
label: "View",
|
||||
label: translate("topBar.view"),
|
||||
childs: [
|
||||
{
|
||||
label: "Zoom in",
|
||||
label: translate("view.zoomIn"),
|
||||
keybind: [CONTROL, NUMPADPLUS],
|
||||
onSelect: _view_zoomin
|
||||
},
|
||||
{
|
||||
label: "Zoom out",
|
||||
label: translate("view.zoomOut"),
|
||||
keybind: [CONTROL, NUMPADMINUS],
|
||||
onSelect: _view_zoomout
|
||||
},
|
||||
{
|
||||
label: "Reset zoom",
|
||||
label: translate("view.resetZoom"),
|
||||
keybind: [CONTROL, NUMPADZERO],
|
||||
onSelect: _view_zoomreset
|
||||
},
|
||||
null,
|
||||
{
|
||||
label: "Character Hitbox?",
|
||||
label: translate("view.charHitbox"),
|
||||
onSelect: _view_character_show_hitbox,
|
||||
icon: Options.characterHitbox ? 1 : 0
|
||||
},
|
||||
{
|
||||
label: "Character Camera?",
|
||||
label: translate("view.charCam"),
|
||||
onSelect: _view_character_show_camera,
|
||||
icon: Options.characterCamera ? 1 : 0
|
||||
},
|
||||
{
|
||||
label: "XY Axis?",
|
||||
label: translate("view.axis"),
|
||||
onSelect: _view_character_show_axis,
|
||||
icon: Options.characterAxis ? 1 : 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Animation >",
|
||||
label: translate("topBar.animation") + " >",
|
||||
childs: [
|
||||
{
|
||||
label: "Play Animation",
|
||||
label: translate("animation.playAnim"),
|
||||
keybind: [SPACE],
|
||||
onSelect: _animation_play,
|
||||
},
|
||||
{
|
||||
label: "Stop Animation",
|
||||
label: translate("animation.stopAnim"),
|
||||
onSelect: _animation_stop
|
||||
},
|
||||
null,
|
||||
{
|
||||
label: "Change Animation ↑",
|
||||
label: translate("animation.changeAnimUp"),
|
||||
keybind: [W],
|
||||
onSelect: _animation_up
|
||||
},
|
||||
{
|
||||
label: "Change Animation ↓",
|
||||
label: translate("animation.changeAnimDown"),
|
||||
keybind: [S],
|
||||
onSelect: _animation_down
|
||||
},
|
||||
null,
|
||||
{
|
||||
label: "Play Animation On Offset?",
|
||||
label: translate("animation.playAnimOnOffset"),
|
||||
onSelect: _animation_toggle_anim_playing_offsets,
|
||||
icon: Options.playAnimOnOffset ? 1 : 0
|
||||
},
|
||||
@@ -433,7 +436,7 @@ class CharacterEditor extends UIState {
|
||||
super.onResize(width, height);
|
||||
if (!UIState.resolutionAware) return;
|
||||
|
||||
if (width < FlxG.initialWidth || height < FlxG.initialHeight) {
|
||||
if ((width < FlxG.initialWidth || height < FlxG.initialHeight) && !Options.bypassEditorsResize) {
|
||||
width = FlxG.initialWidth; height = FlxG.initialHeight;
|
||||
}
|
||||
|
||||
@@ -450,6 +453,7 @@ class CharacterEditor extends UIState {
|
||||
|
||||
function _file_save(_) {
|
||||
#if sys
|
||||
FlxG.sound.play(Paths.sound('editors/save'));
|
||||
CoolUtil.safeSaveFile(
|
||||
'${Paths.getAssetsRoot()}/data/characters/${character.curCharacter}.xml',
|
||||
buildCharacter()
|
||||
@@ -461,6 +465,7 @@ class CharacterEditor extends UIState {
|
||||
}
|
||||
|
||||
function _file_saveas(_) {
|
||||
FlxG.sound.play(Paths.sound('editors/save'));
|
||||
openSubState(new SaveSubstate(buildCharacter(), {
|
||||
defaultSaveFile: '${character.curCharacter}.xml'
|
||||
}));
|
||||
@@ -493,6 +498,7 @@ class CharacterEditor extends UIState {
|
||||
}
|
||||
|
||||
function _undo(undo:CharacterEditorChange) {
|
||||
FlxG.sound.play(Paths.sound(Flags.DEFAULT_EDITOR_UNDO_SOUND));
|
||||
switch (undo) {
|
||||
case null: // do nothing
|
||||
case CCharEditPosition(oldPos, newPos):
|
||||
@@ -563,6 +569,7 @@ class CharacterEditor extends UIState {
|
||||
}
|
||||
|
||||
function _redo(redo:CharacterEditorChange) {
|
||||
FlxG.sound.play(Paths.sound(Flags.DEFAULT_EDITOR_REDO_SOUND));
|
||||
switch (redo) {
|
||||
case null: // do nothing
|
||||
case CCharEditPosition(oldPos, newPos):
|
||||
@@ -682,16 +689,17 @@ class CharacterEditor extends UIState {
|
||||
var stageFileList = Stage.getList(true);
|
||||
if (stageFileList.length == 0) stageFileList = Stage.getList(false);
|
||||
|
||||
var stageText = TU.getRaw("characterEditor.stage.useStage");
|
||||
for (stage in stageFileList)
|
||||
newChilds.push({
|
||||
label: 'Use "$stage"?',
|
||||
label: stageText.format([stage]),
|
||||
icon: currentStage == stage ? 1 : 0,
|
||||
onSelect: (_) -> {changeStage(stage);}
|
||||
});
|
||||
|
||||
newChilds.push(null);
|
||||
newChilds.push({
|
||||
label: "Use No Stage?",
|
||||
label: translate("stage.useNone"),
|
||||
icon: currentStage == null ? 1 : 0,
|
||||
onSelect: (_) -> {changeStage(null);}
|
||||
});
|
||||
|
||||
@@ -40,8 +40,11 @@ class CharacterInfoScreen extends UISubstateWindow {
|
||||
super();
|
||||
}
|
||||
|
||||
inline function translate(id:String, ?args:Array<Dynamic>)
|
||||
return TU.translate("characterInfoScreen." + id, args);
|
||||
|
||||
public override function create() {
|
||||
winTitle = 'Edit Character Properties...';
|
||||
winTitle = translate("win-title");
|
||||
winWidth = 824; winHeight = 400;
|
||||
|
||||
super.create();
|
||||
@@ -50,15 +53,15 @@ class CharacterInfoScreen extends UISubstateWindow {
|
||||
add(new UIText(ui.x, ui.y - 24, 0, text));
|
||||
|
||||
var title:UIText;
|
||||
add(title = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Edit Character Info", 28));
|
||||
add(title = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, translate("title"), 28));
|
||||
|
||||
iconColorPicker = new UIIconColorPicker(title.x, title.y + title.height + 38, character.getIcon(), character.antialiasing, null);
|
||||
add(iconColorPicker);
|
||||
addLabelOn(iconColorPicker, "Icon");
|
||||
addLabelOn(iconColorPicker, translate("icon"));
|
||||
|
||||
iconColorWheel = new UIColorwheel(iconColorPicker.x+12+125+12+22, iconColorPicker.y, character.iconColor);
|
||||
add(iconColorWheel);
|
||||
addLabelOn(iconColorWheel, "Icon Color");
|
||||
addLabelOn(iconColorWheel, translate("iconColor"));
|
||||
|
||||
if (character.iconColor != null)
|
||||
iconColorWheel.colorChanged = true;
|
||||
@@ -66,9 +69,9 @@ class CharacterInfoScreen extends UISubstateWindow {
|
||||
|
||||
durationStepper = new UINumericStepper(iconColorWheel.x, iconColorWheel.y + 125 + 36, character.holdTime == -1 ? 4 : character.holdTime, 0.001, 2, 0, 9999999, 74);
|
||||
add(durationStepper);
|
||||
addLabelOn(durationStepper, "Sing Duration");
|
||||
addLabelOn(durationStepper, translate("singDuration"));
|
||||
|
||||
useDurationCheckbox = new UICheckbox(durationStepper.x + durationStepper.bWidth + 20, durationStepper.y+6, "Use Sing Duration?", character.holdTime != -1);
|
||||
useDurationCheckbox = new UICheckbox(durationStepper.x + durationStepper.bWidth + 20, durationStepper.y+6, translate("useSingDuration"), character.holdTime != -1);
|
||||
useDurationCheckbox.onChecked = (checked:Bool) -> {durationStepper.selectable = checked;};
|
||||
add(useDurationCheckbox);
|
||||
|
||||
@@ -86,15 +89,15 @@ class CharacterInfoScreen extends UISubstateWindow {
|
||||
customPropertiesButtonList.add(new PropertyButton(prop, val, customPropertiesButtonList));
|
||||
|
||||
add(customPropertiesButtonList);
|
||||
addLabelOn(customPropertiesButtonList, "Custom Values (Advanced)");
|
||||
addLabelOn(customPropertiesButtonList, translate("customValues"));
|
||||
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, "Save & Close", function() {
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() {
|
||||
saveCharacterInfo();
|
||||
close();
|
||||
}, 125);
|
||||
add(saveButton);
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 20 - saveButton.bWidth, saveButton.y, "Close", function() {
|
||||
closeButton = new UIButton(saveButton.x - 20 - saveButton.bWidth, saveButton.y, TU.translate("editor.close"), function() {
|
||||
close();
|
||||
}, 125);
|
||||
add(closeButton);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
package funkin.editors.character;
|
||||
|
||||
import funkin.backend.FlxAnimate;
|
||||
@@ -26,6 +27,9 @@ class CharacterPropertiesWindow extends UISliceSprite {
|
||||
|
||||
public var labels:Map<UISprite, UIText> = [];
|
||||
|
||||
inline function translate(id:String, ?args:Array<Dynamic>)
|
||||
return TU.translate("characterEditor.characterProperties." + id, args);
|
||||
|
||||
public function new(x:Float, y:Float, character:Character) @:privateAccess {
|
||||
super(x, y, 424+16, 204+20, "editors/ui/inputbox");
|
||||
|
||||
@@ -40,7 +44,7 @@ class CharacterPropertiesWindow extends UISliceSprite {
|
||||
this.changePosition(positionXStepper.value, null);
|
||||
};
|
||||
members.push(positionXStepper);
|
||||
addLabelOn(positionXStepper, "Position (X,Y)");
|
||||
addLabelOn(positionXStepper, translate("position"));
|
||||
|
||||
members.push(positionXYComma = new UIText(positionXStepper.x+104-32+0, positionXStepper.y + 9, 0, ",", 22));
|
||||
|
||||
@@ -57,17 +61,17 @@ class CharacterPropertiesWindow extends UISliceSprite {
|
||||
this.changeScale(scaleStepper.value);
|
||||
};
|
||||
members.push(scaleStepper);
|
||||
addLabelOn(scaleStepper, "Scale");
|
||||
addLabelOn(scaleStepper, translate("scale"));
|
||||
|
||||
editCharacterButton = new UIButton(scaleStepper.x + 90 -32 + 26, scaleStepper.y-20, "Edit Info", editCharacterInfoUI, 120, 24);
|
||||
editCharacterButton = new UIButton(scaleStepper.x + 90 -32 + 26, scaleStepper.y-20, translate("editInfo"), editCharacterInfoUI, 120, 24);
|
||||
editCharacterButton.field.size -= 2;
|
||||
members.push(editCharacterButton);
|
||||
|
||||
editSpriteButton = new UIButton(editCharacterButton.x, editCharacterButton.y+24+6, "Edit Sprite", editCharacterSpriteUI, 120, 24);
|
||||
editSpriteButton = new UIButton(editCharacterButton.x, editCharacterButton.y+24+6, translate("editSprite"), editCharacterSpriteUI, 120, 24);
|
||||
editSpriteButton.field.size -= 2;
|
||||
members.push(editSpriteButton);
|
||||
|
||||
flipXCheckbox = new UICheckbox(scaleStepper.x+22, scaleStepper.y+32+14, "Flipped?", character.isPlayer ? !character.__baseFlipped : character.__baseFlipped);
|
||||
flipXCheckbox = new UICheckbox(scaleStepper.x+22, scaleStepper.y+32+14, translate("charFlipped"), character.isPlayer ? !character.__baseFlipped : character.__baseFlipped);
|
||||
flipXCheckbox.onChecked = (checked:Bool) -> {this.changeFlipX(checked);};
|
||||
members.push(flipXCheckbox);
|
||||
|
||||
@@ -77,7 +81,7 @@ class CharacterPropertiesWindow extends UISliceSprite {
|
||||
this.changeCamPosition(cameraXStepper.value, null);
|
||||
};
|
||||
members.push(cameraXStepper);
|
||||
addLabelOn(cameraXStepper, "Camera Position (X,Y)");
|
||||
addLabelOn(cameraXStepper, translate("camPosition"));
|
||||
|
||||
members.push(cameraXYComma = new UIText(cameraXStepper.x + 104-32+0, cameraXStepper.y+9, 0, ",", 22));
|
||||
|
||||
@@ -88,7 +92,7 @@ class CharacterPropertiesWindow extends UISliceSprite {
|
||||
};
|
||||
members.push(cameraYStepper);
|
||||
|
||||
antialiasingCheckbox = new UICheckbox(scaleStepper.x+22, flipXCheckbox.y+32, "Antialiased?", character.antialiasing);
|
||||
antialiasingCheckbox = new UICheckbox(scaleStepper.x+22, flipXCheckbox.y+32, translate("antialiased"), character.antialiasing);
|
||||
antialiasingCheckbox.onChecked = (checked:Bool) -> {this.changeAntialiasing(checked);};
|
||||
members.push(antialiasingCheckbox);
|
||||
|
||||
@@ -97,7 +101,7 @@ class CharacterPropertiesWindow extends UISliceSprite {
|
||||
CharacterEditor.instance.changeStagePosition(testAsDropDown.options[index]);
|
||||
};
|
||||
members.push(testAsDropDown);
|
||||
addLabelOn(testAsDropDown, "Test Character As...");
|
||||
addLabelOn(testAsDropDown, translate("testCharAs"));
|
||||
testAsDropDown.bWidth = 193; //REFUSES TO FUCKING SET TO 170 PIECE OF SHIT!!
|
||||
|
||||
designedAsDropDown = new UIDropDown(testAsDropDown.x+193+22, testAsDropDown.y, 193, 32, ["BOYFRIEND", "DAD"], character.playerOffsets ? 0 : 1);
|
||||
@@ -105,7 +109,7 @@ class CharacterPropertiesWindow extends UISliceSprite {
|
||||
CharacterEditor.instance.changeCharacterDesginedAs(designedAsDropDown.options[index] == "BOYFRIEND");
|
||||
};
|
||||
members.push(designedAsDropDown);
|
||||
addLabelOn(designedAsDropDown, "Char Desgined As...");
|
||||
addLabelOn(designedAsDropDown, translate("charDesign"));
|
||||
designedAsDropDown.bWidth = 193;
|
||||
|
||||
alpha = 0.7;
|
||||
|
||||
@@ -12,6 +12,9 @@ import funkin.backend.assets.ModsFolder;
|
||||
|
||||
class CharacterSelection extends EditorTreeMenu
|
||||
{
|
||||
inline function translate(id:String, ?args:Array<Dynamic>)
|
||||
return TU.translate("characterSelection." + id, args);
|
||||
|
||||
public var modsList:Array<String> = [];
|
||||
|
||||
public override function create()
|
||||
@@ -34,13 +37,13 @@ class CharacterSelection extends EditorTreeMenu
|
||||
if(char.endsWith("/")) {
|
||||
var folderName = CoolUtil.getFilename(char.substr(0, char.length-1));
|
||||
|
||||
list.push(new TextOption(folderName + " >", "Press ACCEPT to edit this folder.", function() {
|
||||
list.push(new TextOption(folderName + " >", translate("acceptFolder"), function() {
|
||||
var newModsList = Character.getList(isMods, true, char);
|
||||
var newList:Array<OptionType> = generateList(newModsList, isMods, folderPath + folderName + "/");
|
||||
optionsTree.add(new OptionsScreen(folderPath + folderName, "Select a character to edit in " + folderPath + folderName + "/", newList));
|
||||
optionsTree.add(new OptionsScreen(folderPath + folderName, translate("desc-folder", [folderPath + folderName + "/"]), newList));
|
||||
}));
|
||||
} else {
|
||||
list.push(new IconOption(char, "Press ACCEPT to edit this character.", Character.getIconFromCharName(folderPath + char, char),
|
||||
list.push(new IconOption(char, translate("acceptCharacter"), Character.getIconFromCharName(folderPath + char, char),
|
||||
function() {
|
||||
FlxG.switchState(new CharacterEditor(folderPath + char));
|
||||
})
|
||||
@@ -48,7 +51,7 @@ class CharacterSelection extends EditorTreeMenu
|
||||
}
|
||||
}
|
||||
|
||||
list.insert(0, new NewOption("New Character", "Press ACCEPT to create a new character.", function() {
|
||||
list.insert(0, new NewOption(translate("newCharacter"), translate("newCharacterDesc"), function() {
|
||||
openSubState(new CharacterCreationScreen(createCharacter));
|
||||
}));
|
||||
|
||||
@@ -57,7 +60,7 @@ class CharacterSelection extends EditorTreeMenu
|
||||
|
||||
var list = generateList(modsList, isMods);
|
||||
|
||||
main = new OptionsScreen("Character Editor", "Select a character to edit", list);
|
||||
main = new OptionsScreen(TU.translate("editor.character.name"), translate("desc"), list);
|
||||
|
||||
DiscordUtil.call("onEditorTreeLoaded", ["Character Editor"]);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ class CharacterSpriteScreen extends UISubstateWindow {
|
||||
public var saveButton:UIButton;
|
||||
public var closeButton:UIButton;
|
||||
|
||||
inline function translate(id:String, ?args:Array<Dynamic>)
|
||||
return TU.translate("characterEditor.characterSpriteScreen." + id, args);
|
||||
|
||||
public function new(imagePath:String, ?onSave:(String, Bool)->Void) {
|
||||
super();
|
||||
this.imagePath = imagePath;
|
||||
@@ -23,7 +26,7 @@ class CharacterSpriteScreen extends UISubstateWindow {
|
||||
}
|
||||
|
||||
public override function create() {
|
||||
winTitle = 'Edit character sprite';
|
||||
winTitle = translate("win-title");
|
||||
winWidth = 360; winHeight = 183;
|
||||
|
||||
function addLabelOn(ui:UISprite, text:String):UIText {
|
||||
@@ -36,13 +39,13 @@ class CharacterSpriteScreen extends UISubstateWindow {
|
||||
|
||||
imageExplorer = new UIImageExplorer(20, windowSpr.y + 30 + 16 + 20, imagePath, 320, 58, (_, _) -> {onLoadImage();}, "images/characters");
|
||||
add(imageExplorer);
|
||||
addLabelOn(imageExplorer, "Character Image File").applyMarkup(
|
||||
"Character Image File $* Required$",
|
||||
addLabelOn(imageExplorer, "").applyMarkup(
|
||||
translate('charImageFile'),
|
||||
[new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFAD1212), "$")]);
|
||||
|
||||
ogImageSaveData = imageExplorer.getSaveData();
|
||||
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20, windowSpr.y + windowSpr.bHeight - 20, "Save & Close", function() {
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20, windowSpr.y + windowSpr.bHeight - 20, TU.translate("editor.saveClose"), function() {
|
||||
addToUndo(); // should be async?? -lunar
|
||||
imageExplorer.saveFiles('${Paths.getAssetsRoot()}/images/characters', () -> {
|
||||
onSave('${imageExplorer.saveData.directory.length > 0 ? '${imageExplorer.saveData.directory}/' : ""}' + imageExplorer.imageName, imageExplorer.isAtlas);
|
||||
@@ -53,7 +56,7 @@ class CharacterSpriteScreen extends UISubstateWindow {
|
||||
saveButton.y -= saveButton.bHeight;
|
||||
saveButton.selectable = false;
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 20, saveButton.y, "Cancel", function() {
|
||||
closeButton = new UIButton(saveButton.x - 20, saveButton.y, TU.translate("editor.cancel"), function() {
|
||||
close();
|
||||
}, 125);
|
||||
closeButton.color = 0xFFFF0000;
|
||||
|
||||
@@ -26,7 +26,7 @@ class ChartCreationScreen extends UISubstateWindow {
|
||||
}
|
||||
|
||||
public override function create() {
|
||||
winTitle = "Creating New Chart";
|
||||
winTitle = TU.translate("chartCreation.title");
|
||||
|
||||
winWidth = 652;
|
||||
winHeight = 600;
|
||||
@@ -43,11 +43,11 @@ class ChartCreationScreen extends UISubstateWindow {
|
||||
if (charFileList.length == 0) charFileList = Character.getList(false);
|
||||
|
||||
var chartTitle:UIText;
|
||||
add(chartTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Chart Info", 28));
|
||||
add(chartTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, TU.translate("chartCreation.info"), 28));
|
||||
|
||||
difficultyNameTextBox = new UITextBox(chartTitle.x, chartTitle.y + chartTitle.height + 36, "difficulty", 200);
|
||||
add(difficultyNameTextBox);
|
||||
addLabelOn(difficultyNameTextBox, "Difficulty Name");
|
||||
addLabelOn(difficultyNameTextBox, TU.translate("chartCreation.diffName"));
|
||||
|
||||
var stageFileList = Stage.getList(true);
|
||||
if (stageFileList.length == 0) stageFileList = Stage.getList(false);
|
||||
@@ -55,7 +55,7 @@ class ChartCreationScreen extends UISubstateWindow {
|
||||
stageTextBox = new UIAutoCompleteTextBox(difficultyNameTextBox.x + 200 + 26, difficultyNameTextBox.y, "stage", 180);
|
||||
stageTextBox.suggestItems = stageFileList;
|
||||
add(stageTextBox);
|
||||
addLabelOn(stageTextBox, "Stage");
|
||||
addLabelOn(stageTextBox, TU.translate("chartCreation.stage"));
|
||||
|
||||
scrollSpeedTextBox = new UINumericStepper(stageTextBox.x + 180 + 26, difficultyNameTextBox.y, 1.0, 0.1, 2, null, null, 90);
|
||||
scrollSpeedTextBox.onChange = function (text:String) {
|
||||
@@ -65,7 +65,7 @@ class ChartCreationScreen extends UISubstateWindow {
|
||||
button.scrollSpeedStepper.value = scrollSpeedTextBox.value;
|
||||
}
|
||||
add(scrollSpeedTextBox);
|
||||
addLabelOn(scrollSpeedTextBox, "Scroll Speed");
|
||||
addLabelOn(scrollSpeedTextBox, TU.translate("chartCreation.scrollSpeed"));
|
||||
|
||||
strumLineList = new UIButtonList<StrumLineButton>(difficultyNameTextBox.x, difficultyNameTextBox.y+difficultyNameTextBox.bHeight+36, 620, (552-179)-16, null, FlxPoint.get(620, 246), null, 6);
|
||||
strumLineList.frames = Paths.getFrames('editors/ui/inputbox');
|
||||
@@ -113,20 +113,21 @@ class ChartCreationScreen extends UISubstateWindow {
|
||||
}, strumLineList));
|
||||
|
||||
add(strumLineList);
|
||||
addLabelOn(strumLineList, "Strumlines").applyMarkup(
|
||||
"Strumlines $* Atleast 1 Strumline Required$",
|
||||
addLabelOn(strumLineList, "").applyMarkup(
|
||||
TU.translate("chartCreation.strumLineList"),
|
||||
[new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFAD1212), "$")]);
|
||||
|
||||
strumLineList.dragCallback = (object:StrumLineButton, oldIndex:Int, newIndex:Int) -> {object.idText.text = 'Strumline - #${newIndex}';};
|
||||
var strumlineFormat = TU.getRaw("chartCreation.strumLine.format");
|
||||
strumLineList.dragCallback = (object:StrumLineButton, oldIndex:Int, newIndex:Int) -> {object.idText.text = strumlineFormat.format([newIndex]);};
|
||||
scrollSpeedTextBox.onChange(scrollSpeedTextBox.label.text);
|
||||
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, "Save & Close", function() {
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() {
|
||||
createChart();
|
||||
close();
|
||||
}, 125);
|
||||
add(saveButton);
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 20 - saveButton.bWidth, saveButton.y, "Cancel", function() {
|
||||
closeButton = new UIButton(saveButton.x - 20 - saveButton.bWidth, saveButton.y, TU.translate("editor.close"), function() {
|
||||
close();
|
||||
}, 125);
|
||||
add(closeButton);
|
||||
@@ -211,12 +212,12 @@ class StrumLineButton extends UIButton {
|
||||
charactersList.cameraSpacing = 0;
|
||||
|
||||
charactersList.addButton.callback = function()
|
||||
charactersList.add(new CompactCharacterButton("New Char", subState.charFileList, charactersList));
|
||||
charactersList.add(new CompactCharacterButton(TU.translate("charterStrumLine.newChar"), subState.charFileList, charactersList));
|
||||
for (character in strumLine.characters)
|
||||
charactersList.add(new CompactCharacterButton(character, subState.charFileList, charactersList));
|
||||
|
||||
members.push(charactersList);
|
||||
idText = addLabelOn(charactersList, 'Strumline - #$id');
|
||||
idText = addLabelOn(charactersList, TU.translate("chartCreation.strumLine.format", [id]));
|
||||
|
||||
cameraClipShader = new CustomShader("engine/cameraClip");
|
||||
cameraClipShader.hset("clipRect", [0, 0, 100, 100]);
|
||||
@@ -224,17 +225,17 @@ class StrumLineButton extends UIButton {
|
||||
|
||||
typeDropdown = new UIDropDown(charactersList.x + charactersList.bWidth + 16, 8+26, 200, 32, ["OPPONENT", "PLAYER", "ADDITIONAL"], strumLine.type);
|
||||
members.push(typeDropdown);
|
||||
addLabelOn(typeDropdown, "Type");
|
||||
addLabelOn(typeDropdown, TU.translate("charterStrumLine.type"));
|
||||
|
||||
var stagePositionI = strumLine.position == null ? strumLine.type : ["DAD", "BOYFRIEND", "GIRLFRIEND"].indexOf(strumLine.position.toUpperCase());
|
||||
|
||||
stagePositionDropdown = new UIDropDown(typeDropdown.x + 200 - 32 + 26, typeDropdown.y, 200, 32, ["DAD", "BOYFRIEND", "GIRLFRIEND"], stagePositionI);
|
||||
members.push(stagePositionDropdown);
|
||||
addLabelOn(stagePositionDropdown, "Stage Position");
|
||||
addLabelOn(stagePositionDropdown, TU.translate("charterStrumLine.stagePos"));
|
||||
|
||||
hudScaleStepper = new UINumericStepper(typeDropdown.x, typeDropdown.y + 64, strumLine.strumScale == null ? 1 : strumLine.strumScale, 0.001, 2, null, null, 74);
|
||||
members.push(hudScaleStepper);
|
||||
addLabelOn(hudScaleStepper, "Scale");
|
||||
addLabelOn(hudScaleStepper, TU.translate("charterStrumLine.scale"));
|
||||
|
||||
var strOffset:Float = strumLine.strumLinePos == null ? (strumLine.type == 1 ? 0.75 : 0.25) : strumLine.strumLinePos;
|
||||
|
||||
@@ -244,22 +245,22 @@ class StrumLineButton extends UIButton {
|
||||
|
||||
hudXStepper = new UINumericStepper(hudScaleStepper.x + 80 - 32 + 26, hudScaleStepper.y, startingPos.x, 0.01, 2, 0, 2, 84);
|
||||
members.push(hudXStepper);
|
||||
addLabelOn(hudXStepper, "Hud Position (X [Ratio 0-1],Y)");
|
||||
addLabelOn(hudXStepper, TU.translate("charterStrumLine.hudPos"));
|
||||
|
||||
members.push(XYComma = new UIText(hudXStepper.x + 84 - 32 + 0, hudXStepper.y + 9, 0, ",", 22));
|
||||
|
||||
hudYStepper = new UINumericStepper(hudXStepper.x + 84 - 32 + 26, hudXStepper.y, startingPos.y, 0.001, 2, null, null, 84);
|
||||
members.push(hudYStepper);
|
||||
|
||||
visibleCheckbox = new UICheckbox(hudYStepper.x + hudYStepper.bWidth + 42, hudYStepper.y + 9, "Visible?", strumLine.visible == null ? true : strumLine.visible);
|
||||
visibleCheckbox = new UICheckbox(hudYStepper.x + hudYStepper.bWidth + 42, hudYStepper.y + 9, TU.translate("charterStrumLine.visible"), strumLine.visible == null ? true : strumLine.visible);
|
||||
members.push(visibleCheckbox);
|
||||
|
||||
scrollSpeedStepper = new UINumericStepper(typeDropdown.x, typeDropdown.y + 128, strumLine.scrollSpeed, 0.1, 2, 0, 10, 82);
|
||||
scrollSpeedStepper.selectable = strumLine.scrollSpeed != null;
|
||||
members.push(scrollSpeedStepper);
|
||||
addLabelOn(scrollSpeedStepper, "Scroll Speed");
|
||||
addLabelOn(scrollSpeedStepper, TU.translate("charterStrumLine.scrollSpeed"));
|
||||
|
||||
usesChartScrollSpeed = new UICheckbox(scrollSpeedStepper.x + 104, typeDropdown.y + 135, "Uses charts scroll speed?", strumLine.scrollSpeed == null);
|
||||
usesChartScrollSpeed = new UICheckbox(scrollSpeedStepper.x + 104, typeDropdown.y + 135, TU.translate("charterStrumLine.useChartScrollSpeed"), strumLine.scrollSpeed == null);
|
||||
usesChartScrollSpeed.onChecked = function(b) {
|
||||
if(b) {
|
||||
scrollSpeedStepper.value = subState.scrollSpeedTextBox.value;
|
||||
@@ -318,7 +319,8 @@ class CompactCharacterButton extends UIButton {
|
||||
autoAlpha = false;
|
||||
|
||||
charIcon = new HealthIcon(funkin.game.Character.getIconFromCharName(char));
|
||||
charIcon.scale.set(0.2, 0.2);
|
||||
var size = Std.int(150 * 0.2);
|
||||
charIcon.setUnstretchedGraphicSize(size, size, true);
|
||||
charIcon.updateHitbox();
|
||||
charIcon.setPosition(10, bHeight/2 - charIcon.height / 2);
|
||||
charIcon.scrollFactor.set(1,1);
|
||||
@@ -330,11 +332,10 @@ class CompactCharacterButton extends UIButton {
|
||||
textBox.antialiasing = true;
|
||||
textBox.onChange = function(char:String) {
|
||||
char = funkin.game.Character.getIconFromCharName(char);
|
||||
var image = Paths.image("icons/" + char);
|
||||
if(!Assets.exists(image))
|
||||
image = Paths.image("icons/" + Flags.DEFAULT_HEALTH_ICON);
|
||||
charIcon.loadGraphic(image, true, 150, 150);
|
||||
charIcon.setIcon(char);
|
||||
charIcon.setUnstretchedGraphicSize(size, size, true);
|
||||
charIcon.updateHitbox();
|
||||
charIcon.setPosition(10, bHeight/2 - charIcon.height / 2);
|
||||
}
|
||||
|
||||
deleteButton = new UIButton(textBox.x + 115 + 16, bHeight/2 - (32/2), null, function () {
|
||||
|
||||
@@ -29,7 +29,7 @@ class ChartDataScreen extends UISubstateWindow {
|
||||
Charter.instance.vocals.pause();
|
||||
for (strumLine in Charter.instance.strumLines.members) strumLine.vocals.pause();
|
||||
|
||||
winTitle = 'Chart Properties';
|
||||
winTitle = TU.translate("chartDataScreen.title");
|
||||
winWidth = 420; winHeight = 230; // guys look, the funny numbers!
|
||||
|
||||
super.create();
|
||||
@@ -38,7 +38,7 @@ class ChartDataScreen extends UISubstateWindow {
|
||||
add(new UIText(ui.x, ui.y - 24, 0, text));
|
||||
|
||||
var title:UIText;
|
||||
add(title = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Edit Chart Data", 28));
|
||||
add(title = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, TU.translate("chartDataScreen.editChartData"), 28));
|
||||
|
||||
var stageFileList = Stage.getList(true);
|
||||
if (stageFileList.length == 0) stageFileList = Stage.getList(false);
|
||||
@@ -46,19 +46,19 @@ class ChartDataScreen extends UISubstateWindow {
|
||||
stageTextBox = new UIAutoCompleteTextBox(title.x, title.y + title.height + 38, PlayState.SONG.stage, 200);
|
||||
stageTextBox.suggestItems = stageFileList;
|
||||
add(stageTextBox);
|
||||
addLabelOn(stageTextBox, "Stage");
|
||||
addLabelOn(stageTextBox, TU.translate("chartDataScreen.stage"));
|
||||
|
||||
scrollSpeedStepper = new UINumericStepper(stageTextBox.x + 200 + 26, stageTextBox.y, data.scrollSpeed, 0.1, 2, 0, 10, 82);
|
||||
add(scrollSpeedStepper);
|
||||
addLabelOn(scrollSpeedStepper, "Scroll Speed");
|
||||
addLabelOn(scrollSpeedStepper, TU.translate("chartDataScreen.scrollSpeed"));
|
||||
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, "Save & Close", function() {
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() {
|
||||
saveInfo();
|
||||
close();
|
||||
}, 125);
|
||||
add(saveButton);
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 20, saveButton.y, "Close", function() {
|
||||
closeButton = new UIButton(saveButton.x - 20, saveButton.y, TU.translate("editor.close"), function() {
|
||||
close();
|
||||
}, 125);
|
||||
add(closeButton);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ class CharterAutoSaveUI extends UISliceSprite {
|
||||
icon.animation.play("icon"); icon.animation.curAnim.curFrame = 0;
|
||||
members.push(icon);
|
||||
|
||||
members.push(autosavingText = new UIText(x+12+10+4, y+8, 0, "Autosaving in 10 seconds...", 12));
|
||||
members.push(autosavingText = new UIText(x+12+10+4, y+8, 0, TU.translate("editor.autosavingIn", ["?", "..."]), 12));
|
||||
|
||||
progressBarBack = new FlxSprite(x + 10, y + bHeight - 20).makeGraphic(Std.int(bWidth-20), 10, 0x00000000, true);
|
||||
progressBarBack.drawRoundRect(0, 0, progressBarBack.width, progressBarBack.height, 4, 6, 0xFF727272, null, {smoothing: false});
|
||||
@@ -45,7 +45,8 @@ class CharterAutoSaveUI extends UISliceSprite {
|
||||
|
||||
icon.animation.curAnim.curFrame = 2;
|
||||
(new FlxTimer()).start(1, (_) -> {disappearAnimation(true);});
|
||||
autosavingText.text += " (Canceled)!"; cancelButton.visible = false;
|
||||
autosavingText.text += " " + TU.translate("editor.autosavingCanceledSuffix");
|
||||
cancelButton.visible = false;
|
||||
for (member in [this, autosavingText]) member.color = 0xFFE67F7F;
|
||||
for (member in [progressBar, progressBarBack]) member.color = 0xFFE67F7F;
|
||||
}, 24, 14);
|
||||
@@ -68,8 +69,10 @@ class CharterAutoSaveUI extends UISliceSprite {
|
||||
|
||||
__tween = FlxTween.num(0, 1, time, null, (v:Float) -> {
|
||||
if ((progress = v) < .95) {
|
||||
autosavingText.text = 'Autosaving in ${Math.min(Math.round(time), 1+Math.floor(Math.abs(time-(progress*time))))} seconds';
|
||||
autosavingText.text += [for (i in 0...(Math.floor((progress*time*3)%4))) "."].join("");
|
||||
autosavingText.text = TU.translate("editor.autosavingIn", [
|
||||
Math.min(Math.round(time), 1+Math.floor(Math.abs(time-(progress*time)))),
|
||||
".".repeat(Math.floor((progress*time*3)%4))
|
||||
]);
|
||||
}
|
||||
});
|
||||
__timer = new FlxTimer();
|
||||
@@ -79,6 +82,8 @@ class CharterAutoSaveUI extends UISliceSprite {
|
||||
icon.animation.curAnim.curFrame = 1;
|
||||
for (member in [this, progressBar, progressBarBack, autosavingText]) member.color = 0xFFA3EC95;
|
||||
|
||||
FlxG.sound.play(Paths.sound(Flags.DEFAULT_EDITOR_AUTOSAVE_SOUND));
|
||||
|
||||
(new FlxTimer()).start(1, (_) -> {disappearAnimation();});
|
||||
});
|
||||
}
|
||||
@@ -105,11 +110,11 @@ class CharterAutoSaveUI extends UISliceSprite {
|
||||
bar.follow(this, 10, bHeight-20);
|
||||
bar.alpha = alpha;
|
||||
}
|
||||
progressBar.clipRect = progressBar.clipRect.set(0, 0, progressBar.width*progress,progressBar.height);
|
||||
progressBar.rawClipRect = progressBar.rawClipRect.set(0, 0, progressBar.frameWidth*progress,progressBar.frameHeight);
|
||||
}
|
||||
|
||||
public function appearAnimation() {
|
||||
autosavingText.text = "Autosaving in seconds..."; progress = 0; icon.animation.curAnim.curFrame = 0;
|
||||
progress = 0; icon.animation.curAnim.curFrame = 0;
|
||||
for (member in [this, autosavingText, progressBar, progressBarBack]) member.color = 0xFFFFFFFF;
|
||||
|
||||
x = -(320); alpha=0; FlxTween.cancelTweensOf(this); cancelled = false; cancelButton.visible = true;
|
||||
|
||||
@@ -43,8 +43,6 @@ class CharterBackdropGroup extends FlxTypedGroup<CharterBackdrop> {
|
||||
|
||||
public var conductorSprY:Float = 0;
|
||||
public var bottomLimitY:Float = 0;
|
||||
public var sectionsVisible:Bool = true;
|
||||
public var beatsVisible:Bool = true;
|
||||
|
||||
// Just here so you can update display sprites all dat and above
|
||||
public var strumlinesAmount:Int = 0;
|
||||
@@ -80,8 +78,6 @@ class CharterBackdropGroup extends FlxTypedGroup<CharterBackdrop> {
|
||||
|
||||
grid.conductorFollowerSpr.y = conductorSprY;
|
||||
grid.bottomSeparator.y = (grid.bottomLimit.y = bottomLimitY)-2;
|
||||
grid.sectionSeparator.visible = sectionsVisible;
|
||||
grid.beatSeparator.visible = beatsVisible;
|
||||
|
||||
grid.waveformSprite.shader = strumLine.waveformShader;
|
||||
|
||||
@@ -174,8 +170,7 @@ class CharterBackdrop extends FlxTypedGroup<FlxBasic> {
|
||||
public var waveformSprite:FlxSprite;
|
||||
|
||||
public var conductorFollowerSpr:FlxSprite;
|
||||
public var beatSeparator:FlxBackdrop;
|
||||
public var sectionSeparator:FlxBackdrop;
|
||||
public var beatSeparator:CharterGridSeperator;
|
||||
|
||||
public var notesGroup:NotesDrawGroup = new NotesDrawGroup();
|
||||
public var strumLine:CharterStrumline;
|
||||
@@ -197,23 +192,14 @@ class CharterBackdrop extends FlxTypedGroup<FlxBasic> {
|
||||
waveformSprite.updateHitbox();
|
||||
add(waveformSprite);
|
||||
|
||||
sectionSeparator = new FlxBackdrop(null, Y, 0, 0);
|
||||
sectionSeparator.y = -2;
|
||||
sectionSeparator.visible = Options.charterShowSections;
|
||||
beatSeparator = new CharterGridSeperator();
|
||||
beatSeparator.makeSolid(1, 1, -1);
|
||||
beatSeparator.alpha = 0.5;
|
||||
beatSeparator.scrollFactor.set(1, 1);
|
||||
beatSeparator.scale.set((4 * 40), 2);
|
||||
beatSeparator.updateHitbox();
|
||||
|
||||
beatSeparator = new FlxBackdrop(null, Y, 0, 0);
|
||||
beatSeparator.y = -1;
|
||||
beatSeparator.visible = Options.charterShowBeats;
|
||||
|
||||
for(sep in [sectionSeparator, beatSeparator]) {
|
||||
sep.makeSolid(1, 1, -1);
|
||||
sep.alpha = 0.5;
|
||||
sep.scrollFactor.set(1, 1);
|
||||
sep.scale.set((4 * 40), sep == sectionSeparator ? 4 : 2);
|
||||
sep.updateHitbox();
|
||||
}
|
||||
add(beatSeparator);
|
||||
add(sectionSeparator);
|
||||
add(notesGroup);
|
||||
|
||||
bottomSeparator = new FlxSprite(0,-2);
|
||||
@@ -263,7 +249,7 @@ class CharterBackdrop extends FlxTypedGroup<FlxBasic> {
|
||||
keyCount = strumLine.keyCount;
|
||||
} else alpha = 0.9;
|
||||
|
||||
for (spr in [gridBackDrop, sectionSeparator, beatSeparator, topLimit, bottomLimit,
|
||||
for (spr in [gridBackDrop, beatSeparator, topLimit, bottomLimit,
|
||||
topSeparator, bottomSeparator, conductorFollowerSpr, waveformSprite]) {
|
||||
spr.x = x; if (spr != waveformSprite) spr.alpha = alpha;
|
||||
spr.cameras = this.cameras;
|
||||
@@ -274,9 +260,6 @@ class CharterBackdrop extends FlxTypedGroup<FlxBasic> {
|
||||
if (__lastKeyCount != keyCount) gridShader.hset("segments", keyCount);
|
||||
__lastKeyCount = keyCount;
|
||||
|
||||
sectionSeparator.spacing.y = (10 * Conductor.beatsPerMeasure * Conductor.stepsPerBeat) - 1;
|
||||
beatSeparator.spacing.y = (20 * Conductor.stepsPerBeat) - 1;
|
||||
|
||||
topLimit.scale.set(keyCount * 40, Math.ceil(FlxG.height / cameras[0].zoom));
|
||||
topLimit.updateHitbox();
|
||||
topLimit.y = -topLimit.height;
|
||||
@@ -284,7 +267,7 @@ class CharterBackdrop extends FlxTypedGroup<FlxBasic> {
|
||||
bottomLimit.scale.set(keyCount * 40, Math.ceil(FlxG.height / cameras[0].zoom));
|
||||
bottomLimit.updateHitbox();
|
||||
|
||||
for (spr in [conductorFollowerSpr, sectionSeparator, beatSeparator, topSeparator, bottomSeparator]) {
|
||||
for (spr in [conductorFollowerSpr, beatSeparator, topSeparator, bottomSeparator]) {
|
||||
spr.scale.x = keyCount * 40;
|
||||
spr.updateHitbox();
|
||||
}
|
||||
@@ -310,9 +293,161 @@ class CharterBackdrop extends FlxTypedGroup<FlxBasic> {
|
||||
}
|
||||
}
|
||||
|
||||
class CharterGridSeperatorBase extends FlxSprite {
|
||||
|
||||
private static var minStep:Float = 0;
|
||||
private static var maxStep:Float = 0;
|
||||
|
||||
private static var minBeat:Float = 0;
|
||||
private static var maxBeat:Float = 0;
|
||||
|
||||
private static var minMeasure:Float = 0;
|
||||
private static var maxMeasure:Float = 0;
|
||||
|
||||
private static var lastMinBeat:Float = -1;
|
||||
private static var lastMaxBeat:Float = -1;
|
||||
|
||||
private static var lastMinMeasure:Float = -1;
|
||||
private static var lastMaxMeasure:Float = -1;
|
||||
|
||||
public static var lastConductorSprY:Float = Math.NEGATIVE_INFINITY;
|
||||
|
||||
private static var beatStepTimes:Array<Float> = [];
|
||||
private static var measureStepTimes:Array<Float> = [];
|
||||
private static var timeSignatureChangeGaps:Array<Float> = [];
|
||||
|
||||
private function recalculateBeats() {
|
||||
var conductorSprY = Charter.instance.gridBackdrops.conductorSprY;
|
||||
if (conductorSprY == lastConductorSprY) return;
|
||||
|
||||
var zoomOffset = ((FlxG.height * (1/cameras[0].zoom)) * 0.5);
|
||||
|
||||
minStep = (conductorSprY - zoomOffset)/40;
|
||||
maxStep = (conductorSprY + zoomOffset)/40;
|
||||
|
||||
var minTime:Float = Conductor.getStepsInTime(minStep);
|
||||
var maxTime:Float = Conductor.getStepsInTime(maxStep);
|
||||
|
||||
var minBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(minTime)];
|
||||
var maxBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(maxTime)];
|
||||
|
||||
minBeat = Conductor.getTimeInBeats(minTime);
|
||||
maxBeat = Conductor.getTimeInBeats(maxTime);
|
||||
|
||||
minMeasure = minBpmChange.measureTime + (minBeat - minBpmChange.beatTime) / minBpmChange.beatsPerMeasure;
|
||||
maxMeasure = maxBpmChange.measureTime + (maxBeat - maxBpmChange.beatTime) / maxBpmChange.beatsPerMeasure;
|
||||
|
||||
//cap out the beats/measures at the end of the song
|
||||
var endTime = Conductor.getStepsInTime(Charter.instance.__endStep);
|
||||
var endBeat = Conductor.getTimeInBeats(endTime);
|
||||
var endBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(endTime)];
|
||||
var endMeasure = endBpmChange.measureTime + (endBeat - endBpmChange.beatTime) / endBpmChange.beatsPerMeasure;
|
||||
|
||||
if (maxBeat > endBeat) maxBeat = endBeat;
|
||||
if (maxMeasure > endMeasure) maxMeasure = endMeasure;
|
||||
if (minMeasure < 0) minMeasure = 0;
|
||||
if (minBeat < 0) minBeat = 0;
|
||||
|
||||
//only calculate if needed
|
||||
if ((minBeat != lastMinBeat) || (maxBeat != lastMaxBeat) || (minMeasure != lastMinMeasure) || (maxMeasure != lastMaxMeasure) || lastConductorSprY == Math.NEGATIVE_INFINITY) {
|
||||
calculateTimeSignatureGaps();
|
||||
calculateStepTimes();
|
||||
lastMinBeat = minBeat;
|
||||
lastMaxBeat = maxBeat;
|
||||
lastMinMeasure = minMeasure;
|
||||
lastMaxMeasure = maxMeasure;
|
||||
}
|
||||
|
||||
lastConductorSprY = conductorSprY;
|
||||
}
|
||||
|
||||
private inline function calculateTimeSignatureGaps() {
|
||||
//for time signatures that start mid step
|
||||
timeSignatureChangeGaps.splice(0, timeSignatureChangeGaps.length);
|
||||
for (i => change in Conductor.bpmChangeMap) {
|
||||
if (change.stepTime >= minStep && change.stepTime <= maxStep) {
|
||||
//get step while ignoring the current change
|
||||
var index:Int = CoolUtil.boundInt(i-1, 0, Conductor.bpmChangeMap.length - 1);
|
||||
var step:Float = Conductor.getTimeWithBPMInSteps(change.songTime, index, Conductor.getTimeWithIndexInBPM(change.songTime, index));
|
||||
|
||||
if (Math.ceil(step) - step > 0 && (step - Math.floor(step)) > FlxMath.EPSILON) { //mid step change
|
||||
timeSignatureChangeGaps.push(step);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline function calculateStepTimes() {
|
||||
beatStepTimes.splice(0, beatStepTimes.length);
|
||||
for (i in Math.floor(minBeat)...Math.ceil(maxBeat)) {
|
||||
beatStepTimes.push(Conductor.getTimeInSteps(Conductor.getBeatsInTime(i)));
|
||||
}
|
||||
measureStepTimes.splice(0, measureStepTimes.length);
|
||||
for (i in Math.floor(minMeasure)...Math.ceil(maxMeasure)) {
|
||||
measureStepTimes.push(Conductor.getTimeInSteps(Conductor.getMeasuresInTime(i)));
|
||||
}
|
||||
}
|
||||
|
||||
override public function draw() {
|
||||
|
||||
//should only need to recalculate once per frame and will be shared across each instance
|
||||
recalculateBeats();
|
||||
|
||||
drawTimeSignatureChangeGaps();
|
||||
|
||||
if (Options.charterShowBeats) drawBeats();
|
||||
if (Options.charterShowSections) drawMeasures();
|
||||
}
|
||||
|
||||
private function drawBeats(offset:Float = 0.0) {
|
||||
for (i in beatStepTimes) {
|
||||
y = (i*40)+offset;
|
||||
super.draw();
|
||||
}
|
||||
}
|
||||
private function drawMeasures(offset:Float = 0.0) {
|
||||
for (i in measureStepTimes) {
|
||||
y = (i*40)+offset;
|
||||
super.draw();
|
||||
}
|
||||
}
|
||||
private function drawTimeSignatureChangeGaps() {
|
||||
if (timeSignatureChangeGaps.length == 0) return;
|
||||
var prevColor = color;
|
||||
var prevBlend = blend;
|
||||
|
||||
color = 0xFF888888;
|
||||
blend = MULTIPLY;
|
||||
|
||||
for (step in timeSignatureChangeGaps) {
|
||||
y = step*40;
|
||||
var diff = Math.ceil(step) - step;
|
||||
scale.y = diff*40;
|
||||
updateHitbox();
|
||||
|
||||
super.draw();
|
||||
}
|
||||
|
||||
color = prevColor;
|
||||
blend = prevBlend;
|
||||
}
|
||||
}
|
||||
|
||||
class CharterGridSeperator extends CharterGridSeperatorBase {
|
||||
override private function drawBeats(offset:Float = 0.0) {
|
||||
scale.y = 2;
|
||||
updateHitbox();
|
||||
super.drawBeats(-2);
|
||||
}
|
||||
override private function drawMeasures(offset:Float = 0.0) {
|
||||
scale.y = 4;
|
||||
updateHitbox();
|
||||
super.drawMeasures(-3);
|
||||
}
|
||||
}
|
||||
|
||||
class EventBackdrop extends FlxBackdrop {
|
||||
public var eventBeatSeparator:FlxBackdrop;
|
||||
public var eventSecSeparator:FlxBackdrop;
|
||||
public var eventBeatSeparator:CharterEventGridSeperator;
|
||||
|
||||
public var topSeparator:FlxSprite;
|
||||
public var bottomSeparator:FlxSprite;
|
||||
@@ -325,25 +460,11 @@ class EventBackdrop extends FlxBackdrop {
|
||||
alpha = 0.9;
|
||||
|
||||
// Separators
|
||||
eventSecSeparator = new FlxBackdrop(null, Y, 0, 0);
|
||||
eventSecSeparator.y = -2;
|
||||
eventSecSeparator.visible = Options.charterShowSections;
|
||||
|
||||
eventBeatSeparator = new FlxBackdrop(null, Y, 0, 0);
|
||||
eventBeatSeparator.y = -1;
|
||||
eventBeatSeparator.visible = Options.charterShowBeats;
|
||||
|
||||
for(sep in [eventSecSeparator, eventBeatSeparator]) {
|
||||
sep.makeSolid(1, 1, -1);
|
||||
sep.alpha = 0.5;
|
||||
sep.scrollFactor.set(1, 1);
|
||||
}
|
||||
|
||||
eventSecSeparator.scale.set(20, 4);
|
||||
eventSecSeparator.updateHitbox();
|
||||
|
||||
eventBeatSeparator.scale.set(10, 2);
|
||||
eventBeatSeparator.updateHitbox();
|
||||
eventBeatSeparator = new CharterEventGridSeperator();
|
||||
eventBeatSeparator.makeSolid(1, 1, -1);
|
||||
eventBeatSeparator.alpha = 0.5;
|
||||
eventBeatSeparator.scrollFactor.set(1, 1);
|
||||
eventBeatSeparator.global = global;
|
||||
|
||||
bottomSeparator = new FlxSprite(0,-2);
|
||||
bottomSeparator.makeSolid(1, 1, -1);
|
||||
@@ -364,23 +485,34 @@ class EventBackdrop extends FlxBackdrop {
|
||||
public override function draw() {
|
||||
super.draw();
|
||||
|
||||
eventSecSeparator.spacing.y = (10 * Conductor.beatsPerMeasure * Conductor.stepsPerBeat) - 1;
|
||||
eventBeatSeparator.spacing.y = (20 * Conductor.stepsPerBeat) - 1;
|
||||
|
||||
eventSecSeparator.cameras = cameras;
|
||||
eventSecSeparator.x = this.global ? x : ((x+width) - 20);
|
||||
if (eventSecSeparator.visible) eventSecSeparator.draw();
|
||||
|
||||
eventBeatSeparator.cameras = cameras;
|
||||
eventBeatSeparator.x = this.global ? x : ((x+width) - 10);
|
||||
if (eventBeatSeparator.visible) eventBeatSeparator.draw();
|
||||
eventBeatSeparator.xPos = global ? x : x+width;
|
||||
eventBeatSeparator.draw();
|
||||
|
||||
topSeparator.x = this.global ? x : ((x+width) - 20);
|
||||
topSeparator.x = global ? x : (x+width) - 20;
|
||||
topSeparator.cameras = this.cameras;
|
||||
if (!eventSecSeparator.visible) topSeparator.draw();
|
||||
if (!Options.charterShowSections) topSeparator.draw();
|
||||
|
||||
bottomSeparator.x = this.global ? x : ((x+width) - 20);
|
||||
bottomSeparator.x = global ? x : (x+width) - 20;
|
||||
bottomSeparator.cameras = this.cameras;
|
||||
bottomSeparator.draw();
|
||||
}
|
||||
}
|
||||
|
||||
class CharterEventGridSeperator extends CharterGridSeperatorBase {
|
||||
public var xPos:Float = 0.0;
|
||||
public var global:Bool = false;
|
||||
override private function drawBeats(offset:Float = 0.0) {
|
||||
scale.set(10, 2);
|
||||
updateHitbox();
|
||||
x = global ? xPos : xPos-10;
|
||||
super.drawBeats(-2);
|
||||
}
|
||||
override private function drawMeasures(offset:Float = 0.0) {
|
||||
scale.set(20, 4);
|
||||
updateHitbox();
|
||||
x = global ? xPos : xPos-20;
|
||||
super.drawMeasures(-3);
|
||||
}
|
||||
override private function drawTimeSignatureChangeGaps() {}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package funkin.editors.charter;
|
||||
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.backend.chart.ChartData.ChartBookmark;
|
||||
|
||||
class CharterBookmarkCreation extends UISubstateWindow {
|
||||
public var saveButton:UIButton;
|
||||
public var closeButton:UIButton;
|
||||
public var textBox:UITextBox;
|
||||
public var colorPicker:UICompactColorwheel;
|
||||
|
||||
var callback:Bool->String->FlxColor->Float->Void;
|
||||
|
||||
var bookmarkStep:Float = 0;
|
||||
|
||||
override public function new(step:Float, close:Bool->String->FlxColor->Float->Void)
|
||||
{
|
||||
super();
|
||||
|
||||
winWidth = 320;
|
||||
winHeight = 250;
|
||||
winTitle = TU.translate("charter.bookmarks.createBookmarkTitle");
|
||||
|
||||
callback = close;
|
||||
|
||||
bookmarkStep = step;
|
||||
}
|
||||
|
||||
public override function create() {
|
||||
FlxG.sound.music.pause();
|
||||
Charter.instance.vocals.pause();
|
||||
|
||||
function addLabelOn(ui:UISprite, text:String):UIText {
|
||||
var text:UIText = new UIText(ui.x, ui.y - 24, 0, text);
|
||||
ui.members.push(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
super.create();
|
||||
|
||||
textBox = new UITextBox(windowSpr.x + 20, windowSpr.y + 70, TU.translate("charter.bookmarks.newBookmarkName"), 276);
|
||||
textBox.antialiasing = true;
|
||||
add(textBox);
|
||||
addLabelOn(textBox, TU.translate("charter.bookmarks.createBookmarkName"));
|
||||
|
||||
colorPicker = new UICompactColorwheel(textBox.x, textBox.y + textBox.bHeight + 34, flixel.util.FlxColor.RED);
|
||||
add(colorPicker);
|
||||
addLabelOn(colorPicker, TU.translate("charter.bookmarks.createBookmarkColor"));
|
||||
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() {
|
||||
callback(true, textBox.label.text, colorPicker.curColor, bookmarkStep);
|
||||
close();
|
||||
}, 125);
|
||||
add(saveButton);
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 20, saveButton.y, TU.translate("editor.close"), function() {
|
||||
callback(false, "", FlxColor.TRANSPARENT, -1);
|
||||
close();
|
||||
}, 125);
|
||||
add(closeButton);
|
||||
closeButton.color = 0xFFFF0000;
|
||||
closeButton.x -= closeButton.bWidth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package funkin.editors.charter;
|
||||
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.backend.chart.ChartData.ChartBookmark;
|
||||
|
||||
class CharterBookmarkList extends UISubstateWindow {
|
||||
public var bookmarkList:UIButtonList<BookmarkButton>;
|
||||
public var saveButton:UIButton;
|
||||
public var closeButton:UIButton;
|
||||
|
||||
public override function create() {
|
||||
FlxG.sound.music.pause();
|
||||
Charter.instance.vocals.pause();
|
||||
|
||||
winTitle = TU.translate("charter.bookmarks.editBookmarkListTitle");
|
||||
winWidth = 380;
|
||||
winHeight = 390;
|
||||
|
||||
super.create();
|
||||
|
||||
var title:UIText;
|
||||
add(title = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, TU.translate("charter.bookmarks.editBookmarksTitle"), 28));
|
||||
|
||||
bookmarkList = new UIButtonList<BookmarkButton>(20, title.y + title.height + 10, winWidth - 40, 342 - 85 - 16, null, FlxPoint.get(winWidth - 40, (342 - 85 - 16)/4), null, 0);
|
||||
bookmarkList.cameraSpacing = 0;
|
||||
|
||||
for (b in Charter.instance.getBookmarkList())
|
||||
bookmarkList.add(new BookmarkButton(0, 0, b, bookmarkList));
|
||||
|
||||
add(bookmarkList);
|
||||
bookmarkList.frames = Paths.getFrames('editors/ui/inputbox');
|
||||
|
||||
bookmarkList.content.remove(bookmarkList.addButton); //i dont need it
|
||||
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() {
|
||||
saveList();
|
||||
close();
|
||||
}, 125);
|
||||
add(saveButton);
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 20, saveButton.y, TU.translate("editor.close"), function() {
|
||||
close();
|
||||
}, 125);
|
||||
add(closeButton);
|
||||
closeButton.color = 0xFFFF0000;
|
||||
closeButton.x -= closeButton.bWidth;
|
||||
}
|
||||
|
||||
public function saveList() {
|
||||
var oldList:Array<ChartBookmark> = Charter.instance.getBookmarkList();
|
||||
var newList:Array<ChartBookmark> = [for (btn in bookmarkList.buttons.members) {time: btn.bookmark.time, name: btn.textBox.label.text, color: btn.bookmark.color}];
|
||||
|
||||
PlayState.SONG.bookmarks = newList;
|
||||
Charter.instance.updateBookmarks();
|
||||
Charter.undos.addToUndo(CEditBookmarks(oldList, newList));
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkButton extends UIButton {
|
||||
public var textBox:UITextBox;
|
||||
public var deleteButton:UIButton;
|
||||
public var deleteIcon:FlxSprite;
|
||||
public var bookmark:ChartBookmark;
|
||||
|
||||
public function new(x:Float, y:Float, bookmark:ChartBookmark, parent:UIButtonList<BookmarkButton>) {
|
||||
super(x, y, '', null, Std.int(parent.buttonSize.x), Std.int(parent.buttonSize.y));
|
||||
this.bookmark = bookmark;
|
||||
autoAlpha = false;
|
||||
|
||||
members.push(textBox = new UITextBox(10, 10, bookmark.name, 276));
|
||||
textBox.antialiasing = true;
|
||||
|
||||
deleteButton = new UIButton(textBox.x + textBox.label.width + 10, textBox.y, null, function () {
|
||||
parent.remove(this);
|
||||
}, 32);
|
||||
deleteButton.color = 0xFFFF0000;
|
||||
deleteButton.autoAlpha = false;
|
||||
members.push(deleteButton);
|
||||
|
||||
deleteIcon = new FlxSprite(deleteButton.x + (15/2), deleteButton.y + 8).loadGraphic(Paths.image('editors/delete-button'));
|
||||
deleteIcon.antialiasing = false;
|
||||
members.push(deleteIcon);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float) {
|
||||
textBox.y = (y + bHeight/2) - (textBox.bHeight/2);
|
||||
deleteButton.x = textBox.x + textBox.bWidth + 14; deleteButton.y = textBox.y;
|
||||
deleteIcon.x = deleteButton.x + (15/2); deleteIcon.y = deleteButton.y + 8;
|
||||
|
||||
deleteButton.selectable = selectable;
|
||||
deleteButton.shouldPress = shouldPress;
|
||||
|
||||
super.update(elapsed);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
package funkin.editors.charter;
|
||||
|
||||
import openfl.display.BitmapData;
|
||||
import openfl.geom.ColorTransform;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.system.FlxAssets.FlxGraphicAsset;
|
||||
import funkin.backend.chart.ChartData.ChartEvent;
|
||||
import funkin.backend.scripting.DummyScript;
|
||||
import funkin.backend.scripting.Script;
|
||||
import funkin.backend.system.Conductor;
|
||||
import funkin.editors.charter.Charter.ICharterSelectable;
|
||||
import funkin.editors.charter.CharterBackdropGroup.EventBackdrop;
|
||||
import funkin.game.Character;
|
||||
@@ -58,8 +63,8 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
|
||||
selectedColorTransform(colorTransform);
|
||||
useColorTransform = true;
|
||||
|
||||
for (sprite in icons) {
|
||||
@:privateAccess sprite.colorTransform.__identity();
|
||||
for (sprite in icons) @:privateAccess {
|
||||
sprite.colorTransform.__identity();
|
||||
selectedColorTransform(sprite.colorTransform);
|
||||
}
|
||||
|
||||
@@ -76,25 +81,84 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
|
||||
transform.blueOffset += selected ? 168 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack data is a list of 4 strings separated by `________PACKSEP________`
|
||||
* [0] Event Name
|
||||
* [1] Event Script
|
||||
* [2] Event JSON Info
|
||||
* [3] Event Icon
|
||||
* [4] Event UI Script / Icon Script
|
||||
**/
|
||||
@:dox(hide) public static function getPackData(name:String):Array<String> {
|
||||
var packFile = Paths.pack('events/${name}');
|
||||
if (Assets.exists(packFile)) {
|
||||
return Assets.getText(packFile).split('________PACKSEP________');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@:dox(hide) public static function getUIScript(event:ChartEvent, caller:String):Script {
|
||||
var uiScript = Paths.script('data/events/${event.name}.ui');
|
||||
var script:Script = null;
|
||||
if(Assets.exists(uiScript)) {
|
||||
script = Script.create(uiScript);
|
||||
} else {
|
||||
var packData = getPackData(event.name);
|
||||
if(packData != null) {
|
||||
var scriptFile = packData[4];
|
||||
if(scriptFile != null) {
|
||||
script = Script.fromString(scriptFile, uiScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(script != null && !(script is DummyScript)) {
|
||||
// classes and functions
|
||||
script.set("EventIconGroup", EventIconGroup); // automatically imported
|
||||
script.set("EventNumber", EventNumber); // automatically imported
|
||||
script.set("getIconFromStrumline", getIconFromStrumline);
|
||||
script.set("getIconFromCharName", getIconFromCharName);
|
||||
script.set("generateDefaultIcon", generateDefaultIcon);
|
||||
script.set("generateEventIconDurationArrow", generateEventIconDurationArrow);
|
||||
script.set("generateEventIconNumbers", generateEventIconNumbers);
|
||||
script.set("generateEventIconWarning", generateEventIconWarning);
|
||||
script.set("getPackData", getPackData);
|
||||
script.set("getEventComponent", getEventComponent);
|
||||
// data
|
||||
script.set("event", event);
|
||||
script.set("caller", caller);
|
||||
|
||||
script.load();
|
||||
}
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the default event icon for the wanted event
|
||||
* @param name The name of the event
|
||||
* @return The icon
|
||||
**/
|
||||
private static function generateDefaultIcon(name:String) {
|
||||
var isBase64:Bool = false;
|
||||
var path:String = Paths.image('editors/charter/event-icons/$name');
|
||||
var defaultPath = Paths.image('editors/charter/event-icons/Unknown');
|
||||
if (!Assets.exists(path)) path = defaultPath;
|
||||
if(!Assets.exists(path)) {
|
||||
path = defaultPath;
|
||||
|
||||
var packPath = Paths.pack('events/$name');
|
||||
if (Assets.exists(packPath)) {
|
||||
var packText = Assets.getText(packPath).split('________PACKSEP________');
|
||||
var packImg = packText[3];
|
||||
if(packImg != null && packImg.length > 0) {
|
||||
isBase64 = !packImg.startsWith("assets/");
|
||||
path = packImg;
|
||||
var packData = getPackData(name);
|
||||
if(packData != null) {
|
||||
var packImg = packData[3];
|
||||
if(packImg != null && packImg.length > 0) {
|
||||
isBase64 = !packImg.startsWith("assets/");
|
||||
path = packImg;
|
||||
}
|
||||
}
|
||||
}
|
||||
path = path.trim();
|
||||
|
||||
var graphic:FlxGraphicAsset = try {
|
||||
isBase64 ? openfl.display.BitmapData.fromBase64(path, 'UTF8') : path;
|
||||
isBase64 ? BitmapData.fromBase64(path, 'UTF8') : path;
|
||||
} catch(e:Dynamic) {
|
||||
Logs.trace('Failed to load event icon: ${e.toString()}', ERROR);
|
||||
isBase64 = false;
|
||||
@@ -106,27 +170,176 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
|
||||
graphic = defaultPath;
|
||||
}
|
||||
|
||||
return new FlxSprite().loadGraphic(graphic);
|
||||
return new FlxSprite(graphic);
|
||||
}
|
||||
|
||||
public static function generateEventIcon(event:ChartEvent) {
|
||||
return switch(event.name) {
|
||||
default:
|
||||
generateDefaultIcon(event.name);
|
||||
case "Camera Movement":
|
||||
// custom icon for camera movement
|
||||
var state:Charter = cast FlxG.state;
|
||||
if (event.params != null && event.params[0] != null && event.params[0] >= 0 && event.params[0] < state.strumLines.length) {
|
||||
// camera movement, use health icon
|
||||
var icon = Character.getIconFromCharName(state.strumLines.members[event.params[0]].strumLine.characters[0]);
|
||||
var healthIcon = new HealthIcon(icon);
|
||||
healthIcon.setUnstretchedGraphicSize(32, 32, false);
|
||||
healthIcon.scrollFactor.set(1, 1);
|
||||
healthIcon.active = false;
|
||||
healthIcon;
|
||||
} else
|
||||
generateDefaultIcon(event.name);
|
||||
/**
|
||||
* Gets a component sprite from the editors/charter/event-icons/components folder
|
||||
* If you wanna use a number, please use the EventNumber class instead
|
||||
* @param type The type of component to get
|
||||
* @param x The x position of the sprite (optional)
|
||||
* @param y The y position of the sprite (optional)
|
||||
* @return The component sprite
|
||||
**/
|
||||
public static function getEventComponent(type:String, x:Float = 0.0, y:Float = 0.0) {
|
||||
var componentPath = Paths.image("editors/charter/event-icons/components/" + type);
|
||||
if (Assets.exists(componentPath)) return new FlxSprite(x, y, componentPath);
|
||||
|
||||
Logs.trace('Could not find component $type', WARNING);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expected to be called from inside of a ui script,
|
||||
* calling this elsewhere might cause unexpected results or crashes
|
||||
**/
|
||||
public static function getIconFromStrumline(index:Null<Int>) {
|
||||
var state = cast(FlxG.state, Charter);
|
||||
if (index != null && index >= 0 && index < state.strumLines.length) {
|
||||
return getIconFromCharName(state.strumLines.members[index].strumLine.characters[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getIconFromCharName(name:String) {
|
||||
var icon = Character.getIconFromCharName(name);
|
||||
var healthIcon = new HealthIcon(icon);
|
||||
CoolUtil.setUnstretchedGraphicSize(healthIcon, 32, 32, false);
|
||||
healthIcon.scrollFactor.set(1, 1);
|
||||
healthIcon.active = false;
|
||||
return healthIcon;
|
||||
}
|
||||
|
||||
public static function generateEventIcon(event:ChartEvent, inMenu:Bool = true):FlxSprite {
|
||||
var script = getUIScript(event, "event-icon");
|
||||
if(script != null && !(script is DummyScript)) {
|
||||
script.set("inMenu", inMenu);
|
||||
if(script.get("generateIcon") != null) {
|
||||
var res:FlxSprite = script.call("generateIcon");
|
||||
if(res != null)
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
switch(event.name) {
|
||||
case "Time Signature Change":
|
||||
if(event.params != null && (event.params[0] >= 0 || event.params[1] >= 0)) {
|
||||
var group = new EventIconGroup();
|
||||
group.add(generateDefaultIcon(event.name));
|
||||
group.add({ // top
|
||||
var num = new EventNumber(9, -1, event.params[0], EventNumber.ALIGN_CENTER);
|
||||
num.active = false;
|
||||
num;
|
||||
});
|
||||
group.add({ // bottom
|
||||
var num = new EventNumber(9, 10, event.params[1], EventNumber.ALIGN_CENTER);
|
||||
num.active = false;
|
||||
num;
|
||||
});
|
||||
if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
|
||||
return group;
|
||||
}
|
||||
case "Continuous BPM Change":
|
||||
if(event.params != null && event.params[1] != null) {
|
||||
var group = new EventIconGroup();
|
||||
group.add(generateDefaultIcon("BPM Change Start"));
|
||||
if (!inMenu) {
|
||||
generateEventIconDurationArrow(group, event.params[1]);
|
||||
group.members[0].y -= 2;
|
||||
generateEventIconNumbers(group, event.params[0], 3);
|
||||
}
|
||||
if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
|
||||
return group;
|
||||
} else {
|
||||
return generateDefaultIcon("BPM Change Start");
|
||||
}
|
||||
case "BPM Change":
|
||||
if(event.params != null && event.params[0] != null) {
|
||||
var group = new EventIconGroup();
|
||||
group.add(generateDefaultIcon(event.name));
|
||||
if (!inMenu) {
|
||||
group.members[0].y -= 2;
|
||||
generateEventIconNumbers(group, event.params[0], 3);
|
||||
}
|
||||
if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
case "Scroll Speed Change":
|
||||
if(event.params != null && !inMenu) {
|
||||
var group = new EventIconGroup();
|
||||
group.add(generateDefaultIcon(event.name));
|
||||
if (event.params[0]) generateEventIconDurationArrow(group, event.params[2]);
|
||||
group.members[0].y -= 2;
|
||||
generateEventIconNumbers(group, event.params[1]);
|
||||
return group;
|
||||
}
|
||||
|
||||
case "Camera Movement":
|
||||
// camera movement, use health icon
|
||||
if(event.params != null) {
|
||||
var icon = getIconFromStrumline(event.params[0]);
|
||||
if(icon != null) return icon;
|
||||
}
|
||||
}
|
||||
return generateDefaultIcon(event.name);
|
||||
}
|
||||
|
||||
private static function generateEventIconNumbers(group:EventIconGroup, number:Float, x:Float = 4, y:Float = 15, spacing:Float = 5, precision:Int = 3) {
|
||||
group.add({
|
||||
var num = new EventNumber(x, y, number, EventNumber.ALIGN_CENTER, spacing, precision);
|
||||
if (num.numWidth > 20) {
|
||||
num.scale.x = num.scale.y = 20 / num.numWidth;
|
||||
}
|
||||
num.active = false;
|
||||
num;
|
||||
});
|
||||
}
|
||||
|
||||
private static function generateEventIconDurationArrow(group:EventIconGroup, stepDuration:Float) {
|
||||
//var group = new EventIconGroup();
|
||||
//group.add(generateDefaultIcon(startIcon));
|
||||
|
||||
var xOffset = 4;
|
||||
var yGap = 24;
|
||||
var endGap = 2;
|
||||
|
||||
if (stepDuration >= 0.55) { //min time for showing arrow
|
||||
var tail = new FlxSprite(xOffset, yGap);
|
||||
var arrow = new FlxSprite(xOffset, (stepDuration * 40) + endGap);
|
||||
var arrowSegment = new FlxSprite(xOffset, yGap);
|
||||
tail.frames = arrow.frames = arrowSegment.frames = Paths.getSparrowAtlas("editors/charter/event-icons/components/arrow-down");
|
||||
|
||||
group.add({
|
||||
tail.animation.addByPrefix("tail", "tail");
|
||||
tail.animation.play("tail");
|
||||
tail;
|
||||
});
|
||||
|
||||
group.add({
|
||||
arrowSegment.animation.addByPrefix("segment", "segment");
|
||||
arrowSegment.animation.play("segment");
|
||||
arrowSegment.scale.y = endGap + (stepDuration * 40) - (tail.height + yGap);
|
||||
arrowSegment.updateHitbox();
|
||||
arrowSegment.y += tail.height;
|
||||
arrowSegment;
|
||||
});
|
||||
|
||||
group.add({
|
||||
arrow.animation.addByPrefix("arrow", "arrow");
|
||||
arrow.animation.play("arrow");
|
||||
arrow;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static function generateEventIconWarning(group:EventIconGroup) {
|
||||
for (spr in group) {
|
||||
spr.colorTransform.redMultiplier = spr.colorTransform.greenMultiplier = spr.colorTransform.blueMultiplier = 0.5;
|
||||
spr.colorTransform.redOffset = 100;
|
||||
}
|
||||
group.add(getEventComponent("warning", 16, -8));
|
||||
group.copyColorTransformToChildren = false;
|
||||
}
|
||||
|
||||
public override function onHovered() {
|
||||
@@ -154,19 +367,152 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
|
||||
}
|
||||
|
||||
for(event in events) {
|
||||
var spr = generateEventIcon(event);
|
||||
var spr = generateEventIcon(event, false);
|
||||
icons.push(spr);
|
||||
members.push(spr);
|
||||
}
|
||||
|
||||
draggable = true;
|
||||
for (event in events)
|
||||
if (event.name == "BPM Change") {
|
||||
draggable = false;
|
||||
break;
|
||||
}
|
||||
|
||||
bWidth = 37 + (icons.length * 22);
|
||||
x = (snappedToGrid && eventsBackdrop != null && global ? eventsBackdrop.x - bWidth : (global ? 0 : -bWidth));
|
||||
}
|
||||
}
|
||||
|
||||
class EventIconGroup extends FlxSpriteGroup {
|
||||
public var forceWidth:Float = 16;
|
||||
public var forceHeight:Float = 16;
|
||||
public var dontTransformChildren:Bool = false;
|
||||
public var copyColorTransformToChildren:Bool = true;
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
colorTransform = new ColorTransform();
|
||||
scrollFactor.set(1, 1);
|
||||
}
|
||||
|
||||
override function preAdd(sprite:FlxSprite):Void
|
||||
{
|
||||
super.preAdd(sprite);
|
||||
sprite.scrollFactor.set(1, 1);
|
||||
}
|
||||
|
||||
public override function transformChildren<V>(Function:FlxSprite->V->Void, Value:V):Void
|
||||
{
|
||||
if (dontTransformChildren)
|
||||
return;
|
||||
|
||||
super.transformChildren(Function, Value);
|
||||
}
|
||||
|
||||
override function set_x(Value:Float):Float
|
||||
{
|
||||
if (exists && x != Value)
|
||||
transformChildren(xTransform, Value - x); // offset
|
||||
return x = Value;
|
||||
}
|
||||
|
||||
override function set_y(Value:Float):Float
|
||||
{
|
||||
if (exists && y != Value)
|
||||
transformChildren(yTransform, Value - y); // offset
|
||||
return y = Value;
|
||||
}
|
||||
|
||||
override function get_width() {
|
||||
return forceWidth;
|
||||
}
|
||||
override function get_height() {
|
||||
return forceHeight;
|
||||
}
|
||||
|
||||
override public function draw() {
|
||||
@:privateAccess
|
||||
if (copyColorTransformToChildren && colorTransform != null) for (child in members) child.colorTransform.__copyFrom(colorTransform);
|
||||
super.draw();
|
||||
}
|
||||
|
||||
override function update(elapsed:Float) {
|
||||
super.update(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
class EventNumber extends FlxSprite {
|
||||
public static inline final ALIGN_NORMAL:Int = 0;
|
||||
public static inline final ALIGN_CENTER:Int = 1;
|
||||
|
||||
public var digits:Array<Int> = [];
|
||||
public static inline final FRAME_POINT:Int = 10;
|
||||
public static inline final FRAME_NEGATIVE:Int = 11;
|
||||
|
||||
public var align:Int = ALIGN_NORMAL;
|
||||
public var spacing:Float = 6;
|
||||
|
||||
public function new(x:Float, y:Float, number:Float, ?align:Int, spacing:Float = 6, precision:Int = 3) {
|
||||
super(x, y);
|
||||
this.digits = [];
|
||||
this.align = align == null ? ALIGN_NORMAL : align;
|
||||
this.spacing = spacing;
|
||||
|
||||
if (number == 0) {
|
||||
this.digits.insert(0, 0);
|
||||
}
|
||||
else {
|
||||
var decimals:Float = FlxMath.roundDecimal(Math.abs(number % 1), precision);
|
||||
if (decimals > 0) this.digits.insert(0, FRAME_POINT);
|
||||
while(decimals > 0) {
|
||||
this.digits.push(Math.floor(decimals * 10));
|
||||
decimals = FlxMath.roundDecimal((decimals * 10) % 1, precision);
|
||||
}
|
||||
|
||||
var ints = Std.int(Math.abs(number));
|
||||
if (ints == 0) this.digits.insert(0, 0);
|
||||
while (ints > 0) {
|
||||
this.digits.insert(0, ints % 10);
|
||||
ints = Std.int(ints / 10);
|
||||
}
|
||||
|
||||
if (number < 0) {
|
||||
this.digits.insert(0, FRAME_NEGATIVE);
|
||||
}
|
||||
}
|
||||
|
||||
loadGraphic(Paths.image('editors/charter/event-icons/components/eventNums'), true, 6, 7);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float) {
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
override function draw() {
|
||||
var baseX = x;
|
||||
var offsetX = 0.0;
|
||||
if(align == ALIGN_CENTER) offsetX = -(digits.length - 1) * spacing * Math.abs(scale.x) / 2;
|
||||
|
||||
x = baseX + offsetX;
|
||||
for (i in 0...digits.length) {
|
||||
frame = frames.frames[digits[i]];
|
||||
super.draw();
|
||||
x += spacing * Math.abs(scale.x);
|
||||
}
|
||||
x = baseX;
|
||||
}
|
||||
|
||||
public var numWidth(get, never):Float;
|
||||
private function get_numWidth():Float {
|
||||
return Math.abs(scale.x) * spacing * digits.length;
|
||||
}
|
||||
public var numHeight(get, never):Float;
|
||||
private function get_numHeight():Float {
|
||||
return Math.abs(scale.y) * frameHeight;
|
||||
}
|
||||
|
||||
public override function updateHitbox():Void {
|
||||
var numWidth = this.numWidth;
|
||||
var numHeight = this.numHeight;
|
||||
width = numWidth;
|
||||
height = numHeight;
|
||||
offset.set(-0.5 * (numWidth - spacing * digits.length), -0.5 * (numHeight - frameHeight));
|
||||
centerOrigin();
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class CharterEventAdd extends UISliceSprite {
|
||||
|
||||
this.global = flipX = global;
|
||||
|
||||
sideText = new UIText(0, -40, 0, global ? "Global Event" : "Local Event", 12);
|
||||
sideText = new UIText(0, -40, 0, TU.translate("charter.eventType-" + (global ? "global" : "local")), 12);
|
||||
sideText.alignment = "center"; sideText.alpha = 0.75;
|
||||
|
||||
text = new UIText(0, 0, 0, "");
|
||||
@@ -29,15 +29,27 @@ class CharterEventAdd extends UISliceSprite {
|
||||
if (FlxG.mouse.justReleased && FlxG.state.subState == null) {
|
||||
if (curCharterEvent != null)
|
||||
Charter.instance.openSubState(new CharterEventScreenNew(curCharterEvent));
|
||||
else
|
||||
Charter.instance.openSubState(new CharterEventScreen(step, global));
|
||||
else {
|
||||
CharterEventGroup.stopThisFuckingShitDudeIstg = true;
|
||||
var chartEvent = new CharterEvent(step, [], global);
|
||||
chartEvent.global = global;
|
||||
|
||||
Charter.instance.createSelection([chartEvent]);
|
||||
Charter.instance.openSubState(new CharterEventScreenNew(chartEvent));
|
||||
sprAlpha = 0;
|
||||
alpha = sprAlpha * 0.75;
|
||||
text.alpha = sprAlpha;
|
||||
|
||||
sideText.alpha = sprAlpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float) {
|
||||
super.update(elapsed);
|
||||
|
||||
text.follow(this, global ? bWidth - text.width - (text.text == "Add event" ? 15 : 20) : 20, (bHeight - text.height) / 2);
|
||||
if (FlxG.state.subState != null) return;
|
||||
text.follow(this, global ? bWidth - text.width - (text.text == TU.translate("charter.addEvent") ? 15 : 20) : 20, (bHeight - text.height) / 2);
|
||||
sideText.follow(this, (bWidth/2) - (sideText.fieldWidth/2), -(sideText.height + 2));
|
||||
alpha = sprAlpha * 0.75;
|
||||
text.alpha = sprAlpha;
|
||||
@@ -46,18 +58,20 @@ class CharterEventAdd extends UISliceSprite {
|
||||
}
|
||||
|
||||
public function updatePos(step:Float) {
|
||||
if (FlxG.state.subState != null) return;
|
||||
curCharterEvent = null;
|
||||
this.step = step;
|
||||
this.y = (step * 40) - (bHeight / 2);
|
||||
text.text = "Add event";
|
||||
text.text = TU.translate("charter.addEvent");
|
||||
framesOffset = 0; bWidth = 37 + Math.ceil(text.width);
|
||||
x = global ? Charter.instance.strumLines.members[Charter.instance.strumLines.members.length-1].x + (40*Charter.instance.strumLines.members[Charter.instance.strumLines.members.length-1].keyCount) : -(bWidth);
|
||||
}
|
||||
|
||||
public function updateEdit(event:CharterEvent) {
|
||||
if (FlxG.state.subState != null) return;
|
||||
curCharterEvent = event;
|
||||
this.y = event.y;
|
||||
text.text = "Edit";
|
||||
text.text = TU.translate("charter.editEvent");
|
||||
framesOffset = 9; bWidth = 27 + Math.ceil(text.width) + event.bWidth;
|
||||
x = global ? Charter.instance.strumLines.members[Charter.instance.strumLines.members.length-1].x + (40*Charter.instance.strumLines.members[Charter.instance.strumLines.members.length-1].keyCount) : -(bWidth);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import flixel.util.FlxSort;
|
||||
import funkin.editors.charter.CharterBackdropGroup.EventBackdrop;
|
||||
|
||||
class CharterEventGroup extends FlxTypedGroup<CharterEvent> {
|
||||
public static var stopThisFuckingShitDudeIstg = false;
|
||||
public var eventsBackdrop:EventBackdrop;
|
||||
public var eventsRowText:UIText;
|
||||
|
||||
@@ -11,12 +12,12 @@ class CharterEventGroup extends FlxTypedGroup<CharterEvent> {
|
||||
var __lastSort:Int = 0;
|
||||
|
||||
public override function update(elapsed:Float) {
|
||||
filterEvents();
|
||||
super.update(elapsed);
|
||||
if (!CharterEventGroup.stopThisFuckingShitDudeIstg) filterEvents();
|
||||
if (autoSort && members.length != __lastSort)
|
||||
sortEvents();
|
||||
|
||||
eventsRowText.y = FlxMath.lerp(eventsRowText.y, -40 + (members[0] != null ? Math.min(members[0].y, 0) : 0), 1/20);
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
public override function remove(v:CharterEvent, force:Bool = true):CharterEvent {
|
||||
|
||||
@@ -38,7 +38,7 @@ class CharterEventScreen extends UISubstateWindow {
|
||||
var creatingEvent:Bool = chartEvent == null;
|
||||
if (creatingEvent) chartEvent = new CharterEvent(step, [], global);
|
||||
|
||||
winTitle = creatingEvent ? "Create Event Group" : "Edit Event Group";
|
||||
winTitle = TU.translate("charterEventScreen.title" + (creatingEvent ? "-creating" : "-editing"));
|
||||
winWidth = 960;
|
||||
|
||||
super.create();
|
||||
@@ -59,7 +59,7 @@ class CharterEventScreen extends UISubstateWindow {
|
||||
});
|
||||
eventsList.add(new EventButton(events[events.length-1], CharterEvent.generateEventIcon(events[events.length-1]), events.length-1, this, eventsList));
|
||||
changeTab(events.length-1);
|
||||
}));
|
||||
}, chartEvent.step));
|
||||
for (k=>i in events)
|
||||
eventsList.add(new EventButton(i, CharterEvent.generateEventIcon(i), k, this, eventsList));
|
||||
add(eventsList);
|
||||
@@ -67,10 +67,12 @@ class CharterEventScreen extends UISubstateWindow {
|
||||
paramsPanel = new FlxGroup();
|
||||
add(paramsPanel);
|
||||
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20, windowSpr.y + windowSpr.bHeight - 16 - 32, "Save & Close", function() {
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() {
|
||||
saveCurTab();
|
||||
chartEvent.refreshEventIcons();
|
||||
|
||||
Charter.instance.updateBPMEvents();
|
||||
|
||||
if (events.length <= 0 && !creatingEvent)
|
||||
Charter.instance.deleteSelection([chartEvent]);
|
||||
else if (events.length > 0) {
|
||||
@@ -78,12 +80,11 @@ class CharterEventScreen extends UISubstateWindow {
|
||||
chartEvent.events = [for (i in eventsList.buttons.members) i.event];
|
||||
chartEvent.global = global;
|
||||
|
||||
chartEvent.refreshEventIcons();
|
||||
|
||||
if (creatingEvent && events.length > 0)
|
||||
Charter.instance.createSelection([chartEvent]);
|
||||
else {
|
||||
chartEvent.refreshEventIcons();
|
||||
Charter.instance.updateBPMEvents();
|
||||
|
||||
Charter.undos.addToUndo(CEditEvent(chartEvent, oldEvents, [for (event in events) Reflect.copy(event)]));
|
||||
}
|
||||
}
|
||||
@@ -93,7 +94,7 @@ class CharterEventScreen extends UISubstateWindow {
|
||||
saveButton.x -= saveButton.bWidth;
|
||||
add(saveButton);
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 10, saveButton.y, "Close", ()->close());
|
||||
closeButton = new UIButton(saveButton.x - 10, saveButton.y, TU.translate("editor.close"), ()->close());
|
||||
closeButton.color = 0xFFFF0000;
|
||||
closeButton.x -= closeButton.bWidth;
|
||||
add(closeButton);
|
||||
@@ -120,12 +121,20 @@ class CharterEventScreen extends UISubstateWindow {
|
||||
if (id >= 0 && id < events.length) {
|
||||
curEvent = id;
|
||||
var curEvent = events[curEvent];
|
||||
eventName.text = curEvent.name;
|
||||
|
||||
var visualName = curEvent.name;
|
||||
var tuId = "charter.events." + TU.raw2Id(curEvent.name);
|
||||
if(TU.exists(tuId))
|
||||
visualName = TU.translate(tuId);
|
||||
|
||||
eventName.text = visualName;
|
||||
// add new elements
|
||||
var y:Float = eventName.y + eventName.height + 10;
|
||||
for(k=>param in EventsData.getEventParams(curEvent.name)) {
|
||||
function addLabel() {
|
||||
var label:UIText = new UIText(eventName.x, y, 0, param.name);
|
||||
var visualName = param.name;
|
||||
// TODO: add translations
|
||||
var label:UIText = new UIText(eventName.x, y, 0, visualName);
|
||||
y += label.height + 4;
|
||||
paramsPanel.add(label);
|
||||
};
|
||||
@@ -153,7 +162,11 @@ class CharterEventScreen extends UISubstateWindow {
|
||||
numericStepper;
|
||||
case TStrumLine:
|
||||
addLabel();
|
||||
var dropdown = new UIDropDown(eventName.x, y, 320, 32, [for(k=>s in cast(FlxG.state, Charter).strumLines.members) 'Strumline #${k+1} (${s.strumLine.characters[0]})'], cast value);
|
||||
var strumlineFormat = TU.getRaw("charterEventScreen.strumLine.format");
|
||||
var dropdown = new UIDropDown(eventName.x, y, 320, 32, [
|
||||
for(k=>s in cast(FlxG.state, Charter).strumLines.members)
|
||||
strumlineFormat.format([k+1, s.strumLine.characters[0]])
|
||||
], cast value);
|
||||
paramsPanel.add(dropdown); paramsFields.push(dropdown);
|
||||
dropdown;
|
||||
case TColorWheel:
|
||||
@@ -194,7 +207,7 @@ class CharterEventScreen extends UISubstateWindow {
|
||||
y += cast(lastAdded, FlxSprite).height + 6;
|
||||
}
|
||||
} else {
|
||||
eventName.text = "No event";
|
||||
eventName.text = TU.translate("charterEventScreen.noEvent"); // Maybe should be No Events?
|
||||
curEvent = -1;
|
||||
}
|
||||
}
|
||||
@@ -202,11 +215,11 @@ class CharterEventScreen extends UISubstateWindow {
|
||||
public function saveCurTab() {
|
||||
if (curEvent < 0) return;
|
||||
|
||||
events[curEvent].params = [
|
||||
for(p in paramsFields) {
|
||||
var dataParams = EventsData.getEventParams(events[curEvent].name);
|
||||
var params:Array<Dynamic> = [
|
||||
for (i => p in paramsFields) {
|
||||
if (p is UIDropDown) {
|
||||
var dataParams = EventsData.getEventParams(events[curEvent].name);
|
||||
if (dataParams[paramsFields.indexOf(p)].type == TStrumLine) cast(p, UIDropDown).index;
|
||||
if (dataParams[i].type == TStrumLine) cast(p, UIDropDown).index;
|
||||
else cast(p, UIDropDown).label.text;
|
||||
}
|
||||
else if (p is UINumericStepper) {
|
||||
@@ -227,6 +240,14 @@ class CharterEventScreen extends UISubstateWindow {
|
||||
null;
|
||||
}
|
||||
];
|
||||
|
||||
while(dataParams.length > 0 && {
|
||||
var index = params.length - 1;
|
||||
var dataParam = dataParams[index];
|
||||
dataParam.saveIfDefault == false && params[index] == dataParam.defValue;
|
||||
}) params.pop();
|
||||
|
||||
events[curEvent].params = params;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class CharterEventScreenNew extends MusicBeatSubstate {
|
||||
});
|
||||
eventsList.add(new EventButtonNew(events[events.length-1], CharterEvent.generateEventIcon(events[events.length-1]), events.length-1, this, eventsList));
|
||||
changeTab(events.length-1);
|
||||
}));
|
||||
}, chartEvent.step));
|
||||
};
|
||||
for (k=>i in events)
|
||||
eventsList.add(new EventButtonNew(i, CharterEvent.generateEventIcon(i), k, this, eventsList));
|
||||
@@ -284,19 +284,19 @@ class CharterEventScreenNew extends MusicBeatSubstate {
|
||||
}
|
||||
|
||||
public function quit() {
|
||||
CharterEventGroup.stopThisFuckingShitDudeIstg = false;
|
||||
saveCurTab();
|
||||
chartEvent.refreshEventIcons();
|
||||
|
||||
if (events.length <= 0)
|
||||
Charter.instance.deleteSelection([chartEvent]);
|
||||
else if (events.length > 0) {
|
||||
var oldEvents:Array<ChartEvent> = chartEvent.events.copy();
|
||||
chartEvent.events = [for (i in eventsList.buttons.members) i.event];
|
||||
chartEvent.refreshEventIcons();
|
||||
Charter.instance.updateBPMEvents();
|
||||
|
||||
Charter.undos.addToUndo(CEditEvent(chartEvent, oldEvents, [for (event in events) Reflect.copy(event)]));
|
||||
}
|
||||
chartEvent.refreshEventIcons();
|
||||
Charter.instance.updateBPMEvents();
|
||||
close();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package funkin.editors.charter;
|
||||
|
||||
import funkin.backend.system.Conductor;
|
||||
import funkin.backend.chart.EventsData;
|
||||
|
||||
class CharterEventTypeSelection extends UISubstateWindow {
|
||||
var callback:String->Void;
|
||||
var eventStepTime:Float;
|
||||
|
||||
var buttons:Array<UIButton> = [];
|
||||
|
||||
@@ -13,13 +15,14 @@ class CharterEventTypeSelection extends UISubstateWindow {
|
||||
var upIndicator:UIText;
|
||||
var downIndicator:UIText;
|
||||
|
||||
public function new(callback:String->Void) {
|
||||
public function new(callback:String->Void, eventStepTime:Float) {
|
||||
super();
|
||||
this.callback = callback;
|
||||
this.eventStepTime = eventStepTime;
|
||||
}
|
||||
|
||||
public override function create() {
|
||||
winTitle = "Choose an event type...";
|
||||
winTitle = TU.translate("charterEventTypeSelection.title");
|
||||
super.create();
|
||||
|
||||
var w:Int = winWidth - 20;
|
||||
@@ -32,8 +35,21 @@ class CharterEventTypeSelection extends UISubstateWindow {
|
||||
buttonsBG.frames = Paths.getFrames('editors/ui/inputbox');
|
||||
add(buttonsBG);
|
||||
|
||||
var disableConductorEvents:Bool = false;
|
||||
var disableOnlyContinuousChanges:Bool = false;
|
||||
for (change in Conductor.bpmChangeMap) {
|
||||
if (change.continuous && MathUtil.greaterThanEqual(eventStepTime, change.stepTime) && MathUtil.lessThan(eventStepTime, change.endStepTime)) {
|
||||
disableOnlyContinuousChanges = MathUtil.equal(eventStepTime, change.stepTime); //allow time sig and instant bpm changes on the same event
|
||||
disableConductorEvents = !disableOnlyContinuousChanges;
|
||||
}
|
||||
}
|
||||
|
||||
for(k=>eventName in EventsData.eventsList) {
|
||||
var button = new UIButton(0, (32 * k), eventName, function() {
|
||||
var visualName = eventName;
|
||||
var tuId = "charter.events." + TU.raw2Id(eventName);
|
||||
if(TU.exists(tuId))
|
||||
visualName = TU.translate(tuId);
|
||||
var button = new UIButton(0, (32 * k), visualName, function() {
|
||||
close();
|
||||
callback(eventName);
|
||||
}, w);
|
||||
@@ -52,11 +68,16 @@ class CharterEventTypeSelection extends UISubstateWindow {
|
||||
icon.x = button.x + 8;
|
||||
icon.y = button.y + Math.abs(button.bHeight - icon.height) / 2;
|
||||
add(icon);
|
||||
|
||||
if (disableConductorEvents && (eventName == "Time Signature Change" || eventName == "Continuous BPM Change" || eventName == "BPM Change") || (disableOnlyContinuousChanges && eventName == "Continuous BPM Change")) {
|
||||
button.selectable = button.shouldPress = false;
|
||||
button.autoAlpha = true;
|
||||
}
|
||||
}
|
||||
|
||||
windowSpr.bHeight = 61 + (32 * (17));
|
||||
|
||||
add(new UIButton(10, windowSpr.bHeight-42, "Cancel", function() {
|
||||
add(new UIButton(10, windowSpr.bHeight-42, TU.translate("editor.cancel"), function() {
|
||||
close();
|
||||
}, w));
|
||||
|
||||
@@ -79,7 +100,7 @@ class CharterEventTypeSelection extends UISubstateWindow {
|
||||
sinner += elapsed;
|
||||
|
||||
for (button in buttons)
|
||||
button.selectable = buttonsBG.hovered;
|
||||
if (button.shouldPress) button.selectable = buttonsBG.hovered;
|
||||
|
||||
buttonCameras.zoom = subCam.zoom;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user