Merge branch 'main' into translations-system
This commit is contained in:
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
package external;
|
||||
|
||||
#if (mac && cpp)
|
||||
@:build(external.LinkerMacro.xml('external_code.xml'))
|
||||
@:include('Mac.h')
|
||||
@:native('ExternalMac')
|
||||
extern class ExternalMac
|
||||
{
|
||||
@:native('ExternalMac::setCursorIcon')
|
||||
public static function setCursorIcon(icon:Int, customCursor:cpp.ConstCharStar, customX:Single, customY:Single):Bool;
|
||||
}
|
||||
#end
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
package external;
|
||||
|
||||
import haxe.io.Path;
|
||||
import haxe.macro.Expr;
|
||||
import haxe.macro.Context;
|
||||
import haxe.xml.Printer;
|
||||
import sys.FileSystem;
|
||||
|
||||
class LinkerMacro
|
||||
{
|
||||
public static macro function xml(filename:String = 'Build.xml'):Array<Field>
|
||||
{
|
||||
var pos:Position = Context.currentPos();
|
||||
|
||||
var sourcePath:String = Path.directory(Context.getPosInfos(pos).file);
|
||||
|
||||
if (!Path.isAbsolute(sourcePath))
|
||||
sourcePath = FileSystem.absolutePath(sourcePath);
|
||||
|
||||
sourcePath = Path.removeTrailingSlashes(Path.normalize(sourcePath));
|
||||
|
||||
var includeElement:Xml = Xml.createElement('include');
|
||||
|
||||
includeElement.set('name', Path.join([sourcePath, filename]));
|
||||
|
||||
@:privateAccess
|
||||
{
|
||||
var printer:Printer = new Printer(true);
|
||||
|
||||
printer.writeNode(includeElement, '\n');
|
||||
|
||||
Context.getLocalClass().get().meta.add(':buildXml', [{
|
||||
expr: EConst(CString(printer.output.toString())),
|
||||
pos: pos
|
||||
}], pos);
|
||||
}
|
||||
|
||||
return Context.getBuildFields();
|
||||
}
|
||||
}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xml>
|
||||
<pragma once="true" />
|
||||
|
||||
<files id="haxe">
|
||||
<compilerflag value="-I${this_dir}/include/mac" if="macos" />
|
||||
</files>
|
||||
|
||||
<files id="__main__">
|
||||
<compilerflag value="-I${this_dir}/include/mac" if="macos" />
|
||||
</files>
|
||||
|
||||
<files id="external-code">
|
||||
<section if="macos">
|
||||
<compilerflag value="-I${this_dir}/include/mac" />
|
||||
|
||||
<file name="${this_dir}/src/mac/CursorHelper.mm" />
|
||||
<file name="${this_dir}/src/mac/Mac.mm" />
|
||||
</section>
|
||||
</files>
|
||||
|
||||
<target id="haxe">
|
||||
<section if="macos">
|
||||
<vflag name="-framework" value="IOKit" />
|
||||
<vflag name="-framework" value="AppKit" />
|
||||
<vflag name="-framework" value="Cocoa" />
|
||||
<vflag name="-framework" value="Foundation" />
|
||||
</section>
|
||||
|
||||
<files id="external-code" />
|
||||
</target>
|
||||
</xml>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface CursorHelper : NSObject
|
||||
|
||||
+ (NSCursor *)getCursorForSelector:(SEL)selector defaultCursor:(NSCursor *)defaultCursor;
|
||||
|
||||
@end
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
#ifndef MAC_H
|
||||
#define MAC_H
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
namespace ExternalMac
|
||||
{
|
||||
bool setCursorIcon(int icon, const char *customCursor, float customX, float customY);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "CursorHelper.h"
|
||||
|
||||
@implementation CursorHelper
|
||||
|
||||
+ (NSCursor *)getCursorForSelector:(SEL)selector defaultCursor:(NSCursor *)defaultCursor
|
||||
{
|
||||
NSCursor *cursor = nil;
|
||||
|
||||
if ([NSCursor respondsToSelector:selector])
|
||||
{
|
||||
cursor = [NSCursor performSelector:selector];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"CursorHelper: selector %@ not found", NSStringFromSelector(selector));
|
||||
|
||||
cursor = defaultCursor;
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@end
|
||||
Vendored
+140
@@ -0,0 +1,140 @@
|
||||
#include <unordered_map>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "CursorHelper.h"
|
||||
|
||||
#define ARROW 0
|
||||
#define CROSSHAIR 1
|
||||
#define CLICK 2
|
||||
#define IBEAM 3
|
||||
#define MOVE 4
|
||||
#define HAND 5
|
||||
#define DRAG 6
|
||||
#define DRAG_OPEN 7
|
||||
#define WAIT 8
|
||||
#define WAIT_ARROW 9
|
||||
#define DISABLED 10
|
||||
#define RESIZE_TR 11
|
||||
#define RESIZE_BL 12
|
||||
#define RESIZE_TL 13
|
||||
#define RESIZE_BR 14
|
||||
#define RESIZE_H 15
|
||||
#define RESIZE_V 16
|
||||
#define RESIZE_T 17
|
||||
#define RESIZE_B 18
|
||||
#define RESIZE_L 19
|
||||
#define RESIZE_R 20
|
||||
#define RESIZE_TLBR 21
|
||||
#define RESIZE_TRBL 22
|
||||
#define CUSTOM -1
|
||||
|
||||
namespace ExternalMac
|
||||
{
|
||||
std::unordered_map<const char *, NSCursor *> customCursorCache;
|
||||
|
||||
bool setCursorIcon(int icon, const char *customCursor, float customX, float customY)
|
||||
{
|
||||
NSCursor *cursor = nil;
|
||||
|
||||
switch (icon)
|
||||
{
|
||||
case ARROW:
|
||||
cursor = [NSCursor arrowCursor];
|
||||
break;
|
||||
case CLICK:
|
||||
cursor = [NSCursor pointingHandCursor];
|
||||
break;
|
||||
case CROSSHAIR:
|
||||
cursor = [NSCursor crosshairCursor];
|
||||
break;
|
||||
case HAND:
|
||||
cursor = [NSCursor closedHandCursor];
|
||||
break;
|
||||
case IBEAM:
|
||||
cursor = [NSCursor IBeamCursor];
|
||||
break;
|
||||
case RESIZE_H:
|
||||
cursor = [NSCursor resizeLeftRightCursor];
|
||||
break;
|
||||
case RESIZE_V:
|
||||
cursor = [NSCursor resizeUpDownCursor];
|
||||
break;
|
||||
// TODO: DEFAULTS
|
||||
case RESIZE_TL:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeNorthWestCursor)defaultCursor:nil];
|
||||
break;
|
||||
case RESIZE_TR:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeNorthEastCursor)defaultCursor:nil];
|
||||
break;
|
||||
case RESIZE_BL:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeSouthWestCursor)defaultCursor:nil];
|
||||
break;
|
||||
case RESIZE_BR:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeSouthEastCursor)defaultCursor:nil];
|
||||
break;
|
||||
case RESIZE_T:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeNorthCursor)defaultCursor:nil];
|
||||
break;
|
||||
case RESIZE_B:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeSouthCursor)defaultCursor:nil];
|
||||
break;
|
||||
case RESIZE_L:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeWestCursor)defaultCursor:nil];
|
||||
break;
|
||||
case RESIZE_R:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeEastCursor)defaultCursor:nil];
|
||||
break;
|
||||
case RESIZE_TLBR:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeNorthWestSouthEastCursor)defaultCursor:nil];
|
||||
break;
|
||||
case RESIZE_TRBL:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeNorthEastSouthWestCursor)defaultCursor:nil];
|
||||
break;
|
||||
case WAIT:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_waitCursor)defaultCursor:nil];
|
||||
break;
|
||||
case WAIT_ARROW:
|
||||
cursor = [CursorHelper getCursorForSelector:@selector(_waitCursor)defaultCursor:nil];
|
||||
break;
|
||||
case DISABLED:
|
||||
cursor = [NSCursor operationNotAllowedCursor];
|
||||
break;
|
||||
case DRAG:
|
||||
cursor = [NSCursor closedHandCursor];
|
||||
break;
|
||||
case DRAG_OPEN:
|
||||
cursor = [NSCursor openHandCursor];
|
||||
break;
|
||||
case CUSTOM:
|
||||
if (customCursorCache.find(customCursor) != customCursorCache.end())
|
||||
cursor = customCursorCache[customCursor];
|
||||
else
|
||||
{
|
||||
NSString *cursorImageName = [NSString stringWithUTF8String:customCursor];
|
||||
NSImage *tmpImage = [[NSImage alloc] initWithContentsOfFile:cursorImageName];
|
||||
|
||||
if (tmpImage != nil)
|
||||
{
|
||||
cursor = [[NSCursor alloc] initWithImage:tmpImage hotSpot:NSMakePoint(customX, customY)];
|
||||
customCursorCache[customCursor] = cursor;
|
||||
[tmpImage release];
|
||||
}
|
||||
else
|
||||
NSLog(@"Failed to load image %@", cursorImageName);
|
||||
|
||||
[cursorImageName release];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (cursor != nil)
|
||||
{
|
||||
[cursor set];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,917 @@
|
||||
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();
|
||||
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) {
|
||||
onFinish.removeAll();
|
||||
|
||||
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 if (playing) {
|
||||
_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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class Flx3DCamera extends FlxCamera {
|
||||
var context = new AssetLoaderContext();
|
||||
var noExt = Path.withoutExtension(assetPath);
|
||||
trace(noExt);
|
||||
context.mapUrlToData('${Path.withoutDirectory(noExt)}.mtl', '$noExt.mtl');
|
||||
context.mapUrlToData(Path.withoutDirectory(noExt) + '.mtl', noExt + '.mtl');
|
||||
|
||||
var material:TextureMaterial = null;
|
||||
if (texturePath != null)
|
||||
|
||||
@@ -14,7 +14,7 @@ import flixel.FlxG;
|
||||
* @see https://twitter.com/Ne_Eo_Twitch
|
||||
*/
|
||||
@:access(away3d.core.managers.Stage3DManager)
|
||||
class Flx3DUtil
|
||||
final class Flx3DUtil
|
||||
{
|
||||
#if THREE_D_SUPPORT
|
||||
/**
|
||||
|
||||
@@ -46,7 +46,7 @@ class Flx3DView extends FlxView3D {
|
||||
var context = new AssetLoaderContext();
|
||||
var noExt = Path.withoutExtension(assetPath);
|
||||
trace(noExt);
|
||||
context.mapUrlToData('${Path.withoutDirectory(noExt)}.mtl', '$noExt.mtl');
|
||||
context.mapUrlToData(Path.withoutDirectory(noExt) + '.mtl', noExt + '.mtl');
|
||||
|
||||
var material:TextureMaterial = null;
|
||||
if (texturePath != null)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package funkin;
|
||||
|
||||
@:dox(hide)
|
||||
typedef SingleOrFloat = #if (java || hl || cpp) Single #else Float #end;
|
||||
@@ -32,6 +32,15 @@ enum abstract XMLAnimType(Int)
|
||||
default: def;
|
||||
}
|
||||
}
|
||||
|
||||
@:to public function toString():String {
|
||||
return switch (cast this)
|
||||
{
|
||||
case NONE: "none";
|
||||
case BEAT: "beat";
|
||||
case LOOP: "loop";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements IOffsetCompatible implements IXMLEvents
|
||||
@@ -42,9 +51,10 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
|
||||
public var beatAnims:Array<BeatAnim> = [];
|
||||
public var name:String;
|
||||
public var zoomFactor:Float = 1;
|
||||
public var initialZoom:Float = 1;
|
||||
public var debugMode:Bool = false;
|
||||
public var animDatas:Map<String, AnimData> = [];
|
||||
public var animEnabled:Bool = true;
|
||||
public var zoomFactorEnabled:Bool = true;
|
||||
|
||||
public var globalCurFrame(get, set):Int;
|
||||
|
||||
@@ -75,24 +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.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;
|
||||
}
|
||||
@@ -123,6 +143,7 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
|
||||
{
|
||||
frames = Paths.getFrames(path, true);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public function onPropertySet(property:String, value:Dynamic) {
|
||||
@@ -133,9 +154,9 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
|
||||
private var countedBeat = 0;
|
||||
public function beatHit(curBeat:Int)
|
||||
{
|
||||
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")
|
||||
@@ -151,49 +172,6 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
|
||||
{
|
||||
}
|
||||
|
||||
public override function getScreenBounds(?newRect:FlxRect, ?camera:FlxCamera):FlxRect
|
||||
{
|
||||
__doPreZoomScaleProcedure(camera);
|
||||
var r = super.getScreenBounds(newRect, camera);
|
||||
__doPostZoomScaleProcedure();
|
||||
return r;
|
||||
}
|
||||
|
||||
public override function drawComplex(camera:FlxCamera)
|
||||
{
|
||||
super.drawComplex(camera);
|
||||
}
|
||||
|
||||
public override function doAdditionalMatrixStuff(matrix:FlxMatrix, camera:FlxCamera)
|
||||
{
|
||||
super.doAdditionalMatrixStuff(matrix, camera);
|
||||
matrix.translate(-camera.width / 2, -camera.height / 2);
|
||||
|
||||
var requestedZoom = FlxMath.lerp(1, camera.zoom, zoomFactor);
|
||||
var diff = requestedZoom / camera.zoom;
|
||||
matrix.scale(diff, diff);
|
||||
matrix.translate(camera.width / 2, camera.height / 2);
|
||||
}
|
||||
|
||||
public override function getScreenPosition(?point:FlxPoint, ?Camera:FlxCamera):FlxPoint
|
||||
{
|
||||
if (__shouldDoScaleProcedure())
|
||||
{
|
||||
__oldScrollFactor.set(scrollFactor.x, scrollFactor.y);
|
||||
var requestedZoom = FlxMath.lerp(initialZoom, camera.zoom, zoomFactor);
|
||||
var diff = requestedZoom / camera.zoom;
|
||||
|
||||
scrollFactor.scale(1 / diff);
|
||||
|
||||
var r = super.getScreenPosition(point, Camera);
|
||||
|
||||
scrollFactor.set(__oldScrollFactor.x, __oldScrollFactor.y);
|
||||
|
||||
return r;
|
||||
}
|
||||
return super.getScreenPosition(point, Camera);
|
||||
}
|
||||
|
||||
// ANIMATE ATLAS DRAWING
|
||||
#if REGION
|
||||
public override function draw()
|
||||
@@ -212,7 +190,7 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
|
||||
public function copyAtlasValues()
|
||||
{
|
||||
@:privateAccess {
|
||||
animateAtlas.cameras = cameras;
|
||||
animateAtlas.cameras = cameras; // investigate if we can use _cameras
|
||||
animateAtlas.scrollFactor = scrollFactor;
|
||||
animateAtlas.scale = scale;
|
||||
animateAtlas.offset = offset;
|
||||
@@ -251,33 +229,60 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
|
||||
}
|
||||
#end
|
||||
|
||||
// SCALING FUNCS
|
||||
#if REGION
|
||||
private inline function __shouldDoScaleProcedure()
|
||||
return zoomFactor != 1;
|
||||
// ZOOM FACTOR
|
||||
private inline function __shouldDoZoomFactor()
|
||||
return zoomFactorEnabled && zoomFactor != 1;
|
||||
|
||||
static var __oldScrollFactor:FlxPoint = new FlxPoint();
|
||||
static var __oldScale:FlxPoint = new FlxPoint();
|
||||
var __skipZoomProcedure:Bool = false;
|
||||
|
||||
private function __doPreZoomScaleProcedure(camera:FlxCamera)
|
||||
public override function getScreenBounds(?newRect:FlxRect, ?camera:FlxCamera):FlxRect
|
||||
{
|
||||
if (__skipZoomProcedure = !__shouldDoScaleProcedure())
|
||||
return;
|
||||
__oldScale.set(scale.x, scale.y);
|
||||
var requestedZoom = FlxMath.lerp(initialZoom, camera.zoom, zoomFactor);
|
||||
var diff = requestedZoom * camera.zoom;
|
||||
if (camera == null)
|
||||
camera = FlxG.camera;
|
||||
|
||||
scale.scale(diff);
|
||||
var r = super.getScreenBounds(newRect, camera);
|
||||
|
||||
if(__shouldDoZoomFactor()) {
|
||||
r.x -= camera.width / 2;
|
||||
r.y -= camera.height / 2;
|
||||
|
||||
var ratio = (camera.zoom > 0 ? Math.max : Math.min)(0, FlxMath.lerp(1 / camera.zoom, 1, zoomFactor));
|
||||
r.x *= ratio;
|
||||
r.y *= ratio;
|
||||
r.width *= ratio;
|
||||
r.height *= ratio;
|
||||
|
||||
r.x += camera.width / 2;
|
||||
r.y += camera.height / 2;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
private function __doPostZoomScaleProcedure()
|
||||
override public function isOnScreen(?camera:FlxCamera):Bool
|
||||
{
|
||||
if (__skipZoomProcedure)
|
||||
return;
|
||||
scale.set(__oldScale.x, __oldScale.y);
|
||||
if (forceIsOnScreen)
|
||||
return true;
|
||||
|
||||
if (camera == null)
|
||||
camera = FlxG.camera;
|
||||
|
||||
var bounds = getScreenBounds(_rect, camera);
|
||||
if (bounds.width == 0 && bounds.height == 0)
|
||||
return false;
|
||||
return camera.containsRect(bounds);
|
||||
}
|
||||
|
||||
// ZOOM FACTOR RENDERING
|
||||
public override function doAdditionalMatrixStuff(matrix:FlxMatrix, camera:FlxCamera)
|
||||
{
|
||||
super.doAdditionalMatrixStuff(matrix, camera);
|
||||
if(__shouldDoZoomFactor()) {
|
||||
matrix.translate(-camera.width / 2, -camera.height / 2);
|
||||
|
||||
var requestedZoom = (camera.zoom >= 0 ? Math.max : Math.min)(FlxMath.lerp(1, camera.zoom, zoomFactor), 0);
|
||||
var diff = requestedZoom / camera.zoom;
|
||||
matrix.scale(diff, diff);
|
||||
matrix.translate(camera.width / 2, camera.height / 2);
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
// OFFSETTING
|
||||
#if REGION
|
||||
@@ -403,6 +408,11 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
|
||||
return animateAtlas != null ? animateAtlas.anim.isAtEnd : (animation.curAnim != null ? animation.curAnim.isAtEnd : false);
|
||||
}
|
||||
|
||||
override function updateAnimation(elapsed:Float) {
|
||||
if (animEnabled)
|
||||
super.updateAnimation(elapsed);
|
||||
}
|
||||
|
||||
// Backwards compat (the names used to be all different and it sucked, please lets use the same format in the future) - Nex
|
||||
public inline function hasAnimation(AnimName:String) return hasAnim(AnimName);
|
||||
public inline function removeAnimation(name:String) return removeAnim(name);
|
||||
|
||||
@@ -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
|
||||
@@ -139,7 +141,9 @@ class MusicBeatState extends FlxState implements IBeatReceiver
|
||||
script.load();
|
||||
}
|
||||
}
|
||||
#if EXPERMENTAL_SCRIPT_RELOADING
|
||||
else stateScripts.reload();
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,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();
|
||||
@@ -221,10 +229,6 @@ class MusicBeatState extends FlxState implements IBeatReceiver
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
// TODO: DEBUG MODE!!
|
||||
if (FlxG.keys.justPressed.F5) {
|
||||
loadScript();
|
||||
}
|
||||
call("update", [elapsed]);
|
||||
|
||||
super.update(elapsed);
|
||||
|
||||
@@ -122,7 +122,9 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver
|
||||
script.load();
|
||||
}
|
||||
}
|
||||
#if EXPERMENTAL_SCRIPT_RELOADING
|
||||
else stateScripts.reload();
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,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() {
|
||||
@@ -179,10 +185,6 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
// TODO: DEBUG MODE!!
|
||||
if (FlxG.keys.justPressed.F5) {
|
||||
loadScript();
|
||||
}
|
||||
call("update", [elapsed]);
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import flixel.FlxState;
|
||||
import flixel.tweens.FlxTween;
|
||||
import funkin.backend.scripting.Script;
|
||||
import funkin.backend.scripting.events.CancellableEvent;
|
||||
import funkin.backend.scripting.events.ResizeEvent;
|
||||
import funkin.backend.scripting.events.TransitionCreationEvent;
|
||||
import funkin.backend.utils.FunkinParentDisabler;
|
||||
import funkin.editors.ui.UIState;
|
||||
|
||||
class MusicBeatTransition extends MusicBeatSubstate {
|
||||
public static var script:String = Flags.DEFAULT_TRANSITION_SCRIPT;
|
||||
@@ -52,23 +54,12 @@ class MusicBeatTransition extends MusicBeatSubstate {
|
||||
return;
|
||||
}
|
||||
|
||||
blackSpr = new FlxSprite(0, transOut ? -transitionCamera.height : transitionCamera.height).makeGraphic(1, 1, -1);
|
||||
blackSpr.scale.set(transitionCamera.width, transitionCamera.height);
|
||||
add(blackSpr = new FlxSprite(0, transOut ? -transitionCamera.height : transitionCamera.height).makeGraphic(1, 1, -1));
|
||||
blackSpr.color = 0xFF000000;
|
||||
blackSpr.updateHitbox();
|
||||
add(blackSpr);
|
||||
add(transitionSprite = new FunkinSprite().loadSprite(Paths.image('menus/transitionSpr')));
|
||||
resizeDefaultSprites();
|
||||
|
||||
transitionSprite = new FunkinSprite();
|
||||
transitionSprite.loadSprite(Paths.image('menus/transitionSpr'));
|
||||
if (transitionSprite.animateAtlas == null) {
|
||||
transitionSprite.setGraphicSize(transitionCamera.width, transitionCamera.height);
|
||||
transitionSprite.updateHitbox();
|
||||
} else {
|
||||
transitionSprite.screenCenter();
|
||||
}
|
||||
transitionCamera.flipY = !transOut;
|
||||
add(transitionSprite);
|
||||
|
||||
transitionCamera.scroll.y = transitionCamera.height;
|
||||
transitionTween = FlxTween.tween(transitionCamera.scroll, {y: -transitionCamera.height}, 2/3, {
|
||||
ease: FlxEase.sineOut,
|
||||
@@ -110,6 +101,28 @@ class MusicBeatTransition extends MusicBeatSubstate {
|
||||
transitionScript.call('postUpdate', [elapsed]);
|
||||
}
|
||||
|
||||
public override function onResize(w:Int, h:Int) {
|
||||
super.onResize(w, h);
|
||||
if (FlxG.state is UIState && UIState.resolutionAware) resizeDefaultSprites();
|
||||
transitionScript.call('onResize', [EventManager.get(ResizeEvent).recycle(w, h, null, null)]);
|
||||
}
|
||||
|
||||
public function resizeDefaultSprites() {
|
||||
if (blackSpr != null) {
|
||||
blackSpr.scale.set(transitionCamera.width, transitionCamera.height);
|
||||
blackSpr.updateHitbox();
|
||||
}
|
||||
|
||||
if (transitionSprite != null) {
|
||||
if (transitionSprite.animateAtlas == null) {
|
||||
transitionSprite.setGraphicSize(transitionCamera.width, transitionCamera.height);
|
||||
transitionSprite.updateHitbox();
|
||||
} else {
|
||||
transitionSprite.screenCenter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function finish() {
|
||||
var event = new CancellableEvent();
|
||||
transitionScript.call('onFinish', [event]);
|
||||
|
||||
@@ -81,7 +81,7 @@ class AssetsLibraryList extends AssetLibrary {
|
||||
if (l is IModsAssetLibrary) {
|
||||
var lib = cast(l, IModsAssetLibrary);
|
||||
for(e in lib.getFiles(folder))
|
||||
content.push(e);
|
||||
content.pushOnce(e);
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
@@ -96,13 +96,18 @@ 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)
|
||||
return mods;
|
||||
|
||||
for (modFolder in modsList) {
|
||||
if (FileSystem.isDirectory('${modsPath}${modFolder}')) {
|
||||
if (FileSystem.isDirectory(modsPath + modFolder)) {
|
||||
mods.push(modFolder);
|
||||
} else {
|
||||
var ext = Path.extension(modFolder).toLowerCase();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -100,8 +98,7 @@ class Paths
|
||||
if (!OpenFlAssets.exists(scriptPath)) {
|
||||
var p:String;
|
||||
for(ex in Script.scriptExtensions) {
|
||||
p = '$scriptPath.$ex';
|
||||
if (OpenFlAssets.exists(p)) {
|
||||
if (OpenFlAssets.exists(p = scriptPath + '.' + ex)) {
|
||||
scriptPath = p;
|
||||
break;
|
||||
}
|
||||
@@ -112,8 +109,7 @@ 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);
|
||||
}
|
||||
@@ -191,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.
|
||||
@@ -246,14 +265,13 @@ class Paths
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
public static function getFolderContent(key:String, addPath:Bool = false, source: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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -45,6 +45,8 @@ enum abstract ChartFormat(Int) {
|
||||
}
|
||||
|
||||
class Chart {
|
||||
public static final version:String = "1.6.0";
|
||||
|
||||
public static function cleanSongData(data:Dynamic):Dynamic {
|
||||
if (Reflect.hasField(data, "song")) {
|
||||
var field:Dynamic = Reflect.field(data, "song");
|
||||
@@ -70,43 +72,40 @@ 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 {
|
||||
data = Json.parse(Assets.getText(path)).events;
|
||||
} catch(e) {
|
||||
Logs.error('Failed to load song event data for ${songName} ($path): ${Std.string(e)}');
|
||||
}
|
||||
for (event in data) event.global = true;
|
||||
} catch(e) Logs.trace('Failed to load song event data for ${songName} ($path): ${Std.string(e)}', ERROR);
|
||||
}
|
||||
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.error('Failed to load song metadata for ${songName} ($path): ${Std.string(e)}');
|
||||
}
|
||||
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);
|
||||
@@ -117,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,11 +158,9 @@ class Chart {
|
||||
valid = false;
|
||||
}
|
||||
var data:Dynamic = null;
|
||||
try {
|
||||
if (valid)
|
||||
data = Json.parse(Assets.getText(chartPath));
|
||||
} catch(e) {
|
||||
Logs.error('Could not parse chart for song ${songName} ($difficulty): ${Std.string(e)}');
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,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",
|
||||
@@ -195,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
|
||||
@@ -220,6 +212,14 @@ class Chart {
|
||||
base.events = base.events.concat(extraEvents);
|
||||
#end
|
||||
|
||||
/**
|
||||
* Set defaults on strum lines
|
||||
*/
|
||||
for(strumLine in base.strumLines) {
|
||||
if(strumLine.keyCount == null)
|
||||
strumLine.keyCount = 4;
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
@@ -229,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;
|
||||
}
|
||||
@@ -249,9 +248,10 @@ class Chart {
|
||||
if (saveSettings == null) saveSettings = {};
|
||||
|
||||
if (saveSettings.saveMetaInChart == null) saveSettings.saveMetaInChart = true;
|
||||
if (saveSettings.saveEventsInChart == null) saveSettings.saveEventsInChart = true;
|
||||
if (saveSettings.saveLocalEvents == null) saveSettings.saveLocalEvents = true;
|
||||
if (saveSettings.saveGlobalEvents == null) saveSettings.saveGlobalEvents = false;
|
||||
|
||||
var filteredChart = filterChartForSaving(chart, saveSettings.saveMetaInChart, saveSettings.saveEventsInChart);
|
||||
var filteredChart = filterChartForSaving(chart, saveSettings.saveMetaInChart, saveSettings.saveLocalEvents, saveSettings.saveGlobalEvents);
|
||||
var meta = filteredChart.meta;
|
||||
|
||||
#if sys
|
||||
@@ -265,39 +265,56 @@ 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;
|
||||
}
|
||||
|
||||
public static function filterChartForSaving(chart:ChartData, ?saveMetaInChart:Null<Bool>, ?saveEventsInChart:Null<Bool>):ChartData {
|
||||
public static function filterChartForSaving(chart:ChartData, ?saveMetaInChart:Bool, ?saveLocalEvents:Bool, ?saveGlobalEvents:Bool):ChartData {
|
||||
var data = Reflect.copy(chart); // make a copy of the chart to leave the OG intact
|
||||
if (saveMetaInChart != true) {
|
||||
data.meta = null;
|
||||
} else {
|
||||
data.meta = Reflect.copy(chart.meta); // also make a copy of the metadata to leave the OG intact.
|
||||
if(data.meta != null && Reflect.hasField(data.meta, "parsedColor")) Reflect.deleteField(data.meta, "parsedColor");
|
||||
if (data.meta != null && Reflect.hasField(data.meta, "parsedColor")) Reflect.deleteField(data.meta, "parsedColor");
|
||||
}
|
||||
|
||||
// in this part abt the events, i gotta account that these booleans can be null - Nex
|
||||
if (saveLocalEvents != true && saveGlobalEvents != true) data.events = null;
|
||||
else {
|
||||
data.events = [];
|
||||
for (event in chart.events) if ((saveLocalEvents == true && event.global != true) || (saveGlobalEvents == true && event.global == true)) {
|
||||
var copy = Reflect.copy(event);
|
||||
if (saveLocalEvents == true ? event.global != true : event.global == true) Reflect.deleteField(copy, "global"); // should NOT delete the field when saving with the local events and the event should have been global - Nex
|
||||
data.events.push(copy);
|
||||
}
|
||||
if (data.events.length == 0) data.events = null;
|
||||
}
|
||||
|
||||
data.events = saveEventsInChart != true ? null : Reflect.copy(chart.events); // same here once again
|
||||
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 = {
|
||||
var ?overrideExistingMeta:Bool;
|
||||
var ?saveMetaInChart:Bool;
|
||||
var ?saveEventsInChart:Bool;
|
||||
var ?saveLocalEvents:Bool;
|
||||
var ?saveGlobalEvents:Bool;
|
||||
var ?prettyPrint:Bool;
|
||||
var ?folder:String;
|
||||
}
|
||||
@@ -10,7 +10,9 @@ 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;
|
||||
}
|
||||
|
||||
@@ -21,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 = {
|
||||
@@ -41,8 +39,10 @@ typedef ChartStrumLine = {
|
||||
var ?visible:Null<Bool>;
|
||||
var ?strumPos:Array<Float>;
|
||||
var ?strumScale:Float;
|
||||
var ?strumSpacing:Float;
|
||||
var ?scrollSpeed:Float;
|
||||
var ?vocalsSuffix:String;
|
||||
var ?keyCount:Int; // default=4
|
||||
|
||||
var ?strumLinePos:Float; // Backwards compatibility
|
||||
}
|
||||
@@ -54,10 +54,17 @@ 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;
|
||||
var params:Array<Dynamic>;
|
||||
var ?global:Bool; // If its from a global event file like events.json; this field might be saved rarely in the chart json directly but modders can mess with this for messing with the saving funcs too - Nex
|
||||
}
|
||||
|
||||
enum abstract ChartStrumLineType(Int) from Int to Int {
|
||||
|
||||
@@ -10,20 +10,75 @@ import openfl.Assets;
|
||||
using StringTools;
|
||||
|
||||
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"];
|
||||
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 @@ 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 @@ 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();
|
||||
@@ -135,6 +202,7 @@ 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
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package funkin.backend.scripting;
|
||||
import flixel.FlxState;
|
||||
import funkin.backend.scripting.events.*;
|
||||
|
||||
class EventManager {
|
||||
final class EventManager {
|
||||
// map doesn't work for that
|
||||
public static var eventValues:Array<CancellableEvent> = [];
|
||||
public static var eventKeys:Array<Class<CancellableEvent>> = [];
|
||||
|
||||
@@ -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,17 +50,13 @@ class GlobalScript {
|
||||
});
|
||||
FlxG.signals.postUpdate.add(function() {
|
||||
call("postUpdate", [FlxG.elapsed]);
|
||||
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;
|
||||
}
|
||||
if (FlxG.keys.justPressed.F2)
|
||||
|
||||
if (PlayerSettings.solo.controls.DEV_CONSOLE)
|
||||
NativeAPI.allocConsole();
|
||||
});
|
||||
FlxG.signals.preDraw.add(function() {
|
||||
@@ -74,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 {
|
||||
@@ -93,6 +106,7 @@ class GlobalScript {
|
||||
if (scripts != null)
|
||||
scripts.call(name, args);
|
||||
}
|
||||
|
||||
public static function onModSwitch(newMod:String) {
|
||||
call("destroy");
|
||||
scripts = FlxDestroyUtil.destroy(scripts);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -300,7 +300,7 @@ class Script extends FlxBasic implements IFlxDestroyable {
|
||||
if(remappedNames.exists(fileName))
|
||||
fileName = remappedNames.get(fileName);
|
||||
Logs.traceColored([
|
||||
Logs.logText('${fileName}: ', GREEN),
|
||||
Logs.logText(fileName + ': ', GREEN),
|
||||
Logs.logText(Std.string(v))
|
||||
], TRACE);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
**/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,7 +2,7 @@ package funkin.backend.system;
|
||||
|
||||
#if sys
|
||||
import sys.FileSystem;
|
||||
class CommandLineHandler {
|
||||
final class CommandLineHandler {
|
||||
public static function parseCommandLine(cmd:Array<String>) {
|
||||
var i:Int = 0;
|
||||
while(i < cmd.length) {
|
||||
|
||||
@@ -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,25 @@ 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> = [];
|
||||
|
||||
public var gamepadsAdded:Array<Int> = [];
|
||||
@@ -219,13 +243,13 @@ class Controls extends FlxActionSet
|
||||
macro_forEachBound(control, (action, _) -> removeKeys(action, keys));
|
||||
}
|
||||
|
||||
inline static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState)
|
||||
public inline static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState)
|
||||
{
|
||||
for (key in keys)
|
||||
action.addKey(key, state);
|
||||
}
|
||||
|
||||
static function removeKeys(action:FlxActionDigital, keys:Array<FlxKey>)
|
||||
public static function removeKeys(action:FlxActionDigital, keys:Array<FlxKey>)
|
||||
{
|
||||
var i = action.inputs.length;
|
||||
while (i-- > 0)
|
||||
@@ -302,13 +326,13 @@ class Controls extends FlxActionSet
|
||||
macro_forEachBound(control, (action, _) -> removeButtons(action, gamepadID, buttons));
|
||||
}
|
||||
|
||||
inline static function addButtons(action:FlxActionDigital, buttons:Array<FlxGamepadInputID>, state, id)
|
||||
public inline static function addButtons(action:FlxActionDigital, buttons:Array<FlxGamepadInputID>, state, id)
|
||||
{
|
||||
for (button in buttons)
|
||||
action.addGamepad(button, state, id);
|
||||
}
|
||||
|
||||
static function removeButtons(action:FlxActionDigital, gamepadID:Int, buttons:Array<FlxGamepadInputID>)
|
||||
public static function removeButtons(action:FlxActionDigital, gamepadID:Int, buttons:Array<FlxGamepadInputID>)
|
||||
{
|
||||
var i = action.inputs.length;
|
||||
while (i-- > 0)
|
||||
@@ -319,8 +343,21 @@ class Controls extends FlxActionSet
|
||||
}
|
||||
}
|
||||
|
||||
inline static function isGamepad(input:FlxActionInput, deviceID:Int)
|
||||
public inline static function isGamepad(input:FlxActionInput, deviceID:Int)
|
||||
{
|
||||
return input.device == GAMEPAD && (deviceID == FlxInputDeviceID.ALL || input.deviceID == deviceID);
|
||||
}
|
||||
}
|
||||
|
||||
@:nullSafety(Off)
|
||||
public inline function getJustPressed(name:String) {
|
||||
return ControlsUtil.getJustPressed(this, name);
|
||||
}
|
||||
@:nullSafety(Off)
|
||||
public inline function getJustReleased(name:String) {
|
||||
return ControlsUtil.getJustReleased(this, name);
|
||||
}
|
||||
@:nullSafety(Off)
|
||||
public inline function getPressed(name:String) {
|
||||
return ControlsUtil.getPressed(this, name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package funkin.backend.system;
|
||||
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.graphics.tile.FlxDrawTrianglesItem;
|
||||
import flixel.math.FlxMatrix;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.system.FlxAssets.FlxShader;
|
||||
import openfl.display.BitmapData;
|
||||
import openfl.display.BlendMode;
|
||||
import openfl.geom.ColorTransform;
|
||||
import openfl.geom.Point;
|
||||
import openfl.geom.Rectangle;
|
||||
|
||||
class FakeCamera extends FlxCamera {
|
||||
public static final instance = new FakeCamera();
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
|
||||
visible = false;
|
||||
}
|
||||
|
||||
override function startTrianglesBatch(graphic:FlxGraphic, smoothing:Bool = false, isColored:Bool = false, ?blend:BlendMode, ?hasColorOffsets:Bool, ?shader:FlxShader) { return null;}
|
||||
override function startQuadBatch(graphic:FlxGraphic, colored:Bool, hasColorOffsets:Bool = false, ?blend:BlendMode, smooth:Bool = false, ?shader:FlxShader) { return null;}
|
||||
override function clearDrawStack() {}
|
||||
override function render() {}
|
||||
|
||||
public override function drawPixels(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader) {}
|
||||
public override function copyPixels(?frame:FlxFrame, ?pixels:BitmapData, ?sourceRect:Rectangle, destPoint:Point, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader) {}
|
||||
public override function drawTriangles(graphic:FlxGraphic, vertices:DrawData<Float>, indices:DrawData<Int>, uvtData:DrawData<Float>, ?colors:DrawData<Int>, ?position:FlxPoint, ?blend:BlendMode, repeat:Bool = false, smoothing:Bool = false, ?transform:ColorTransform, ?shader:FlxShader):Void {}
|
||||
|
||||
public override function update(elapsed:Float) {}
|
||||
|
||||
// override function set_zoom(Zoom:Float):Float return zoom = (Zoom == 0) ? FlxCamera.defaultZoom : Zoom;
|
||||
|
||||
public override function setScale(X:Float, Y:Float):Void {}
|
||||
}
|
||||
|
||||
class FakeCallCamera extends FakeCamera {
|
||||
public static final instance = new FakeCallCamera();
|
||||
public var ignoreDraws:Bool = false;
|
||||
|
||||
public dynamic function onDraw(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader) {
|
||||
}
|
||||
|
||||
override function drawPixels(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader) {
|
||||
if (!ignoreDraws) onDraw(frame, pixels, matrix, transform, blend, smoothing, shader);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,40 @@
|
||||
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_API_VERSION:Int;
|
||||
public static var MOD_DOWNLOADED_LINK:String;
|
||||
public static var MOD_NAME:String = "";
|
||||
public static var MOD_DESCRIPTION:String = "";
|
||||
public static var MOD_API_VERSION:Int = 1;
|
||||
public static var MOD_DOWNLOAD_LINK:String = "";
|
||||
public static var MOD_DEPENDENCIES:Array<String> = [];
|
||||
|
||||
public static var MOD_ICON64:String;
|
||||
public static var MOD_ICON32:String;
|
||||
public static var MOD_ICON16:String;
|
||||
public static var MOD_ICON:String;
|
||||
public static var MOD_ICON64:String = "";
|
||||
public static var MOD_ICON32:String = "";
|
||||
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_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)';
|
||||
@@ -42,10 +48,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.
|
||||
@@ -55,7 +57,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)
|
||||
@@ -88,23 +90,25 @@ class Flags {
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -122,11 +126,13 @@ 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
|
||||
@@ -196,40 +202,19 @@ class Flags {
|
||||
// -- 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);
|
||||
}
|
||||
@@ -248,20 +233,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 TRANSLATIONS_SUPPORT
|
||||
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
|
||||
if(!(lib is funkin.backend.assets.TranslatedAssetLibrary))
|
||||
#end
|
||||
if(lib.exists(flagsPath, AssetType.TEXT))
|
||||
lib.getAsset(flagsPath, AssetType.TEXT)
|
||||
];
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import funkin.backend.utils.NativeAPI.ConsoleColor;
|
||||
import funkin.backend.utils.NativeAPI;
|
||||
import haxe.Log;
|
||||
|
||||
class Logs {
|
||||
final class Logs {
|
||||
private static var __showing:Bool = false;
|
||||
|
||||
public static var nativeTrace = Log.trace;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package funkin.backend.system;
|
||||
|
||||
import funkin.backend.utils.translations.FormatUtil;
|
||||
import sys.io.File;
|
||||
import sys.FileSystem;
|
||||
import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond;
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import flixel.addons.transition.TransitionData;
|
||||
@@ -32,7 +33,7 @@ class Main extends Sprite
|
||||
public static var instance:Main;
|
||||
|
||||
public static var modToLoad:String = null;
|
||||
public static var forceGPUOnlyBitmapsOff:Bool = #if windows false #else true #end;
|
||||
public static var forceGPUOnlyBitmapsOff:Bool = #if desktop false #else true #end;
|
||||
public static var noTerminalColor:Bool = false;
|
||||
public static var verbose:Bool = false;
|
||||
|
||||
@@ -86,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;
|
||||
|
||||
@@ -174,6 +175,9 @@ class Main extends Sprite
|
||||
|
||||
ModsFolder.init();
|
||||
#if MOD_SUPPORT
|
||||
if (FileSystem.exists("mods/autoload.txt"))
|
||||
modToLoad = File.getContent("mods/autoload.txt").trim();
|
||||
|
||||
ModsFolder.switchMod(modToLoad.getDefault(Options.lastLoadedMod));
|
||||
#end
|
||||
|
||||
@@ -181,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;
|
||||
@@ -239,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) {
|
||||
@@ -29,31 +36,71 @@ class MainState extends FlxState {
|
||||
#end
|
||||
Options.save();
|
||||
|
||||
ControlsUtil.resetCustomControls();
|
||||
FlxG.bitmap.reset();
|
||||
FlxG.sound.destroy(true);
|
||||
|
||||
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();
|
||||
@@ -65,22 +112,29 @@ class MainState extends FlxState {
|
||||
Main.refreshAssets();
|
||||
DiscordUtil.init();
|
||||
EventsData.reloadEvents();
|
||||
ControlsUtil.loadCustomControls();
|
||||
TitleState.initialized = false;
|
||||
|
||||
if(Framerate.isLoaded)
|
||||
if (Framerate.isLoaded)
|
||||
Framerate.instance.reload();
|
||||
|
||||
FlxG.switchState(new funkin.editors.alphabet.AlphabetEditor("normal"));
|
||||
/*
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FlxG.switchState(new TitleState());
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,5 @@ class CodenameBuildField extends TextField {
|
||||
}
|
||||
|
||||
public function reload()
|
||||
text = 'Codename Engine ${Flags.RELEASE_CYCLE}\n${Flags.COMMIT_MESSAGE}';
|
||||
text = '${Flags.VERSION_MESSAGE}\n${Flags.COMMIT_MESSAGE}';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -118,7 +118,7 @@ class Framerate extends Sprite {
|
||||
x = 10 + offset.x;
|
||||
y = 2 + offset.y;
|
||||
|
||||
var width = Math.max(fpsCounter.width, #if SHOW_BUILD_ON_FPS Math.max(memoryCounter.width, codenameBuildField.width) #else memoryCounter.width #end) + (x*2);
|
||||
var width = MathUtil.maxSmart(fpsCounter.width, memoryCounter.width #if SHOW_BUILD_ON_FPS , codenameBuildField.width #end) + (x*2);
|
||||
var height = #if SHOW_BUILD_ON_FPS codenameBuildField.y + codenameBuildField.height #else memoryCounter.y + memoryCounter.height #end;
|
||||
bgSprite.x = -x;
|
||||
bgSprite.y = offset.x;
|
||||
|
||||
@@ -3,6 +3,10 @@ package funkin.backend.system.framerate;
|
||||
import funkin.backend.system.Logs;
|
||||
import funkin.backend.utils.MemoryUtil;
|
||||
import funkin.backend.utils.native.HiddenProcess;
|
||||
#if cpp
|
||||
import cpp.Float64;
|
||||
import cpp.UInt64;
|
||||
#end
|
||||
|
||||
using StringTools;
|
||||
|
||||
@@ -17,7 +21,7 @@ class SystemInfo extends FramerateCategory {
|
||||
|
||||
static var __formattedSysText:String = "";
|
||||
|
||||
public static inline function init() {
|
||||
public static function init() {
|
||||
#if linux
|
||||
var process = new HiddenProcess("cat", ["/etc/os-release"]);
|
||||
if (process.exitCode() != 0) Logs.error('Unable to grab OS Label');
|
||||
@@ -49,6 +53,25 @@ 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}';
|
||||
@@ -58,10 +81,7 @@ class SystemInfo extends FramerateCategory {
|
||||
|
||||
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';
|
||||
@@ -82,7 +102,7 @@ class SystemInfo extends FramerateCategory {
|
||||
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
|
||||
@@ -91,11 +111,13 @@ class SystemInfo extends FramerateCategory {
|
||||
#end
|
||||
|
||||
if(openfl.display3D.Context3D.__glMemoryTotalAvailable != -1) {
|
||||
var vRAMBytes:UInt = cast flixel.FlxG.stage.context3D.gl.getParameter(openfl.display3D.Context3D.__glMemoryTotalAvailable);
|
||||
var vRAMBytes:Int = cast flixel.FlxG.stage.context3D.gl.getParameter(openfl.display3D.Context3D.__glMemoryTotalAvailable);
|
||||
if (vRAMBytes == 1000 || vRAMBytes == 1 || vRAMBytes <= 0)
|
||||
Logs.error('Unable to grab GPU VRAM');
|
||||
else
|
||||
vRAM = CoolUtil.getSizeString(vRAMBytes * 1000);
|
||||
Logs.trace('Unable to grab GPU VRAM', ERROR, RED);
|
||||
else {
|
||||
var vRAMBytesFloat:#if cpp Float64 #else Float #end = vRAMBytes*1024;
|
||||
vRAM = CoolUtil.getSizeString64(vRAMBytesFloat);
|
||||
}
|
||||
}
|
||||
} else
|
||||
Logs.error('Unable to grab GPU Info');
|
||||
|
||||
@@ -7,6 +7,8 @@ class BuildInfo {
|
||||
public static function printBuildInfo() {
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
@@ -12,58 +14,56 @@ import lime.media.AudioManager;
|
||||
*/
|
||||
@:dox(hide)
|
||||
class AudioSwitchFix {
|
||||
@:noCompletion
|
||||
private static function onStateSwitch(state:FlxState):Void {
|
||||
#if windows
|
||||
if (Main.audioDisconnected) {
|
||||
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;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import openfl.errors.Error;
|
||||
import openfl.events.ErrorEvent;
|
||||
import openfl.events.UncaughtErrorEvent;
|
||||
|
||||
class CrashHandler {
|
||||
final class CrashHandler {
|
||||
public static function init() {
|
||||
Lib.current.loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, onUncaughtError);
|
||||
#if cpp
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ class UpdateAvailableScreen extends MusicBeatState {
|
||||
public override function update(elapsed:Float) {
|
||||
super.update(elapsed);
|
||||
|
||||
destY = FlxMath.bound(destY - (FlxG.mouse.wheel * 75), 0, Math.max(0, changeLogText.height - FlxG.height + versionCheckBG.height + 20 + optionsBG.height));
|
||||
destY = CoolUtil.bound(destY - (FlxG.mouse.wheel * 75), 0, Math.max(0, changeLogText.height - FlxG.height + versionCheckBG.height + 20 + optionsBG.height));
|
||||
FlxG.camera.scroll.y = lerp(FlxG.camera.scroll.y, destY, 1/3);
|
||||
|
||||
if (controls.LEFT_P || controls.RIGHT_P) {
|
||||
|
||||
@@ -3,38 +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 bytesStartPos:Int = Math.floor(startPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.bitsPerSample;
|
||||
var bytesEndPos:Int = Math.floor(endPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.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 % buffer.bitsPerSample;
|
||||
bytesEndPos -= bytesEndPos % buffer.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 maxByte:Int = 0;
|
||||
for(i in 0...Math.floor((bytesEndPos - bytesStartPos) / buffer.bitsPerSample)) {
|
||||
var byte:Int = buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample))
|
||||
| (buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample) + 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
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package funkin.backend.utils;
|
||||
|
||||
import flixel.util.FlxColor;
|
||||
import openfl.display.BitmapData;
|
||||
import openfl.geom.Point;
|
||||
import openfl.geom.Rectangle;
|
||||
|
||||
class BitmapUtil {
|
||||
final class BitmapUtil {
|
||||
/**
|
||||
* Returns the most present color in a Bitmap.
|
||||
* @param bmap Bitmap
|
||||
@@ -18,7 +20,7 @@ class BitmapUtil {
|
||||
for(y in 0...bmap.height) {
|
||||
for(x in 0...bmap.width) {
|
||||
color = bmap.getPixel32(x, y);
|
||||
fixedColor = 0xFF000000 + (color % 0x1000000);
|
||||
fixedColor = 0xFF000000 | (color & 0xFFFFFF);
|
||||
if (!colorMap.exists(fixedColor))
|
||||
colorMap[fixedColor] = 0;
|
||||
colorMap[fixedColor] += color.alphaFloat;
|
||||
@@ -49,7 +51,7 @@ class BitmapUtil {
|
||||
for(y in 0...bmap.height) {
|
||||
for(x in 0...bmap.width) {
|
||||
color = bmap.getPixel32(x, y);
|
||||
fixedColor = 0xFF000000 + (color % 0x1000000);
|
||||
fixedColor = 0xFF000000 | (color & 0xFFFFFF);
|
||||
if (!colorMap.exists(fixedColor))
|
||||
colorMap[fixedColor] = 0;
|
||||
colorMap[fixedColor] += color.alphaFloat * 0.33 + (0.67 * (color.saturation * (2 * (color.lightness > 0.5 ? 0.5 - (color.lightness) : color.lightness))));
|
||||
@@ -66,4 +68,49 @@ class BitmapUtil {
|
||||
}
|
||||
return mostPresentColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new bitmap without any empty transperent space on the edges
|
||||
* @param bitmap The bitmap to be cropped
|
||||
*/
|
||||
public static function crop(bitmap:BitmapData) {
|
||||
var bitmapBounds:Rectangle = BitmapUtil.bounds(bitmap);
|
||||
|
||||
var croppedBitmap:BitmapData = new BitmapData(Std.int(bitmapBounds.width), Std.int(bitmapBounds.height), true, 0x00000000);
|
||||
croppedBitmap.copyPixels(bitmap, bitmapBounds, new Point(0,0));
|
||||
return croppedBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bounds of non empty pixels in the bitmap
|
||||
* @param bitmap
|
||||
* @return
|
||||
*/
|
||||
public static function bounds(bitmap:BitmapData, ?limit:Rectangle = null):Rectangle {
|
||||
var hasLimit:Bool = limit != null;
|
||||
// Searching bounds
|
||||
var startX:Int = hasLimit ? Std.int(limit.x) : 0;
|
||||
var startY:Int = hasLimit ? Std.int(limit.y) : 0;
|
||||
var endX:Int = hasLimit ? Std.int(limit.width) : bitmap.width;
|
||||
var endY:Int = hasLimit ? Std.int(limit.height) : bitmap.height;
|
||||
|
||||
// Detected bounds
|
||||
var minX:Int = endX;
|
||||
var minY:Int = endY;
|
||||
var maxX:Int = startX;
|
||||
var maxY:Int = startY;
|
||||
|
||||
for (y in startY...endY) {
|
||||
for (x in startX...endX) {
|
||||
if (bitmap.getPixel32(x, y) != 0x00000000) {
|
||||
if (x < minX) minX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Rectangle(minX, minY, maxX-minX+1, maxY-minY+1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
import funkin.options.PlayerSettings;
|
||||
import flixel.input.FlxInput;
|
||||
import flixel.input.actions.FlxAction;
|
||||
import funkin.backend.system.Controls;
|
||||
import flixel.input.actions.FlxActionInput;
|
||||
import flixel.input.actions.FlxActionManager;
|
||||
import flixel.input.actions.FlxActionSet;
|
||||
import flixel.input.gamepad.FlxGamepadInputID;
|
||||
import flixel.input.keyboard.FlxKey;
|
||||
import haxe.xml.Access;
|
||||
|
||||
class ControlsUtil {
|
||||
|
||||
private static var storedCustomControls:Array<String> = [];
|
||||
|
||||
public static function initCustomControl(controls:Controls, name:String) {
|
||||
if (controls.byName.exists(name)) return;
|
||||
|
||||
controls.byName.set(name, new FlxActionDigital(name));
|
||||
controls.byName.set(name+"_P", new FlxActionDigital(name+"_P"));
|
||||
controls.byName.set(name+"_R", new FlxActionDigital(name+"_R"));
|
||||
storedCustomControls.push(name);
|
||||
}
|
||||
|
||||
public static function addKeysToCustomControl(controls:Controls, name:String, keys:Array<FlxKey>) {
|
||||
if (!controls.byName.exists(name)) return;
|
||||
|
||||
var pressed:FlxActionDigital = controls.byName.get(name);
|
||||
if (pressed != null) Controls.addKeys(pressed, keys, PRESSED);
|
||||
|
||||
var justPressed:FlxActionDigital = controls.byName.get(name+"_P");
|
||||
if (justPressed != null) Controls.addKeys(justPressed, keys, JUST_PRESSED);
|
||||
|
||||
var justReleased:FlxActionDigital = controls.byName.get(name+"_R");
|
||||
if (justReleased != null) Controls.addKeys(justReleased, keys, JUST_RELEASED);
|
||||
}
|
||||
|
||||
public static function removeKeysFromCustomControl(controls:Controls, name:String, keys:Array<FlxKey>) {
|
||||
if (!controls.byName.exists(name)) return;
|
||||
|
||||
var pressed:FlxActionDigital = controls.byName.get(name);
|
||||
if (pressed != null) Controls.removeKeys(pressed, keys);
|
||||
|
||||
var justPressed:FlxActionDigital = controls.byName.get(name+"_P");
|
||||
if (justPressed != null) Controls.removeKeys(justPressed, keys);
|
||||
|
||||
var justReleased:FlxActionDigital = controls.byName.get(name+"_R");
|
||||
if (justReleased != null) Controls.removeKeys(justReleased, keys);
|
||||
}
|
||||
|
||||
public static function resetCustomControls() {
|
||||
removeCustomControls(PlayerSettings.solo.controls);
|
||||
removeCustomControls(PlayerSettings.player1.controls);
|
||||
removeCustomControls(PlayerSettings.player2.controls);
|
||||
storedCustomControls = [];
|
||||
}
|
||||
|
||||
private static inline function removeCustomControls(controls:Controls) {
|
||||
for (name in storedCustomControls) {
|
||||
var pressed:FlxActionDigital = controls.byName.get(name);
|
||||
if (pressed != null) pressed.destroy();
|
||||
|
||||
var justPressed:FlxActionDigital = controls.byName.get(name+"_P");
|
||||
if (justPressed != null) justPressed.destroy();
|
||||
|
||||
var justReleased:FlxActionDigital = controls.byName.get(name+"_R");
|
||||
if (justReleased != null) justReleased.destroy();
|
||||
|
||||
controls.byName.remove(name);
|
||||
controls.byName.remove(name+"_P");
|
||||
controls.byName.remove(name+"_R");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static inline function checkControl(controls:Controls, name:String) {
|
||||
var control = getControl(controls, name);
|
||||
if (control != null) return control.check();
|
||||
return false;
|
||||
}
|
||||
|
||||
public static inline function getControl(controls:Controls, name:String) : FlxActionDigital {
|
||||
return controls.byName.get(name);
|
||||
}
|
||||
|
||||
public static inline function getJustPressed(controls:Controls, name:String) {
|
||||
return checkControl(controls, name + "_P");
|
||||
}
|
||||
public static inline function getJustReleased(controls:Controls, name:String) {
|
||||
return checkControl(controls, name + "_R");
|
||||
}
|
||||
public static inline function getPressed(controls:Controls, name:String) {
|
||||
return checkControl(controls, name);
|
||||
}
|
||||
|
||||
public static function loadCustomControls() {
|
||||
var xmlPath = Paths.xml("config/controls");
|
||||
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 {
|
||||
access = new Access(Xml.parse(Paths.assetsTree.getSpecificAsset(xmlPath, "TEXT", source)).firstElement());
|
||||
} catch(e) {
|
||||
Logs.trace('Error while parsing controls.xml: ${Std.string(e)}', ERROR);
|
||||
}
|
||||
|
||||
if (access != null) {
|
||||
parseControlsXml(access);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static inline function parseControlsXml(xml:Access) {
|
||||
for (category in xml.elements) {
|
||||
for (control in category.elements) {
|
||||
if (!control.has.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = control.getAtt("name");
|
||||
var saveName = control.getAtt("saveName").getDefault("");
|
||||
var defaultKeyP1 = control.getAtt("keyP1").getDefault("");
|
||||
var defaultKeyP2 = control.getAtt("keyP2").getDefault("");
|
||||
|
||||
var keyP1:Null<FlxKey> = 0;
|
||||
var keyP2:Null<FlxKey> = 0;
|
||||
|
||||
ControlsUtil.initCustomControl(PlayerSettings.solo.controls, name);
|
||||
ControlsUtil.initCustomControl(PlayerSettings.player1.controls, name);
|
||||
ControlsUtil.initCustomControl(PlayerSettings.player2.controls, name);
|
||||
|
||||
if (saveName != "") {
|
||||
var p1:Null<FlxKey> = Reflect.getProperty(FlxG.save.data, "P1_"+saveName);
|
||||
var p2:Null<FlxKey> = Reflect.getProperty(FlxG.save.data, "P2_"+saveName);
|
||||
|
||||
//check for save data (and load default if it doesnt exist)
|
||||
if (p1 != null) {
|
||||
keyP1 = p1;
|
||||
} else {
|
||||
if (defaultKeyP1 != "") keyP1 = FlxKey.fromString(defaultKeyP1);
|
||||
Reflect.setProperty(FlxG.save.data, "P1_"+saveName, keyP1);
|
||||
}
|
||||
|
||||
if (p2 != null) {
|
||||
keyP2 = p2;
|
||||
} else {
|
||||
if (defaultKeyP2 != "") keyP2 = FlxKey.fromString(defaultKeyP2);
|
||||
Reflect.setProperty(FlxG.save.data, "P2_"+saveName, keyP2);
|
||||
}
|
||||
|
||||
} else {
|
||||
//if no save data just load the default
|
||||
if (defaultKeyP1 != "") keyP1 = FlxKey.fromString(defaultKeyP1);
|
||||
if (defaultKeyP2 != "") keyP2 = FlxKey.fromString(defaultKeyP2);
|
||||
}
|
||||
|
||||
ControlsUtil.addKeysToCustomControl(PlayerSettings.solo.controls, name, [keyP1, keyP2]);
|
||||
ControlsUtil.addKeysToCustomControl(PlayerSettings.player1.controls, name, [keyP1, 0]);
|
||||
ControlsUtil.addKeysToCustomControl(PlayerSettings.player2.controls, name, [0, keyP2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
#if cpp
|
||||
import cpp.Float64;
|
||||
#end
|
||||
import flxanimate.data.AnimationData.AnimAtlas;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import openfl.display.BitmapData;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
#if sys
|
||||
import sys.FileSystem;
|
||||
#end
|
||||
@@ -37,6 +45,9 @@ import haxe.io.Path;
|
||||
import haxe.xml.Access;
|
||||
import lime.utils.Assets;
|
||||
import openfl.geom.ColorTransform;
|
||||
import flixel.math.FlxPoint;
|
||||
import haxe.Constraints.IMap;
|
||||
import haxe.EnumTools.EnumValueTools;
|
||||
|
||||
using StringTools;
|
||||
|
||||
@@ -44,7 +55,7 @@ using StringTools;
|
||||
* Various utilities, that have no specific Util class.
|
||||
**/
|
||||
@:allow(funkin.game.PlayState)
|
||||
class CoolUtil
|
||||
final class CoolUtil
|
||||
{
|
||||
/**
|
||||
* Gets the last exception stack. Useful for debugging.
|
||||
@@ -166,7 +177,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -204,7 +215,7 @@ 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;
|
||||
@@ -265,10 +276,8 @@ class CoolUtil
|
||||
* Whenever a value is NaN or not.
|
||||
* @param v Value
|
||||
*/
|
||||
public static inline function isNaN(v:Dynamic) {
|
||||
if (v is Float || v is Int)
|
||||
return Math.isNaN(cast(v, Float));
|
||||
return false;
|
||||
public static inline function isNaN(v:Dynamic):Bool {
|
||||
return (v is Float) ? Math.isNaN(cast(v, Float)) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -326,22 +335,41 @@ class CoolUtil
|
||||
return str;
|
||||
}
|
||||
|
||||
private static var sizeLabels:Array<String> = ["B", "KB", "MB", "GB", "TB"];
|
||||
|
||||
/**
|
||||
* Returns a string representation of a size, following this format: `1.02 GB`, `134.00 MB`
|
||||
* @param size Size to convert to string
|
||||
* @return String Result string representation
|
||||
*/
|
||||
public static function getSizeString(size:Float):String {
|
||||
var labels = ["B", "KB", "MB", "GB", "TB"];
|
||||
var rSize:Float = size;
|
||||
var label:Int = 0;
|
||||
while(rSize > 1024 && label < labels.length-1) {
|
||||
var len = sizeLabels.length;
|
||||
while(rSize >= 1024 && label < len-1) {
|
||||
label++;
|
||||
rSize /= 1024;
|
||||
}
|
||||
return '${Std.int(rSize) + "." + addZeros(Std.string(Std.int((rSize % 1) * 100)), 2)}${labels[label]}';
|
||||
return Std.int(rSize) + ((label <= 1) ? "" : "." + addZeros(Std.string(Std.int((rSize % 1) * 100)), 2)) + sizeLabels[label];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of a size, following this format: `1.02 GB`, `134.00 MB`, using Float64 on cpp targets
|
||||
* @param size Size to convert to string
|
||||
* @return String Result string representation
|
||||
*/
|
||||
public static function getSizeString64(size: #if cpp Float64 #else Float #end):String {
|
||||
var rSize: #if cpp Float64 #else Float #end = size;
|
||||
var label:Int = 0;
|
||||
var len = sizeLabels.length;
|
||||
while(rSize >= 1024 && label < len-1) {
|
||||
label++;
|
||||
rSize /= 1024;
|
||||
}
|
||||
return Std.int(rSize) + ((label <= 1) ? "" : "." + addZeros(Std.string(Std.int((rSize % 1) * 100)), 2)) + sizeLabels[label];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replaces in a string any kind of IP with `[Your IP]` making the string safer to trace.
|
||||
* @param msg String to check and edit
|
||||
@@ -380,7 +408,7 @@ class CoolUtil
|
||||
* @return FPS-Modified Ratio
|
||||
*/
|
||||
@:noUsing public static inline function getFPSRatio(ratio:Float):Float {
|
||||
return FlxMath.bound(ratio * 60 * FlxG.elapsed, 0, 1);
|
||||
return CoolUtil.bound(ratio * 60 * FlxG.elapsed, 0.0, 1.0);
|
||||
}
|
||||
/**
|
||||
* Tries to get a color from a `Dynamic` variable.
|
||||
@@ -427,7 +455,6 @@ 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);
|
||||
}
|
||||
@@ -457,32 +484,34 @@ 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 = 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"]).getDefaultFloat(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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,6 +554,23 @@ class CoolUtil
|
||||
* @return Array<Int> Final array
|
||||
*/
|
||||
@:noUsing public static inline function numberArray(max:Int, ?min:Int = 0):Array<Int>
|
||||
{
|
||||
/*return [for (i in min...max) i];*/
|
||||
// Reason for change:
|
||||
// Old one uses push instead of creating an array with the wanted size.
|
||||
var len:Int = CoolUtil.maxInt(max - min, 0);
|
||||
#if cpp
|
||||
var arr:Array<Int> = untyped __cpp__("::Array_obj< int >::__new({0})", len);
|
||||
#else
|
||||
var arr:Array<Int> = [];
|
||||
arr.resize(len);
|
||||
#end
|
||||
for(i in 0...len)
|
||||
arr[i] = i + min;
|
||||
return arr;
|
||||
}
|
||||
|
||||
@:noUsing public static inline function numberArrayOld(max:Int, ?min:Int = 0):Array<Int>
|
||||
{
|
||||
return [for (i in min...max) i];
|
||||
}
|
||||
@@ -557,6 +603,21 @@ class CoolUtil
|
||||
sprite.scale.set(nScale, nScale);
|
||||
}
|
||||
|
||||
public static function setGraphicSizeFloat(sprite:FlxSprite, Width:Float = 0, Height:Float = 0):Void
|
||||
{
|
||||
if (Width <= 0 && Height <= 0)
|
||||
return;
|
||||
|
||||
var newScaleX:Float = Width / sprite.frameWidth;
|
||||
var newScaleY:Float = Height / sprite.frameHeight;
|
||||
sprite.scale.set(newScaleX, newScaleY);
|
||||
|
||||
if (Width <= 0)
|
||||
sprite.scale.x = newScaleY;
|
||||
else if (Height <= 0)
|
||||
sprite.scale.y = newScaleX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a simple string representation of a FlxKey. Used in Controls options.
|
||||
* @param key Key
|
||||
@@ -614,7 +675,6 @@ class CoolUtil
|
||||
case Y:
|
||||
obj.y = (cam.height - obj.height) / 2;
|
||||
case NONE:
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,8 +696,16 @@ class CoolUtil
|
||||
* @param name Name of the attribute
|
||||
*/
|
||||
public static inline function getAtt(xml:Access, name:String) {
|
||||
if (!xml.has.resolve(name)) return null;
|
||||
return xml.att.resolve(name);
|
||||
/*if (!xml.has.resolve(name)) return null;
|
||||
return xml.att.resolve(name);*/
|
||||
// Reason for change:
|
||||
// Old one has error checking 4 times, this one has it once.
|
||||
var xml:Xml = xml.x;
|
||||
if (xml.nodeType != Element) {
|
||||
throw 'Bad node type, expected Element but found ${xml.nodeType}';
|
||||
}
|
||||
@:privateAccess
|
||||
return xml.attributeMap.exists(name) ? xml.attributeMap.get(name) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -686,11 +754,11 @@ 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");
|
||||
}
|
||||
|
||||
@@ -736,7 +804,7 @@ class CoolUtil
|
||||
* Gets the macro class created by hscript-improved for an abstract / enum
|
||||
*/
|
||||
@:noUsing public static inline function getMacroAbstractClass(className:String) {
|
||||
return Type.resolveClass('${className}_HSC');
|
||||
return Type.resolveClass(className + '_HSC');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -753,6 +821,8 @@ class CoolUtil
|
||||
i--;
|
||||
}
|
||||
return i;*/
|
||||
// Reason for change:
|
||||
// Old one was made because at the time we didnt know the lastIndexOf function.
|
||||
return array.lastIndexOf(element);
|
||||
}
|
||||
|
||||
@@ -793,6 +863,24 @@ class CoolUtil
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Browse a path in the operating system's explorer
|
||||
* @param path
|
||||
*/
|
||||
public static inline function browsePath(path:String) {
|
||||
var formattedPath:String = Path.normalize(path);
|
||||
|
||||
#if windows
|
||||
formattedPath = formattedPath.replace("/", "\\");
|
||||
Sys.command("explorer", [formattedPath]);
|
||||
#elseif mac
|
||||
Sys.command("open", [formattedPath]);
|
||||
#elseif linux
|
||||
var cmd = Sys.command("xdg-open", [formattedPath]);
|
||||
if (cmd != 0) cmd = Sys.command("/usr/bin/xdg-open", [formattedPath]);
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a timestamp to a readable format such as `01:22` (`mm:ss`)
|
||||
*/
|
||||
@@ -818,6 +906,15 @@ class CoolUtil
|
||||
@:noUsing public static inline function maxInt(p1:Int, p2:Int)
|
||||
return p1 < p2 ? p2 : p1;
|
||||
|
||||
/**
|
||||
* Equivalent of `Math.min`, except doesn't require a Int -> Float -> Int conversion.
|
||||
* @param p1
|
||||
* @param p2
|
||||
* @return return p1 > p2 ? p2 : p1
|
||||
*/
|
||||
@:noUsing public static inline function minInt(p1:Int, p2:Int)
|
||||
return p1 > p2 ? p2 : p1;
|
||||
|
||||
/**
|
||||
* Equivalent of `Math.floor`, except doesn't require a Int -> Float -> Int conversion.
|
||||
* @param e Value to get the floor of.
|
||||
@@ -849,10 +946,11 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -860,8 +958,67 @@ 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.
|
||||
* @param path Path to get the filename from
|
||||
* @return Filename
|
||||
*/
|
||||
@:noUsing public static inline function getFilename(file:String) {
|
||||
var file = new haxe.io.Path(file);
|
||||
return file.file;
|
||||
}
|
||||
|
||||
@:noUsing public static function getClosestAngle(angle:Float, targetAngle:Float):Float {
|
||||
var diff:Float = angle - targetAngle;
|
||||
if (diff < -180) diff += 360;
|
||||
else if (diff > 180) diff -= 360;
|
||||
return angle - diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the screen position of an object, while taking the camera zoom into account.
|
||||
*
|
||||
* @param object Any `FlxObject`
|
||||
* @param camera The desired "screen" coordinate space. If `null`, `FlxG.camera` is used.
|
||||
* @param result Optional arg for the returning point
|
||||
* @return The screen position of the object.
|
||||
*/
|
||||
public static function worldToScreenPosition(object:FlxObject, ?camera:FlxCamera, ?result:FlxPoint) {
|
||||
if (result == null)
|
||||
result = FlxPoint.get();
|
||||
if (camera == null)
|
||||
camera = FlxG.camera;
|
||||
|
||||
result.set(object.x, object.y);
|
||||
result.x = (((result.x - camera.scroll.x * object.scrollFactor.x) * camera.zoom) - ((camera.width * 0.5) * (camera.zoom - camera.initialZoom)));
|
||||
result.y = (((result.y - camera.scroll.y * object.scrollFactor.y) * camera.zoom) - ((camera.height * 0.5) * (camera.zoom - camera.initialZoom)));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the screen position of an point, while taking the camera zoom into account.
|
||||
*
|
||||
* @param object Any `FlxObject`
|
||||
* @param camera The desired "screen" coordinate space. If `null`, `FlxG.camera` is used.
|
||||
* @param result Optional arg for the returning point
|
||||
* @return The screen position of the object.
|
||||
*/
|
||||
public static function pointToScreenPosition(object:FlxPoint, ?camera:FlxCamera, ?result:FlxPoint) {
|
||||
if (result == null)
|
||||
result = FlxPoint.get();
|
||||
if (camera == null)
|
||||
camera = FlxG.camera;
|
||||
|
||||
result.set(object.x, object.y);
|
||||
result.x = (((result.x - camera.scroll.x) * camera.zoom) - ((camera.width * 0.5) * (camera.zoom - camera.initialZoom)));
|
||||
result.y = (((result.y - camera.scroll.y) * camera.zoom) - ((camera.height * 0.5) * (camera.zoom - camera.initialZoom)));
|
||||
|
||||
object.putWeak();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts an array alphabetically.
|
||||
@@ -913,15 +1070,35 @@ class CoolUtil
|
||||
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;
|
||||
var _hx_tmp2:Float = Min;
|
||||
var _hx_tmp3:Float = Max;
|
||||
return untyped __cpp__("((({0}) < ({1})) ? ({1}) : (({0}) > ({2})) ? ({2}) : ({0}))", _hx_tmp1, _hx_tmp2, _hx_tmp3);
|
||||
#else
|
||||
return (Value < Min) ? Min : (Value > Max) ? Max : Value;
|
||||
#end
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the filename of a path, without the extension.
|
||||
* @param path Path to get the filename from
|
||||
* @return Filename
|
||||
*/
|
||||
@:noUsing public static inline function getFilename(file:String) {
|
||||
var file = new haxe.io.Path(file);
|
||||
return file.file;
|
||||
public static inline function boundInt(Value:Int, Min:Int, Max:Int):Int {
|
||||
#if cpp
|
||||
var _hx_tmp1:Int = Value;
|
||||
var _hx_tmp2:Int = Min;
|
||||
var _hx_tmp3:Int = Max;
|
||||
return untyped __cpp__("((({0}) < ({1})) ? ({1}) : (({0}) > ({2})) ? ({2}) : ({0}))", _hx_tmp1, _hx_tmp2, _hx_tmp3);
|
||||
#else
|
||||
return (Value < Min) ? Min : (Value > Max) ? Max : Value;
|
||||
#end
|
||||
}
|
||||
|
||||
public static inline function boolToInt(b:Bool):Int {
|
||||
#if cpp
|
||||
return untyped __cpp__("(({0}) ? 1 : 0)", b);
|
||||
#else
|
||||
return b ? 1 : 0;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1028,6 +1205,301 @@ 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
|
||||
* @return Bool
|
||||
*/
|
||||
public static function imageHasFrameData(path:String):String {
|
||||
if (FileSystem.exists(Path.withExtension(path, "xml"))) return "xml";
|
||||
if (FileSystem.exists(Path.withExtension(path, "txt"))) return "txt";
|
||||
if (FileSystem.exists(Path.withExtension(path, "json"))) return "json";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// loads frames with no image, but with data, so you can parse it
|
||||
public static function loadFramesFromData(data:String, ext:String = null):FlxFramesCollection {
|
||||
var frames:FlxFramesCollection = null;
|
||||
var tempBitmap:BitmapData = new BitmapData(1, 1, false);
|
||||
|
||||
// Worlds hackiest work around alert???
|
||||
var graphic:FlxGraphic = FlxG.bitmap.add(tempBitmap);
|
||||
@:privateAccess {
|
||||
// prevent errors from being shown, such as Size is too small
|
||||
graphic.width = 9999999;
|
||||
graphic.height = 9999999;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (ext) {
|
||||
case "xml": frames = FlxAtlasFrames.fromSparrow(graphic, Xml.parse(data));
|
||||
case "txt": frames = FlxAtlasFrames.fromSpriteSheetPacker(graphic, data);
|
||||
case "json": frames = FlxAtlasFrames.fromAseprite(graphic, data);
|
||||
}
|
||||
} catch (e) {trace(e);}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
public static function removeBOM(str:String):String {
|
||||
return StringTools.replace(str, String.fromCharCode(0xFEFF), "");
|
||||
}
|
||||
|
||||
public static function getAnimsListFromFrames(frames:FlxFramesCollection, ext:String = null):Array<String> {
|
||||
if (frames == null) return [];
|
||||
|
||||
var animsList:Array<String> = [];
|
||||
for (frame in frames.frames) {
|
||||
var animName:String = ext == "txt" ? frame.name.split("_")[0] : frame.name.substr(0, frame.name.length-4);
|
||||
if (!animsList.contains(animName))
|
||||
animsList.push(animName);
|
||||
}
|
||||
|
||||
return animsList;
|
||||
}
|
||||
|
||||
public static function getAnimsListFromAtlas(atlas:AnimAtlas):Array<String> {
|
||||
if (atlas == null) return [];
|
||||
|
||||
var animsList:Array<String> = [];
|
||||
if (atlas.AN.SN != null) animsList.push(atlas.AN.SN);
|
||||
if (atlas.SD != null)
|
||||
for (symbol in atlas.SD.S)
|
||||
if (symbol.SN != null) animsList.push(symbol.SN);
|
||||
|
||||
return animsList;
|
||||
}
|
||||
|
||||
public static function getAnimsListFromSprite(spr:FunkinSprite):Array<String> {
|
||||
if (spr.animateAtlas != null) {
|
||||
return [for (symbol => timeline in spr.animateAtlas.anim.symbolDictionary) symbol];
|
||||
} else
|
||||
return getAnimsListFromFrames(spr.frames);
|
||||
}
|
||||
|
||||
// TODO: check this for bugs
|
||||
// Code from https://github.com/elnabo/equals/blob/master/src/equals/Equal.hx, (MIT License), but updated to work with haxe 4
|
||||
public static function deepEqual<T> (a:T, b:T) : Bool {
|
||||
if (a == b) { return true; } // if physical equality
|
||||
if (isNull(a) || isNull(b)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (Type.typeof(a)) {
|
||||
case TNull, TInt, TBool, TUnknown:
|
||||
return a == b;
|
||||
case TFloat:
|
||||
return Math.isNaN(cast a) && Math.isNaN(cast b); // only valid true result remaining
|
||||
case TFunction:
|
||||
return Reflect.compareMethods(a, b); // only physical equality can be tested for function
|
||||
case TEnum(_):
|
||||
if (EnumValueTools.getIndex(cast a) != EnumValueTools.getIndex(cast b)) {
|
||||
return false;
|
||||
}
|
||||
var a_args = EnumValueTools.getParameters(cast a);
|
||||
var b_args = EnumValueTools.getParameters(cast b);
|
||||
return deepEqual(a_args, b_args);
|
||||
case TClass(_):
|
||||
if ((a is String) && (b is String)) {
|
||||
return a == b;
|
||||
}
|
||||
if ((a is Array) && (b is Array)) {
|
||||
var a = cast(a, Array<Dynamic>);
|
||||
var b = cast(b, Array<Dynamic>);
|
||||
if (a.length != b.length) { return false; }
|
||||
for (i in 0...a.length) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((a is IMap) && (b is IMap)) {
|
||||
var a = cast(a, IMap<Dynamic, Dynamic>);
|
||||
var b = cast(b, IMap<Dynamic, Dynamic>);
|
||||
var a_keys = [ for (key in a.keys()) key ];
|
||||
var b_keys = [ for (key in b.keys()) key ];
|
||||
a_keys.sort(Reflect.compare);
|
||||
b_keys.sort(Reflect.compare);
|
||||
if (!deepEqual(a_keys, b_keys)) { return false; }
|
||||
for (key in a_keys) {
|
||||
if (!deepEqual(a.get(key), b.get(key))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((a is Date) && (b is Date)) {
|
||||
return cast(a, Date).getTime() == cast(b, Date).getTime();
|
||||
}
|
||||
|
||||
if ((a is haxe.io.Bytes) && (b is haxe.io.Bytes)) {
|
||||
return deepEqual(cast(a, haxe.io.Bytes).getData(), cast(b, haxe.io.Bytes).getData());
|
||||
}
|
||||
|
||||
case TObject:
|
||||
}
|
||||
|
||||
for (field in Reflect.fields(a)) {
|
||||
var pa = Reflect.field(a, field);
|
||||
var pb = Reflect.field(b, field);
|
||||
if (isFunction(pa)) {
|
||||
// ignore function as only physical equality can be tested, unless null
|
||||
if (isNull(pa) != isNull(pb)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!deepEqual(pa, pb)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline function isNull(a:Dynamic):Bool {
|
||||
return Type.enumEq(Type.typeof(a), TNull);
|
||||
}
|
||||
|
||||
static inline function isFunction(a:Dynamic):Bool {
|
||||
return Type.enumEq(Type.typeof(a), TFunction);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ import lime.app.Application;
|
||||
import sys.thread.Thread;
|
||||
#end
|
||||
|
||||
class DiscordUtil
|
||||
final class DiscordUtil
|
||||
{
|
||||
public static var currentID(default, set):String = null;
|
||||
public static var discordThread:#if DISCORD_RPC Thread #else Dynamic #end = null;
|
||||
@@ -89,6 +89,10 @@ 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
import flixel.math.FlxRect;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.math.FlxAngle;
|
||||
import flixel.math.FlxPoint;
|
||||
|
||||
final class DrawUtil {
|
||||
public static var line:FlxSprite = null;
|
||||
public static var dot:FlxSprite = null;
|
||||
public static var square:FlxSprite = null;
|
||||
|
||||
public static inline function drawSquare(x:Float, y:Float, ?scale:Float = 1) {
|
||||
if (square == null) createDrawers();
|
||||
|
||||
square.setPosition(x, y);
|
||||
|
||||
square.scale.set(scale, scale);
|
||||
square.updateHitbox();
|
||||
|
||||
square.x -= square.width / 2;
|
||||
square.y -= square.height / 2;
|
||||
square.draw();
|
||||
}
|
||||
|
||||
public static inline function drawDot(x:Float, y:Float, ?scale:Float = 1) {
|
||||
if (dot == null) createDrawers();
|
||||
|
||||
dot.setPosition(x, y);
|
||||
|
||||
dot.scale.set(scale, scale);
|
||||
dot.updateHitbox();
|
||||
|
||||
dot.x -= dot.width / 2;
|
||||
dot.y -= dot.height / 2;
|
||||
dot.draw();
|
||||
}
|
||||
|
||||
public static inline function drawLine(point1:FlxPoint, point2:FlxPoint, thickness:Float = 1, ?color:Null<FlxColor>) {
|
||||
if (line == null) createDrawers();
|
||||
|
||||
var dx:Float = point2.x - point1.x;
|
||||
var dy:Float = point2.y - point1.y;
|
||||
|
||||
var angle:Float = Math.atan2(dy, dx);
|
||||
var distance:Float = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Math.ceil to prevent flickering
|
||||
line.setPosition(point1.x, point1.y);
|
||||
line.angle = angle * FlxAngle.TO_DEG;
|
||||
line.origin.set(0, line.frameHeight / 2);
|
||||
line.scale.x = distance / line.frameWidth;
|
||||
line.scale.y = thickness;
|
||||
line.y -= line.height / 2;
|
||||
if (color != null) line.color = color;
|
||||
line.draw();
|
||||
|
||||
line.angle = 0;
|
||||
line.scale.x = line.scale.y = 1;
|
||||
line.updateHitbox();
|
||||
|
||||
point1.putWeak(); point2.putWeak();
|
||||
}
|
||||
|
||||
public static inline function drawRect(rect:FlxRect, thickness:Float = 1, ?color:Null<FlxColor>) {
|
||||
if (rect.width <= 0 || rect.height <= 0) return;
|
||||
DrawUtil.drawLine(FlxPoint.weak(rect.x, rect.y), FlxPoint.weak(rect.x + rect.width, rect.y), thickness, color);
|
||||
DrawUtil.drawLine(FlxPoint.weak(rect.x, rect.y), FlxPoint.weak(rect.x, rect.y + rect.height), thickness, color);
|
||||
DrawUtil.drawLine(FlxPoint.weak(rect.x + rect.width, rect.y), FlxPoint.weak(rect.x + rect.width, rect.y + rect.height), thickness, color);
|
||||
DrawUtil.drawLine(FlxPoint.weak(rect.x, rect.y + rect.height), FlxPoint.weak(rect.x + rect.width, rect.y + rect.height), thickness, color);
|
||||
}
|
||||
|
||||
public static inline function createDrawers() {
|
||||
dot = new FlxSprite().loadGraphic(Paths.image("editors/stage/selectionDot"), true, 32, 32);
|
||||
dot.antialiasing = true;
|
||||
dot.animation.add("default", [0], 0, false);
|
||||
dot.animation.add("hollow", [1], 0, false);
|
||||
dot.animation.play("default");
|
||||
dot.camera = FlxG.camera;
|
||||
dot.forceIsOnScreen = true;
|
||||
|
||||
line = new FlxSprite().makeGraphic(1, 1, FlxColor.WHITE);
|
||||
line.camera = FlxG.camera;
|
||||
line.forceIsOnScreen = true;
|
||||
|
||||
square = new FlxSprite().makeGraphic(32, 32, FlxColor.WHITE);
|
||||
square.camera = FlxG.camera;
|
||||
square.forceIsOnScreen = true;
|
||||
}
|
||||
|
||||
public static function destroyDrawers() {
|
||||
if(dot != null) {
|
||||
dot.destroy();
|
||||
dot = null;
|
||||
}
|
||||
if(line != null) {
|
||||
line.destroy();
|
||||
line = null;
|
||||
}
|
||||
if(square != null) {
|
||||
square.destroy();
|
||||
square = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package funkin.backend.utils;
|
||||
import funkin.backend.scripting.MultiThreadedScript;
|
||||
import funkin.backend.scripting.Script;
|
||||
|
||||
class EngineUtil {
|
||||
final class EngineUtil {
|
||||
/**
|
||||
* Starts a new multithreaded script.
|
||||
* This script will share all the variables with the current one, which means already existing callbacks will be replaced by new ones on conflict.
|
||||
|
||||
@@ -2,7 +2,7 @@ package funkin.backend.utils;
|
||||
|
||||
import haxe.Http;
|
||||
|
||||
class HttpUtil
|
||||
final class HttpUtil
|
||||
{
|
||||
public static var userAgent:String = Flags.USER_AGENT;
|
||||
public static function requestText(url:String)
|
||||
|
||||
@@ -1,42 +1,33 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
typedef IniMap = Map<String, String>;
|
||||
|
||||
/**
|
||||
* Parses an ini file and returns a map of its contents.
|
||||
* WARNING: This is not a full ini parser, it only supports the basics. So no sections
|
||||
*/
|
||||
class IniUtil {
|
||||
public static inline function parseAsset(assetPath:String, ?defaultVariables:IniMap):IniMap
|
||||
return parseString(Assets.getText(assetPath), defaultVariables);
|
||||
public static inline function parseAsset(assetPath:String)
|
||||
return parseString(Assets.getText(assetPath));
|
||||
|
||||
public static function parseString(data:String, ?defaultVariables:IniMap):IniMap {
|
||||
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:IniMap = [];
|
||||
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;
|
||||
// sections; 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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package funkin.backend.utils;
|
||||
import flixel.text.FlxText;
|
||||
import flixel.util.FlxColor;
|
||||
|
||||
class MarkdownUtil {
|
||||
final class MarkdownUtil {
|
||||
public static function applyMarkdownText(text:FlxText, str:String) {
|
||||
text.textField.htmlText = Markdown.markdownToHtml(prepareMarkdown(str));
|
||||
var changes:Array<FlxTextFormatMarkerPair> = [
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
import haxe.macro.Expr;
|
||||
|
||||
final class MathUtil {
|
||||
/**
|
||||
* Returns the maximum value in the arguments.
|
||||
* @param args Array of values
|
||||
*
|
||||
* @return The maximum value
|
||||
**/
|
||||
public static function maxInt(...args:Int):Int {
|
||||
var max = args[0];
|
||||
for(i in 1...args.length) {
|
||||
var arg = args[i];
|
||||
if(arg > max)
|
||||
max = arg;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum value in the arguments.
|
||||
* @param args Array of values
|
||||
*
|
||||
* @return The minimum value
|
||||
**/
|
||||
public static function minInt(...args:Int):Int {
|
||||
var min = args[0];
|
||||
for(i in 1...args.length) {
|
||||
var arg = args[i];
|
||||
if(arg < min)
|
||||
min = arg;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum value in the arguments.
|
||||
*
|
||||
* NOTE: If you are using this in compile time, you should use `MathUtil.maxSmart` instead of this for better performance.
|
||||
*
|
||||
* @param args Array of values
|
||||
*
|
||||
* @return The maximum value
|
||||
**/
|
||||
public static function max(...args:Float):Float {
|
||||
var max = args[0];
|
||||
for(i in 1...args.length) {
|
||||
var arg = args[i];
|
||||
if(arg > max)
|
||||
max = arg;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum value in the arguments.
|
||||
*
|
||||
* NOTE: If you are using this in compile time, you should use `MathUtil.minSmart` instead of this for better performance.
|
||||
*
|
||||
* @param args Array of values
|
||||
*
|
||||
* @return The minimum value
|
||||
**/
|
||||
public static function min(...args:Float):Float {
|
||||
var min = args[0];
|
||||
for(i in 1...args.length) {
|
||||
var arg = args[i];
|
||||
if(arg < min)
|
||||
min = arg;
|
||||
}
|
||||
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
|
||||
*
|
||||
* Might not preserve the order of arguments, please test this.
|
||||
*
|
||||
* Dont use this in hscript, it doesnt work, it only works on compile time
|
||||
**/
|
||||
@:dox(hide) public static macro function maxSmart(..._args:Expr):Expr {
|
||||
return genericMinMaxSmart(_args.toArray(), "Math.max");
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to `Math.min` but with infinite amount of arguments
|
||||
*
|
||||
* Might not preserve the order of arguments, please test this.
|
||||
*
|
||||
* Dont use this in hscript, it doesnt work, it only works on compile time
|
||||
**/
|
||||
@:dox(hide) public static macro function minSmart(..._args:Expr):Expr {
|
||||
return genericMinMaxSmart(_args.toArray(), "Math.min");
|
||||
}
|
||||
|
||||
#if macro
|
||||
@:dox(hide) private static function genericMinMaxSmart(_args:Array<Expr>, funcPath:String):Expr {
|
||||
var args = _args.copy();
|
||||
if (args.length == 0) return macro 0;
|
||||
|
||||
var func = funcPath.split(".");
|
||||
|
||||
function nested(lst:Array<Expr>):Expr {
|
||||
if (lst.length == 1) {
|
||||
return macro ${lst[0]};
|
||||
} else if (lst.length == 2) {
|
||||
return macro $p{func}(${lst[0]}, ${lst[1]});
|
||||
} else {
|
||||
var mid = Std.int(lst.length / 2);
|
||||
return macro $p{func}(${nested(lst.slice(0, mid))}, ${nested(lst.slice(mid, lst.length))});
|
||||
}
|
||||
}
|
||||
|
||||
var expr = nested(args);
|
||||
|
||||
//var printer = new haxe.macro.Printer();
|
||||
//trace(printer.printExpr(expr));
|
||||
|
||||
return macro $expr;
|
||||
}
|
||||
#end
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.FlxCamera;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.util.typeLimit.OneOfTwo;
|
||||
import funkin.backend.system.FakeCamera;
|
||||
import funkin.backend.system.FakeCamera.FakeCallCamera;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.graphics.tile.FlxDrawTrianglesItem;
|
||||
import flixel.math.FlxMatrix;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.system.FlxAssets.FlxShader;
|
||||
import openfl.display.BitmapData;
|
||||
import openfl.display.BlendMode;
|
||||
import openfl.geom.ColorTransform;
|
||||
import openfl.geom.Point;
|
||||
import openfl.geom.Rectangle;
|
||||
|
||||
interface IPrePostDraw {
|
||||
public function preDraw():Void;
|
||||
public function postDraw():Void;
|
||||
}
|
||||
|
||||
@:access(flixel.FlxCamera)
|
||||
@:access(flixel.FlxSprite)
|
||||
@:access(flixel.math.FlxMatrix)
|
||||
@:access(openfl.geom.Matrix)
|
||||
final class MatrixUtil {
|
||||
public static function getMatrixPosition(sprite:FlxSprite, points:OneOfTwo<FlxPoint, Array<FlxPoint>>, ?camera:FlxCamera, _width:Float = 1, _height:Float = 1):Array<FlxPoint>
|
||||
{
|
||||
//if(_width == -1) _width = sprite.width;
|
||||
//if(_height == -1) _height = sprite.height;
|
||||
if(camera == null) camera = sprite.camera;
|
||||
if(points is FlxBasePoint) points = [points];
|
||||
var isFunkinSprite = sprite is FunkinSprite;
|
||||
var funkinSprite:FunkinSprite = null;
|
||||
if(isFunkinSprite) funkinSprite = cast sprite;
|
||||
var isAnimateAtlas = isFunkinSprite && funkinSprite.animateAtlas != null;
|
||||
|
||||
var nc:FakeCamera = isAnimateAtlas ? FakeCallCamera.instance : FakeCamera.instance;
|
||||
nc.zoom = camera.zoom;
|
||||
nc.scroll.set(camera.scroll.x, camera.scroll.y);
|
||||
nc.pixelPerfectRender = camera.pixelPerfectRender;
|
||||
|
||||
var points:Array<FlxPoint> = cast points;
|
||||
|
||||
if(isAnimateAtlas) {
|
||||
var cnc = FakeCallCamera.instance;
|
||||
var sprite = funkinSprite;
|
||||
var oldOnDraw = cnc.onDraw;
|
||||
|
||||
// TODO: fix rotation
|
||||
// TODO: fix skew
|
||||
|
||||
var boundsTopLeft = new FlxPoint(Math.POSITIVE_INFINITY, Math.POSITIVE_INFINITY);
|
||||
var boundsTopRight = new FlxPoint(Math.NEGATIVE_INFINITY, Math.POSITIVE_INFINITY);
|
||||
var boundsBottomLeft = new FlxPoint(Math.POSITIVE_INFINITY, Math.NEGATIVE_INFINITY);
|
||||
var boundsBottomRight = new FlxPoint(Math.NEGATIVE_INFINITY, Math.NEGATIVE_INFINITY);
|
||||
|
||||
cnc.onDraw = function(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader) {
|
||||
// cnc is already perfect rn
|
||||
// so we need to manually process the limb
|
||||
var points = [
|
||||
// corners
|
||||
FlxPoint.get(0, 0),
|
||||
FlxPoint.get(1, 0),
|
||||
FlxPoint.get(0, 1),
|
||||
FlxPoint.get(1, 1)
|
||||
];
|
||||
|
||||
rawTransformPoints(points, matrix, frame.sourceSize.x, frame.sourceSize.y);
|
||||
|
||||
// sort the points, incase like they were transformed with a scale -1 matrix
|
||||
|
||||
var topLeft:FlxPoint = points[0];
|
||||
var topRight:FlxPoint = points[1];
|
||||
var bottomLeft:FlxPoint = points[2];
|
||||
var bottomRight:FlxPoint = points[3];
|
||||
|
||||
if (points[0].y > points[1].y) {
|
||||
topLeft = points[1];
|
||||
topRight = points[0];
|
||||
}
|
||||
if (points[2].y > points[3].y) {
|
||||
bottomLeft = points[3];
|
||||
bottomRight = points[2];
|
||||
}
|
||||
|
||||
if (topLeft.x > topRight.x) {
|
||||
var temp = topLeft;
|
||||
topLeft = topRight;
|
||||
topRight = temp;
|
||||
}
|
||||
|
||||
if (bottomLeft.x > bottomRight.x) {
|
||||
var temp = bottomLeft;
|
||||
bottomLeft = bottomRight;
|
||||
bottomRight = temp;
|
||||
}
|
||||
|
||||
// points should now contain the corners of the limb
|
||||
|
||||
// trace("Corners of limb: " + points);
|
||||
|
||||
// set the max bounds of each corner of the limb
|
||||
if(topLeft.x < boundsTopLeft.x) boundsTopLeft.x = topLeft.x;
|
||||
if(topLeft.y < boundsTopLeft.y) boundsTopLeft.y = topLeft.y;
|
||||
if(topRight.x > boundsTopRight.x) boundsTopRight.x = topRight.x;
|
||||
if(topRight.y < boundsTopRight.y) boundsTopRight.y = topRight.y;
|
||||
if(bottomLeft.x < boundsBottomLeft.x) boundsBottomLeft.x = bottomLeft.x;
|
||||
if(bottomLeft.y > boundsBottomLeft.y) boundsBottomLeft.y = bottomLeft.y;
|
||||
if(bottomRight.x > boundsBottomRight.x) boundsBottomRight.x = bottomRight.x;
|
||||
if(bottomRight.y > boundsBottomRight.y) boundsBottomRight.y = bottomRight.y;
|
||||
|
||||
// recycle the points
|
||||
for(point in points) point.put();
|
||||
}
|
||||
var cameras = sprite._cameras;
|
||||
var oldVisible = cnc.visible;
|
||||
cnc.visible = true;
|
||||
sprite._cameras = [cnc];
|
||||
sprite.draw();
|
||||
sprite._cameras = cameras;
|
||||
cnc.visible = oldVisible;
|
||||
cnc.onDraw = oldOnDraw;
|
||||
|
||||
// fake transform
|
||||
|
||||
//trace("");
|
||||
//trace("topLeft: " + boundsTopLeft);
|
||||
//trace("topRight: " + boundsTopRight);
|
||||
//trace("bottomLeft: " + boundsBottomLeft);
|
||||
//trace("bottomRight: " + boundsBottomRight);
|
||||
//trace("Before transform");
|
||||
//trace(points);
|
||||
|
||||
for(point in points) {
|
||||
//var x = matrix.__transformX(point.x * _width, point.y * _height);
|
||||
//var y = matrix.__transformY(point.x * _width, point.y * _height);
|
||||
// apply using boundsTopLeft, boundsTopRight, boundsBottomLeft, boundsBottomRight
|
||||
// matrix doesnt work for this
|
||||
var x = FlxMath.lerp(
|
||||
FlxMath.lerp(boundsTopLeft.x, boundsTopRight.x, point.x),
|
||||
FlxMath.lerp(boundsBottomLeft.x, boundsBottomRight.x, point.x),
|
||||
point.y
|
||||
);
|
||||
var y = FlxMath.lerp(
|
||||
FlxMath.lerp(boundsTopLeft.y, boundsTopRight.y, point.x),
|
||||
FlxMath.lerp(boundsBottomLeft.y, boundsBottomRight.y, point.x),
|
||||
point.y
|
||||
);
|
||||
|
||||
//trace("(" + point.x + ", " + point.y + ") -> (" + x + ", " + y + ")");
|
||||
|
||||
// reset to ingame coords
|
||||
x += camera.scroll.x;
|
||||
y += camera.scroll.y;
|
||||
|
||||
if(isFunkinSprite) {
|
||||
var ratio = 1 - FlxMath.lerp(1 / camera.zoom, 1, funkinSprite.zoomFactor);
|
||||
x += camera.width / 2 * ratio;
|
||||
y += camera.height / 2 * ratio;
|
||||
}
|
||||
point.set(x, y);
|
||||
}
|
||||
|
||||
//trace("After transform");
|
||||
//trace(points);
|
||||
} else {
|
||||
if(sprite is IPrePostDraw) {
|
||||
var postDraw = cast(sprite, IPrePostDraw);
|
||||
postDraw.preDraw();
|
||||
sprite.drawComplex(nc);
|
||||
postDraw.postDraw();
|
||||
} else {
|
||||
sprite.drawComplex(nc);
|
||||
}
|
||||
transformPoints(sprite, points, sprite._matrix, camera, _width, _height);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning: modifies the points in the array
|
||||
**/
|
||||
public static function transformPoints(sprite:FlxSprite, points:Array<FlxPoint>, matrix:FlxMatrix, ?camera:FlxCamera, _width:Float = 1, _height:Float = 1, doCameraTransform:Bool = true):Array<FlxPoint> {
|
||||
var isFunkinSprite = sprite is FunkinSprite;
|
||||
var funkinSprite:FunkinSprite = null;
|
||||
if(isFunkinSprite) funkinSprite = cast sprite;
|
||||
|
||||
for(point in points) {
|
||||
var x = matrix.__transformX(point.x * _width, point.y * _height);
|
||||
var y = matrix.__transformY(point.x * _width, point.y * _height);
|
||||
|
||||
if(doCameraTransform) {
|
||||
// reset to ingame coords
|
||||
x += camera.scroll.x;
|
||||
y += camera.scroll.y;
|
||||
|
||||
if(isFunkinSprite) {
|
||||
var ratio = 1 - FlxMath.lerp(1 / camera.zoom, 1, funkinSprite.zoomFactor);
|
||||
x += camera.width / 2 * ratio;
|
||||
y += camera.height / 2 * ratio;
|
||||
}
|
||||
}
|
||||
point.set(x, y);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
private static function rawTransformPoints(points:Array<FlxPoint>, matrix:FlxMatrix, _width:Float = 1, _height:Float = 1):Array<FlxPoint> {
|
||||
for(point in points) {
|
||||
var x = matrix.__transformX(point.x * _width, point.y * _height);
|
||||
var y = matrix.__transformY(point.x * _width, point.y * _height);
|
||||
|
||||
point.set(x, y);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ using StringTools;
|
||||
*
|
||||
* DISCLAIMER: Hardware info is only available on Native platforms.
|
||||
**/
|
||||
class MemoryUtil {
|
||||
final class MemoryUtil {
|
||||
public static var disableCount:Int = 0;
|
||||
|
||||
public static function askDisable() {
|
||||
@@ -88,14 +88,18 @@ class MemoryUtil {
|
||||
*/
|
||||
public static function getTotalMem():Float
|
||||
{
|
||||
#if windows
|
||||
return funkin.backend.utils.native.Windows.getTotalRam();
|
||||
#elseif mac
|
||||
return funkin.backend.utils.native.Mac.getTotalRam();
|
||||
#elseif linux
|
||||
return funkin.backend.utils.native.Linux.getTotalRam();
|
||||
#if cpp
|
||||
#if windows
|
||||
return funkin.backend.utils.native.Windows.getTotalRam();
|
||||
#elseif mac
|
||||
return funkin.backend.utils.native.Mac.getTotalRam();
|
||||
#elseif linux
|
||||
return funkin.backend.utils.native.Linux.getTotalRam();
|
||||
#else
|
||||
return 0;
|
||||
#end
|
||||
#else
|
||||
return 0;
|
||||
return 0;
|
||||
#end
|
||||
}
|
||||
|
||||
@@ -123,7 +127,7 @@ 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",
|
||||
@@ -162,9 +166,9 @@ 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: (.+)/;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
import haxe.macro.Expr.Case;
|
||||
import openfl.ui.Mouse;
|
||||
import openfl.ui.MouseCursor;
|
||||
import funkin.backend.utils.native.*;
|
||||
import flixel.util.typeLimit.OneOfTwo;
|
||||
import flixel.util.typeLimit.OneOfThree;
|
||||
@@ -200,6 +203,17 @@ class NativeAPI {
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cursor icon.
|
||||
**/
|
||||
public static function setCursorIcon(icon:CodeCursor) {
|
||||
#if (mac && cpp)
|
||||
Mac.setMouseCursorIcon(cast icon);
|
||||
#else
|
||||
Mouse.cursor = icon.toOpenFL();
|
||||
#end
|
||||
}
|
||||
|
||||
public static function consoleColorToANSI(color:ConsoleColor) {
|
||||
return switch(color) {
|
||||
case BLACK: 30;
|
||||
@@ -289,4 +303,102 @@ enum abstract MessageBoxIcon(Int) {
|
||||
var MSG_QUESTION = 0x00000020;
|
||||
var MSG_WARNING = 0x00000030;
|
||||
var MSG_INFORMATION = 0x00000040;
|
||||
}
|
||||
|
||||
enum abstract CodeCursor(String) {
|
||||
var CUSTOM;// = "arrow";
|
||||
var ARROW;// = "arrow";
|
||||
var CLICK;// = "click";
|
||||
var CROSSHAIR;// = "crosshair";
|
||||
var HAND;// = "hand";
|
||||
var IBEAM;// = "ibeam";
|
||||
var MOVE;// = "move";
|
||||
|
||||
var RESIZE_H;// = "resize_we";
|
||||
var RESIZE_V;// = "resize_ns";
|
||||
var RESIZE_TL;// = "resize_nw";
|
||||
var RESIZE_TR;// = "resize_ne";
|
||||
var RESIZE_BL;// = "resize_sw";
|
||||
var RESIZE_BR;// = "resize_se";
|
||||
var RESIZE_T;// = "resize_n";
|
||||
var RESIZE_B;// = "resize_b";
|
||||
var RESIZE_L;// = "resize_w";
|
||||
var RESIZE_R;// = "resize_e";
|
||||
|
||||
var RESIZE_TLBR;// = "resize_nw_se";
|
||||
var RESIZE_TRBL;// = "resize_ne_sw";
|
||||
|
||||
var WAIT;// = "wait";
|
||||
var WAIT_ARROW;// = "waitarrow";
|
||||
var DISABLED;// = "disabled";
|
||||
var DRAG;// = "drag";
|
||||
var DRAG_OPEN;// = "dragopen";
|
||||
|
||||
@:to public function toOpenFL():MouseCursor {
|
||||
return @:privateAccess switch(cast this) {
|
||||
case ARROW: MouseCursor.ARROW;
|
||||
case CROSSHAIR: MouseCursor.__CROSSHAIR;
|
||||
case CLICK: MouseCursor.BUTTON;
|
||||
case IBEAM: MouseCursor.IBEAM;
|
||||
case MOVE: MouseCursor.__MOVE;
|
||||
case HAND: MouseCursor.HAND;
|
||||
case DRAG: MouseCursor.HAND;
|
||||
case DRAG_OPEN: MouseCursor.ARROW; // Could be HAND, but it might be better to use ARROW since on windows it would be weird to have the dragging cursor
|
||||
case WAIT: MouseCursor.__WAIT;
|
||||
case WAIT_ARROW: MouseCursor.__WAIT_ARROW;
|
||||
|
||||
case DISABLED: MouseCursor.ARROW;
|
||||
|
||||
case RESIZE_TR: MouseCursor.__RESIZE_NESW;
|
||||
case RESIZE_BL: MouseCursor.__RESIZE_NESW;
|
||||
case RESIZE_TL: MouseCursor.__RESIZE_NWSE;
|
||||
case RESIZE_BR: MouseCursor.__RESIZE_NWSE;
|
||||
case RESIZE_H: MouseCursor.__RESIZE_WE;
|
||||
case RESIZE_V: MouseCursor.__RESIZE_NS;
|
||||
|
||||
case RESIZE_T: MouseCursor.__RESIZE_NS;
|
||||
case RESIZE_B: MouseCursor.__RESIZE_NS;
|
||||
case RESIZE_L: MouseCursor.__RESIZE_WE;
|
||||
case RESIZE_R: MouseCursor.__RESIZE_WE;
|
||||
|
||||
case RESIZE_TLBR: MouseCursor.__RESIZE_NWSE;
|
||||
case RESIZE_TRBL: MouseCursor.__RESIZE_NESW;
|
||||
case CUSTOM: MouseCursor.__CUSTOM;
|
||||
//default: ARROW;
|
||||
}
|
||||
}
|
||||
|
||||
@:to public function toInt():Int {
|
||||
return switch(cast this) {
|
||||
case ARROW: 0;
|
||||
case CROSSHAIR: 1;
|
||||
case CLICK: 2;
|
||||
case IBEAM: 3;
|
||||
case MOVE: 4;
|
||||
case HAND: 5;
|
||||
case DRAG: 6;
|
||||
case DRAG_OPEN: 7;
|
||||
case WAIT: 8;
|
||||
case WAIT_ARROW: 9;
|
||||
|
||||
case DISABLED: 10;
|
||||
|
||||
case RESIZE_TR: 11;
|
||||
case RESIZE_BL: 12;
|
||||
case RESIZE_TL: 13;
|
||||
case RESIZE_BR: 14;
|
||||
case RESIZE_H: 15;
|
||||
case RESIZE_V: 16;
|
||||
|
||||
case RESIZE_T: 17;
|
||||
case RESIZE_B: 18;
|
||||
case RESIZE_L: 19;
|
||||
case RESIZE_R: 20;
|
||||
|
||||
case RESIZE_TLBR: 21;
|
||||
case RESIZE_TRBL: 22;
|
||||
|
||||
case CUSTOM: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ package funkin.backend.utils;
|
||||
* - The Function cannot be found in the NDLL
|
||||
* then an empty function will be returned instead, and a message will be shown in logs.
|
||||
*/
|
||||
class NdllUtil {
|
||||
final class NdllUtil {
|
||||
#if NDLLS_SUPPORTED
|
||||
#if windows public static final os:String = "windows"; #end
|
||||
#if linux public static final os:String = "linux"; #end
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package funkin.backend.utils;
|
||||
|
||||
import openfl.display.Sprite;
|
||||
|
||||
class ShaderResizeFix {
|
||||
final class ShaderResizeFix {
|
||||
public static var doResizeFix:Bool = true;
|
||||
|
||||
public static function init() {
|
||||
|
||||
@@ -5,7 +5,7 @@ package funkin.backend.utils;
|
||||
*
|
||||
* DISCLAIMER: All types of utils only usable in a sorted array.
|
||||
*/
|
||||
class SortedArrayUtil {
|
||||
final class SortedArrayUtil {
|
||||
/**
|
||||
* Gets the index of a possible new element of an Array of T using an efficient algorithm.
|
||||
* @param array Array of T to check in
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package funkin.backend.utils;
|
||||
|
||||
#if ALLOW_MULTITHREADING
|
||||
class ThreadUtil {
|
||||
final class ThreadUtil {
|
||||
/**
|
||||
* Creates a new Thread with an error handler.
|
||||
* @param func Function to execute
|
||||
|
||||
@@ -38,7 +38,7 @@ final class TranslationUtil
|
||||
/**
|
||||
* Returns the current language config.
|
||||
*/
|
||||
public static var config:IniMap = [];
|
||||
public static var config:Map<String, String> = [];
|
||||
/**
|
||||
* Returns the current language.
|
||||
*/
|
||||
@@ -65,15 +65,15 @@ final class TranslationUtil
|
||||
|
||||
// Private
|
||||
private static inline var LANG_FOLDER:String = "languages";
|
||||
private static var langConfigs:Map<String, IniMap> = [];
|
||||
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, IniMap> {
|
||||
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):IniMap {
|
||||
@:noUsing private static inline function getDefaultConfig(name:String):Map<String, String> {
|
||||
return ["name" => getLanguageName(name), "credits" => "", "version" => "1.0.0"];
|
||||
}
|
||||
|
||||
@@ -200,7 +200,9 @@ final class TranslationUtil
|
||||
var config = getDefaultConfig(lang);
|
||||
|
||||
if(Assets.exists(path)) {
|
||||
config = IniUtil.parseAsset(path, config);
|
||||
var c = IniUtil.parseAsset(path);
|
||||
for (key => value in c)
|
||||
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") {
|
||||
@@ -313,7 +315,7 @@ final class TranslationUtil
|
||||
return reverseMap.exists(name) ? reverseMap.get(name) : name;
|
||||
}
|
||||
|
||||
public static function getConfig(lang:String):IniMap {
|
||||
public static function getConfig(lang:String):Map<String, String> {
|
||||
return langConfigs.exists(lang) ? langConfigs.get(lang) : getDefaultConfig(lang);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package funkin.backend.utils;
|
||||
|
||||
import openfl.Lib;
|
||||
|
||||
class WindowUtils {
|
||||
final class WindowUtils {
|
||||
public static var title(default, set):String;
|
||||
private static function set_title(value:String):String {
|
||||
title = value;
|
||||
|
||||
@@ -34,7 +34,7 @@ typedef TextFormat = { text:String, format:Dynamic }
|
||||
* Class made to make XML parsing easier.
|
||||
* Used in Stage.hx, Character.hx, and more.
|
||||
*/
|
||||
class XMLUtil {
|
||||
final class XMLUtil {
|
||||
/**
|
||||
* Applies a property XML node to an object.
|
||||
* The format for the XML is as follows:
|
||||
@@ -114,12 +114,13 @@ 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) {
|
||||
@@ -200,10 +201,15 @@ class XMLUtil {
|
||||
if(node.has.color)
|
||||
spr.color = FlxColor.fromString(node.getAtt("color")).getDefault(0xFFFFFFFF);
|
||||
|
||||
if(node.has.angle)
|
||||
spr.angle = Std.parseFloat(node.getAtt("angle")).getDefault(spr.angle);
|
||||
|
||||
if (node.has.playOnCountdown)
|
||||
spr.skipNegativeBeats = node.att.playOnCountdown == "true";
|
||||
if (node.has.beatInterval)
|
||||
spr.beatInterval = Std.parseInt(node.att.beatInterval);
|
||||
if (node.has.interval)
|
||||
spr.beatInterval = Std.parseInt(node.att.interval);
|
||||
if (node.has.beatOffset)
|
||||
spr.beatOffset = Std.parseInt(node.att.beatOffset);
|
||||
|
||||
@@ -234,10 +240,10 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -23,7 +24,7 @@ using StringTools;
|
||||
// import ZipUtils; ZipUtils.uncompressZip(ZipUtils.openZip("E:\\Desktop\\test\\termination lua.ycemod"), "E:\\Desktop\\test\\uncompressed\\");
|
||||
// import ZipUtils; var e = ZipUtils.createZipFile("file.ycemod"); ZipUtils.writeFolderToZip(e, "./mods/Friday Night Funkin'/", "Friday Night Funkin'/"); e.flush(); e.close();
|
||||
|
||||
class ZipUtil {
|
||||
final class ZipUtil {
|
||||
public static var bannedNames:Array<String> = [".git", ".gitignore", ".github", ".vscode", ".gitattributes", "readme.txt"];
|
||||
|
||||
/**
|
||||
@@ -135,12 +136,12 @@ 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 @@ 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>();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@ package funkin.backend.utils.native;
|
||||
|
||||
#if linux
|
||||
@:cppFileCode("#include <stdio.h>")
|
||||
class Linux {
|
||||
@:dox(hide)
|
||||
final class Linux {
|
||||
@:functionCode('
|
||||
FILE *meminfo = fopen("/proc/meminfo", "r");
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package funkin.backend.utils.native;
|
||||
|
||||
#if mac
|
||||
@:cppFileCode("#include <sys/sysctl.h>")
|
||||
class Mac {
|
||||
#if (mac && cpp)
|
||||
import funkin.backend.utils.NativeAPI.CodeCursor;
|
||||
import openfl.ui.Mouse;
|
||||
|
||||
@:headerInclude('sys/sysctl.h')
|
||||
@:dox(hide)
|
||||
final class Mac
|
||||
{
|
||||
@:functionCode('
|
||||
int mib [] = { CTL_HW, HW_MEMSIZE };
|
||||
int64_t value = 0;
|
||||
@@ -17,5 +22,39 @@ class Mac {
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static function setMouseCursorIcon(icon:CodeCursor):Void
|
||||
{
|
||||
final valid:Bool = external.ExternalMac.setCursorIcon(icon.toInt(), null, 0, 0);
|
||||
|
||||
if (!valid)
|
||||
Mouse.cursor = icon.toOpenFL();
|
||||
}
|
||||
}
|
||||
#end
|
||||
#end
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Cursor
|
||||
|
||||
Description
|
||||
The arrow cursor (arrow)
|
||||
The I-beam cursor for indicating insertion points (iBeam)
|
||||
The cross-hair cursor (crosshair)
|
||||
The closed-hand cursor (closedHand)
|
||||
The open-hand cursor (openHand)
|
||||
The pointing-hand cursor (pointingHand)
|
||||
The resize-left cursor (resizeLeft)
|
||||
The resize-right cursor (resizeRight)
|
||||
The resize-left-and-right cursor (resizeLeftRight)
|
||||
The resize-up cursor (resizeUp)
|
||||
The resize-down cursor (resizeDown)
|
||||
The resize-up-and-down cursor (resizeUpDown)
|
||||
The disappearing item cursor (disappearingItem)
|
||||
The I-Beam text cursor for vertical layout (iBeamCursorForVerticalLayout).
|
||||
The not allowed cursor (operationNotAllowed).
|
||||
The drag link cursor (dragLink).
|
||||
The drag copy cursor (dragCopy).
|
||||
The contextual menu cursor (contextualMenu).
|
||||
*/
|
||||
|
||||
@@ -114,7 +114,7 @@ class AudioFixClient : public IMMNotificationClient {
|
||||
AudioFixClient *curAudioFix;
|
||||
')
|
||||
@:dox(hide)
|
||||
class Windows {
|
||||
final class Windows {
|
||||
|
||||
public static var __audioChangeCallback:Void->Void = function() {
|
||||
trace("test");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -36,6 +36,10 @@ class DebugOptionsScreen extends OptionsScreen {
|
||||
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...)",
|
||||
"editorsResizable"));
|
||||
add(new Checkbox(
|
||||
getName("editorSFX"),
|
||||
getDesc("editorSFX"),
|
||||
|
||||
@@ -26,7 +26,7 @@ class EditorPicker extends MusicBeatSubstate {
|
||||
{
|
||||
name: "Stage Editor",
|
||||
id: "stage",
|
||||
state: null
|
||||
state: funkin.editors.stage.StageSelection
|
||||
},
|
||||
#if (debug || debug_ui)
|
||||
{
|
||||
|
||||
@@ -7,25 +7,28 @@ 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();
|
||||
|
||||
UIState.setResolutionAware();
|
||||
|
||||
FlxG.camera.fade(0xFF000000, 0.5, true);
|
||||
|
||||
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,74 @@
|
||||
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"
|
||||
|
||||
# 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.ico"
|
||||
|
||||
[Flags]
|
||||
DISABLE_BETA_WARNING_SCREEN=true
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class SaveWarning {
|
||||
if (closingWindow) WindowUtils.resetClosing();
|
||||
} else {
|
||||
WindowUtils.preventClosing = false; WindowUtils.resetClosing();
|
||||
Sys.exit(0);
|
||||
openfl.system.System.exit(0);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -51,7 +51,7 @@ class SaveWarning {
|
||||
if (closingWindow) WindowUtils.resetClosing();
|
||||
} else {
|
||||
WindowUtils.preventClosing = false; WindowUtils.resetClosing();
|
||||
Sys.exit(0);
|
||||
openfl.system.System.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
package funkin.editors;
|
||||
|
||||
import funkin.editors.ui.old.OldUISliceSpriteTri;
|
||||
import funkin.editors.ui.UITopMenu.UITopMenuButton;
|
||||
import flixel.tweens.FlxTween;
|
||||
import funkin.editors.ui.*;
|
||||
import funkin.game.Character;
|
||||
|
||||
class UIDebugState extends UIState {
|
||||
public var topMenuSpr:UITopMenu;
|
||||
|
||||
public var testingUIItems:Array<FlxSprite> = [];
|
||||
public var testingUIHidden:Bool = false;
|
||||
|
||||
public override function create() {
|
||||
super.create();
|
||||
|
||||
@@ -14,7 +22,11 @@ class UIDebugState extends UIState {
|
||||
bg.scrollFactor.set();
|
||||
add(bg);
|
||||
|
||||
add(new UITopMenu([
|
||||
var gf = new Character(0, 0, "gf");
|
||||
gf.dance();
|
||||
add(gf);
|
||||
|
||||
add(topMenuSpr = new UITopMenu([
|
||||
{
|
||||
label: "File",
|
||||
childs: [
|
||||
@@ -45,15 +57,57 @@ class UIDebugState extends UIState {
|
||||
},
|
||||
{
|
||||
label: "Help"
|
||||
},
|
||||
{
|
||||
label: "9-Splice Benchmark >",
|
||||
childs: [
|
||||
{
|
||||
label: "Hide Other UI Items",
|
||||
onSelect: (t) -> {
|
||||
testingUIHidden = !testingUIHidden;
|
||||
t.icon = testingUIHidden ? 1 : 0;
|
||||
|
||||
for (item in testingUIItems)
|
||||
item.visible = !testingUIHidden;
|
||||
},
|
||||
icon: testingUIHidden ? 1 : 0
|
||||
},
|
||||
{
|
||||
label: "Clear Benchmark Sprites",
|
||||
onSelect: (t) -> {
|
||||
for (splice in spliceSprites) {
|
||||
splice.kill();
|
||||
splice.destroy();
|
||||
|
||||
this.remove(splice);
|
||||
}
|
||||
|
||||
spliceSprites.resize(0);
|
||||
MemoryUtil.clearMajor();
|
||||
}
|
||||
},
|
||||
null,
|
||||
{
|
||||
label: "Draw Triangles?",
|
||||
onSelect: (t) -> {
|
||||
spliceUseTris = !spliceUseTris;
|
||||
t.icon = spliceUseTris ? 1 : 0;
|
||||
|
||||
spliceCl = spliceUseTris ? OldUISliceSpriteTri : UISliceSprite;
|
||||
trace(spliceCl);
|
||||
},
|
||||
icon: spliceUseTris ? 1 : 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]));
|
||||
|
||||
add(new UICheckbox(10, 40, "Test unchecked", false));
|
||||
add(new UICheckbox(10, 70, "Test checked", true));
|
||||
add(new UIButton(10, 100, "Test button", function() {
|
||||
testingUIItems.push(cast add(new UICheckbox(10, 40, "Test unchecked", false)));
|
||||
testingUIItems.push(cast add(new UICheckbox(10, 70, "Test checked", true)));
|
||||
testingUIItems.push(cast add(new UIButton(10, 100, "Test button", function() {
|
||||
trace("Hello, World!");
|
||||
}, 130, 32));
|
||||
add(new UIButton(10, 140, "Warning test", function() {
|
||||
}, 130, 32)));
|
||||
testingUIItems.push(cast add(new UIButton(10, 140, "Warning test", function() {
|
||||
openSubState(new UIWarningSubstate("Test", "This is a test message", [
|
||||
{
|
||||
label: "Alt. Choice",
|
||||
@@ -68,8 +122,8 @@ class UIDebugState extends UIState {
|
||||
}
|
||||
}
|
||||
]));
|
||||
}, 130, 32));
|
||||
add(new UIButton(10, 180, "Warning test (Overflowing)", function() {
|
||||
}, 130, 32)));
|
||||
testingUIItems.push(cast add(new UIButton(10, 180, "Warning test (Overflowing)", function() {
|
||||
openSubState(new UIWarningSubstate("Test", "This is a test message", [
|
||||
{
|
||||
label: "Alt. Choice",
|
||||
@@ -98,18 +152,54 @@ class UIDebugState extends UIState {
|
||||
onClick: function(t) {}
|
||||
}
|
||||
]));
|
||||
}, 130, 48));
|
||||
add(new UITextBox(10, 220, ""));
|
||||
}, 130, 48)));
|
||||
testingUIItems.push(cast add(new UITextBox(10, 220, "")));
|
||||
|
||||
var spliceSprite:UISliceSprite = new UISliceSprite(650, 350, 30, 30, "editors/ui/context-bg");
|
||||
add(spliceSprite);
|
||||
FlxTween.num(0, 1, 2, {type: PINGPONG}, function (v:Float) {
|
||||
spliceSprite.bWidth = Std.int(lerp(12, 200, v)); spliceSprite.bHeight = Std.int(lerp(12, 200, v));
|
||||
});
|
||||
spliceText = new UIText(0, 0, 0, "(0) Sprites");
|
||||
add(spliceText);
|
||||
}
|
||||
|
||||
public var spliceSprites:Array<FlxSprite> = [];
|
||||
public var spliceText:UIText;
|
||||
|
||||
public var spliceTimer:Float = 0;
|
||||
public var spliceCoolDown:Float = 0.04;
|
||||
|
||||
public var spliceUseTris:Bool = false;
|
||||
public var spliceCl:Class<FlxSprite> = UISliceSprite;
|
||||
|
||||
public override function update(elapsed:Float) {
|
||||
super.update(elapsed);
|
||||
|
||||
if (FlxG.keys.pressed.SPACE) {
|
||||
spliceTimer += elapsed;
|
||||
if (spliceTimer >= spliceCoolDown) {
|
||||
spliceTimer -= spliceCoolDown;
|
||||
|
||||
for (i in 0...3) {
|
||||
var width:Int = FlxG.random.int(10, 200);
|
||||
var height:Int = FlxG.random.int(10, 200);
|
||||
|
||||
var spliceSprite:FlxSprite = Type.createInstance(spliceCl,
|
||||
[
|
||||
FlxG.random.float(0, FlxG.width-width),
|
||||
FlxG.random.float(22, FlxG.height-height),
|
||||
width, height, "editors/ui/context-bg"
|
||||
]
|
||||
);
|
||||
|
||||
spliceSprites.push(cast add(spliceSprite));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var spliceTopButton:UITopMenuButton = cast topMenuSpr.members[topMenuSpr.members.length-1];
|
||||
|
||||
spliceText.x = spliceTopButton.x + spliceTopButton.bWidth - 2;
|
||||
spliceText.y = spliceTopButton.label.y+1;
|
||||
|
||||
spliceText.text = '(${spliceSprites.length}) 9-Splice Sprites';
|
||||
|
||||
if (FlxG.mouse.justReleasedRight) {
|
||||
openContextMenu([
|
||||
{
|
||||
|
||||
@@ -35,4 +35,10 @@ class UndoList<T> {
|
||||
|
||||
public inline function save()
|
||||
savedLength = undoList.length;
|
||||
|
||||
public inline function clear() {
|
||||
undoList = [];
|
||||
redoList = [];
|
||||
save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
package funkin.editors.character;
|
||||
|
||||
import flixel.text.FlxText.FlxTextFormat;
|
||||
import flixel.text.FlxText.FlxTextFormatMarkerPair;
|
||||
import flxanimate.animate.FlxAnim.FlxSymbolAnimation;
|
||||
import flxanimate.animate.FlxSymbol;
|
||||
import flxanimate.animate.FlxElement;
|
||||
import flxanimate.animate.SymbolParameters;
|
||||
import flxanimate.animate.FlxKeyFrame;
|
||||
import flxanimate.animate.FlxTimeline;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.animation.FlxAnimation;
|
||||
import openfl.geom.Rectangle;
|
||||
import funkin.backend.utils.XMLUtil.AnimData;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.util.FlxColor;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class CharacterAnimButton extends UIButton {
|
||||
public var anim:String = null;
|
||||
public var valid:Bool = true;
|
||||
public var data:AnimData = null;
|
||||
public var parent:CharacterAnimsWindow;
|
||||
|
||||
public var animationDisplayBG:UISliceSprite;
|
||||
public var nameTextBox:UITextBox;
|
||||
public var animTextBox:UIAutoCompleteTextBox;
|
||||
public var positionXStepper:UINumericStepper;
|
||||
public var positionYStepper:UINumericStepper;
|
||||
public var fpsStepper:UINumericStepper;
|
||||
public var loopedCheckbox:UICheckbox;
|
||||
public var indicesTextBox:UITextBox;
|
||||
public var XYComma:UIText;
|
||||
|
||||
public var editButton:UIButton;
|
||||
public var editIcon:FlxSprite;
|
||||
|
||||
public var deleteButton:UIButton;
|
||||
public var deleteIcon:FlxSprite;
|
||||
public var ghostButton:UIButton;
|
||||
public var ghostIcon:FlxSprite;
|
||||
|
||||
public var animIcon:FlxSprite;
|
||||
|
||||
public var labels:Map<UISprite, UIText> = [];
|
||||
|
||||
public var foldableButtons:Array<FlxSprite> = [];
|
||||
public var closed:Bool = true;
|
||||
|
||||
public function new(x:Float, y:Float, animData:AnimData, parent:CharacterAnimsWindow) {
|
||||
this.anim = animData.name;
|
||||
this.data = animData;
|
||||
this.parent = parent;
|
||||
super(x,y, '${animData.name} (${animData.x}, ${animData.y})', function () {
|
||||
CharacterEditor.instance.playAnimation(this.anim);
|
||||
}, Std.int(500-16-32), 208);
|
||||
|
||||
function addLabelOn(ui:UISprite, text:String, ?size:Int):UIText {
|
||||
var uiText:UIText = new UIText(ui.x, ui.y-18, 0, text, size);
|
||||
members.push(uiText); labels.set(ui, uiText);
|
||||
return uiText;
|
||||
}
|
||||
|
||||
autoAlpha = autoFrames = autoFollow = false;
|
||||
|
||||
frames = Paths.getFrames('editors/ui/inputbox');
|
||||
field.fieldWidth = 0; framesOffset = 9;
|
||||
field.size = 14;
|
||||
|
||||
animationDisplayBG = new UISliceSprite(x+12, y+12+18+12, 128, 128, 'editors/ui/inputbox-small');
|
||||
members.push(animationDisplayBG);
|
||||
foldableButtons.push(animationDisplayBG);
|
||||
|
||||
nameTextBox = new UITextBox(animationDisplayBG.x+126+16, animationDisplayBG.y, animData.name, 104, 22, false, true);
|
||||
nameTextBox.onChange = (newName:String) -> {this.changeName(newName);};
|
||||
members.push(nameTextBox);
|
||||
foldableButtons.push(nameTextBox);
|
||||
addLabelOn(nameTextBox, "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);
|
||||
|
||||
positionXStepper = new UINumericStepper(animTextBox.x, animTextBox.y+32+18, animData.x, 0.001, 2, null, null, 64, 22, true);
|
||||
positionXStepper.onChange = (text:String) -> {
|
||||
@:privateAccess positionXStepper.__onChange(text);
|
||||
this.changeOffset(positionXStepper.value, null);
|
||||
};
|
||||
members.push(positionXStepper);
|
||||
foldableButtons.push(positionXStepper);
|
||||
addLabelOn(positionXStepper, "Position (X,Y)", 12);
|
||||
|
||||
members.push(XYComma = new UIText(positionXStepper.x+104-32+0, positionXStepper.y + 9, 0, ",", 18));
|
||||
foldableButtons.push(XYComma);
|
||||
|
||||
positionYStepper = new UINumericStepper(positionXStepper.x+104-32+26, positionXStepper.y, animData.y, 0.001, 2, null, null, 64, 22, true);
|
||||
positionYStepper.onChange = (text:String) -> {
|
||||
@:privateAccess positionYStepper.__onChange(text);
|
||||
this.changeOffset(null, positionYStepper.value);
|
||||
};
|
||||
members.push(positionYStepper);
|
||||
foldableButtons.push(positionYStepper);
|
||||
|
||||
fpsStepper = new UINumericStepper(animTextBox.x + 200 + 26, animTextBox.y, animData.fps, 0.1, 2, 1, 100, 52, 22, true);
|
||||
fpsStepper.onChange = (text:String) -> {
|
||||
@:privateAccess fpsStepper.__onChange(text);
|
||||
this.changeFPS(fpsStepper.value);
|
||||
};
|
||||
members.push(fpsStepper);
|
||||
foldableButtons.push(fpsStepper);
|
||||
addLabelOn(fpsStepper, "FPS", 12);
|
||||
|
||||
loopedCheckbox = new UICheckbox(fpsStepper.x + 82 - 32 + 26, fpsStepper.y, "Looping?", animData.loop, 0, true);
|
||||
loopedCheckbox.onChecked = (newLooping:Bool) -> {this.changeLooping(newLooping);};
|
||||
members.push(loopedCheckbox);
|
||||
foldableButtons.push(loopedCheckbox);
|
||||
|
||||
loopedCheckbox.x += 8; loopedCheckbox.y += 6;
|
||||
|
||||
indicesTextBox = new UITextBox(nameTextBox.x, nameTextBox.y, CoolUtil.formatNumberRange(animData.indices.getDefault([]), ", "), 278, 22, false, true);
|
||||
indicesTextBox.onChange = (text:String) -> {
|
||||
this.changeIndicies(CoolUtil.parseNumberRange(text));
|
||||
}
|
||||
members.push(indicesTextBox);
|
||||
foldableButtons.push(indicesTextBox);
|
||||
addLabelOn(indicesTextBox, "Indices (frames)", 12);
|
||||
|
||||
animIcon = new FlxSprite(x-(10+16), y+8).loadGraphic(Paths.image("editors/character/anim-icons"), true, 16, 12);
|
||||
animIcon.animation.add("play", [0]);
|
||||
animIcon.animation.add("error", [1]);
|
||||
// animIcon.color = 0xFFD60E0E;
|
||||
animIcon.animation.play("play");
|
||||
animIcon.antialiasing = false;
|
||||
animIcon.updateHitbox();
|
||||
members.push(animIcon);
|
||||
|
||||
deleteButton = new UIButton(0, 0, null, () -> {parent.deleteAnimation(this);}, 28*2,24);
|
||||
deleteButton.frames = Paths.getFrames("editors/ui/grayscale-button");
|
||||
deleteButton.color = 0xFFAC3D3D;
|
||||
members.push(deleteButton);
|
||||
|
||||
deleteIcon = new FlxSprite(x-(10+16), y+8).loadGraphic(Paths.image("editors/deleter"));
|
||||
deleteIcon.color = 0xFFD60E0E;
|
||||
deleteIcon.antialiasing = false;
|
||||
members.push(deleteIcon);
|
||||
|
||||
editButton = new UIButton(0, 0, null, () -> {
|
||||
if (!this.closed) UIUtil.confirmUISelections(this);
|
||||
this.closed = !this.closed;
|
||||
}, 28,24);
|
||||
editButton.frames = Paths.getFrames("editors/ui/grayscale-button");
|
||||
editButton.color = 0xFFAFAA12;
|
||||
members.push(editButton);
|
||||
|
||||
editIcon = new FlxSprite().loadGraphic(Paths.image('editors/character/edit-button'));
|
||||
//editIcon.color = 0xFFE8E801;
|
||||
editIcon.antialiasing = false;
|
||||
members.push(editIcon);
|
||||
|
||||
ghostButton = new UIButton(0, 0, null, toggleGhost, 28, 24);
|
||||
ghostButton.frames = Paths.getFrames("editors/ui/grayscale-button");
|
||||
ghostButton.color = 0xFFADADAD;
|
||||
ghostButton.alpha = 0.5;
|
||||
members.push(ghostButton);
|
||||
|
||||
ghostIcon = new FlxSprite(x-(10+16), y+8).loadGraphic(Paths.image('editors/character/ghost'), true, 16, 12);
|
||||
ghostIcon.animation.add("alive", [0]);
|
||||
ghostIcon.animation.add("dead", [1]);
|
||||
ghostIcon.animation.play("dead");
|
||||
ghostIcon.color = 0xFFADADAD;
|
||||
ghostIcon.antialiasing = false;
|
||||
ghostIcon.updateHitbox();
|
||||
members.push(ghostIcon);
|
||||
|
||||
checkValid();
|
||||
}
|
||||
|
||||
public inline function updateButtonsPos() {
|
||||
animIcon.follow(this, 22, (52/2) - (animIcon.height/2) + (animIcon.animation.name == "error" ? 2 : 0));
|
||||
field.follow(this, 22+16+10, (52/2) - (field.height/2) + 1);
|
||||
|
||||
deleteButton.follow(this, 380, 14);
|
||||
deleteIcon.follow(deleteButton, (deleteButton.bWidth/2)-6.5, (deleteButton.bHeight/2)-6);
|
||||
|
||||
editButton.follow(this, 340, 14);
|
||||
editIcon.follow(editButton, (editButton.bWidth/2)-6, (editButton.bHeight/2)-6);
|
||||
|
||||
ghostButton.follow(this, 300, 14);
|
||||
ghostIcon.follow(ghostButton, (ghostButton.bWidth/2)-8, (ghostButton.bHeight/2)-6);
|
||||
|
||||
animationDisplayBG.follow(this, 16, 61);
|
||||
nameTextBox.follow(this, 158, 72);
|
||||
animTextBox.follow(this, 282, 72);
|
||||
|
||||
positionXStepper.follow(this, 158, 118);
|
||||
positionYStepper.follow(this, 218, 118);
|
||||
XYComma.follow(positionXStepper, 64-24, 6);
|
||||
|
||||
fpsStepper.follow(this, 282, 118);
|
||||
loopedCheckbox.follow(this, 336, 122);
|
||||
|
||||
indicesTextBox.follow(this, 158, 164);
|
||||
|
||||
for (ui => text in labels) {
|
||||
text.follow(ui, -(2+(ui is UICheckbox?12:0)), -(18+(ui is UICheckbox?6:0)));
|
||||
text.visible = text.active = ui.visible;
|
||||
}
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float) {
|
||||
bHeight = closed ? 52 : 208;
|
||||
|
||||
for (button in foldableButtons)
|
||||
button.visible = button.active = !closed;
|
||||
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
public function changeName(newName:String, addToUndo:Bool = true) @:privateAccess {
|
||||
if (newName == anim) return;
|
||||
|
||||
if (parent.character.getNameList().indexOf(newName) != -1) {
|
||||
nameTextBox.label.text = anim;
|
||||
return;
|
||||
}
|
||||
|
||||
var oldName:String = anim;
|
||||
if (parent.character.animateAtlas != null) {
|
||||
var animSymbol:FlxSymbolAnimation = __getAnimationSymbol();
|
||||
|
||||
parent.character.animateAtlas.anim.animsMap.remove(anim);
|
||||
parent.character.animateAtlas.anim.animsMap.set(newName, animSymbol);
|
||||
} else {
|
||||
var flxAnimation:FlxAnimation = __getFlxAnimation();
|
||||
flxAnimation.name = newName;
|
||||
|
||||
parent.character.animation._animations.remove(anim);
|
||||
parent.character.animation._animations.set(newName, flxAnimation);
|
||||
}
|
||||
|
||||
var animData:AnimData = parent.character.animDatas[anim];
|
||||
animData.name = newName;
|
||||
|
||||
parent.character.animDatas.remove(anim);
|
||||
parent.character.animDatas.set(newName, animData);
|
||||
|
||||
var animOffset:FlxPoint = parent.character.animOffsets[anim];
|
||||
parent.character.animOffsets.remove(anim);
|
||||
parent.character.animOffsets.set(newName, animOffset);
|
||||
|
||||
var displayFrame:{scale:Float, animBounds:Rectangle, frame:Int} = parent.displayAnimsFramesList[anim];
|
||||
parent.displayAnimsFramesList.remove(anim);
|
||||
parent.displayAnimsFramesList.set(newName, displayFrame);
|
||||
|
||||
this.anim = newName;
|
||||
nameTextBox.label.text = newName;
|
||||
updateText();
|
||||
|
||||
if (addToUndo) CharacterEditor.undos.addToUndo(CAnimEditName(ID, oldName, newName));
|
||||
}
|
||||
|
||||
public function changeAnim(newAnim:String, addToUndo:Bool = true) @:privateAccess {
|
||||
var animData:AnimData = parent.character.animDatas[anim];
|
||||
if (newAnim == animData.anim) return;
|
||||
|
||||
var oldAnim:String = animData.anim;
|
||||
animData.anim = newAnim;
|
||||
|
||||
if (parent.character.animateAtlas != null)
|
||||
__refreshAnimation();
|
||||
else {
|
||||
var flxAnimation:FlxAnimation = __getFlxAnimation();
|
||||
flxAnimation.prefix = newAnim;
|
||||
|
||||
refreshFlxAnimationFrames(flxAnimation, animData);
|
||||
|
||||
if (valid) {
|
||||
parent.buildAnimDisplay(anim, data);
|
||||
animationDisplayBG.alpha = 1;
|
||||
} else {
|
||||
parent.removeAnimDisplay(anim);
|
||||
animationDisplayBG.alpha = 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
if (parent.character.getAnimName() == anim)
|
||||
CharacterEditor.instance.playAnimation(anim);
|
||||
|
||||
animTextBox.label.text = newAnim;
|
||||
|
||||
if (addToUndo) CharacterEditor.undos.addToUndo(CAnimEditAnim(ID, oldAnim, newAnim));
|
||||
}
|
||||
|
||||
public function changeOffset(newOffsetX:Null<Float>, newOffsetY:Null<Float>, addToUndo:Bool = true) {
|
||||
var animData:AnimData = parent.character.animDatas[anim];
|
||||
|
||||
if (newOffsetX != null && newOffsetY != null && newOffsetX == animData.x && newOffsetY == animData.y)
|
||||
return;
|
||||
|
||||
var oldPosition:FlxPoint = FlxPoint.get(animData.x, animData.y);
|
||||
|
||||
if (newOffsetX != null) animData.x = newOffsetX;
|
||||
if (newOffsetY != null) animData.y = newOffsetY;
|
||||
|
||||
parent.character.animOffsets[anim].set(animData.x, animData.y);
|
||||
|
||||
if (parent.character.getAnimName() == anim && Options.playAnimOnOffset)
|
||||
CharacterEditor.instance.playAnimation(anim);
|
||||
|
||||
positionXStepper.label.text = Std.string(animData.x);
|
||||
positionYStepper.label.text = Std.string(animData.y);
|
||||
|
||||
updateText();
|
||||
|
||||
if (addToUndo) CharacterEditor.undos.addToUndo(CAnimEditOffset(ID, oldPosition, FlxPoint.get(animData.x, animData.y)));
|
||||
}
|
||||
|
||||
public function changeFPS(newFPS:Float, addToUndo:Bool = true) @:privateAccess {
|
||||
var animData:AnimData = parent.character.animDatas[anim];
|
||||
var oldFPS:Float = animData.fps;
|
||||
animData.fps = newFPS;
|
||||
|
||||
if (parent.character.animateAtlas != null) {
|
||||
var animSymbol:FlxSymbolAnimation = __getAnimationSymbol();
|
||||
animSymbol.frameRate = newFPS;
|
||||
} else {
|
||||
var flxAnimation:FlxAnimation = __getFlxAnimation();
|
||||
flxAnimation.frameRate = newFPS;
|
||||
}
|
||||
|
||||
if (parent.character.getAnimName() == anim)
|
||||
CharacterEditor.instance.playAnimation(anim);
|
||||
|
||||
fpsStepper.label.text = Std.string(newFPS);
|
||||
|
||||
if (addToUndo) CharacterEditor.undos.addToUndo(CAnimEditFPS(ID, oldFPS, newFPS));
|
||||
}
|
||||
|
||||
public function changeLooping(newLooping:Bool, addToUndo:Bool = true) @:privateAccess {
|
||||
var animData:AnimData = parent.character.animDatas[anim];
|
||||
|
||||
animData.loop = newLooping;
|
||||
|
||||
if (parent.character.animateAtlas != null) {
|
||||
var animSymbol:FlxSymbolAnimation = __getAnimationSymbol();
|
||||
animSymbol.instance.symbol.loop = animData.loop ? Loop : PlayOnce;
|
||||
} else {
|
||||
var flxAnimation:FlxAnimation = __getFlxAnimation();
|
||||
flxAnimation.looped = animData.loop;
|
||||
}
|
||||
|
||||
if (parent.character.getAnimName() == anim)
|
||||
CharacterEditor.instance.playAnimation(anim);
|
||||
|
||||
loopedCheckbox.checked = newLooping;
|
||||
|
||||
if (addToUndo) CharacterEditor.undos.addToUndo(CAnimEditLooping(ID, newLooping));
|
||||
}
|
||||
|
||||
public function changeIndicies(indicies:Array<Int>, addToUndo:Bool = true) @:privateAccess {
|
||||
var animData:AnimData = parent.character.animDatas[anim];
|
||||
var oldIndices:Array<Int> = animData.indices.copy();
|
||||
|
||||
animData.indices = indicies;
|
||||
|
||||
__refreshAnimation();
|
||||
|
||||
if (parent.character.getAnimName() == anim)
|
||||
CharacterEditor.instance.playAnimation(anim);
|
||||
|
||||
indicesTextBox.label.text = animData.indices.join(", ");
|
||||
|
||||
if (addToUndo) CharacterEditor.undos.addToUndo(CAnimEditIndices(ID, oldIndices, indicies));
|
||||
}
|
||||
|
||||
public inline function refreshFlxAnimationFrames(flxAnimation:FlxAnimation, animData:AnimData) @:privateAccess {
|
||||
try {
|
||||
if (animData.indices.length > 0) {
|
||||
var frameIndices:Array<Int> = new Array<Int>();
|
||||
parent.character.animation.byIndicesHelper(frameIndices, flxAnimation.prefix, animData.indices, "");
|
||||
|
||||
flxAnimation.frames = frameIndices;
|
||||
} else {
|
||||
final animFrames:Array<FlxFrame> = new Array<FlxFrame>();
|
||||
parent.character.animation.findByPrefix(animFrames, flxAnimation.prefix);
|
||||
|
||||
final frameIndices:Array<Int> = [];
|
||||
parent.character.animation.byPrefixHelper(frameIndices, animFrames, flxAnimation.prefix);
|
||||
|
||||
flxAnimation.frames = frameIndices;
|
||||
}
|
||||
|
||||
if (flxAnimation.frames.length <= 0) invalidate();
|
||||
else validate();
|
||||
} catch (e) {
|
||||
trace('ERROR REFRESHING FLXANIMATION FRAMES: $e');
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public inline function refreshSymbolKeyFrames(symbol:FlxSymbolAnimation, animData:AnimData) @:privateAccess {
|
||||
var wasRefreshed:Bool = false;
|
||||
|
||||
try {
|
||||
parent.character.animateAtlas.anim.animsMap.remove(animData.name);
|
||||
if (animData.indices.length > 0)
|
||||
parent.character.animateAtlas.anim.addBySymbolIndices(animData.name, animData.anim, animData.indices, animData.fps, animData.loop);
|
||||
else
|
||||
parent.character.animateAtlas.anim.addBySymbol(animData.name, animData.anim, animData.fps, animData.loop);
|
||||
wasRefreshed = parent.character.animateAtlas.anim.animsMap.exists(animData.name);
|
||||
if (!wasRefreshed) __getAnimationSymbol();
|
||||
} catch (e) {
|
||||
trace('ERROR REFRESHING SYMBOL FRAMES: $e');
|
||||
}
|
||||
|
||||
validate(wasRefreshed);
|
||||
}
|
||||
|
||||
public function toggleGhost() {
|
||||
if (valid && parent.ghosts.indexOf(anim) == -1) {
|
||||
parent.ghosts.push(anim);
|
||||
ghostIcon.animation.play("alive", true);
|
||||
ghostIcon.color = 0xFFFFFFFF;
|
||||
} else {
|
||||
parent.ghosts.remove(anim);
|
||||
ghostIcon.animation.play("dead", true);
|
||||
ghostIcon.color = 0xFFADADAD;
|
||||
}
|
||||
}
|
||||
|
||||
public override function draw() {
|
||||
updateButtonsPos();
|
||||
super.draw();
|
||||
|
||||
if (!closed && parent.displayAnimsFramesList.exists(anim)) {
|
||||
var displayData:{frame:Int, scale:Float, animBounds:Rectangle} = parent.displayAnimsFramesList.get(anim);
|
||||
parent.displayWindowSprite.frame = parent.displayWindowSprite.frames.frames[displayData.frame];
|
||||
|
||||
parent.displayWindowSprite.scale.x = parent.displayWindowSprite.scale.y = displayData.scale;
|
||||
parent.displayWindowSprite.updateHitbox();
|
||||
|
||||
parent.displayWindowSprite.follow(
|
||||
this, 16+(128/2)-((parent.displayWindowSprite.frame.sourceSize.x*displayData.scale)/2),
|
||||
8+32+8+2+11+(128/2)-((parent.displayWindowSprite.frame.sourceSize.y*displayData.scale)/2)
|
||||
);
|
||||
parent.displayWindowSprite.draw();
|
||||
}
|
||||
}
|
||||
|
||||
public function updateText() {
|
||||
field.applyMarkup(
|
||||
'${anim} ' + (valid ? '(${parent.character.animOffsets[anim].x}, ${parent.character.animOffsets[anim].y})' : "|(Invalid Animation!!!)|"),
|
||||
[new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFF29797), "|")]);
|
||||
}
|
||||
|
||||
public function validate(isValid:Bool = true) {
|
||||
var wasValid:Bool = valid;
|
||||
this.valid = isValid;
|
||||
|
||||
animIcon.animation.play(valid ? "play" :"error");
|
||||
color = valid ? 0xFFFFFFFF : 0xFFD60E0E;
|
||||
|
||||
ghostIcon.visible = ghostButton.visible = isValid;
|
||||
XYComma.alpha = isValid ? 1 : 0.4;
|
||||
for (uiElement in [nameTextBox, positionXStepper, positionYStepper, fpsStepper, loopedCheckbox, indicesTextBox]) {
|
||||
uiElement.selectable = isValid;
|
||||
if (labels[uiElement] != null)
|
||||
labels[uiElement].alpha = isValid ? 1 : 0.4;
|
||||
}
|
||||
if (!valid) toggleGhost();
|
||||
updateText();
|
||||
|
||||
if (!wasValid && isValid && CharacterEditor.instance.characterFakeAnim == anim)
|
||||
CharacterEditor.instance.playAnimation(this.anim);
|
||||
}
|
||||
|
||||
public inline function invalidate()
|
||||
validate(false);
|
||||
|
||||
public inline function __getFlxAnimation():Null<FlxAnimation> @:privateAccess {
|
||||
if (!parent.character.animation._animations.exists(anim)) {
|
||||
final flxanim = new FlxAnimation(parent.character.animation, anim, [], data.fps, data.loop);
|
||||
flxanim.prefix = data.anim; // sobbing
|
||||
|
||||
parent.character.animation._animations.set(anim, flxanim);
|
||||
parent.character.animDatas.set(anim, data);
|
||||
}
|
||||
return parent.character.animation._animations[anim];
|
||||
}
|
||||
|
||||
public inline function __getAnimationSymbol():Null<FlxSymbolAnimation> @:privateAccess {
|
||||
if (!parent.character.animateAtlas.anim.animsMap.exists(anim))
|
||||
XMLUtil.addAnimToSprite(parent.character, data);
|
||||
return parent.character.animateAtlas.anim.animsMap[anim];
|
||||
}
|
||||
|
||||
|
||||
@:noCompletion function __refreshAnimation() @:privateAccess {
|
||||
if (parent.character.animateAtlas != null) refreshSymbolKeyFrames(__getAnimationSymbol(), data);
|
||||
else refreshFlxAnimationFrames(__getFlxAnimation(), data);
|
||||
|
||||
if (valid) {
|
||||
parent.buildAnimDisplay(anim, data);
|
||||
animationDisplayBG.alpha = 1;
|
||||
} else {
|
||||
parent.removeAnimDisplay(anim);
|
||||
animationDisplayBG.alpha = 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
public inline function checkValid()
|
||||
__refreshAnimation();
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package funkin.editors.character;
|
||||
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.util.FlxColor;
|
||||
|
||||
class CharacterAnimButtons extends UIButton {
|
||||
public var anim:String = "";
|
||||
|
||||
public var editButton:UIButton;
|
||||
public var editIcon:FlxSprite;
|
||||
|
||||
public var ghostButton:UIButton;
|
||||
public var ghostIcon:FlxSprite;
|
||||
|
||||
public var deleteButton:UIButton;
|
||||
public var deleteIcon:FlxSprite;
|
||||
|
||||
public function new(x:Float,y:Float,anim:String, offset:FlxPoint) {
|
||||
this.anim = anim;
|
||||
super(x,y, '$anim (${offset.x}, ${offset.y})', function () {
|
||||
CharacterEditor.instance.playAnimation(this.anim);
|
||||
}, 282);
|
||||
autoAlpha = false;
|
||||
|
||||
ghostButton = new UIButton(x+282+17, y, "", function () {
|
||||
CharacterEditor.instance.ghostAnim(this.anim);
|
||||
}, 32);
|
||||
ghostButton.autoAlpha = false;
|
||||
members.push(ghostButton);
|
||||
|
||||
ghostIcon = new FlxSprite(ghostButton.x + 8, ghostButton.y + 8).loadGraphic(Paths.image('editors/character/ghost-button'), true, 16, 16);
|
||||
ghostIcon.animation.add("alive", [0]);
|
||||
ghostIcon.animation.add("dead", [1]);
|
||||
ghostIcon.animation.play("dead"); ghostIcon.alpha = 0.5;
|
||||
ghostIcon.antialiasing = false;
|
||||
ghostIcon.updateHitbox();
|
||||
members.push(ghostIcon);
|
||||
|
||||
editButton = new UIButton(ghostButton.x+32+17, y, "", function () {
|
||||
CharacterEditor.instance.editAnimWithUI(this.anim);
|
||||
}, 32);
|
||||
editButton.frames = Paths.getFrames("editors/ui/grayscale-button");
|
||||
editButton.color = FlxColor.YELLOW;
|
||||
editButton.autoAlpha = false;
|
||||
members.push(editButton);
|
||||
|
||||
editIcon = new FlxSprite(editButton.x + 8, editButton.y + 8).loadGraphic(Paths.image('editors/character/edit-button'));
|
||||
editIcon.antialiasing = false;
|
||||
members.push(editIcon);
|
||||
|
||||
deleteButton = new UIButton(editButton.x+32+17, y, "", function () {
|
||||
CharacterEditor.instance.deleteAnim(this.anim);
|
||||
}, 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);
|
||||
}
|
||||
|
||||
public override function update(elapsed:Float) {
|
||||
editButton.selectable = ghostButton.selectable = deleteButton.selectable = selectable;
|
||||
editButton.shouldPress = ghostButton.shouldPress = deleteButton.shouldPress = shouldPress;
|
||||
|
||||
hovered = !deleteButton.hovered;
|
||||
updatePos();
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
public function updateInfo(anim:String, offset:FlxPoint, ghost:Bool) {
|
||||
this.anim = anim;
|
||||
|
||||
field.text = '$anim (${offset.x}, ${offset.y})';
|
||||
ghostIcon.animation.play(ghost ? "alive" : "dead");
|
||||
ghostIcon.alpha = ghost ? 1 : 0.5;
|
||||
}
|
||||
|
||||
public inline function updatePos() {
|
||||
// buttons
|
||||
deleteButton.x = (editButton.x = (ghostButton.x = (x+282+17))+32+17)+32+17;
|
||||
deleteButton.y = editButton.y = ghostButton.y = y;
|
||||
// icons
|
||||
ghostIcon.x = ghostButton.x + 8; ghostIcon.y = ghostButton.y + 8;
|
||||
editIcon.x = editButton.x + 8; editIcon.y = editButton.y + 8;
|
||||
deleteIcon.x = deleteButton.x + (15/2); deleteIcon.y = deleteButton.y + 8;
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package funkin.editors.character;
|
||||
|
||||
import flixel.animation.FlxAnimation;
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.backend.utils.XMLUtil.AnimData;
|
||||
|
||||
class CharacterAnimScreen extends UISubstateWindow {
|
||||
public var animData:AnimData;
|
||||
|
||||
public var nameTextBox:UITextBox;
|
||||
public var animTextBox:UITextBox;
|
||||
public var fpsStepper:UINumericStepper;
|
||||
public var loopedCheckbox:UICheckbox;
|
||||
public var indicesTextBox:UITextBox;
|
||||
public var offsetXStepper:UINumericStepper;
|
||||
public var offsetYStepper:UINumericStepper;
|
||||
|
||||
public var saveButton:UIButton;
|
||||
public var closeButton:UIButton;
|
||||
|
||||
public var onSave:(animData:AnimData) -> Void = null;
|
||||
|
||||
public function new(animData:AnimData, ?onSave:(animData:AnimData) -> Void) {
|
||||
this.animData = animData;
|
||||
this.onSave = onSave;
|
||||
super();
|
||||
}
|
||||
|
||||
inline function translate(id:String, ?args:Array<Dynamic>)
|
||||
return TU.translate("characterAnimScreen." + id, args);
|
||||
|
||||
public override function create() {
|
||||
var creatingAnim:Bool = animData == null;
|
||||
|
||||
if (creatingAnim)
|
||||
animData = {
|
||||
name: "name",
|
||||
anim: "anim",
|
||||
fps: 24,
|
||||
loop: false,
|
||||
animType: NONE,
|
||||
x: 0,
|
||||
y: 0,
|
||||
indices: []
|
||||
};
|
||||
|
||||
winTitle = translate("win-title-" + (creatingAnim ? "creating" : "editing"), [animData.name]);
|
||||
winWidth = 690; winHeight = 314;
|
||||
|
||||
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, translate("title-" + (creatingAnim ? "creating" : "editing")), 28));
|
||||
|
||||
nameTextBox = new UITextBox(title.x, title.y + title.height + 38, animData.name, 160);
|
||||
add(nameTextBox);
|
||||
addLabelOn(nameTextBox, translate("name"));
|
||||
|
||||
animTextBox = new UITextBox(nameTextBox.x + 160 + 26, nameTextBox.y, animData.anim, 200);
|
||||
add(animTextBox);
|
||||
addLabelOn(animTextBox, translate("anim"));
|
||||
|
||||
fpsStepper = new UINumericStepper(animTextBox.x + 200 + 26, animTextBox.y, animData.fps, 0.1, 2, 1, 100, 82);
|
||||
add(fpsStepper);
|
||||
addLabelOn(fpsStepper, translate("fps"));
|
||||
|
||||
loopedCheckbox = new UICheckbox(fpsStepper.x + 82 - 32 + 26, fpsStepper.y, translate("looped"), animData.loop);
|
||||
add(loopedCheckbox);
|
||||
addLabelOn(loopedCheckbox, translate("looping"));
|
||||
loopedCheckbox.x += 8; loopedCheckbox.y += 6;
|
||||
|
||||
indicesTextBox = new UITextBox(nameTextBox.x, nameTextBox.y + 32 + 40, CoolUtil.formatNumberRange(animData.indices, ", "), 270);
|
||||
add(indicesTextBox);
|
||||
addLabelOn(indicesTextBox, translate("indices"));
|
||||
|
||||
offsetXStepper = new UINumericStepper(indicesTextBox.x + 270 + 26, indicesTextBox.y, animData.x, 0.001, 2, null, null, 84);
|
||||
add(offsetXStepper);
|
||||
addLabelOn(offsetXStepper, translate("offset"));
|
||||
|
||||
add(new UIText(offsetXStepper.x + 84 - 32 + 0, offsetXStepper.y + 9, 0, ",", 22));
|
||||
|
||||
offsetYStepper = new UINumericStepper(offsetXStepper.x + 84 - 32 + 26, offsetXStepper.y, animData.y, 0.001, 2, null, null, 84);
|
||||
add(offsetYStepper);
|
||||
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() {
|
||||
saveAnimData();
|
||||
close();
|
||||
}, 125);
|
||||
add(saveButton);
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 20, saveButton.y, TU.translate("editor." + (creatingAnim ? "cancel" : "close")), function() {
|
||||
if (creatingAnim && onSave != null) onSave(null);
|
||||
close();
|
||||
}, 125);
|
||||
add(closeButton);
|
||||
closeButton.color = 0xFFFF0000;
|
||||
closeButton.x -= closeButton.bWidth;
|
||||
}
|
||||
|
||||
public function saveAnimData() {
|
||||
for (stepper in [offsetXStepper, offsetYStepper, fpsStepper])
|
||||
@:privateAccess stepper.__onChange(stepper.label.text);
|
||||
|
||||
animData = {
|
||||
name: nameTextBox.label.text,
|
||||
anim: animTextBox.label.text,
|
||||
fps: fpsStepper.value,
|
||||
loop: loopedCheckbox.checked,
|
||||
animType: NONE,
|
||||
x: offsetXStepper.value,
|
||||
y: offsetYStepper.value,
|
||||
indices: CoolUtil.parseNumberRange(indicesTextBox.label.text)
|
||||
};
|
||||
|
||||
if (onSave != null) onSave(animData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package funkin.editors.character;
|
||||
|
||||
import flixel.util.typeLimit.OneOfTwo;
|
||||
import funkin.backend.utils.XMLUtil.AnimData;
|
||||
import flixel.animation.FlxAnimation;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import openfl.geom.Rectangle;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import funkin.game.Character;
|
||||
import flixel.animation.FlxPrerotatedAnimation;
|
||||
|
||||
using funkin.backend.utils.BitmapUtil;
|
||||
|
||||
class CharacterAnimsWindow extends UIButtonList<CharacterAnimButton> {
|
||||
public var character:CharacterGhost;
|
||||
|
||||
public var displayWindowSprite:FlxSprite;
|
||||
public var displayWindowGraphic:FlxGraphic;
|
||||
public var displayAnimsFramesList:Map<String, {scale:Float, animBounds:Rectangle, frame:OneOfTwo<Int, String>}> = [];
|
||||
|
||||
public var animButtons:Map<String, CharacterAnimButton> = [];
|
||||
public var animsList:Array<String> = [];
|
||||
|
||||
public function new(x:Float, y:Float, character:CharacterGhost) {
|
||||
super(x, y, Std.int(500-16), 419, null, FlxPoint.get(Std.int(500-16-32), 208));
|
||||
this.character = character;
|
||||
|
||||
cameraSpacing = 0;
|
||||
frames = Paths.getFrames('editors/ui/inputbox');
|
||||
|
||||
buttonCameras.pixelPerfectRender = true;
|
||||
|
||||
if (Assets.exists(Paths.image('characters/${character.sprite}')) && character.animateAtlas == null)
|
||||
displayWindowGraphic = FlxG.bitmap.add(Assets.getBitmapData(Paths.image('characters/${character.sprite}'), true, false));
|
||||
|
||||
displayWindowSprite = new FlxSprite();
|
||||
displayWindowSprite.loadGraphicFromSprite(character);
|
||||
displayWindowSprite.antialiasing = character.antialiasing;
|
||||
displayWindowSprite.flipX = character.flipX;
|
||||
|
||||
alpha = 0.7;
|
||||
|
||||
for (anim in character.getAnimOrder())
|
||||
addAnimation(character.animDatas.get(anim), -1, false);
|
||||
addButton.callback = generateAnimation;
|
||||
|
||||
setAnimAutoComplete(CoolUtil.getAnimsListFromSprite(character));
|
||||
|
||||
nextscrollY = 0;
|
||||
// dragCallback = (button:CharacterAnimButton, oldID:Int, newID:Int) -> {}
|
||||
}
|
||||
|
||||
public var ghosts:Array<String> = [];
|
||||
var __movingAnimOldOrder:Int = -1;
|
||||
public override function update(elapsed:Float) {
|
||||
var __oldMoving:CharacterAnimButton = curMoving;
|
||||
super.update(elapsed);
|
||||
|
||||
if (curMoving != null && __oldMoving == null)
|
||||
__movingAnimOldOrder = curMoving.ID;
|
||||
|
||||
if (curMoving == null && __oldMoving != null)
|
||||
if (__movingAnimOldOrder != __oldMoving.ID)
|
||||
CharacterEditor.undos.addToUndo(CAnimEditOrder(__movingAnimOldOrder, __oldMoving.ID));
|
||||
|
||||
|
||||
animsList = [for (button in buttons) button.anim];
|
||||
character.ghosts = ghosts;
|
||||
}
|
||||
|
||||
public function buildAnimDisplay(name:String, anim:AnimData) @:privateAccess {
|
||||
if (character.animateAtlas == null) {
|
||||
var anim:FlxAnimation = character.animation._animations[anim.name];
|
||||
if (anim == null || anim.frames.length <= 0) return;
|
||||
|
||||
var frameIndex:Int = anim.frames.getDefault([0])[0];
|
||||
var frame:FlxFrame = displayWindowSprite.frames.frames[frameIndex];
|
||||
|
||||
var frameRect:Rectangle = new Rectangle(frame.offset.x, frame.offset.y, frame.sourceSize.x, frame.sourceSize.y);
|
||||
var animBounds:Rectangle = displayWindowGraphic != null ? displayWindowGraphic.bitmap.bounds(frameRect) : frameRect;
|
||||
|
||||
displayAnimsFramesList.set(name, {frame: anim.frames.getDefault([0])[0], scale: 104/Math.max(animBounds.width, animBounds.height), animBounds: animBounds});
|
||||
} else {
|
||||
character.storeAtlasState();
|
||||
|
||||
/*
|
||||
character.animateAtlas.anim.play(anim.name, true, false, 0);
|
||||
character.animateAtlas.anim.stop();
|
||||
|
||||
var animBounds:Rectangle = MatrixUtil.getBounds(character).copyToFlash();
|
||||
displayAnimsFramesList.set(name, {frame: anim.anim, scale: 104/animBounds.height, animBounds: animBounds});
|
||||
*/
|
||||
|
||||
character.restoreAtlasState();
|
||||
}
|
||||
}
|
||||
|
||||
public function removeAnimDisplay(name:String)
|
||||
displayAnimsFramesList.remove(name);
|
||||
|
||||
public function deleteAnimation(button:CharacterAnimButton, addToUndo:Bool = true) {
|
||||
if (buttons.members.length <= 1) return;
|
||||
if (character.getAnimName() == button.anim)
|
||||
@:privateAccess CharacterEditor.instance._animation_down(null);
|
||||
|
||||
character.removeAnimation(button.anim);
|
||||
if (character.animOffsets.exists(button.anim)) character.animOffsets.remove(button.anim);
|
||||
if (character.animDatas.exists(button.anim)) character.animDatas.remove(button.anim);
|
||||
|
||||
if (addToUndo) CharacterEditor.undos.addToUndo(CAnimDelete(button.ID, button.data));
|
||||
remove(button); button.destroy();
|
||||
}
|
||||
|
||||
public function generateAnimation() {
|
||||
var animName:String = "New Anim";
|
||||
var animNames:Array<String> = character.getNameList();
|
||||
|
||||
var newAnimCount:Int = 0;
|
||||
while (animNames.indexOf(animName) != -1) {
|
||||
newAnimCount++;
|
||||
animName = 'New Anim - $newAnimCount';
|
||||
}
|
||||
|
||||
if (__autoCompleteAnims.length <= 0)
|
||||
setAnimAutoComplete(CoolUtil.getAnimsListFromSprite(character));
|
||||
|
||||
var animData:AnimData = {
|
||||
name: animName,
|
||||
anim: __autoCompleteAnims[0],
|
||||
fps: 24, loop: false,
|
||||
x: 0, y: 0,
|
||||
indices: [],
|
||||
animType: NONE,
|
||||
};
|
||||
addAnimation(animData);
|
||||
}
|
||||
|
||||
public function addAnimation(animData:AnimData, animID:Int = -1, addToUndo:Bool = true) @:privateAccess {
|
||||
XMLUtil.addAnimToSprite(character, animData);
|
||||
|
||||
var newButton:CharacterAnimButton = new CharacterAnimButton(0, 0, animData, this);
|
||||
newButton.alpha = 0.25; animButtons.set(animData.name, newButton);
|
||||
newButton.animTextBox.suggestItems = __autoCompleteAnims;
|
||||
|
||||
if (animID == -1) add(newButton);
|
||||
else insert(newButton, animID);
|
||||
|
||||
if (newButton.valid)
|
||||
buildAnimDisplay(animData.name, animData);
|
||||
|
||||
if (addToUndo)
|
||||
CharacterEditor.undos.addToUndo(CAnimCreate(newButton.ID, newButton.data));
|
||||
|
||||
var nextButtonY:Float = 0;
|
||||
for (buttonID in 0...newButton.ID)
|
||||
nextButtonY += buttons.members[buttonID].bHeight + buttonOffset.y;
|
||||
nextscrollY = nextButtonY;
|
||||
}
|
||||
|
||||
@:noCompletion var __autoCompleteAnims:Array<String> = [];
|
||||
public inline function setAnimAutoComplete(anims:Array<String>) {
|
||||
__autoCompleteAnims = anims.copy();
|
||||
for (button in buttons)
|
||||
button.animTextBox.suggestItems = __autoCompleteAnims;
|
||||
}
|
||||
|
||||
public function findValid():Null<String> {
|
||||
for (button in buttons) if (button.valid) return button.anim;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package funkin.editors.character;
|
||||
|
||||
import haxe.xml.Printer;
|
||||
import funkin.game.Character;
|
||||
import funkin.editors.ui.UIImageExplorer.ImageSaveData;
|
||||
import flixel.text.FlxText.FlxTextFormat;
|
||||
import flixel.text.FlxText.FlxTextFormatMarkerPair;
|
||||
|
||||
class CharacterCreationScreen extends UISubstateWindow {
|
||||
private var onSave:(String, ImageSaveData, Xml)-> Void = null;
|
||||
|
||||
public var characterNameTextBox:UITextBox;
|
||||
public var imageExplorer:UIImageExplorer;
|
||||
|
||||
public var saveButton:UIButton;
|
||||
public var closeButton:UIButton;
|
||||
|
||||
public function new(?onSave:(String, ImageSaveData, Xml)->Void) {
|
||||
super();
|
||||
if (onSave != null) this.onSave = onSave;
|
||||
}
|
||||
|
||||
public override function create() {
|
||||
winTitle = "Creating New Character";
|
||||
|
||||
winWidth = 360;
|
||||
winHeight = 520;
|
||||
|
||||
super.create();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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$",
|
||||
[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$",
|
||||
[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() {
|
||||
close();
|
||||
createCharacter();
|
||||
}, 125);
|
||||
add(saveButton);
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 20 - saveButton.bWidth, saveButton.y, "Cancel", function() {
|
||||
close();
|
||||
}, 125);
|
||||
add(closeButton);
|
||||
closeButton.color = 0xFFFF0000;
|
||||
|
||||
onLoadImage();
|
||||
}
|
||||
|
||||
public function onLoadImage() {
|
||||
refreshWindowSize();
|
||||
checkRequired();
|
||||
}
|
||||
|
||||
public function refreshWindowSize() {
|
||||
if (imageExplorer == null) return;
|
||||
windowSpr.bWidth = 20 + imageExplorer.bWidth + 20;
|
||||
windowSpr.bHeight = 30 + 16 + 20 + 32 + 30 + 10 + imageExplorer.bHeight + 14 + saveButton.bHeight + 14;
|
||||
|
||||
saveButton.x = windowSpr.x + windowSpr.bWidth - 20 - saveButton.bWidth;
|
||||
closeButton.x = saveButton.x - 20 - closeButton.bWidth;
|
||||
closeButton.y = saveButton.y = imageExplorer.y + imageExplorer.bHeight + 14;
|
||||
}
|
||||
|
||||
public function checkRequired() {
|
||||
saveButton.selectable = characterNameTextBox.label.text.length > 0 && !CoolUtil.isMapEmpty(imageExplorer.imageFiles) && (imageExplorer.animationList.length > 0);
|
||||
}
|
||||
|
||||
public function createCharacter() {
|
||||
var imageSaveData:ImageSaveData = imageExplorer.getSaveData();
|
||||
|
||||
var xml:Xml = Xml.createElement("character");
|
||||
xml.attributeOrder = Character.characterProperties.copy();
|
||||
|
||||
xml.set("sprite", '${imageSaveData.directory.length > 0 ? '${imageSaveData.directory}/' : ""}' + imageSaveData.imageName);
|
||||
|
||||
// Look for animations >:D
|
||||
var animationList:Array<String> = imageExplorer.animationList.copy();
|
||||
animationList.sort((a, b) -> {
|
||||
var lengthCompare = a.length - b.length;
|
||||
if (lengthCompare != 0) return lengthCompare;
|
||||
|
||||
// Miss animations don't show up as regular if they are the same length as regular >:D
|
||||
var aIsMiss = a.toLowerCase().contains("miss");
|
||||
var bIsMiss = b.toLowerCase().contains("miss");
|
||||
return aIsMiss == bIsMiss ? 0 : (aIsMiss ? 1 : -1);
|
||||
});
|
||||
|
||||
var animationsFound:Map<String, String> = [
|
||||
"singLEFT" => null,
|
||||
"singRIGHT" => null,
|
||||
"singUP" => null,
|
||||
"singDOWN" => null,
|
||||
"idle" => null
|
||||
];
|
||||
|
||||
for (anim => found in animationsFound) {
|
||||
var animToLookFor:String = StringTools.replace(anim, "sing", "").toLowerCase();
|
||||
for (imageAnim in animationList)
|
||||
if (imageAnim.toLowerCase().contains(animToLookFor)) {
|
||||
animationsFound.set(anim, imageAnim);
|
||||
animationList.remove(imageAnim);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add said animations >:D
|
||||
for (anim => found in animationsFound) {
|
||||
var animXml:Xml = Xml.createElement('anim');
|
||||
animXml.attributeOrder = Character.characterAnimProperties;
|
||||
|
||||
animXml.set("name", anim);
|
||||
animXml.set("anim", found.getDefault(animationList[0]));
|
||||
|
||||
xml.addChild(animXml);
|
||||
}
|
||||
|
||||
// Do the rest only if not atlas (until we make it animations and not symbols >:D)
|
||||
if (!imageSaveData.isAtlas) {
|
||||
for (imageAnim in animationList) {
|
||||
var animXml:Xml = Xml.createElement('anim');
|
||||
animXml.attributeOrder = Character.characterAnimProperties;
|
||||
|
||||
animXml.set("name", imageAnim);
|
||||
animXml.set("anim", imageAnim);
|
||||
|
||||
xml.addChild(animXml);
|
||||
}
|
||||
}
|
||||
|
||||
if (onSave != null) onSave(characterNameTextBox.label.text, imageSaveData, xml);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
package funkin.editors.character;
|
||||
|
||||
import funkin.backend.system.FakeCamera.FakeCallCamera;
|
||||
import openfl.geom.ColorTransform;
|
||||
import flixel.animation.FlxAnimation;
|
||||
import funkin.game.Character;
|
||||
|
||||
typedef AtlasState = {
|
||||
var oldAnim:String;
|
||||
var oldFrame:Int;
|
||||
var oldTick:Float;
|
||||
var oldPlaying:Bool;
|
||||
};
|
||||
|
||||
class CharacterGhost extends Character {
|
||||
public var ghosts:Array<String> = [];
|
||||
public override function draw() @:privateAccess {
|
||||
ghostDraw = FakeCallCamera.instance.ignoreDraws = true;
|
||||
|
||||
var wasInvalidFrame:Bool = !colorTransform.__isDefault(false);
|
||||
colorTransform.__identity();
|
||||
|
||||
if (animateAtlas != null) {
|
||||
storeAtlasState();
|
||||
for (anim in ghosts) {
|
||||
animateAtlas.anim.play(anim, true, false, 0);
|
||||
setAnimOffset(anim);
|
||||
|
||||
alpha = 0.4; color = 0xFFAEAEAE;
|
||||
super.draw();
|
||||
}
|
||||
|
||||
if (ghosts.length > 0) restoreAtlasState();
|
||||
}
|
||||
else {
|
||||
for (anim in ghosts) @:privateAccess {
|
||||
alpha = 0.4; color = 0xFFAEAEAE;
|
||||
|
||||
var flxanim:FlxAnimation = animation._animations.get(anim);
|
||||
var frameIndex:Int = flxanim.frames.getDefault([0])[0];
|
||||
frame = frames.frames[frameIndex];
|
||||
|
||||
setAnimOffset(anim);
|
||||
super.draw();
|
||||
}
|
||||
|
||||
if (ghosts.length > 0)
|
||||
frame = frames.frames[animation.frameIndex];
|
||||
}
|
||||
ghostDraw = FakeCallCamera.instance.ignoreDraws = false;
|
||||
|
||||
alpha = 1; color = 0xFFFFFFFF;
|
||||
if (wasInvalidFrame) {
|
||||
frameOffset.set(0, 0);
|
||||
offset.set(globalOffset.x * (isPlayer != playerOffsets ? 1 : -1), -globalOffset.y);
|
||||
colorTransform.color = 0xFFEF0202;
|
||||
} else
|
||||
setAnimOffset(animateAtlas != null ? atlasPlayingAnim : animation.name);
|
||||
|
||||
super.draw();
|
||||
}
|
||||
|
||||
public function setAnimOffset(anim:String) {
|
||||
var daOffset:FlxPoint = animOffsets.get(anim);
|
||||
if (daOffset != null) {
|
||||
frameOffset.set(daOffset.x, daOffset.y);
|
||||
daOffset.putWeak();
|
||||
}
|
||||
|
||||
offset.set(globalOffset.x * (isPlayer != playerOffsets ? 1 : -1), -globalOffset.y);
|
||||
}
|
||||
|
||||
// This gets annoying lmao -lunar
|
||||
private var atlasState:AtlasState = null;
|
||||
public function storeAtlasState():AtlasState @:privateAccess {
|
||||
return atlasState = {
|
||||
oldAnim: atlasPlayingAnim,
|
||||
oldFrame: animateAtlas.anim.curFrame,
|
||||
oldTick: animateAtlas.anim._tick,
|
||||
oldPlaying: animateAtlas.anim.isPlaying,
|
||||
};
|
||||
}
|
||||
|
||||
public function restoreAtlasState(state:AtlasState = null) @:privateAccess {
|
||||
if (state == null) state = atlasState;
|
||||
|
||||
animateAtlas.anim.play(state.oldAnim, true, false, state.oldFrame);
|
||||
animateAtlas.anim._tick = state.oldTick;
|
||||
animateAtlas.anim.isPlaying = state.oldPlaying;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package funkin.editors.character;
|
||||
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.game.Character;
|
||||
import haxe.xml.Access;
|
||||
|
||||
class CharacterGhostsHandler extends FlxTypedGroup<Character> {
|
||||
public var character:Character;
|
||||
public var animGhosts:Map<String, Character> = [];
|
||||
|
||||
public function new(character:Character) {
|
||||
this.character = character;
|
||||
super();
|
||||
|
||||
for (anim in character.getNameList())
|
||||
createGhost(anim);
|
||||
}
|
||||
|
||||
public function createGhost(anim:String) {
|
||||
var ghost:Character = new Character(0,0, character.curCharacter);
|
||||
ghost.applyXML(character.xml); // apply cur character stuff
|
||||
ghost.playAnim(anim);
|
||||
ghost.stopAnimation();
|
||||
ghost.alpha = 0.5;
|
||||
ghost.visible = false;
|
||||
add(ghost);
|
||||
|
||||
animGhosts.set(anim, ghost);
|
||||
}
|
||||
|
||||
public function removeGhost(anim:String) {
|
||||
var ghost:Character = animGhosts.get(anim);
|
||||
@:privateAccess if (ghost != null) {
|
||||
ghost.animation._curAnim = null;
|
||||
animGhosts.remove(anim);
|
||||
ghost.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public function updateInfos(xml:Xml) {
|
||||
for (anim => ghost in animGhosts) {
|
||||
ghost.applyXML(new Access(xml));
|
||||
ghost.playAnim(anim);
|
||||
ghost.stopAnimation();
|
||||
}
|
||||
|
||||
rePositionGhosts();
|
||||
}
|
||||
|
||||
public function updateOffsets(name:String, change:FlxPoint) {
|
||||
for (anim => ghost in animGhosts)
|
||||
ghost.animOffsets.set(name, ghost.getAnimOffset(name) + change);
|
||||
|
||||
rePositionGhosts();
|
||||
}
|
||||
|
||||
public function setOffsets(name:String, offset:FlxPoint) {
|
||||
for (anim => ghost in animGhosts)
|
||||
ghost.animOffsets.set(name, offset);
|
||||
|
||||
rePositionGhosts();
|
||||
}
|
||||
|
||||
public function clearOffsets() {
|
||||
for (anim => ghost in animGhosts)
|
||||
ghost.animOffsets[anim].zero();
|
||||
|
||||
rePositionGhosts();
|
||||
}
|
||||
|
||||
inline function rePositionGhosts() {
|
||||
for (ghost in animGhosts)
|
||||
ghost.frameOffset.set(ghost.getAnimOffset(ghost.getAnimName()).x, ghost.getAnimOffset(ghost.getAnimName()).y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package funkin.editors.character;
|
||||
|
||||
import funkin.editors.stage.StageEditor;
|
||||
import flixel.math.FlxRect;
|
||||
import funkin.game.Character;
|
||||
|
||||
class CharacterGizmos extends FlxSprite {
|
||||
public var character:Character;
|
||||
|
||||
public var boxGizmo:Bool = true;
|
||||
public var cameraGizmo:Bool = true;
|
||||
|
||||
public override function draw() {
|
||||
super.draw();
|
||||
|
||||
if (character == null) return;
|
||||
|
||||
if (boxGizmo) drawHitbox();
|
||||
if (cameraGizmo) drawCamera();
|
||||
}
|
||||
|
||||
public function drawHitbox() {
|
||||
for (camera in cameras) {
|
||||
if (character.animateAtlas != null) {
|
||||
var bounds:FlxRect = cast character.extra.get(StageEditor.exID("bounds"));
|
||||
character._rect.copyFrom(bounds.getDefault(FlxRect.weak()));
|
||||
|
||||
character._rect.x -= character.cameras[0].viewX;
|
||||
character._rect.y -= character.cameras[0].viewY;
|
||||
} else if (character._matrix != null && character.frame != null) {
|
||||
character._rect.set(
|
||||
character._matrix.tx, character._matrix.ty,
|
||||
Math.abs(character.frame.frame.width * character._matrix.a),
|
||||
Math.abs(character.frame.frame.height * character._matrix.d)
|
||||
);
|
||||
if (character._matrix.a < 0) character._rect.x -= character._rect.width;
|
||||
if (character._matrix.d < 0) character._rect.y -= character._rect.height;
|
||||
|
||||
character._rect.offset(-character.cameras[0].viewMarginLeft, -character.cameras[0].viewMarginTop);
|
||||
}
|
||||
|
||||
character._rect.x *= character.cameras[0].zoom;
|
||||
character._rect.y *= character.cameras[0].zoom;
|
||||
character._rect.width *= character.cameras[0].zoom;
|
||||
character._rect.height *= character.cameras[0].zoom;
|
||||
|
||||
if (DrawUtil.line == null) DrawUtil.createDrawers();
|
||||
DrawUtil.line.camera = camera; DrawUtil.line.alpha = 0.85;
|
||||
|
||||
DrawUtil.drawRect(character._rect, 1, 0xFF007B8F);
|
||||
}
|
||||
}
|
||||
|
||||
public function drawCamera() {
|
||||
for (camera in cameras) {
|
||||
var camPos:FlxPoint = character.getCameraPosition();
|
||||
camPos -= camera.scroll;
|
||||
|
||||
if (DrawUtil.line == null) DrawUtil.createDrawers();
|
||||
DrawUtil.line.camera = camera; DrawUtil.line.alpha = 1;
|
||||
|
||||
DrawUtil.drawLine(
|
||||
CoolUtil.pointToScreenPosition(FlxPoint.weak(camPos.x - 8, camPos.y), FlxG.camera, FlxPoint.weak()),
|
||||
CoolUtil.pointToScreenPosition(FlxPoint.weak(camPos.x + 8, camPos.y), FlxG.camera, FlxPoint.weak()),
|
||||
1, 0xFF00A0B9);
|
||||
|
||||
DrawUtil.drawLine(
|
||||
CoolUtil.pointToScreenPosition(FlxPoint.weak(camPos.x, camPos.y - 8), FlxG.camera, FlxPoint.weak()),
|
||||
CoolUtil.pointToScreenPosition(FlxPoint.weak(camPos.x, camPos.y + 8), FlxG.camera, FlxPoint.weak()),
|
||||
1, 0xFF00A0B9);
|
||||
|
||||
camPos.put();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,40 @@
|
||||
package funkin.editors.character;
|
||||
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.editors.stage.StageEditor;
|
||||
import funkin.editors.extra.PropertyButton;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import openfl.geom.Point;
|
||||
import openfl.geom.Rectangle;
|
||||
import openfl.display.BitmapData;
|
||||
import funkin.game.Character;
|
||||
import haxe.xml.Access;
|
||||
|
||||
using funkin.backend.utils.BitmapUtil;
|
||||
|
||||
typedef CharacterExtraInfo = {
|
||||
var icon:String;
|
||||
var iconColor:Null<FlxColor>;
|
||||
var holdTime:Float;
|
||||
var customProperties:Map<String, Dynamic>;
|
||||
}
|
||||
|
||||
class CharacterInfoScreen extends UISubstateWindow {
|
||||
public var character:Character;
|
||||
|
||||
public var spriteTextBox:UITextBox;
|
||||
public var iconTextBox:UITextBox;
|
||||
public var iconSprite:FlxSprite;
|
||||
public var gameOverCharTextBox:UITextBox;
|
||||
public var antialiasingCheckbox:UICheckbox;
|
||||
public var flipXCheckbox:UICheckbox;
|
||||
public var iconColorPicker:UIIconColorPicker;
|
||||
public var iconColorWheel:UIColorwheel;
|
||||
public var positionXStepper:UINumericStepper;
|
||||
public var positionYStepper:UINumericStepper;
|
||||
public var cameraXStepper:UINumericStepper;
|
||||
public var cameraYStepper:UINumericStepper;
|
||||
public var scaleStepper:UINumericStepper;
|
||||
public var singTimeStepper:UINumericStepper;
|
||||
|
||||
public var useDurationCheckbox:UICheckbox;
|
||||
public var durationStepper:UINumericStepper;
|
||||
|
||||
public var customPropertiesButtonList:UIButtonList<PropertyButton>;
|
||||
public var isPlayerCheckbox:UICheckbox;
|
||||
public var isGFCheckbox:UICheckbox;
|
||||
|
||||
public var saveButton:UIButton;
|
||||
public var closeButton:UIButton;
|
||||
|
||||
public var onSave:(xml:Xml) -> Void = null;
|
||||
public var onSave:(info:CharacterExtraInfo) -> Void = null;
|
||||
|
||||
public function new(character:Character, ?onSave:(xml:Xml) -> Void) {
|
||||
public function new(character:Character, onSave:(info:CharacterExtraInfo) -> Void) {
|
||||
this.character = character;
|
||||
this.onSave = onSave;
|
||||
super();
|
||||
@@ -41,8 +45,7 @@ class CharacterInfoScreen extends UISubstateWindow {
|
||||
|
||||
public override function create() {
|
||||
winTitle = translate("win-title");
|
||||
winWidth = 1014;
|
||||
winHeight = 600;
|
||||
winWidth = 824; winHeight = 400;
|
||||
|
||||
super.create();
|
||||
|
||||
@@ -50,161 +53,68 @@ 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, translate("spriteData"), 28));
|
||||
add(title = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Edit Character Info", 28));
|
||||
|
||||
spriteTextBox = new UITextBox(title.x, title.y + title.height + 38, character.sprite, 200);
|
||||
add(spriteTextBox);
|
||||
addLabelOn(spriteTextBox, translate("sprite"));
|
||||
iconColorPicker = new UIIconColorPicker(title.x, title.y + title.height + 38, character.getIcon(), character.antialiasing, null);
|
||||
add(iconColorPicker);
|
||||
addLabelOn(iconColorPicker, "Icon");
|
||||
|
||||
iconTextBox = new UITextBox(spriteTextBox.x + 200 + 26, spriteTextBox.y, character.getIcon(), 150);
|
||||
iconTextBox.onChange = (newIcon:String) -> {updateIcon(newIcon);}
|
||||
add(iconTextBox);
|
||||
addLabelOn(iconTextBox, translate("icon"));
|
||||
|
||||
updateIcon(character.getIcon());
|
||||
|
||||
gameOverCharTextBox = new UITextBox(iconTextBox.x + 150 + (75 + 12), iconTextBox.y, character.gameOverCharacter, 200);
|
||||
add(gameOverCharTextBox);
|
||||
addLabelOn(gameOverCharTextBox, translate("gameOverChar"));
|
||||
|
||||
antialiasingCheckbox = new UICheckbox(spriteTextBox.x, spriteTextBox.y + 10 + 32 + 28, translate("antialiasing"), character.antialiasing);
|
||||
add(antialiasingCheckbox);
|
||||
addLabelOn(antialiasingCheckbox, translate("antialiased"));
|
||||
|
||||
flipXCheckbox = new UICheckbox(antialiasingCheckbox.x + 172, spriteTextBox.y + 10 + 32 + 28, translate("flipX"), character.flipX);
|
||||
add(flipXCheckbox);
|
||||
addLabelOn(flipXCheckbox, translate("flipped"));
|
||||
|
||||
iconColorWheel = new UIColorwheel(gameOverCharTextBox.x + 200 + 20, gameOverCharTextBox.y, character.iconColor);
|
||||
iconColorWheel = new UIColorwheel(iconColorPicker.x+12+125+12+22, iconColorPicker.y, character.iconColor);
|
||||
add(iconColorWheel);
|
||||
addLabelOn(iconColorWheel, translate("iconColor"));
|
||||
|
||||
if (character.iconColor != null)
|
||||
iconColorWheel.colorChanged = true;
|
||||
iconColorPicker.colorWheel = iconColorWheel;
|
||||
|
||||
add(title = new UIText(spriteTextBox.x, spriteTextBox.y + 10 + 46 + 84, 0, translate("characterData"), 28));
|
||||
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");
|
||||
|
||||
positionXStepper = new UINumericStepper(title.x, title.y + title.height + 36, character.globalOffset.x, 0.001, 2, null, null, 84);
|
||||
add(positionXStepper);
|
||||
addLabelOn(positionXStepper, translate("position"));
|
||||
useDurationCheckbox = new UICheckbox(durationStepper.x + durationStepper.bWidth + 20, durationStepper.y+6, "Use Sing Duration?", character.holdTime != -1);
|
||||
useDurationCheckbox.onChecked = (checked:Bool) -> {durationStepper.selectable = checked;};
|
||||
add(useDurationCheckbox);
|
||||
|
||||
add(new UIText(positionXStepper.x + 84 - 32 + 0, positionXStepper.y + 9, 0, ",", 22));
|
||||
durationStepper.selectable = useDurationCheckbox.checked;
|
||||
|
||||
positionYStepper = new UINumericStepper(positionXStepper.x + 84 - 32 + 26, positionXStepper.y, character.globalOffset.y, 0.001, 2, null, null, 84);
|
||||
add(positionYStepper);
|
||||
|
||||
cameraXStepper = new UINumericStepper(positionYStepper.x + 36 + 84 - 32, positionYStepper.y, character.cameraOffset.x, 0.001, 2, null, null, 84);
|
||||
add(cameraXStepper);
|
||||
addLabelOn(cameraXStepper, translate("cameraPosition"));
|
||||
|
||||
add(new UIText(cameraXStepper.x + 84 - 32 + 0, cameraXStepper.y + 9, 0, ",", 22));
|
||||
|
||||
cameraYStepper = new UINumericStepper(cameraXStepper.x + 84 - 32 + 26, cameraXStepper.y, character.cameraOffset.y, 0.001, 2, null, null, 84);
|
||||
add(cameraYStepper);
|
||||
|
||||
scaleStepper = new UINumericStepper(cameraYStepper.x + 84 - 32 + 90, cameraYStepper.y, character.scale.x, 0.001, 2, null, null, 74);
|
||||
add(scaleStepper);
|
||||
addLabelOn(scaleStepper, translate("scale"));
|
||||
|
||||
singTimeStepper = new UINumericStepper(scaleStepper.x + 74 - 32 + 36, scaleStepper.y, character.holdTime, 0.001, 2, null, null, 74);
|
||||
add(singTimeStepper);
|
||||
addLabelOn(singTimeStepper, translate("singDuration"));
|
||||
|
||||
customPropertiesButtonList = new UIButtonList<PropertyButton>(singTimeStepper.x + singTimeStepper.width + 200, singTimeStepper.y, 290, 200, '', FlxPoint.get(280, 35), null, 5);
|
||||
customPropertiesButtonList = new UIButtonList<PropertyButton>(iconColorWheel.x+iconColorWheel.bWidth+22, iconColorWheel.y, 290, 200, '', FlxPoint.get(280, 35), null, 5);
|
||||
customPropertiesButtonList.frames = Paths.getFrames('editors/ui/inputbox');
|
||||
customPropertiesButtonList.cameraSpacing = 0;
|
||||
customPropertiesButtonList.addButton.callback = function() {
|
||||
customPropertiesButtonList.add(new PropertyButton("newProperty", "valueHere", customPropertiesButtonList));
|
||||
}
|
||||
|
||||
for (prop=>val in character.extra)
|
||||
customPropertiesButtonList.add(new PropertyButton(prop, val, customPropertiesButtonList));
|
||||
if (prop != StageEditor.exID("bounds"))
|
||||
customPropertiesButtonList.add(new PropertyButton(prop, val, customPropertiesButtonList));
|
||||
|
||||
add(customPropertiesButtonList);
|
||||
addLabelOn(customPropertiesButtonList, translate("customValues"));
|
||||
|
||||
isPlayerCheckbox = new UICheckbox(positionXStepper.x, positionXStepper.y + 10 + 32 + 28, translate("isPlayer"), character.playerOffsets);
|
||||
add(isPlayerCheckbox);
|
||||
|
||||
isGFCheckbox = new UICheckbox(isPlayerCheckbox.x + 128, positionXStepper.y + 10 + 32 + 28, translate("isGF"), false);
|
||||
add(isGFCheckbox);
|
||||
|
||||
for (checkbox in [isPlayerCheckbox, isGFCheckbox, antialiasingCheckbox, flipXCheckbox])
|
||||
{checkbox.y += 4; checkbox.x += 6;}
|
||||
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20, windowSpr.y + windowSpr.bHeight- 20, TU.translate("editor.saveClose"), function() {
|
||||
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() {
|
||||
saveCharacterInfo();
|
||||
close();
|
||||
}, 125);
|
||||
saveButton.x -= saveButton.bWidth;
|
||||
saveButton.y -= saveButton.bHeight;
|
||||
add(saveButton);
|
||||
|
||||
closeButton = new UIButton(saveButton.x - 20, saveButton.y, TU.translate("editor.close"), function() {
|
||||
if (onSave != null) onSave(null);
|
||||
closeButton = new UIButton(saveButton.x - 20 - saveButton.bWidth, saveButton.y, TU.translate("editor.close"), function() {
|
||||
close();
|
||||
}, 125);
|
||||
closeButton.x -= closeButton.bWidth;
|
||||
closeButton.color = 0xFFFF0000;
|
||||
add(closeButton);
|
||||
add(saveButton);
|
||||
closeButton.color = 0xFFFF0000;
|
||||
}
|
||||
|
||||
function updateIcon(icon:String) {
|
||||
if (iconSprite == null) add(iconSprite = new FlxSprite());
|
||||
public function saveCharacterInfo() {
|
||||
UIUtil.confirmUISelections(this);
|
||||
|
||||
if (iconSprite.animation.exists(icon)) return;
|
||||
@:privateAccess iconSprite.animation.clearAnimations();
|
||||
|
||||
var path:String = Paths.image('icons/$icon');
|
||||
if (!Assets.exists(path)) path = Paths.image('icons/' + Flags.DEFAULT_HEALTH_ICON);
|
||||
|
||||
iconSprite.loadGraphic(path, true, 150, 150);
|
||||
iconSprite.animation.add(icon, [0], 0, false);
|
||||
iconSprite.antialiasing = true;
|
||||
iconSprite.animation.play(icon);
|
||||
|
||||
iconSprite.scale.set(0.5, 0.5);
|
||||
iconSprite.updateHitbox();
|
||||
iconSprite.setPosition(iconTextBox.x + 150 + 8, (iconTextBox.y + 16) - (iconSprite.height/2));
|
||||
}
|
||||
|
||||
function saveCharacterInfo() {
|
||||
for (stepper in [positionXStepper, positionYStepper, cameraXStepper, cameraYStepper, singTimeStepper, scaleStepper])
|
||||
@:privateAccess stepper.__onChange(stepper.label.text);
|
||||
|
||||
var xml = Xml.createElement("character");
|
||||
xml.set("isPlayer", isPlayerCheckbox.checked ? "true" : "false");
|
||||
xml.set("x", Std.string(positionXStepper.value));
|
||||
xml.set("y", Std.string(positionYStepper.value));
|
||||
xml.set("gameOverChar", gameOverCharTextBox.label.text);
|
||||
xml.set("camx", Std.string(cameraXStepper.value));
|
||||
xml.set("camy", Std.string(cameraYStepper.value));
|
||||
xml.set("holdTime", Std.string(singTimeStepper.value));
|
||||
xml.set("flipX", Std.string(flipXCheckbox.checked));
|
||||
xml.set("icon", iconTextBox.label.text);
|
||||
xml.set("scale", Std.string(scaleStepper.value));
|
||||
xml.set("antialiasing", antialiasingCheckbox.checked ? "true" : "false");
|
||||
xml.set("sprite", spriteTextBox.label.text);
|
||||
if (iconColorWheel.colorChanged)
|
||||
xml.set("color", iconColorWheel.curColor.toWebString());
|
||||
for (val in customPropertiesButtonList.buttons.members)
|
||||
xml.set(val.propertyText.label.text, val.valueText.label.text);
|
||||
|
||||
for (anim in character.animDatas)
|
||||
{
|
||||
var animXml:Xml = Xml.createElement('anim');
|
||||
animXml.set("name", anim.name);
|
||||
animXml.set("anim", anim.anim);
|
||||
animXml.set("loop", Std.string(anim.loop));
|
||||
animXml.set("fps", Std.string(anim.fps));
|
||||
var offset:FlxPoint = character.getAnimOffset(anim.name);
|
||||
animXml.set("x", Std.string(offset.x));
|
||||
animXml.set("y", Std.string(offset.y));
|
||||
offset.put();
|
||||
|
||||
if (anim.indices.length > 0)
|
||||
animXml.set("indices", CoolUtil.formatNumberRange(anim.indices));
|
||||
xml.addChild(animXml);
|
||||
}
|
||||
|
||||
if (onSave != null) onSave(xml);
|
||||
if (onSave != null) onSave({
|
||||
icon: iconColorPicker.iconTextBox.label.text,
|
||||
iconColor: iconColorWheel.curColor,
|
||||
holdTime: useDurationCheckbox.checked ? durationStepper.value : -1,
|
||||
customProperties: [
|
||||
for (val in customPropertiesButtonList.buttons.members)
|
||||
val.propertyText.label.text => val.valueText.label.text
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user