Merge branch 'rewrite/master' into feature/pico-flicker

This commit is contained in:
Cameron Taylor 2024-03-13 21:51:58 -07:00
commit e0eb00d01a
31 changed files with 571 additions and 209 deletions

View file

@ -77,7 +77,7 @@ jobs:
key: ${{ runner.os }}-build-win-${{ github.ref_name }} key: ${{ runner.os }}-build-win-${{ github.ref_name }}
- name: Build game - name: Build game
run: | run: |
haxelib run lime build windows -v -release -DNO_REDIRECT_ASSETS_FOLDER -DGITHUB_BUILD haxelib run lime build windows -v -release -DGITHUB_BUILD
env: env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache" HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"
- name: Upload build artifacts - name: Upload build artifacts

View file

@ -194,7 +194,7 @@
<!-- Uncomment this to wipe your input settings. --> <!-- Uncomment this to wipe your input settings. -->
<!-- <haxedef name="CLEAR_INPUT_SAVE"/> --> <!-- <haxedef name="CLEAR_INPUT_SAVE"/> -->
<section if="debug" unless="NO_REDIRECT_ASSETS_FOLDER || html5"> <section if="debug" unless="NO_REDIRECT_ASSETS_FOLDER || html5 || GITHUB_BUILD">
<!-- <!--
Use the parent assets folder rather than the exported one Use the parent assets folder rather than the exported one
No more will we accidentally undo our changes! No more will we accidentally undo our changes!

2
assets

@ -1 +1 @@
Subproject commit 46bad6850be34afa3742640f57da07d89d5573b8 Subproject commit ae92cf9bf353f0fa958ff68f2c78fdcb377fe9ce

View file

@ -30,7 +30,7 @@ import funkin.modding.module.ModuleHandler;
import funkin.ui.title.TitleState; import funkin.ui.title.TitleState;
import funkin.util.CLIUtil; import funkin.util.CLIUtil;
import funkin.util.CLIUtil.CLIParams; import funkin.util.CLIUtil.CLIParams;
import funkin.util.tools.TimerTools; import funkin.util.TimerUtil;
import funkin.ui.transition.LoadingState; import funkin.ui.transition.LoadingState;
import funkin.util.TrackerUtil; import funkin.util.TrackerUtil;
#if discord_rpc #if discord_rpc
@ -90,69 +90,7 @@ class InitState extends FlxState
// Set the game to a lower frame rate while it is in the background. // Set the game to a lower frame rate while it is in the background.
FlxG.game.focusLostFramerate = 30; FlxG.game.focusLostFramerate = 30;
// setupFlixelDebug();
// FLIXEL DEBUG SETUP
//
#if (debug || FORCE_DEBUG_VERSION)
// Disable using ~ to open the console (we use that for the Editor menu)
FlxG.debugger.toggleKeys = [F2];
TrackerUtil.initTrackers();
// Adds an additional Close Debugger button.
// This big obnoxious white button is for MOBILE, so that you can press it
// easily with your finger when debug bullshit pops up during testing lol!
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
FlxG.debugger.visible = false;
});
// Adds a red button to the debugger.
// This pauses the game AND the music! This ensures the Conductor stops.
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function() {
if (FlxG.vcr.paused)
{
FlxG.vcr.resume();
for (snd in FlxG.sound.list)
{
snd.resume();
}
FlxG.sound.music.resume();
}
else
{
FlxG.vcr.pause();
for (snd in FlxG.sound.list)
{
snd.pause();
}
FlxG.sound.music.pause();
}
});
// Adds a blue button to the debugger.
// This skips forward in the song.
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() {
FlxG.game.debugger.vcr.onStep();
for (snd in FlxG.sound.list)
{
snd.pause();
snd.time += FlxG.elapsed * 1000;
}
FlxG.sound.music.pause();
FlxG.sound.music.time += FlxG.elapsed * 1000;
});
#end
// Make errors and warnings less annoying.
// Forcing this always since I have never been happy to have the debugger to pop up
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
// //
// FLIXEL TRANSITIONS // FLIXEL TRANSITIONS
@ -221,7 +159,7 @@ class InitState extends FlxState
// NOTE: Registries must be imported and not referenced with fully qualified names, // NOTE: Registries must be imported and not referenced with fully qualified names,
// to ensure build macros work properly. // to ensure build macros work properly.
trace('Parsing game data...'); trace('Parsing game data...');
var perfStart:Float = TimerTools.start(); var perfStart:Float = TimerUtil.start();
SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry. SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry.
SongRegistry.instance.loadEntries(); SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries();
@ -238,7 +176,7 @@ class InitState extends FlxState
ModuleHandler.loadModuleCache(); ModuleHandler.loadModuleCache();
ModuleHandler.callOnCreate(); ModuleHandler.callOnCreate();
trace('Parsing game data took: ${TimerTools.ms(perfStart)}'); trace('Parsing game data took: ${TimerUtil.ms(perfStart)}');
} }
/** /**
@ -349,6 +287,80 @@ class InitState extends FlxState
}); });
} }
function setupFlixelDebug():Void
{
//
// FLIXEL DEBUG SETUP
//
#if (debug || FORCE_DEBUG_VERSION)
// Make errors and warnings less annoying.
// Forcing this always since I have never been happy to have the debugger to pop up
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
// Disable using ~ to open the console (we use that for the Editor menu)
FlxG.debugger.toggleKeys = [F2];
TrackerUtil.initTrackers();
// Adds an additional Close Debugger button.
// This big obnoxious white button is for MOBILE, so that you can press it
// easily with your finger when debug bullshit pops up during testing lol!
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
FlxG.debugger.visible = false;
// Make errors and warnings less annoying.
// Forcing this always since I have never been happy to have the debugger to pop up
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
});
// Adds a red button to the debugger.
// This pauses the game AND the music! This ensures the Conductor stops.
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function() {
if (FlxG.vcr.paused)
{
FlxG.vcr.resume();
for (snd in FlxG.sound.list)
{
snd.resume();
}
FlxG.sound.music.resume();
}
else
{
FlxG.vcr.pause();
for (snd in FlxG.sound.list)
{
snd.pause();
}
FlxG.sound.music.pause();
}
});
// Adds a blue button to the debugger.
// This skips forward in the song.
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() {
FlxG.game.debugger.vcr.onStep();
for (snd in FlxG.sound.list)
{
snd.pause();
snd.time += FlxG.elapsed * 1000;
}
FlxG.sound.music.pause();
FlxG.sound.music.time += FlxG.elapsed * 1000;
});
#end
}
function defineSong():String function defineSong():String
{ {
return MacroUtil.getDefine('SONG'); return MacroUtil.getDefine('SONG');

View file

@ -8,6 +8,8 @@ import flixel.sound.FlxSound;
import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.system.FlxAssets.FlxSoundAsset; import flixel.system.FlxAssets.FlxSoundAsset;
import funkin.util.tools.ICloneable; import funkin.util.tools.ICloneable;
import funkin.data.song.SongData.SongMusicData;
import funkin.data.song.SongRegistry;
import funkin.audio.waveform.WaveformData; import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser; import funkin.audio.waveform.WaveformDataParser;
import flixel.math.FlxMath; import flixel.math.FlxMath;
@ -28,7 +30,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
/** /**
* Using `FunkinSound.load` will override a dead instance from here rather than creating a new one, if possible! * Using `FunkinSound.load` will override a dead instance from here rather than creating a new one, if possible!
*/ */
static var cache(default, null):FlxTypedGroup<FunkinSound> = new FlxTypedGroup<FunkinSound>(); static var pool(default, null):FlxTypedGroup<FunkinSound> = new FlxTypedGroup<FunkinSound>();
public var muted(default, set):Bool = false; public var muted(default, set):Bool = false;
@ -265,23 +267,55 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
} }
/** /**
* Creates a new `FunkinSound` object. * Creates a new `FunkinSound` object and loads it as the current music track.
* *
* @param embeddedSound The embedded sound resource you want to play. To stream, use the optional URL parameter instead. * @param key The key of the music you want to play. Music should be at `music/<key>/<key>.ogg`.
* @param volume How loud to play it (0 to 1). * @param overrideExisting Whether to override music if it is already playing.
* @param looped Whether to loop this sound. * @param mapTimeChanges Whether to check for `SongMusicData` to update the Conductor with.
* @param group The group to add this sound to. * Data should be at `music/<key>/<key>-metadata.json`.
* @param autoDestroy Whether to destroy this sound when it finishes playing. */
public static function playMusic(key:String, overrideExisting:Bool = false, mapTimeChanges:Bool = true):Void
{
if (!overrideExisting && FlxG.sound.music?.playing) return;
if (mapTimeChanges)
{
var songMusicData:Null<SongMusicData> = SongRegistry.instance.parseMusicData(key);
// Will fall back and return null if the metadata doesn't exist or can't be parsed.
if (songMusicData != null)
{
Conductor.instance.mapTimeChanges(songMusicData.timeChanges);
}
else
{
FlxG.log.warn('Tried and failed to find music metadata for $key');
}
}
FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'));
// Prevent repeat update() and onFocus() calls.
FlxG.sound.list.remove(FlxG.sound.music);
}
/**
* Creates a new `FunkinSound` object synchronously.
*
* @param embeddedSound The embedded sound resource you want to play. To stream, use the optional URL parameter instead.
* @param volume How loud to play it (0 to 1).
* @param looped Whether to loop this sound.
* @param group The group to add this sound to.
* @param autoDestroy Whether to destroy this sound when it finishes playing.
* Leave this value set to `false` if you want to re-use this `FunkinSound` instance. * Leave this value set to `false` if you want to re-use this `FunkinSound` instance.
* @param autoPlay Whether to play the sound immediately or wait for a `play()` call. * @param autoPlay Whether to play the sound immediately or wait for a `play()` call.
* @param onComplete Called when the sound finished playing. * @param onComplete Called when the sound finished playing.
* @param onLoad Called when the sound finished loading. Called immediately for succesfully loaded embedded sounds. * @param onLoad Called when the sound finished loading. Called immediately for succesfully loaded embedded sounds.
* @return A `FunkinSound` object. * @return A `FunkinSound` object.
*/ */
public static function load(embeddedSound:FlxSoundAsset, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, autoPlay:Bool = false, public static function load(embeddedSound:FlxSoundAsset, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, autoPlay:Bool = false,
?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound ?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound
{ {
var sound:FunkinSound = cache.recycle(construct); var sound:FunkinSound = pool.recycle(construct);
// Load the sound. // Load the sound.
// Sets `exists = true` as a side effect. // Sets `exists = true` as a side effect.
@ -297,9 +331,11 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
sound.persist = true; sound.persist = true;
if (autoPlay) sound.play(); if (autoPlay) sound.play();
// Call OnlLoad() because the sound already loaded // Call onLoad() because the sound already loaded
if (onLoad != null && sound._sound != null) onLoad(); if (onLoad != null && sound._sound != null) onLoad();
FlxG.sound.list.remove(FlxG.sound.music);
return sound; return sound;
} }
@ -307,7 +343,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
{ {
var sound:FunkinSound = new FunkinSound(); var sound:FunkinSound = new FunkinSound();
cache.add(sound); pool.add(sound);
FlxG.sound.list.add(sound); FlxG.sound.list.add(sound);
return sound; return sound;

View file

@ -1,6 +1,6 @@
package funkin.audio.waveform; package funkin.audio.waveform;
import funkin.util.tools.TimerTools; import funkin.util.TimerUtil;
class WaveformDataParser class WaveformDataParser
{ {
@ -73,7 +73,7 @@ class WaveformDataParser
var outputData:Array<Int> = []; var outputData:Array<Int> = [];
var perfStart:Float = TimerTools.start(); var perfStart:Float = TimerUtil.start();
for (pointIndex in 0...outputPointCount) for (pointIndex in 0...outputPointCount)
{ {
@ -110,7 +110,7 @@ class WaveformDataParser
var outputDataLength:Int = Std.int(outputData.length / channels / 2); var outputDataLength:Int = Std.int(outputData.length / channels / 2);
var result = new WaveformData(null, channels, sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData); var result = new WaveformData(null, channels, sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData);
trace('[WAVEFORM] Interpreted audio buffer in ${TimerTools.seconds(perfStart)}.'); trace('[WAVEFORM] Interpreted audio buffer in ${TimerUtil.seconds(perfStart)}.');
return result; return result;
} }

View file

@ -1,6 +1,5 @@
package funkin.data; package funkin.data;
import openfl.Assets;
import funkin.util.assets.DataAssets; import funkin.util.assets.DataAssets;
import funkin.util.VersionUtil; import funkin.util.VersionUtil;
import haxe.Constraints.Constructible; import haxe.Constraints.Constructible;
@ -19,12 +18,23 @@ typedef EntryConstructorFunction = String->Void;
@:generic @:generic
abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J> abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J>
{ {
/**
* The ID of the registry. Used when logging.
*/
public final registryId:String; public final registryId:String;
final dataFilePath:String; final dataFilePath:String;
/**
* A map of entry IDs to entries.
*/
final entries:Map<String, T>; final entries:Map<String, T>;
/**
* A map of entry IDs to scripted class names.
*/
final scriptedEntryIds:Map<String, String>;
/** /**
* The version rule to use when loading entries. * The version rule to use when loading entries.
* If the entry's version does not match this rule, migration is needed. * If the entry's version does not match this rule, migration is needed.
@ -37,17 +47,18 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param registryId A readable ID for this registry, used when logging. * @param registryId A readable ID for this registry, used when logging.
* @param dataFilePath The path (relative to `assets/data`) to search for JSON files. * @param dataFilePath The path (relative to `assets/data`) to search for JSON files.
*/ */
public function new(registryId:String, dataFilePath:String, versionRule:thx.semver.VersionRule = null) public function new(registryId:String, dataFilePath:String, ?versionRule:thx.semver.VersionRule)
{ {
this.registryId = registryId; this.registryId = registryId;
this.dataFilePath = dataFilePath; this.dataFilePath = dataFilePath;
this.versionRule = versionRule == null ? "1.0.x" : versionRule; this.versionRule = versionRule == null ? '1.0.x' : versionRule;
this.entries = new Map<String, T>(); this.entries = new Map<String, T>();
this.scriptedEntryIds = [];
} }
/** /**
* TODO: Create a `loadEntriesAsync()` function. * TODO: Create a `loadEntriesAsync(onProgress, onComplete)` function.
*/ */
public function loadEntries():Void public function loadEntries():Void
{ {
@ -66,7 +77,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
{ {
entry = createScriptedEntry(entryCls); entry = createScriptedEntry(entryCls);
} }
catch (e:Dynamic) catch (e)
{ {
log('Failed to create scripted entry (${entryCls})'); log('Failed to create scripted entry (${entryCls})');
continue; continue;
@ -76,6 +87,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
{ {
log('Successfully created scripted entry (${entryCls} = ${entry.id})'); log('Successfully created scripted entry (${entryCls} = ${entry.id})');
entries.set(entry.id, entry); entries.set(entry.id, entry);
scriptedEntryIds.set(entry.id, entryCls);
} }
else else
{ {
@ -102,7 +114,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
entries.set(entry.id, entry); entries.set(entry.id, entry);
} }
} }
catch (e:Dynamic) catch (e)
{ {
// Print the error. // Print the error.
trace(' Failed to load entry data: ${entryId}'); trace(' Failed to load entry data: ${entryId}');
@ -130,6 +142,36 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
return entries.size(); return entries.size();
} }
/**
* Return whether the entry ID is known to have an attached script.
* @param id The ID of the entry.
* @return `true` if the entry has an attached script, `false` otherwise.
*/
public function isScriptedEntry(id:String):Bool
{
return scriptedEntryIds.exists(id);
}
/**
* Return the class name of the scripted entry with the given ID, if it exists.
* @param id The ID of the entry.
* @return The class name, or `null` if it does not exist.
*/
public function getScriptedEntryClassName(id:String):String
{
return scriptedEntryIds.get(id);
}
/**
* Return whether the registry has successfully parsed an entry with the given ID.
* @param id The ID of the entry.
* @return `true` if the entry exists, `false` otherwise.
*/
public function hasEntry(id:String):Bool
{
return entries.exists(id);
}
/** /**
* Fetch an entry by its ID. * Fetch an entry by its ID.
* @param id The ID of the entry to fetch. * @param id The ID of the entry to fetch.
@ -145,6 +187,11 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
return 'Registry(' + registryId + ', ${countEntries()} entries)'; return 'Registry(' + registryId + ', ${countEntries()} entries)';
} }
/**
* Retrieve the data for an entry and parse its Semantic Version.
* @param id The ID of the entry.
* @return The entry's version, or `null` if it does not exist or is invalid.
*/
public function fetchEntryVersion(id:String):Null<thx.semver.Version> public function fetchEntryVersion(id:String):Null<thx.semver.Version>
{ {
var entryStr:String = loadEntryFile(id).contents; var entryStr:String = loadEntryFile(id).contents;
@ -185,6 +232,8 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* Read, parse, and validate the JSON data and produce the corresponding data object. * Read, parse, and validate the JSON data and produce the corresponding data object.
* *
* NOTE: Must be implemented on the implementation class. * NOTE: Must be implemented on the implementation class.
* @param id The ID of the entry.
* @return The created entry.
*/ */
public abstract function parseEntryData(id:String):Null<J>; public abstract function parseEntryData(id:String):Null<J>;
@ -194,6 +243,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* NOTE: Must be implemented on the implementation class. * NOTE: Must be implemented on the implementation class.
* @param contents The JSON as a string. * @param contents The JSON as a string.
* @param fileName An optional file name for error reporting. * @param fileName An optional file name for error reporting.
* @return The created entry.
*/ */
public abstract function parseEntryDataRaw(contents:String, ?fileName:String):Null<J>; public abstract function parseEntryDataRaw(contents:String, ?fileName:String):Null<J>;
@ -202,6 +252,9 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* accounting for old versions of the data. * accounting for old versions of the data.
* *
* NOTE: Extend this function to handle migration. * NOTE: Extend this function to handle migration.
* @param id The ID of the entry.
* @param version The entry's version (use `fetchEntryVersion(id)`).
* @return The created entry.
*/ */
public function parseEntryDataWithMigration(id:String, version:thx.semver.Version):Null<J> public function parseEntryDataWithMigration(id:String, version:thx.semver.Version):Null<J>
{ {
@ -220,12 +273,17 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
throw '[${registryId}] Entry ${id} does not support migration to version ${versionRule}.'; throw '[${registryId}] Entry ${id} does not support migration to version ${versionRule}.';
} }
// Example: /*
// if (VersionUtil.validateVersion(version, "0.1.x")) { * An example of what you should override this with:
// return parseEntryData_v0_1_x(id); *
// } else { * ```haxe
// super.parseEntryDataWithMigration(id, version); * if (VersionUtil.validateVersion(version, "0.1.x")) {
// } * return parseEntryData_v0_1_x(id);
* } else {
* super.parseEntryDataWithMigration(id, version);
* }
* ```
*/
} }
/** /**
@ -255,10 +313,15 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
trace('[${registryId}] Failed to parse entry data: ${id}'); trace('[${registryId}] Failed to parse entry data: ${id}');
for (error in errors) for (error in errors)
{
DataError.printError(error); DataError.printError(error);
}
} }
} }
/**
* A pair of a file name and its contents.
*/
typedef JsonFile = typedef JsonFile =
{ {
fileName:String, fileName:String,

View file

@ -68,6 +68,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
{ {
log('Successfully created scripted entry (${entryCls} = ${entry.id})'); log('Successfully created scripted entry (${entryCls} = ${entry.id})');
entries.set(entry.id, entry); entries.set(entry.id, entry);
scriptedEntryIds.set(entry.id, entryCls);
} }
else else
{ {
@ -441,6 +442,13 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return {fileName: entryFilePath, contents: rawJson}; return {fileName: entryFilePath, contents: rawJson};
} }
function hasMusicDataFile(id:String, ?variation:String):Bool
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json');
return openfl.Assets.exists(entryFilePath);
}
function loadEntryChartFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile> function loadEntryChartFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
{ {
variation = variation == null ? Constants.DEFAULT_VARIATION : variation; variation = variation == null ? Constants.DEFAULT_VARIATION : variation;

View file

@ -81,9 +81,10 @@ class FunkinSprite extends FlxSprite
*/ */
public function loadTexture(key:String):FunkinSprite public function loadTexture(key:String):FunkinSprite
{ {
if (!isTextureCached(key)) FlxG.log.warn('Texture not cached, may experience stuttering! $key'); var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
loadGraphic(key); loadGraphic(graphicKey);
return this; return this;
} }
@ -95,7 +96,7 @@ class FunkinSprite extends FlxSprite
*/ */
public function loadSparrow(key:String):FunkinSprite public function loadSparrow(key:String):FunkinSprite
{ {
var graphicKey = Paths.image(key); var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey'); if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
this.frames = Paths.getSparrowAtlas(key); this.frames = Paths.getSparrowAtlas(key);
@ -110,7 +111,7 @@ class FunkinSprite extends FlxSprite
*/ */
public function loadPacker(key:String):FunkinSprite public function loadPacker(key:String):FunkinSprite
{ {
var graphicKey = Paths.image(key); var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey'); if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
this.frames = Paths.getPackerAtlas(key); this.frames = Paths.getPackerAtlas(key);

View file

@ -215,7 +215,7 @@ class Countdown
if (spritePath == null) return; if (spritePath == null) return;
var countdownSprite:FunkinSprite = FunkinSprite.create(Paths.image(spritePath)); var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath);
countdownSprite.scrollFactor.set(0, 0); countdownSprite.scrollFactor.set(0, 0);
if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE)); if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));

View file

@ -28,7 +28,7 @@ class GitarooPause extends MusicBeatState
{ {
if (FlxG.sound.music != null) FlxG.sound.music.stop(); if (FlxG.sound.music != null) FlxG.sound.music.stop();
var bg:FunkinSprite = FunkinSprite.create(Paths.image('pauseAlt/pauseBG')); var bg:FunkinSprite = FunkinSprite.create('pauseAlt/pauseBG');
add(bg); add(bg);
var bf:FunkinSprite = FunkinSprite.createSparrow(0, 30, 'pauseAlt/bfLol'); var bf:FunkinSprite = FunkinSprite.createSparrow(0, 30, 'pauseAlt/bfLol');

View file

@ -1,5 +1,6 @@
package funkin.play; package funkin.play;
import funkin.audio.FunkinSound;
import flixel.addons.display.FlxPieDial; import flixel.addons.display.FlxPieDial;
import flixel.addons.display.FlxPieDial; import flixel.addons.display.FlxPieDial;
import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.FlxTransitionableState;
@ -1416,7 +1417,7 @@ class PlayState extends MusicBeatSubState
function initHealthBar():Void function initHealthBar():Void
{ {
var healthBarYPos:Float = Preferences.downscroll ? FlxG.height * 0.1 : FlxG.height * 0.9; var healthBarYPos:Float = Preferences.downscroll ? FlxG.height * 0.1 : FlxG.height * 0.9;
healthBarBG = FunkinSprite.create(0, healthBarYPos, Paths.image('healthBar')); healthBarBG = FunkinSprite.create(0, healthBarYPos, 'healthBar');
healthBarBG.screenCenter(X); healthBarBG.screenCenter(X);
healthBarBG.scrollFactor.set(0, 0); healthBarBG.scrollFactor.set(0, 0);
healthBarBG.zIndex = 800; healthBarBG.zIndex = 800;
@ -1453,7 +1454,7 @@ class PlayState extends MusicBeatSubState
function initMinimalMode():Void function initMinimalMode():Void
{ {
// Create the green background. // Create the green background.
var menuBG = FunkinSprite.create(Paths.image('menuDesat')); var menuBG = FunkinSprite.create('menuDesat');
menuBG.color = 0xFF4CAF50; menuBG.color = 0xFF4CAF50;
menuBG.setGraphicSize(Std.int(menuBG.width * 1.1)); menuBG.setGraphicSize(Std.int(menuBG.width * 1.1));
menuBG.updateHitbox(); menuBG.updateHitbox();
@ -2711,7 +2712,7 @@ class PlayState extends MusicBeatSubState
if (targetSongId == null) if (targetSongId == null)
{ {
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu')); FunkinSound.playMusic('freakyMenu');
// transIn = FlxTransitionableState.defaultTransIn; // transIn = FlxTransitionableState.defaultTransIn;
// transOut = FlxTransitionableState.defaultTransOut; // transOut = FlxTransitionableState.defaultTransOut;

View file

@ -6,7 +6,7 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxDirection; import flixel.util.FlxDirection;
import funkin.graphics.FunkinSprite; import funkin.graphics.FunkinSprite;
import funkin.play.PlayState; import funkin.play.PlayState;
import funkin.util.tools.TimerTools; import funkin.util.TimerUtil;
class PopUpStuff extends FlxTypedGroup<FlxSprite> class PopUpStuff extends FlxTypedGroup<FlxSprite>
{ {
@ -17,7 +17,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
public function displayRating(daRating:String) public function displayRating(daRating:String)
{ {
var perfStart:Float = TimerTools.start(); var perfStart:Float = TimerUtil.start();
if (daRating == null) daRating = "good"; if (daRating == null) daRating = "good";
@ -25,7 +25,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel"; if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel";
var rating:FunkinSprite = FunkinSprite.create(0, 0, Paths.image(ratingPath)); var rating:FunkinSprite = FunkinSprite.create(0, 0, ratingPath);
rating.scrollFactor.set(0.2, 0.2); rating.scrollFactor.set(0.2, 0.2);
rating.zIndex = 1000; rating.zIndex = 1000;
@ -59,12 +59,12 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
startDelay: Conductor.instance.beatLengthMs * 0.001 startDelay: Conductor.instance.beatLengthMs * 0.001
}); });
trace('displayRating took: ${TimerTools.seconds(perfStart)}'); trace('displayRating took: ${TimerUtil.seconds(perfStart)}');
} }
public function displayCombo(?combo:Int = 0):Int public function displayCombo(?combo:Int = 0):Int
{ {
var perfStart:Float = TimerTools.start(); var perfStart:Float = TimerUtil.start();
if (combo == null) combo = 0; if (combo == null) combo = 0;
@ -76,7 +76,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
pixelShitPart1 = 'weeb/pixelUI/'; pixelShitPart1 = 'weeb/pixelUI/';
pixelShitPart2 = '-pixel'; pixelShitPart2 = '-pixel';
} }
var comboSpr:FunkinSprite = FunkinSprite.create(Paths.image(pixelShitPart1 + 'combo' + pixelShitPart2)); var comboSpr:FunkinSprite = FunkinSprite.create(pixelShitPart1 + 'combo' + pixelShitPart2);
comboSpr.y = FlxG.camera.height * 0.4 + 80; comboSpr.y = FlxG.camera.height * 0.4 + 80;
comboSpr.x = FlxG.width * 0.50; comboSpr.x = FlxG.width * 0.50;
// comboSpr.x -= FlxG.camera.scroll.x * 0.2; // comboSpr.x -= FlxG.camera.scroll.x * 0.2;
@ -124,7 +124,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
var daLoop:Int = 1; var daLoop:Int = 1;
for (i in seperatedScore) for (i in seperatedScore)
{ {
var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, Paths.image(pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2)); var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2);
if (PlayState.instance.currentStageId.startsWith('school')) if (PlayState.instance.currentStageId.startsWith('school'))
{ {
@ -157,7 +157,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
daLoop++; daLoop++;
} }
trace('displayCombo took: ${TimerTools.seconds(perfStart)}'); trace('displayCombo took: ${TimerUtil.seconds(perfStart)}');
return combo; return combo;
} }

View file

@ -57,6 +57,9 @@ class FocusCameraSongEvent extends SongEvent
// Does nothing if there is no PlayState camera or stage. // Does nothing if there is no PlayState camera or stage.
if (PlayState.instance == null || PlayState.instance.currentStage == null) return; if (PlayState.instance == null || PlayState.instance.currentStage == null) return;
// Does nothing if we are minimal mode.
if (PlayState.instance.isMinimalMode) return;
var posX:Null<Float> = data.getFloat('x'); var posX:Null<Float> = data.getFloat('x');
if (posX == null) posX = 0.0; if (posX == null) posX = 0.0;
var posY:Null<Float> = data.getFloat('y'); var posY:Null<Float> = data.getFloat('y');

View file

@ -55,7 +55,10 @@ class ZoomCameraSongEvent extends SongEvent
public override function handleEvent(data:SongEventData):Void public override function handleEvent(data:SongEventData):Void
{ {
// Does nothing if there is no PlayState camera or stage. // Does nothing if there is no PlayState camera or stage.
if (PlayState.instance == null) return; if (PlayState.instance == null || PlayState.instance.currentStage == null) return;
// Does nothing if we are minimal mode.
if (PlayState.instance.isMinimalMode) return;
var zoom:Null<Float> = data.getFloat('zoom'); var zoom:Null<Float> = data.getFloat('zoom');
if (zoom == null) zoom = 1.0; if (zoom == null) zoom = 1.0;

View file

@ -1,6 +1,5 @@
package funkin.play.song; package funkin.play.song;
import flixel.sound.FlxSound;
import funkin.audio.VoicesGroup; import funkin.audio.VoicesGroup;
import funkin.audio.FunkinSound; import funkin.audio.FunkinSound;
import funkin.data.IRegistryEntry; import funkin.data.IRegistryEntry;
@ -13,9 +12,8 @@ import funkin.data.song.SongData.SongOffsets;
import funkin.data.song.SongData.SongTimeChange; import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongData.SongTimeFormat; import funkin.data.song.SongData.SongTimeFormat;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
import funkin.data.song.SongRegistry; import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import funkin.modding.IScriptedClass;
import funkin.util.SortUtil; import funkin.util.SortUtil;
import openfl.utils.Assets; import openfl.utils.Assets;
@ -31,14 +29,44 @@ import openfl.utils.Assets;
@:nullSafety @:nullSafety
class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMetadata> class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMetadata>
{ {
public static final DEFAULT_SONGNAME:String = "Unknown"; /**
public static final DEFAULT_ARTIST:String = "Unknown"; * The default value for the song's name
*/
public static final DEFAULT_SONGNAME:String = 'Unknown';
/**
* The default value for the song's artist
*/
public static final DEFAULT_ARTIST:String = 'Unknown';
/**
* The default value for the song's time format
*/
public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS; public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS;
/**
* The default value for the song's divisions
*/
public static final DEFAULT_DIVISIONS:Null<Int> = null; public static final DEFAULT_DIVISIONS:Null<Int> = null;
/**
* The default value for whether the song loops.
*/
public static final DEFAULT_LOOPED:Bool = false; public static final DEFAULT_LOOPED:Bool = false;
public static final DEFAULT_STAGE:String = "mainStage";
/**
* The default value for the song's playable stage.
*/
public static final DEFAULT_STAGE:String = 'mainStage';
/**
* The default value for the song's scroll speed.
*/
public static final DEFAULT_SCROLLSPEED:Float = 1.0; public static final DEFAULT_SCROLLSPEED:Float = 1.0;
/**
* The internal ID of the song.
*/
public final id:String; public final id:String;
/** /**
@ -53,6 +81,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
final _metadata:Map<String, SongMetadata>; final _metadata:Map<String, SongMetadata>;
final difficulties:Map<String, SongDifficulty>; final difficulties:Map<String, SongDifficulty>;
/**
* The list of variations a song has.
*/
public var variations(get, never):Array<String>; public var variations(get, never):Array<String>;
function get_variations():Array<String> function get_variations():Array<String>
@ -65,6 +96,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*/ */
public var validScore:Bool = true; public var validScore:Bool = true;
/**
* The readable name of the song.
*/
public var songName(get, never):String; public var songName(get, never):String;
function get_songName():String function get_songName():String
@ -74,6 +108,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return DEFAULT_SONGNAME; return DEFAULT_SONGNAME;
} }
/**
* The artist of the song.
*/
public var songArtist(get, never):String; public var songArtist(get, never):String;
function get_songArtist():String function get_songArtist():String
@ -101,7 +138,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
{ {
for (vari in _data.playData.songVariations) for (vari in _data.playData.songVariations)
{ {
var variMeta = fetchVariationMetadata(id, vari); var variMeta:Null<SongMetadata> = fetchVariationMetadata(id, vari);
if (variMeta != null) _metadata.set(variMeta.variation, variMeta); if (variMeta != null) _metadata.set(variMeta.variation, variMeta);
} }
} }
@ -115,27 +152,62 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
populateDifficulties(); populateDifficulties();
} }
@:allow(funkin.play.song.Song) /**
* Build a song from existing metadata rather than loading it from the `assets` folder.
* Used by the Chart Editor.
*
* @param songId The ID of the song.
* @param metadata The metadata of the song.
* @param variations The list of variations this song has.
* @param charts The chart data for each variation.
* @param includeScript Whether to initialize the scripted class tied to the song, if it exists.
* @param validScore Whether the song is elegible for highscores.
* @return The constructed song object.
*/
public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variations:Array<String>, charts:Map<String, SongChartData>, public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variations:Array<String>, charts:Map<String, SongChartData>,
validScore:Bool = false):Song includeScript:Bool = true, validScore:Bool = false):Song
{ {
var result:Song = new Song(songId); @:privateAccess
var result:Null<Song>;
if (includeScript && SongRegistry.instance.isScriptedEntry(songId))
{
var songClassName:String = SongRegistry.instance.getScriptedEntryClassName(songId);
@:privateAccess
result = SongRegistry.instance.createScriptedEntry(songClassName);
}
else
{
@:privateAccess
result = SongRegistry.instance.createEntry(songId);
}
if (result == null) throw 'ERROR: Could not build Song instance ($songId), is the attached script bad?';
result._metadata.clear(); result._metadata.clear();
for (meta in metadata) for (meta in metadata)
{
result._metadata.set(meta.variation, meta); result._metadata.set(meta.variation, meta);
}
result.difficulties.clear(); result.difficulties.clear();
result.populateDifficulties(); result.populateDifficulties();
for (variation => chartData in charts) for (variation => chartData in charts)
{
result.applyChartData(chartData, variation); result.applyChartData(chartData, variation);
}
result.validScore = validScore; result.validScore = validScore;
return result; return result;
} }
/**
* Retrieve a list of the raw metadata for the song.
* @return The metadata JSON objects for the song's variations.
*/
public function getRawMetadata():Array<SongMetadata> public function getRawMetadata():Array<SongMetadata>
{ {
return _metadata.values(); return _metadata.values();
@ -192,6 +264,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
/** /**
* Parse and cache the chart for all difficulties of this song. * Parse and cache the chart for all difficulties of this song.
* @param force Whether to forcibly clear the list of charts first.
*/ */
public function cacheCharts(force:Bool = false):Void public function cacheCharts(force:Bool = false):Void
{ {

View file

@ -212,7 +212,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
else else
{ {
// Initalize static sprite. // Initalize static sprite.
propSprite.loadTexture(Paths.image(dataProp.assetPath)); propSprite.loadTexture(dataProp.assetPath);
// Disables calls to update() for a performance boost. // Disables calls to update() for a performance boost.
propSprite.active = false; propSprite.active = false;

View file

@ -602,6 +602,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var enabledDebuggerPopup:Bool = true; var enabledDebuggerPopup:Bool = true;
/**
* Whether song scripts should be enabled during playtesting.
* You should probably check the box if the song has custom mechanics.
*/
var playtestSongScripts:Bool = true;
// Visuals // Visuals
/** /**
@ -1396,7 +1402,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_currentSongId():String function get_currentSongId():String
{ {
return currentSongName.toLowerKebabCase().replace('.', '').replace(' ', '-'); return currentSongName.toLowerKebabCase().replace(' ', '-').sanitize();
} }
var currentSongArtist(get, set):String; var currentSongArtist(get, set):String;
@ -5320,7 +5326,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var targetSong:Song; var targetSong:Song;
try try
{ {
targetSong = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, false); targetSong = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, playtestSongScripts, false);
} }
catch (e) catch (e)
{ {
@ -5328,9 +5334,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return; return;
} }
LogStyle.WARNING.openConsole = enabledDebuggerPopup;
LogStyle.ERROR.openConsole = enabledDebuggerPopup;
// TODO: Rework asset system so we can remove this. // TODO: Rework asset system so we can remove this.
switch (currentSongStage) switch (currentSongStage)
{ {
@ -5348,7 +5351,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
Paths.setCurrentLevel('week6'); Paths.setCurrentLevel('week6');
case 'tankmanBattlefield': case 'tankmanBattlefield':
Paths.setCurrentLevel('week7'); Paths.setCurrentLevel('week7');
case 'phillyStreets' | 'phillyBlazin': case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2':
Paths.setCurrentLevel('weekend1'); Paths.setCurrentLevel('weekend1');
} }

View file

@ -7,7 +7,7 @@ import funkin.audio.FunkinSound;
import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.BaseCharacter.CharacterType;
import funkin.util.FileUtil; import funkin.util.FileUtil;
import funkin.util.assets.SoundUtil; import funkin.util.assets.SoundUtil;
import funkin.util.tools.TimerTools; import funkin.util.TimerUtil;
import funkin.audio.waveform.WaveformData; import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser; import funkin.audio.waveform.WaveformDataParser;
import funkin.audio.waveform.WaveformSprite; import funkin.audio.waveform.WaveformSprite;
@ -129,41 +129,41 @@ class ChartEditorAudioHandler
public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool
{ {
var perfA:Float = TimerTools.start(); var perfA:Float = TimerUtil.start();
var result:Bool = playInstrumental(state, instId); var result:Bool = playInstrumental(state, instId);
if (!result) return false; if (!result) return false;
var perfB:Float = TimerTools.start(); var perfB:Float = TimerUtil.start();
stopExistingVocals(state); stopExistingVocals(state);
var perfC:Float = TimerTools.start(); var perfC:Float = TimerUtil.start();
result = playVocals(state, BF, playerId, instId); result = playVocals(state, BF, playerId, instId);
var perfD:Float = TimerTools.start(); var perfD:Float = TimerUtil.start();
// if (!result) return false; // if (!result) return false;
result = playVocals(state, DAD, opponentId, instId); result = playVocals(state, DAD, opponentId, instId);
// if (!result) return false; // if (!result) return false;
var perfE:Float = TimerTools.start(); var perfE:Float = TimerUtil.start();
state.hardRefreshOffsetsToolbox(); state.hardRefreshOffsetsToolbox();
var perfF:Float = TimerTools.start(); var perfF:Float = TimerUtil.start();
state.hardRefreshFreeplayToolbox(); state.hardRefreshFreeplayToolbox();
var perfG:Float = TimerTools.start(); var perfG:Float = TimerUtil.start();
trace('Switched to instrumental in ${TimerTools.seconds(perfA, perfB)}.'); trace('Switched to instrumental in ${TimerUtil.seconds(perfA, perfB)}.');
trace('Stopped existing vocals in ${TimerTools.seconds(perfB, perfC)}.'); trace('Stopped existing vocals in ${TimerUtil.seconds(perfB, perfC)}.');
trace('Played BF vocals in ${TimerTools.seconds(perfC, perfD)}.'); trace('Played BF vocals in ${TimerUtil.seconds(perfC, perfD)}.');
trace('Played DAD vocals in ${TimerTools.seconds(perfD, perfE)}.'); trace('Played DAD vocals in ${TimerUtil.seconds(perfD, perfE)}.');
trace('Hard refreshed offsets toolbox in ${TimerTools.seconds(perfE, perfF)}.'); trace('Hard refreshed offsets toolbox in ${TimerUtil.seconds(perfE, perfF)}.');
trace('Hard refreshed freeplay toolbox in ${TimerTools.seconds(perfF, perfG)}.'); trace('Hard refreshed freeplay toolbox in ${TimerUtil.seconds(perfF, perfG)}.');
return true; return true;
} }
@ -175,9 +175,9 @@ class ChartEditorAudioHandler
{ {
if (instId == '') instId = 'default'; if (instId == '') instId = 'default';
var instTrackData:Null<Bytes> = state.audioInstTrackData.get(instId); var instTrackData:Null<Bytes> = state.audioInstTrackData.get(instId);
var perfStart:Float = TimerTools.start(); var perfStart:Float = TimerUtil.start();
var instTrack:Null<FunkinSound> = SoundUtil.buildSoundFromBytes(instTrackData); var instTrack:Null<FunkinSound> = SoundUtil.buildSoundFromBytes(instTrackData);
trace('Built instrumental track in ${TimerTools.seconds(perfStart)} seconds.'); trace('Built instrumental track in ${TimerUtil.seconds(perfStart)} seconds.');
if (instTrack == null) return false; if (instTrack == null) return false;
stopExistingInstrumental(state); stopExistingInstrumental(state);
@ -205,9 +205,9 @@ class ChartEditorAudioHandler
{ {
var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}'; var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
var vocalTrackData:Null<Bytes> = state.audioVocalTrackData.get(trackId); var vocalTrackData:Null<Bytes> = state.audioVocalTrackData.get(trackId);
var perfStart:Float = TimerTools.start(); var perfStart:Float = TimerUtil.start();
var vocalTrack:Null<FunkinSound> = SoundUtil.buildSoundFromBytes(vocalTrackData); var vocalTrack:Null<FunkinSound> = SoundUtil.buildSoundFromBytes(vocalTrackData);
trace('Built vocal track in ${TimerTools.seconds(perfStart)}.'); trace('Built vocal track in ${TimerUtil.seconds(perfStart)}.');
if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup(); if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup();
@ -218,9 +218,9 @@ class ChartEditorAudioHandler
case BF: case BF:
state.audioVocalTrackGroup.addPlayerVoice(vocalTrack); state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
var perfStart:Float = TimerTools.start(); var perfStart:Float = TimerUtil.start();
var waveformData:Null<WaveformData> = vocalTrack.waveformData; var waveformData:Null<WaveformData> = vocalTrack.waveformData;
trace('Interpreted waveform data in ${TimerTools.seconds(perfStart)}.'); trace('Interpreted waveform data in ${TimerUtil.seconds(perfStart)}.');
if (waveformData != null) if (waveformData != null)
{ {
@ -244,9 +244,9 @@ class ChartEditorAudioHandler
case DAD: case DAD:
state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
var perfStart:Float = TimerTools.start(); var perfStart:Float = TimerUtil.start();
var waveformData:Null<WaveformData> = vocalTrack.waveformData; var waveformData:Null<WaveformData> = vocalTrack.waveformData;
trace('Interpreted waveform data in ${TimerTools.seconds(perfStart)}.'); trace('Interpreted waveform data in ${TimerUtil.seconds(perfStart)}.');
if (waveformData != null) if (waveformData != null)
{ {

View file

@ -318,6 +318,17 @@ class ChartEditorToolboxHandler
state.enabledDebuggerPopup = checkboxDebugger.selected; state.enabledDebuggerPopup = checkboxDebugger.selected;
}; };
var checkboxSongScripts:Null<CheckBox> = toolbox.findComponent('playtestSongScriptsCheckbox', CheckBox);
if (checkboxSongScripts == null)
throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestSongScriptsCheckbox component.';
state.playtestSongScripts = checkboxSongScripts.selected;
checkboxSongScripts.onClick = _ -> {
state.playtestSongScripts = checkboxSongScripts.selected;
};
return toolbox; return toolbox;
} }

View file

@ -7,7 +7,7 @@ import funkin.audio.waveform.WaveformDataParser;
import funkin.ui.debug.charting.commands.SetFreeplayPreviewCommand; import funkin.ui.debug.charting.commands.SetFreeplayPreviewCommand;
import funkin.ui.haxeui.components.WaveformPlayer; import funkin.ui.haxeui.components.WaveformPlayer;
import funkin.ui.freeplay.FreeplayState; import funkin.ui.freeplay.FreeplayState;
import funkin.util.tools.TimerTools; import funkin.util.TimerUtil;
import haxe.ui.backend.flixel.components.SpriteWrapper; import haxe.ui.backend.flixel.components.SpriteWrapper;
import haxe.ui.components.Button; import haxe.ui.components.Button;
import haxe.ui.components.HorizontalSlider; import haxe.ui.components.HorizontalSlider;
@ -289,12 +289,12 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox
// Build player waveform. // Build player waveform.
// waveformMusic.waveform.forceUpdate = true; // waveformMusic.waveform.forceUpdate = true;
var perfStart:Float = TimerTools.start(); var perfStart:Float = TimerUtil.start();
var waveformData1 = playerVoice?.waveformData; var waveformData1 = playerVoice?.waveformData;
var waveformData2 = opponentVoice?.waveformData ?? playerVoice?.waveformData; // this null check is for songs that only have 1 vocals file! var waveformData2 = opponentVoice?.waveformData ?? playerVoice?.waveformData; // this null check is for songs that only have 1 vocals file!
var waveformData3 = chartEditorState.audioInstTrack.waveformData; var waveformData3 = chartEditorState.audioInstTrack.waveformData;
var waveformData = waveformData3.merge(waveformData1).merge(waveformData2); var waveformData = waveformData3.merge(waveformData1).merge(waveformData2);
trace('Waveform data merging took: ${TimerTools.seconds(perfStart)}'); trace('Waveform data merging took: ${TimerUtil.seconds(perfStart)}');
waveformMusic.waveform.waveformData = waveformData; waveformMusic.waveform.waveformData = waveformData;
// Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it. // Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it.

View file

@ -1,21 +1,19 @@
package funkin.ui.freeplay; package funkin.ui.freeplay;
import flash.text.TextField; import openfl.text.TextField;
import flixel.addons.display.FlxGridOverlay; import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.ui.FlxInputText; import flixel.addons.ui.FlxInputText;
import flixel.FlxCamera; import flixel.FlxCamera;
import flixel.FlxGame; import flixel.FlxGame;
import flixel.FlxSprite; import flixel.FlxSprite;
import funkin.graphics.FunkinSprite;
import flixel.FlxState; import flixel.FlxState;
import flixel.group.FlxGroup; import flixel.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.input.touch.FlxTouch; import flixel.input.touch.FlxTouch;
import flixel.math.FlxAngle; import flixel.math.FlxAngle;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import funkin.graphics.FunkinCamera;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
import flixel.system.debug.watch.Tracker.TrackerProfile; import flixel.system.debug.watch.Tracker.TrackerProfile;
import flixel.text.FlxText; import flixel.text.FlxText;
@ -24,9 +22,12 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import flixel.util.FlxSpriteUtil; import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.audio.FunkinSound;
import funkin.data.level.LevelRegistry; import funkin.data.level.LevelRegistry;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite;
import funkin.graphics.shaders.AngleMask; import funkin.graphics.shaders.AngleMask;
import funkin.graphics.shaders.HSVShader; import funkin.graphics.shaders.HSVShader;
import funkin.graphics.shaders.PureColor; import funkin.graphics.shaders.PureColor;
@ -187,10 +188,7 @@ class FreeplayState extends MusicBeatSubState
isDebug = true; isDebug = true;
#end #end
if (FlxG.sound.music == null || (FlxG.sound.music != null && !FlxG.sound.music.playing)) FunkinSound.playMusic('freakyMenu');
{
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
}
// Add a null entry that represents the RANDOM option // Add a null entry that represents the RANDOM option
songs.push(null); songs.push(null);
@ -227,7 +225,7 @@ class FreeplayState extends MusicBeatSubState
trace(FlxG.camera.initialZoom); trace(FlxG.camera.initialZoom);
trace(FlxCamera.defaultZoom); trace(FlxCamera.defaultZoom);
var pinkBack:FunkinSprite = FunkinSprite.create(Paths.image('freeplay/pinkBack')); var pinkBack:FunkinSprite = FunkinSprite.create('freeplay/pinkBack');
pinkBack.color = 0xFFffd4e9; // sets it to pink! pinkBack.color = 0xFFffd4e9; // sets it to pink!
pinkBack.x -= pinkBack.width; pinkBack.x -= pinkBack.width;
@ -590,7 +588,7 @@ class FreeplayState extends MusicBeatSubState
}); });
} }
public function generateSongList(?filterStuff:SongFilter, force:Bool = false) public function generateSongList(?filterStuff:SongFilter, force:Bool = false):Void
{ {
curSelected = 1; curSelected = 1;
@ -693,7 +691,7 @@ class FreeplayState extends MusicBeatSubState
var busy:Bool = false; // Set to true once the user has pressed enter to select a song. var busy:Bool = false; // Set to true once the user has pressed enter to select a song.
override function update(elapsed:Float) override function update(elapsed:Float):Void
{ {
super.update(elapsed); super.update(elapsed);
@ -983,7 +981,7 @@ class FreeplayState extends MusicBeatSubState
} }
} }
function changeDiff(change:Int = 0) function changeDiff(change:Int = 0):Void
{ {
touchTimer = 0; touchTimer = 0;
@ -1173,7 +1171,7 @@ class FreeplayState extends MusicBeatSubState
difficultyStars.difficulty = daSong?.songRating ?? 0; difficultyStars.difficulty = daSong?.songRating ?? 0;
} }
function changeSelection(change:Int = 0) function changeSelection(change:Int = 0):Void
{ {
// NGio.logEvent('Fresh'); // NGio.logEvent('Fresh');
FlxG.sound.play(Paths.sound('scrollMenu'), 0.4); FlxG.sound.play(Paths.sound('scrollMenu'), 0.4);
@ -1228,7 +1226,7 @@ class FreeplayState extends MusicBeatSubState
// TODO: Stream the instrumental of the selected song? // TODO: Stream the instrumental of the selected song?
if (prevSelected == 0) if (prevSelected == 0)
{ {
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu')); FunkinSound.playMusic('freakyMenu');
FlxG.sound.music.fadeIn(2, 0, 0.8); FlxG.sound.music.fadeIn(2, 0, 0.8);
} }
} }
@ -1259,7 +1257,7 @@ class DifficultySelector extends FlxSprite
flipX = flipped; flipX = flipped;
} }
override function update(elapsed:Float) override function update(elapsed:Float):Void
{ {
if (flipX && controls.UI_RIGHT_P) moveShitDown(); if (flipX && controls.UI_RIGHT_P) moveShitDown();
if (!flipX && controls.UI_LEFT_P) moveShitDown(); if (!flipX && controls.UI_LEFT_P) moveShitDown();
@ -1267,7 +1265,7 @@ class DifficultySelector extends FlxSprite
super.update(elapsed); super.update(elapsed);
} }
function moveShitDown() function moveShitDown():Void
{ {
offset.y -= 5; offset.y -= 5;

View file

@ -12,8 +12,10 @@ import flixel.util.typeLimit.NextState;
import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.input.touch.FlxTouch; import flixel.input.touch.FlxTouch;
import flixel.text.FlxText; import flixel.text.FlxText;
import funkin.data.song.SongData.SongMusicData;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinCamera;
import funkin.audio.FunkinSound;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import funkin.ui.MusicBeatState; import funkin.ui.MusicBeatState;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
@ -51,7 +53,7 @@ class MainMenuState extends MusicBeatState
if (!(FlxG?.sound?.music?.playing ?? false)) if (!(FlxG?.sound?.music?.playing ?? false))
{ {
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu')); playMenuMusic();
} }
persistentUpdate = persistentDraw = true; persistentUpdate = persistentDraw = true;
@ -151,6 +153,11 @@ class MainMenuState extends MusicBeatState
// NG.core.calls.event.logEvent('swag').send(); // NG.core.calls.event.logEvent('swag').send();
} }
function playMenuMusic():Void
{
FunkinSound.playMusic('freakyMenu');
}
function resetCamStuff() function resetCamStuff()
{ {
FlxG.cameras.reset(new FunkinCamera()); FlxG.cameras.reset(new FunkinCamera());

View file

@ -16,6 +16,7 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.data.level.LevelRegistry; import funkin.data.level.LevelRegistry;
import funkin.audio.FunkinSound;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher; import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.PlayState; import funkin.play.PlayState;
@ -234,17 +235,7 @@ class StoryMenuState extends MusicBeatState
function playMenuMusic():Void function playMenuMusic():Void
{ {
if (FlxG.sound.music == null || !FlxG.sound.music.playing) FunkinSound.playMusic('freakyMenu');
{
var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu');
if (freakyMenuMetadata != null)
{
Conductor.instance.mapTimeChanges(freakyMenuMetadata.timeChanges);
}
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7);
}
} }
function updateData():Void function updateData():Void

View file

@ -18,6 +18,7 @@ import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState; import funkin.ui.MusicBeatState;
import funkin.data.song.SongData.SongMusicData; import funkin.data.song.SongData.SongMusicData;
import funkin.graphics.shaders.TitleOutline; import funkin.graphics.shaders.TitleOutline;
import funkin.audio.FunkinSound;
import funkin.ui.freeplay.FreeplayState; import funkin.ui.freeplay.FreeplayState;
import funkin.ui.AtlasText; import funkin.ui.AtlasText;
import openfl.Assets; import openfl.Assets;
@ -219,16 +220,11 @@ class TitleState extends MusicBeatState
function playMenuMusic():Void function playMenuMusic():Void
{ {
if (FlxG.sound.music == null || !FlxG.sound.music.playing) var shouldFadeIn = (FlxG.sound.music == null);
{ // Load music. Includes logic to handle BPM changes.
var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu'); FunkinSound.playMusic('freakyMenu', false, true);
if (freakyMenuMetadata != null) // Fade from 0.0 to 0.7 over 4 seconds
{ if (shouldFadeIn) FlxG.sound.music.fadeIn(4, 0, 0.7);
Conductor.instance.mapTimeChanges(freakyMenuMetadata.timeChanges);
}
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7);
}
} }
function getIntroTextShit():Array<Array<String>> function getIntroTextShit():Array<Array<String>>

View file

@ -48,7 +48,7 @@ class LoadingState extends MusicBeatState
var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, 0xFFcaff4d); var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, 0xFFcaff4d);
add(bg); add(bg);
funkay = FunkinSprite.create(Paths.image('funkay')); funkay = FunkinSprite.create('funkay');
funkay.setGraphicSize(0, FlxG.height); funkay.setGraphicSize(0, FlxG.height);
funkay.updateHitbox(); funkay.updateHitbox();
add(funkay); add(funkay);
@ -238,11 +238,38 @@ class LoadingState extends MusicBeatState
FunkinSprite.cacheTexture(Paths.image('shit', 'shared')); FunkinSprite.cacheTexture(Paths.image('shit', 'shared'));
FunkinSprite.cacheTexture(Paths.image('miss', 'shared')); // TODO: remove this FunkinSprite.cacheTexture(Paths.image('miss', 'shared')); // TODO: remove this
// List all image assets in the level's library.
// This is crude and I want to remove it when we have a proper asset caching system.
// TODO: Get rid of this junk!
var library = openfl.utils.Assets.getLibrary(PlayStatePlaylist.campaignId);
var assets = library.list(lime.utils.AssetType.IMAGE);
trace('Got ${assets.length} assets: ${assets}');
// TODO: assets includes non-images! This is a bug with Polymod
for (asset in assets)
{
// Exclude items of the wrong type.
var path = '${PlayStatePlaylist.campaignId}:${asset}';
// TODO DUMB HACK DUMB HACK why doesn't filtering by AssetType.IMAGE above work
// I will fix this properly later I swear -eric
if (!path.endsWith('.png')) continue;
FunkinSprite.cacheTexture(path);
// Another dumb hack: FlxAnimate fetches from OpenFL's BitmapData cache directly and skips the FlxGraphic cache.
// Since FlxGraphic tells OpenFL to not cache it, we have to do it manually.
if (path.endsWith('spritemap1.png'))
{
openfl.Assets.getBitmapData(path, true);
}
}
// FunkinSprite.cacheAllNoteStyleTextures(noteStyle) // This will replace the stuff above! // FunkinSprite.cacheAllNoteStyleTextures(noteStyle) // This will replace the stuff above!
// FunkinSprite.cacheAllCharacterTextures(player) // FunkinSprite.cacheAllCharacterTextures(player)
// FunkinSprite.cacheAllCharacterTextures(girlfriend) // FunkinSprite.cacheAllCharacterTextures(girlfriend)
// FunkinSprite.cacheAllCharacterTextures(opponent) // FunkinSprite.cacheAllCharacterTextures(opponent)
// FunkinSprite.cacheAllStageTextures(stage) // FunkinSprite.cacheAllStageTextures(stage)
// FunkinSprite.cacheAllSongTextures(stage)
FunkinSprite.purgeCache(); FunkinSprite.purgeCache();
@ -389,9 +416,15 @@ class MultiCallback
public function getUnfired():Array<Void->Void> public function getUnfired():Array<Void->Void>
return unfired.array(); return unfired.array();
/**
* Perform an FlxG.switchState with a nice transition
* @param state
* @param transitionTex
* @param time
*/
public static function coolSwitchState(state:NextState, transitionTex:String = "shaderTransitionStuff/coolDots", time:Float = 2) public static function coolSwitchState(state:NextState, transitionTex:String = "shaderTransitionStuff/coolDots", time:Float = 2)
{ {
var screenShit:FunkinSprite = FunkinSprite.create(Paths.image("shaderTransitionStuff/coolDots")); var screenShit:FunkinSprite = FunkinSprite.create('shaderTransitionStuff/coolDots');
var screenWipeShit:ScreenWipeShader = new ScreenWipeShader(); var screenWipeShit:ScreenWipeShader = new ScreenWipeShader();
screenWipeShit.funnyShit.input = screenShit.pixels; screenWipeShit.funnyShit.input = screenShit.pixels;

View file

@ -313,7 +313,7 @@ class StickerSprite extends FunkinSprite
public function new(x:Float, y:Float, stickerSet:String, stickerName:String):Void public function new(x:Float, y:Float, stickerSet:String, stickerName:String):Void
{ {
super(x, y); super(x, y);
loadTexture(Paths.image('transitionSwag/' + stickerSet + '/' + stickerName)); loadTexture('transitionSwag/' + stickerSet + '/' + stickerName);
updateHitbox(); updateHitbox();
scrollFactor.set(); scrollFactor.set();
} }

View file

@ -1,9 +1,9 @@
package funkin.util.tools; package funkin.util;
import funkin.util.tools.FloatTools; import funkin.util.tools.FloatTools;
import haxe.Timer; import haxe.Timer;
class TimerTools class TimerUtil
{ {
public static function start():Float public static function start():Float
{ {

View file

@ -0,0 +1,76 @@
package funkin.util.logging;
/**
* A small utility class for timing how long functions take.
* Specify a string as a label (or don't, by default it uses the name of the function it was called from.)
*
* Example:
* ```haxe
*
* var perf = new Perf();
* ...
* perf.print();
* ```
*/
class Perf
{
final startTime:Float;
final label:Null<String>;
final posInfos:Null<haxe.PosInfos>;
/**
* Create a new performance marker.
* @param label Optionally specify a label to use for the performance marker. Defaults to the function name.
* @param posInfos The position of the calling function. Used to build the default label.
* Note: `haxe.PosInfos` is magic and automatically populated by the compiler!
*/
public function new(?label:String, ?posInfos:haxe.PosInfos)
{
this.label = label;
this.posInfos = posInfos;
startTime = current();
}
/**
* The current timestamp, in fractional seconds.
* @return The current timestamp.
*/
static function current():Float
{
#if sys
// This one is more accurate if it's available.
return Sys.time();
#else
return haxe.Timer.stamp();
#end
}
/**
* The duration in seconds since this `Perf` was created.
* @return The duration in seconds
*/
public function duration():Float
{
return current() - startTime;
}
/**
* A rounded millisecond duration
* @return The duration in milliseconds
*/
public function durationClean():Float
{
var round:Float = 100;
return Math.floor(duration() * Constants.MS_PER_SEC * round) / round;
}
/**
* Cleanly prints the duration since this `Perf` was created.
*/
public function print():Void
{
var label:String = label ?? (posInfos == null ? 'unknown' : '${posInfos.className}#${posInfos.methodName}()');
trace('[PERF] [$label] Took ${durationClean()}ms.');
}
}

View file

@ -1,7 +1,7 @@
package funkin.util.plugins; package funkin.util.plugins;
import flixel.FlxBasic; import flixel.FlxBasic;
import funkin.util.tools.TimerTools; import funkin.util.TimerUtil;
/** /**
* A plugin which adds functionality to press `Ins` to immediately perform memory garbage collection. * A plugin which adds functionality to press `Ins` to immediately perform memory garbage collection.
@ -24,9 +24,9 @@ class MemoryGCPlugin extends FlxBasic
if (FlxG.keys.justPressed.INSERT) if (FlxG.keys.justPressed.INSERT)
{ {
var perfStart:Float = TimerTools.start(); var perfStart:Float = TimerUtil.start();
funkin.util.MemoryUtil.collect(true); funkin.util.MemoryUtil.collect(true);
trace('Memory GC took: ${TimerTools.seconds(perfStart)}'); trace('Memory GC took: ${TimerUtil.seconds(perfStart)}');
} }
} }

View file

@ -13,20 +13,50 @@ class StringTools
*/ */
public static function toTitleCase(value:String):String public static function toTitleCase(value:String):String
{ {
var words:Array<String> = value.split(" "); var words:Array<String> = value.split(' ');
var result:String = ""; var result:String = '';
for (i in 0...words.length) for (i in 0...words.length)
{ {
var word:String = words[i]; var word:String = words[i];
result += word.charAt(0).toUpperCase() + word.substr(1).toLowerCase(); result += word.charAt(0).toUpperCase() + word.substr(1).toLowerCase();
if (i < words.length - 1) if (i < words.length - 1)
{ {
result += " "; result += ' ';
} }
} }
return result; return result;
} }
/**
* Strip a given prefix from a string.
* @param value The string to strip.
* @param prefix The prefix to strip. If the prefix isn't found, the original string is returned.
* @return The stripped string.
*/
public static function stripPrefix(value:String, prefix:String):String
{
if (value.startsWith(prefix))
{
return value.substr(prefix.length);
}
return value;
}
/**
* Strip a given suffix from a string.
* @param value The string to strip.
* @param suffix The suffix to strip. If the suffix isn't found, the original string is returned.
* @return The stripped string.
*/
public static function stripSuffix(value:String, suffix:String):String
{
if (value.endsWith(suffix))
{
return value.substr(0, value.length - suffix.length);
}
return value;
}
/** /**
* Converts a string to lower kebab case. For example, "Hello World" becomes "hello-world". * Converts a string to lower kebab case. For example, "Hello World" becomes "hello-world".
* *
@ -35,7 +65,7 @@ class StringTools
*/ */
public static function toLowerKebabCase(value:String):String public static function toLowerKebabCase(value:String):String
{ {
return value.toLowerCase().replace(' ', "-"); return value.toLowerCase().replace(' ', '-');
} }
/** /**
@ -46,13 +76,30 @@ class StringTools
*/ */
public static function toUpperKebabCase(value:String):String public static function toUpperKebabCase(value:String):String
{ {
return value.toUpperCase().replace(' ', "-"); return value.toUpperCase().replace(' ', '-');
}
/**
* The regular expression to sanitize strings.
*/
static final SANTIZE_REGEX:EReg = ~/[^-a-zA-Z0-9]/g;
/**
* Remove all instances of symbols other than alpha-numeric characters (and dashes)from a string.
* @param value The string to sanitize.
* @return The sanitized string.
*/
public static function sanitize(value:String):String
{
return SANTIZE_REGEX.replace(value, '');
} }
/** /**
* Parses the string data as JSON and returns the resulting object. * Parses the string data as JSON and returns the resulting object.
* This is here so you can use `string.parseJSON()` when `using StringTools`. * This is here so you can use `string.parseJSON()` when `using StringTools`.
* *
* TODO: Remove this and replace with `json2object`
* @param value The
* @return The parsed object. * @return The parsed object.
*/ */
public static function parseJSON(value:String):Dynamic public static function parseJSON(value:String):Dynamic