Merge remote-tracking branch 'upstream/main' into new-editors-main

# Conflicts:
# commandline/commands/Compiler.hx
# source/funkin/backend/FunkinSprite.hx
# source/funkin/backend/MusicBeatState.hx
# source/funkin/backend/MusicBeatSubstate.hx
# source/funkin/backend/chart/Chart.hx
# source/funkin/backend/chart/EventsData.hx
# source/funkin/backend/scripting/GlobalScript.hx
# source/funkin/backend/system/Controls.hx
# source/funkin/backend/system/MainState.hx
# source/funkin/backend/utils/AudioAnalyzer.hx
# source/funkin/backend/utils/CoolUtil.hx
# source/funkin/editors/character/CharacterInfoScreen.hx
# source/funkin/editors/charter/Charter.hx
# source/funkin/editors/charter/CharterSelection.hx
# source/funkin/editors/charter/CharterStrumline.hx
# source/funkin/editors/charter/CharterStrumlineButton.hx
# source/funkin/editors/charter/SongCreationScreen.hx
# source/funkin/editors/ui/UIDropDown.hx
# source/funkin/editors/ui/UIFileExplorer.hx
# source/funkin/game/Note.hx
# source/funkin/game/PlayState.hx
# source/funkin/game/SplashGroup.hx
# source/funkin/game/Stage.hx
# source/funkin/game/Strum.hx
# source/funkin/menus/FreeplayState.hx
# source/funkin/options/Options.hx
# source/funkin/options/keybinds/KeybindsOptions.hx
This commit is contained in:
Ralty
2025-07-19 00:57:08 +07:00
372 changed files with 3244 additions and 1248 deletions
+4
View File
@@ -0,0 +1,4 @@
package funkin;
@:dox(hide)
typedef SingleOrFloat = #if (java || hl || cpp) Single #else Float #end;
+17 -8
View File
@@ -85,25 +85,34 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
moves = false;
}
public static function copyFrom(source:FunkinSprite)
/**
* Gets the graphics and copies other properties from another sprite (Works both for `FlxSprite` and `FunkinSprite`!).
*/
public static function copyFrom(source:FlxSprite):FunkinSprite
{
var spr = new FunkinSprite();
var casted:FunkinSprite = null;
if (source is FunkinSprite)
casted = cast source;
@:privateAccess {
spr.setPosition(source.x, source.y);
spr.frames = source.frames;
if (source.animateAtlas != null && source.atlasPath != null)
spr.loadSprite(source.atlasPath);
if (casted != null && casted.animateAtlas != null && casted.atlasPath != null)
spr.loadSprite(casted.atlasPath);
spr.animation.copyFrom(source.animation);
spr.visible = source.visible;
spr.alpha = source.alpha;
spr.antialiasing = source.antialiasing;
spr.scale.set(source.scale.x, source.scale.y);
spr.scrollFactor.set(source.scrollFactor.x, source.scrollFactor.y);
spr.skew.set(source.skew.x, source.skew.y);
spr.transformMatrix = source.transformMatrix;
spr.matrixExposed = source.matrixExposed;
spr.zoomFactor = source.zoomFactor;
spr.animOffsets = source.animOffsets.copy();
if (casted != null) {
spr.skew.set(casted.skew.x, casted.skew.y);
spr.transformMatrix = casted.transformMatrix;
spr.matrixExposed = casted.matrixExposed;
spr.animOffsets = casted.animOffsets.copy();
}
}
return spr;
}
+11 -11
View File
@@ -105,6 +105,8 @@ class MusicBeatState extends FlxState implements IBeatReceiver
public static var skipTransOut:Bool = false;
public static var skipTransIn:Bool = false;
public static var ALLOW_DEV_RELOAD:Bool = true;
inline function get_controls():Controls
return PlayerSettings.solo.controls;
inline function get_controlsP1():Controls
@@ -153,16 +155,20 @@ class MusicBeatState extends FlxState implements IBeatReceiver
call("postUpdate", [elapsed]);
}
if (_requestSubStateReset)
{
if (_requestSubStateReset) {
_requestSubStateReset = false;
resetSubState();
}
if (subState != null)
{
subState.tryUpdate(elapsed);
if (/*subState == null && */(ALLOW_DEV_RELOAD && controls.DEV_RELOAD)) {
Logs.trace("Reloading Current State...", INFO, YELLOW);
FlxG.resetState();
}
if (subState != null)
subState.tryUpdate(elapsed);
}
override function create()
{
loadScript();
@@ -223,12 +229,6 @@ class MusicBeatState extends FlxState implements IBeatReceiver
override function update(elapsed:Float)
{
// TODO: DEBUG MODE!!
#if EXPERMENTAL_SCRIPT_RELOADING
if (FlxG.keys.justPressed.F5) {
loadScript();
}
#end
call("update", [elapsed]);
super.update(elapsed);
+8 -10
View File
@@ -136,15 +136,19 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver
call("postUpdate", [elapsed]);
}
if (_requestSubStateReset)
{
// if (subState == null && (MusicBeatState.ALLOW_DEV_RELOAD && controls.DEV_RELOAD)) {
// Logs.trace("Reloading Current SubState...", INFO, YELLOW);
// var test = Type.createInstance(Type.getClass(this), [this.scriptsAllowed, this.scriptName]);
// parent.openSubState(test);
// }
if (_requestSubStateReset) {
_requestSubStateReset = false;
resetSubState();
}
if (subState != null)
{
subState.tryUpdate(elapsed);
}
}
override function close() {
@@ -181,12 +185,6 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver
override function update(elapsed:Float)
{
#if EXPERMENTAL_SCRIPT_RELOADING
// TODO: DEBUG MODE!!
if (FlxG.keys.justPressed.F5) {
loadScript();
}
#end
call("update", [elapsed]);
super.update(elapsed);
}
@@ -63,7 +63,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
}
@@ -86,6 +86,11 @@ class ModsFolder {
public static function getModsList():Array<String> {
var mods:Array<String> = [];
#if MOD_SUPPORT
if (!FileSystem.exists(modsPath)) {
// Mods directory does not exist yet, create it
FileSystem.createDirectory(modsPath);
}
final modsList:Array<String> = FileSystem.readDirectory(modsPath);
if (modsList == null || modsList.length <= 0)
@@ -25,10 +25,7 @@ class ModsFolderLibrary extends AssetLibrary implements IModsAssetLibrary {
this.folderPath = folderPath;
this.libName = libName;
this.prefix = 'assets/$libName/';
if(modName == null)
this.modName = libName;
else
this.modName = modName;
this.modName = modName == null ? libName : modName;
super();
}
+30 -10
View File
@@ -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);
}
@@ -111,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);
}
@@ -193,6 +190,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.
@@ -247,13 +267,13 @@ class Paths
}
return content;
}
static public function getFolderContent(key:String, addPath:Bool = false, source:AssetsLibraryList.AssetSource = BOTH):Array<String> {
static public function getFolderContent(key:String, addPath:Bool = false, source:AssetsLibraryList.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
}
+38 -43
View File
@@ -72,7 +72,7 @@ class Chart {
}
public static function loadEventsJson(songName:String) {
var path = Paths.file('songs/${songName.toLowerCase()}/events.json');
var path = Paths.file('songs/${songName}/events.json');
var data:Array<ChartEvent> = null;
if (Assets.exists(path)) {
try {
@@ -83,31 +83,29 @@ class Chart {
return data;
}
public static function loadChartMeta(songName:String, ?difficulty:String, fromMods:Bool = true) {
public static function loadChartMeta(songName:String, ?difficulty:String, fromMods:Bool = true):ChartMetaData {
if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY;
var songNameLower = songName.toLowerCase();
var metaPath = Paths.file('songs/${songNameLower}/meta.json');
var metaDiffPath = Paths.file('songs/${songNameLower}/meta-${difficulty.toLowerCase()}.json');
var metaPath = Paths.file('songs/${songName}/meta.json');
var metaDiffPath = Paths.file('songs/${songName}/meta-${difficulty}.json');
var data:ChartMetaData = null;
var fromMods:Bool = fromMods;
for(path in [metaDiffPath, metaPath]) {
if (Assets.exists(path)) {
fromMods = Paths.assetsTree.existsSpecific(path, "TEXT", MODS);
try {
data = Json.parse(Assets.getText(path));
} catch(e) {
Logs.trace('Failed to load song metadata for ${songName} ($path): ${Std.string(e)}', ERROR);
}
if (data != null) break;
}
for (path in [metaDiffPath, metaPath]) if (Assets.exists(path)) {
fromMods = Paths.assetsTree.existsSpecific(path, "TEXT", MODS);
try {
var tempData = Json.parse(Assets.getText(path));
tempData.color = CoolUtil.getColorFromDynamic(tempData.color).getDefault(Flags.DEFAULT_COLOR);
data = tempData;
} catch(e) Logs.trace('Failed to load song metadata for ${songName} ($path): ${Std.string(e)}', ERROR);
if (data != null) break;
}
if (data == null)
data = {
name: songName
};
if (data == null) data = {
name: songName,
bpm: 100
};
data.setFieldDefault("name", songName);
data.setFieldDefault("bpm", Flags.DEFAULT_BPM);
data.setFieldDefault("beatsPerMeasure", Flags.DEFAULT_BEATS_PER_MEASURE);
@@ -118,24 +116,19 @@ class Chart {
data.setFieldDefault("coopAllowed", Flags.DEFAULT_COOP_ALLOWED);
data.setFieldDefault("opponentModeAllowed", Flags.DEFAULT_OPPONENT_MODE_ALLOWED);
data.setFieldDefault("displayName", data.name);
data.setFieldDefault("parsedColor", data.color.getColorFromDynamic().getDefault(Flags.DEFAULT_COLOR));
if (data.difficulties.length <= 0) {
data.difficulties = [for(f in Paths.getFolderContent('songs/${songNameLower}/charts/', false, fromMods ? MODS : SOURCE)) if (Path.extension(f = f.toUpperCase()) == "JSON") Path.withoutExtension(f)];
data.difficulties = [for(f in Paths.getFolderContent('songs/${songName}/charts/', false, fromMods ? MODS : SOURCE)) if (Path.extension(f.toUpperCase()) == "JSON") Path.withoutExtension(f)];
var tempDiffs = [];
if (data.difficulties.length == 3) {
var hasHard = false, hasNormal = false, hasEasy = false;
for(d in data.difficulties) {
switch(d) {
case "EASY": hasEasy = true;
case "NORMAL": hasNormal = true;
case "HARD": hasHard = true;
switch(d.toLowerCase()) {
case "easy": tempDiffs.insert(0, d);
case "normal": tempDiffs.insert(1, d);
case "hard": tempDiffs.insert(2, d);
}
}
if (hasHard && hasNormal && hasEasy) {
data.difficulties[0] = "EASY";
data.difficulties[1] = "NORMAL";
data.difficulties[2] = "HARD";
}
data.difficulties = tempDiffs;
}
}
@@ -165,11 +158,9 @@ class Chart {
valid = false;
}
var data:Dynamic = null;
try {
if (valid)
data = Json.parse(Assets.getText(chartPath));
} catch(e) {
Logs.trace('Could not parse chart for song ${songName} ($difficulty): ${Std.string(e)}', ERROR, RED);
if (valid) {
try data = Json.parse(Assets.getText(chartPath))
catch(e) Logs.trace('Could not parse chart for song ${songName} ($difficulty): ${Std.string(e)}', ERROR, RED);
}
/**
@@ -178,7 +169,7 @@ class Chart {
#if REGION
if (data != null) switch (detectChartFormat(data)) {
case CODENAME:
// backward compat on events since its caused problems
// backward compat on events since it caused problems
var eventTypesToString:Map<Int, String> = [
-1 => "HScript Call",
0 => "Unknown",
@@ -196,7 +187,7 @@ class Chart {
base = data;
case PSYCH_NEW: PsychParser.parse(data, base);
case VSLICE: // TODO
case VSLICE: Logs.trace("Couldn't parse V-Slice chart because it's not supported on runtime, it MUST be imported and converted in the new song editor window.", ERROR, RED);
case LEGACY: FNFLegacyParser.parse(data, base);
}
#end
@@ -238,8 +229,7 @@ class Chart {
return 0;
default:
var index = chart.noteTypes.indexOf(noteTypeName);
if (index > -1)
return index+1;
if (index > -1) return index + 1;
chart.noteTypes.push(noteTypeName);
return chart.noteTypes.length;
}
@@ -275,9 +265,8 @@ class Chart {
CoolUtil.safeSaveFile(chartPath, Json.stringify(filteredChart, null, saveSettings.prettyPrint == true ? Flags.JSON_PRETTY_PRINT : null));
// idk how null reacts to it so better be sure
if (saveSettings.overrideExistingMeta == true || !FileSystem.exists(metaPath))
CoolUtil.safeSaveFile(metaPath, Json.stringify(meta, null, saveSettings.prettyPrint == true ? Flags.JSON_PRETTY_PRINT : null));
CoolUtil.safeSaveFile(metaPath, makeMetaSaveable(meta));
#end
return filteredChart;
}
@@ -306,13 +295,19 @@ class Chart {
data.fromMods = null;
var sortedData:Dynamic = {};
for(f in Reflect.fields(data)) {
for (f in Reflect.fields(data)) {
var v = Reflect.field(data, f);
if (v != null)
Reflect.setField(sortedData, f, v);
}
return sortedData;
}
public static inline function makeMetaSaveable(meta:ChartMetaData, prettyPrint:Bool = true):String {
var data:Dynamic = Reflect.copy(meta);
if (data.color != null) data.color = new FlxColor(data.color).toWebString(); // dont even ask me - Nex
return Json.stringify(data, null, prettyPrint ? Flags.JSON_PRETTY_PRINT : null);
}
}
typedef ChartSaveSettings = {
+1 -5
View File
@@ -22,16 +22,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 = {
+70 -6
View File
@@ -9,18 +9,71 @@ import openfl.Assets;
using StringTools;
final class EventsData {
public static var defaultEventsList:Array<String> = ["HScript Call", "Camera Movement", "Add Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"];
class EventsData {
public static var defaultEventsList:Array<String> = ["HScript Call", "Camera Movement", "Camera Position", "Add Camera Zoom", "Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "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}
@@ -42,13 +95,23 @@ final class EventsData {
defValue: "linear"
},
{
name: "Tween Type (ex: InOut)",
name: "Tween Type (excluded if linear, ex: InOut)",
type: TDropDown(['In', 'Out', 'InOut']),
defValue: "In"
}
},
{name: "Multiplicative?", type: TBool, defValue: false}
],
"Alt Animation Toggle" => [{name: "Enable On Sing Poses", type: TBool, defValue: true}, {name: "Enable On Idle", type: TBool, defValue: true}, {name: "Strumline", type: TStrumLine, defValue: 0}],
"Play Animation" => [{name: "Character", type: TStrumLine, defValue: 0}, {name: "Animation", type: TString, defValue: "animation"}, {name: "Is forced?", type: TBool, defValue: true}],
"Play Animation" => [
{name: "Character", type: TStrumLine, defValue: 0},
{name: "Animation", type: TString, defValue: "animation"},
{name: "Is forced?", type: TBool, defValue: true},
{
name: "Animation Context",
type: TDropDown(["NONE", "SING", "DANCE", "MISS", "LOCK"]),
defValue: "NONE"
}
],
];
public static var eventsList:Array<String> = defaultEventsList.copy();
@@ -136,6 +199,7 @@ typedef EventParamInfo = {
var name:String;
var type:EventParamType;
var defValue:Dynamic;
var ?saveIfDefault:Bool;
}
enum EventParamType {
+362
View File
@@ -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
}
+27 -15
View File
@@ -4,6 +4,8 @@ import flixel.FlxState;
import funkin.backend.assets.ModsFolder;
import funkin.backend.scripting.events.CancellableEvent;
import funkin.backend.system.Conductor;
import funkin.options.PlayerSettings;
#if GLOBAL_SCRIPT
/**
* Class for THE Global Script, aka script that runs in the background at all times.
@@ -12,6 +14,8 @@ class GlobalScript {
public static var scripts:ScriptPack;
private static var initialized:Bool = false;
private static var reloading:Bool = false;
private static var _lastAllow_Reload:Bool = false;
public static function init() {
if(initialized) return;
@@ -46,19 +50,13 @@ class GlobalScript {
});
FlxG.signals.postUpdate.add(function() {
call("postUpdate", [FlxG.elapsed]);
#if EXPERMENTAL_SCRIPT_RELOADING
if (FlxG.keys.justPressed.F5) {
if (scripts.scripts.length > 0) {
Logs.trace('Reloading global script...', WARNING, YELLOW);
scripts.reload();
Logs.trace('Global script successfully reloaded.', WARNING, GREEN);
} else {
Logs.trace('Loading global script...', WARNING, YELLOW);
onModSwitch(#if MOD_SUPPORT ModsFolder.currentModFolder #else null #end);
}
if (reloading) {
reloading = false;
MusicBeatState.ALLOW_DEV_RELOAD = _lastAllow_Reload;
}
#end
if (FlxG.keys.justPressed.F2)
if (PlayerSettings.solo.controls.DEV_CONSOLE)
NativeAPI.allocConsole();
});
FlxG.signals.preDraw.add(function() {
@@ -76,13 +74,26 @@ class GlobalScript {
FlxG.signals.preStateSwitch.add(function() {
call("preStateSwitch", []);
});
FlxG.signals.preUpdate.add(function() {
call("preUpdate", [FlxG.elapsed]);
call("update", [FlxG.elapsed]);
});
// Commented this out, because it might be the cause why global runs twice - Neo
//onModSwitch(#if MOD_SUPPORT ModsFolder.currentModFolder #else null #end);
if (FlxG.keys.pressed.SHIFT) {
// If we want, we could just make reseting GlobalScript it's own keybind, but for now this works.
if (PlayerSettings.solo.controls.DEV_RELOAD) {
reloading = true;
Logs.trace("Reloading Global Scripts...", INFO, YELLOW);
// yeah its a bit messy, sorry. This just prevents actually reloading the actual state.
_lastAllow_Reload = MusicBeatState.ALLOW_DEV_RELOAD;
MusicBeatState.ALLOW_DEV_RELOAD = false;
// Would be better to just re-initalize GlobalScript so there aren't any lose ends.
onModSwitch(#if MOD_SUPPORT ModsFolder.currentModFolder #else null #end);
}
}
});
}
public static function event<T:CancellableEvent>(name:String, event:T):T {
@@ -95,6 +106,7 @@ class GlobalScript {
if (scripts != null)
scripts.call(name, args);
}
public static function onModSwitch(newMod:String) {
call("destroy");
scripts = FlxDestroyUtil.destroy(scripts);
@@ -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() {
@@ -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
**/
+24 -13
View File
@@ -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;
+23
View File
@@ -24,6 +24,11 @@ enum Control
PAUSE;
//CHEAT;
SWITCHMOD;
// Debugs
DEV_ACCESS;
DEV_CONSOLE;
DEV_RELOAD;
}
enum KeyboardScheme
@@ -150,6 +155,24 @@ class Controls extends FlxActionSet
@:pressed("switchmod") public var SWITCHMOD_HOLD(get, set): Bool;
@:justReleased("switchmod") public var SWITCHMOD_R(get, set): Bool;
@:devModeOnly
@:gamepad([])
@:justPressed("dev-access") public var DEV_ACCESS(get, set): Bool;
@:pressed("dev-access") public var DEV_ACCESS_HOLD(get, set): Bool;
@:justReleased("dev-access") public var DEV_ACCESS_R(get, set): Bool;
@:devModeOnly
@:gamepad([])
@:justPressed("dev-console") public var DEV_CONSOLE(get, set): Bool;
@:pressed("dev-console") public var DEV_CONSOLE_HOLD(get, set): Bool;
@:justReleased("dev-console") public var DEV_CONSOLE_R(get, set): Bool;
@:devModeOnly
@:gamepad([])
@:justPressed("dev-reload") public var DEV_RELOAD(get, set): Bool;
@:pressed("dev-reload") public var DEV_RELOAD_HOLD(get, set): Bool;
@:justReleased("dev-reload") public var DEV_RELOAD_R(get, set): Bool;
@:allow(funkin.backend.utils.ControlsUtil)
var byName:Map<String, FlxActionDigital> = [];
+19 -23
View File
@@ -1,13 +1,12 @@
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.IModsAssetLibrary;
import funkin.backend.assets.ScriptedAssetLibrary;
import funkin.backend.system.macros.GitCommitMacro;
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).
@@ -28,10 +27,6 @@ class Flags {
public static var REPO_OWNER:String = "CodenameCrew";
public static var REPO_URL:String = 'https://github.com/$REPO_OWNER/$REPO_NAME';
// make this empty once you guys are done with the project.
// good luck /gen <3 @crowplexus
public static var RELEASE_CYCLE:String = "Beta";
/**
* Preferred sound extension for the game's audio files.
* Currently is set to `mp3` for web targets, and `ogg` for other targets.
@@ -41,7 +36,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)
@@ -59,23 +54,25 @@ class Flags {
public static var WEEKS_LIST_MOD_MODE:Allow<"prepend", "override", "append"> = "override";
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;
@@ -97,7 +94,7 @@ class Flags {
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;
public static var DEFAULT_PAUSE_ITEMS:Array<String> = ['Resume', 'Restart Song', 'Change Controls', 'Change Options', 'Exit to menu', "Exit to charter"];
public static var DEFAULT_CUTSCENE_PAUSE_ITEMS:Array<String> = ['Resume Cutscene', 'Skip Cutscene', 'Restart Cutscene', 'Exit to menu'];
@@ -157,7 +154,7 @@ 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> = [];
@@ -187,10 +184,9 @@ class Flags {
}
}
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);
}
+6 -4
View File
@@ -81,12 +81,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;
@@ -180,6 +180,8 @@ class Main extends Sprite
}
public static function refreshAssets() @:privateAccess {
FunkinCache.instance.clearSecondLayer();
var game = FlxG.game;
var daSndTray = Type.createInstance(game._customSoundTray = funkin.menus.ui.FunkinSoundTray, []);
var index:Int = game.numChildren - 1;
@@ -238,9 +240,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
}
+66 -26
View File
@@ -6,16 +6,21 @@ import sys.FileSystem;
import flixel.FlxState;
import funkin.backend.assets.ModsFolder;
import funkin.backend.chart.EventsData;
import funkin.menus.BetaWarningState;
import funkin.menus.TitleState;
import funkin.backend.system.framerate.Framerate;
import funkin.menus.TitleState;
import haxe.io.Path;
@dox(hide)
typedef AddonInfo = {
var name:String;
var path:String;
}
/**
* Simple state used for loading the game
*/
class MainState extends FlxState {
public static var initiated:Bool = false;
public static var betaWarningShown:Bool = false;
public override function create() {
super.create();
if (!initiated) {
@@ -36,25 +41,64 @@ class MainState extends FlxState {
Paths.assetsTree.reset();
#if MOD_SUPPORT
var _lowPriorityAddons:Array<String> = [];
var _highPriorityAddons:Array<String> = [];
var _noPriorityAddons:Array<String> = [];
if (FileSystem.exists(ModsFolder.addonsPath) && FileSystem.isDirectory(ModsFolder.addonsPath)) {
for(i=>addon in [for(dir in FileSystem.readDirectory(ModsFolder.addonsPath)) if (FileSystem.isDirectory(ModsFolder.addonsPath + dir)) dir]) {
if (addon.startsWith("[LOW]")) _lowPriorityAddons.insert(0, addon);
else if (addon.startsWith("[HIGH]")) _highPriorityAddons.insert(0, addon);
else _noPriorityAddons.insert(0, addon);
}
for (addon in _lowPriorityAddons)
Paths.assetsTree.addLibrary(ModsFolder.loadModLib(ModsFolder.addonsPath + addon, StringTools.ltrim(addon.substr("[LOW]".length))));
}
if (ModsFolder.currentModFolder != null)
Paths.assetsTree.addLibrary(ModsFolder.loadModLib(ModsFolder.modsPath + ModsFolder.currentModFolder, ModsFolder.currentModFolder));
inline function isDirectory(path:String):Bool
return FileSystem.exists(path) && FileSystem.isDirectory(path);
if (FileSystem.exists(ModsFolder.addonsPath) && FileSystem.isDirectory(ModsFolder.addonsPath)){
for (addon in _noPriorityAddons) Paths.assetsTree.addLibrary(ModsFolder.loadModLib(ModsFolder.addonsPath + addon, addon));
for (addon in _highPriorityAddons) Paths.assetsTree.addLibrary(ModsFolder.loadModLib(ModsFolder.addonsPath + addon, StringTools.ltrim(addon.substr("[HIGH]".length))));
inline function ltrim(str:String, prefix:String):String
return str.substr(prefix.length).ltrim();
inline function loadLib(path:String, name:String)
Paths.assetsTree.addLibrary(ModsFolder.loadModLib(path, name));
var _lowPriorityAddons:Array<AddonInfo> = [];
var _highPriorityAddons:Array<AddonInfo> = [];
var _noPriorityAddons:Array<AddonInfo> = [];
var addonPaths = [
ModsFolder.addonsPath,
(
ModsFolder.currentModFolder != null ?
ModsFolder.modsPath + ModsFolder.currentModFolder + "/addons/" :
null
)
];
for(path in addonPaths) {
if (path == null) continue;
if (!isDirectory(path)) continue;
for(addon in FileSystem.readDirectory(path)) {
if(!FileSystem.isDirectory(path + addon)) {
switch(Path.extension(addon).toLowerCase()) {
case 'zip':
addon = Path.withoutExtension(addon);
default:
continue;
}
}
var data:AddonInfo = {
name: addon,
path: path + addon
};
if (addon.startsWith("[LOW]")) _lowPriorityAddons.insert(0, data);
else if (addon.startsWith("[HIGH]")) _highPriorityAddons.insert(0, data);
else _noPriorityAddons.insert(0, data);
}
}
for (addon in _lowPriorityAddons)
loadLib(addon.path, ltrim(addon.name, "[LOW]"));
if (ModsFolder.currentModFolder != null)
loadLib(ModsFolder.modsPath + ModsFolder.currentModFolder, ModsFolder.currentModFolder);
for (addon in _noPriorityAddons)
loadLib(addon.path, addon.name);
for (addon in _highPriorityAddons)
loadLib(addon.path, ltrim(addon.name, "[HIGH]"));
#end
Flags.load();
@@ -67,14 +111,10 @@ class MainState extends FlxState {
ControlsUtil.loadCustomControls();
TitleState.initialized = false;
if(Framerate.isLoaded)
if (Framerate.isLoaded)
Framerate.instance.reload();
if (Flags.DISABLE_BETA_WARNING_SCREEN || betaWarningShown)
FlxG.switchState(new TitleState());
else
FlxG.switchState(new BetaWarningState());
betaWarningShown = true;
FlxG.switchState(new TitleState());
#if sys
CoolUtil.safeAddAttributes('./.temp/', NativeAPI.FileAttribute.HIDDEN);
@@ -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,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;
@@ -53,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}';
@@ -62,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';
@@ -86,7 +102,7 @@ class SystemInfo extends FramerateCategory {
Logs.trace('Unable to grab CPU Name: $e', ERROR, RED);
}
@: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
@@ -7,6 +7,8 @@ class BuildInfo {
public static function printBuildInfo() {
if(haxe.macro.Context.defined("display")) 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,
+1 -1
View File
@@ -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) {
}
+12 -12
View File
@@ -21,21 +21,21 @@ class AudioAnalyzer {
}
public function analyze(startPos:Float, endPos:Float):Float {
var bitsPerSample = buffer.bitsPerSample;
var multi = __timeMulti / 4000 / bitsPerSample;
var bytesStartPos:Int = Math.floor(startPos * multi) * bitsPerSample;
var bytesEndPos:Int = Math.floor(endPos * multi) * bitsPerSample;
var bytesStartPos:Int = Math.floor(startPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.bitsPerSample;
var bytesEndPos:Int = Math.floor(endPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.bitsPerSample;
bytesStartPos -= bytesStartPos % bitsPerSample;
bytesEndPos -= bytesEndPos % bitsPerSample;
var dataBuffer = buffer.data.buffer;
bytesStartPos -= bytesStartPos % buffer.bitsPerSample;
bytesEndPos -= bytesEndPos % buffer.bitsPerSample;
var maxByte:Int = 0;
for(i in 0...Math.floor((bytesEndPos - bytesStartPos) / bitsPerSample)) {
var bytePos = bytesStartPos + (i * bitsPerSample);
// What if bitsPerSample is 8? or 32? or 64? -Ne_Eo
var byte:Int = dataBuffer.get(bytePos) | (dataBuffer.get(bytePos + 1) << 8);
for(i in 0...Math.floor((bytesEndPos - bytesStartPos) / buffer.bitsPerSample)) {
#if js
var byte:Int = buffer.data[bytesStartPos + (i * buffer.bitsPerSample)]
| (buffer.data[bytesStartPos + (i * buffer.bitsPerSample) + 1] << 8);
#else
var byte:Int = buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample))
| (buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample) + 1) << 8);
#end
if (byte > 256 * 128) byte -= 256 * 256;
if (maxByte < byte) maxByte = byte;
}
+129 -7
View File
@@ -145,7 +145,7 @@ final class CoolUtil
if(result.isNothing) Logs.trace('The file where it has been tried to get the attributes from, might be corrupted or inexistent (code: ${result.getValue()})', WARNING);
return result;
#else
return 0;
return new FileAttributeWrapper(0);
#end
}
@@ -183,7 +183,7 @@ final class CoolUtil
addMissingFolders(Path.directory(path));
var result = NativeAPI.addFileAttributes(path, attrib, useAbsolute);
if(result == 0) Logs.trace('Failed to add attributes to $path with a code of: $result', WARNING);
if (result == 0) Logs.trace('Failed to add attributes to $path with a code of: $result', WARNING);
return result;
#else
return 0;
@@ -453,9 +453,13 @@ final class CoolUtil
if (Assets.exists(infoPath)) {
var musicInfo = IniUtil.parseAsset(infoPath, [
"BPM" => null,
"TimeSignature" => Flags.DEFAULT_BEATS_PER_MEASURE + "/" + Flags.DEFAULT_STEPS_PER_BEAT
"TimeSignature" => Flags.DEFAULT_BEATS_PER_MEASURE + "/" + Flags.DEFAULT_STEPS_PER_BEAT,
"LoopTime" => '${Flags.DEFAULT_LOOP_TIME}'
]);
if (FlxG.sound.music != null)
FlxG.sound.music.loopTime = Std.parseFloat(musicInfo["LoopTime"]) * 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;
@@ -712,11 +716,11 @@ final class CoolUtil
* @param spr Sprite to load the graphic for
* @param path Path to the graphic
*/
public static function loadAnimatedGraphic(spr:FlxSprite, path:String) {
public static function loadAnimatedGraphic(spr:FlxSprite, path:String, fps:Float = 24.0) {
spr.frames = Paths.getFrames(path, true);
if (spr.frames != null && spr.frames.frames != null) {
spr.animation.add("idle", [for(i in 0...spr.frames.frames.length) i], 24, true);
spr.animation.add("idle", [for(i in 0...spr.frames.frames.length) i], fps, true);
spr.animation.play("idle");
}
@@ -915,8 +919,8 @@ final class CoolUtil
* @param mainEase Main ease
* @param suffix Suffix (Ignored if `mainEase` is `linear`)
*/
@:noUsing public static inline function flxeaseFromString(mainEase:String, suffix:String)
return Reflect.field(FlxEase, mainEase + (mainEase == "linear" ? "" : suffix));
@:noUsing public static inline function flxeaseFromString(mainEase:String, ?suffix:String)
return Reflect.field(FlxEase, mainEase + (mainEase == "linear" || suffix == null ? "" : suffix));
/*
* Returns the filename of a path, without the extension.
@@ -1323,6 +1327,124 @@ final class CoolUtil
public static inline function isMapEmpty<K, V>(map: Map<K, V>): Bool {
return !map.keys().hasNext();
}
public inline static function parsePropertyString(fieldPath:String):Array<OneOfTwo<String, Int>> {
return FlxTween.parseFieldString(fieldPath);
}
public static function stringifyFieldsPath(fields:Array<OneOfTwo<String, Int>>):String {
var str = new StringBuf();
var first = true;
for (field in fields) {
if (Type.typeof(field) == TInt) {
str.add('[${field}]');
} else {
if (!first)
str.add('.');
str.add(field);
}
first = false;
}
return str.toString();
}
public static function parseProperty(target:Dynamic, fields:OneOfTwo<String, Array<OneOfTwo<String, Int>>>):Dynamic {
var fields:Array<OneOfTwo<String, Int>> = {
if((fields is String)) CoolUtil.parsePropertyString(fields);
else fields;
}
var field = CoolUtil.last(fields);
for (i in 0...fields.length - 1) {
var component = fields[i];
if (Type.typeof(component) == TInt) {
if ((target is Array)) {
var index:Int = cast component;
var arr:Array<Dynamic> = cast target;
target = arr[index];
}
} else { // TClass(String)
target = Reflect.getProperty(target, component);
}
if (!Reflect.isObject(target) && !(target is Array))
throw 'The object does not have the property "$component" in "${stringifyFieldsPath(fields)}"';
}
return new PropertyInfo(target, field);
}
public static function cloneProperty(toTarget:Dynamic, fields:OneOfTwo<String, Array<OneOfTwo<String, Int>>>, fromTarget:Dynamic):Dynamic {
var fields:Array<OneOfTwo<String, Int>> = {
if((fields is String)) CoolUtil.parsePropertyString(fields);
else fields;
}
var toProperty = CoolUtil.parseProperty(toTarget, fields);
var fromProperty = CoolUtil.parseProperty(fromTarget, fields);
return toProperty.setValue(fromProperty.getValue());
}
}
class PropertyInfo {
public var object:Dynamic;
public var field:OneOfTwo<String, Int>;
public var typeOfField:Type.ValueType;
#if hscript_improved
public var isCustom:Bool = false;
public var custom:hscript.IHScriptCustomBehaviour;
#end
public function new(object:Dynamic, field:OneOfTwo<String, Int>) {
this.object = object;
this.field = field;
#if hscript_improved
if (object is hscript.IHScriptCustomBehaviour)
{
isCustom = true;
custom = cast object;
}
#end
typeOfField = Type.typeof(field);
}
public function getValue():Dynamic
{
if (typeOfField == TInt)
{
var index:Int = cast field;
var arr:Array<Dynamic> = cast object;
return arr[index];
}
else
{
#if hscript_improved
if (isCustom)
return custom.hget(field);
else
#end
return Reflect.getProperty(object, field);
}
}
public function setValue(value:Dynamic):Void
{
if (typeOfField == TInt)
{
var index:Int = cast field;
var arr:Array<Dynamic> = cast object;
arr[index] = value;
}
else
{
#if hscript_improved
if (isCustom)
custom.hset(field, value);
else
#end
Reflect.setProperty(object, field, value);
}
}
}
/**
+3 -3
View File
@@ -127,7 +127,7 @@ final class MemoryUtil {
public static function getMemType():String {
#if windows
var memoryMap:Map<Int, String> = [
0 => "Unknown",
0 => null,
1 => "Other",
2 => "DRAM",
3 => "Synchronous DRAM",
@@ -166,9 +166,9 @@ final class MemoryUtil {
];
var memoryOutput:Int = -1;
var process = new HiddenProcess("wmic", ["memorychip", "get", "SMBIOSMemoryType"]);
var process = new HiddenProcess("powershell", ["-Command", "Get-CimInstance Win32_PhysicalMemory | Select-Object -ExpandProperty SMBIOSMemoryType" ]);
if (process.exitCode() == 0) memoryOutput = Std.int(Std.parseFloat(process.stdout.readAll().toString().trim().split("\n")[1]));
if (memoryOutput != -1) return memoryMap[memoryOutput];
if (memoryOutput != -1) return memoryMap[memoryOutput] == null ? 'Unknown ($memoryOutput)' : memoryMap[memoryOutput];
#elseif mac
var process = new HiddenProcess("system_profiler", ["SPMemoryDataType"]);
var reg = ~/Type: (.+)/;
+120
View File
@@ -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;
}
}
+5 -4
View File
@@ -114,12 +114,13 @@ final class XMLUtil {
* @param parentFolder The parent folder
* @param defaultAnimType The default animation type
*/
public static function loadSpriteFromXML(spr:FunkinSprite, node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT):FunkinSprite {
public static function loadSpriteFromXML(spr:FunkinSprite, node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT, loadGraphic:Bool = true):FunkinSprite {
if (parentFolder == null) parentFolder = "";
spr.name = node.getAtt("name");
spr.antialiasing = true;
spr.loadSprite(Paths.image('$parentFolder${node.getAtt("sprite").getDefault(spr.name)}', null, true));
if (loadGraphic)
spr.loadSprite(Paths.image('$parentFolder${node.getAtt("sprite").getDefault(spr.name)}', null, true));
spr.spriteAnimType = defaultAnimType;
if (node.has.type) {
@@ -239,10 +240,10 @@ final class XMLUtil {
* @param cl The class to create (advanced)
* @param args The arguments to pass to the class (advanced)
*/
public static inline function createSpriteFromXML(node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT, ?cl:Class<FunkinSprite>, ?args:Array<Dynamic>):FunkinSprite {
public static inline function createSpriteFromXML(node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT, ?cl:Class<FunkinSprite>, ?args:Array<Dynamic>, loadGraphic:Bool = true):FunkinSprite {
if(cl == null) cl = FunkinSprite;
if(args == null) args = [];
return loadSpriteFromXML(Type.createInstance(cl, args), node, parentFolder, defaultAnimType);
return loadSpriteFromXML(Type.createInstance(cl, args), node, parentFolder, defaultAnimType, loadGraphic);
}
/**
+22 -21
View File
@@ -1,5 +1,8 @@
package funkin.backend.utils;
import haxe.zip.Entry;
import haxe.zip.Reader;
#if sys
#if (!macro && sys)
import openfl.display.BitmapData;
@@ -9,8 +12,6 @@ import haxe.Exception;
import haxe.Json;
import haxe.crypto.Crc32;
import haxe.zip.Compress;
import haxe.zip.Entry;
import haxe.zip.Reader;
import haxe.zip.Tools;
import haxe.zip.Uncompress;
import haxe.zip.Writer;
@@ -135,12 +136,12 @@ final class ZipUtil {
if (prog == null) prog = new ZipProgress();
try {
var curPath:Array<String> = ['$path'];
var curPath:Array<String> = [path];
var destPath:Array<String> = [];
if (prefix != "") {
prefix = prefix.replace("\\", "/");
while(prefix.charAt(0) == "/") prefix = prefix.substr(1);
while(prefix.charAt(prefix.length-1) == "/") prefix = prefix.substr(0, prefix.length-1);
while(prefix.charCodeAt(0) == "/".code) prefix = prefix.substr(1);
while(prefix.charCodeAt(prefix.length-1) == "/".code) prefix = prefix.substr(0, prefix.length-1);
destPath.push(prefix);
}
@@ -160,7 +161,7 @@ final class ZipUtil {
} else {
// is file, put it in the list
var zipPath = '$zipPath/$e';
while(zipPath.charAt(0) == "/") zipPath = zipPath.substr(1);
while(zipPath.charCodeAt(0) == "/".code) zipPath = zipPath.substr(1);
files.push(new StrNameLabel('$path/$e', zipPath));
}
}
@@ -227,20 +228,6 @@ class ZipProgress {
public function new() {}
}
class ZipReader extends Reader {
public var files:List<Entry>;
public override function read() {
if (files != null) return files;
try {
var files = super.read();
return this.files = files;
} catch(e) {
}
return new List<Entry>();
}
}
class ZipWriter extends Writer {
public function flush() {
o.flush();
@@ -265,4 +252,18 @@ class StrNameLabel {
this.label = label;
}
}
#end
#end
class ZipReader extends Reader {
public var files:List<Entry>;
public override function read() {
if (files != null) return files;
try {
var files = super.read();
return this.files = files;
} catch(e) {
}
return new List<Entry>();
}
}
+92
View File
@@ -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)
)
};
}
}
+31
View File
@@ -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;
}
+7 -6
View File
@@ -7,6 +7,7 @@ import funkin.options.*;
class EditorTreeMenu extends TreeMenu {
public var bg:FlxBackdrop;
public var bgType:String = "default";
public var bgMovement:FlxPoint = new FlxPoint();
public override function create() {
super.create();
@@ -18,16 +19,16 @@ class EditorTreeMenu extends TreeMenu {
bg = new FlxBackdrop();
bg.loadGraphic(Paths.image('editors/bgs/${bgType}'));
bg.antialiasing = true;
setBackgroundRotation(-5);
add(bg);
}
public inline function setBackgroundRotation(rotation:Float) {
bg.rotation = rotation;
bg.velocity.set(85, 0).degrees = bg.rotation;
}
public override function exit() {
FlxG.switchState(new MainMenuState());
}
public override function update(elapsed:Float) {
super.update(elapsed);
bg.x += elapsed * 125;
bg.y += elapsed * 125;
}
}
+2 -2
View File
@@ -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);
}
}
}
@@ -8,6 +8,8 @@ import openfl.geom.Point;
import openfl.geom.Rectangle;
import openfl.display.BitmapData;
import funkin.game.Character;
import funkin.game.HealthIcon;
import haxe.xml.Access;
using funkin.backend.utils.BitmapUtil;
@@ -22,6 +24,12 @@ class CharacterInfoScreen extends UISubstateWindow {
public var character:Character;
public var iconColorPicker:UIIconColorPicker;
public var spriteTextBox:UITextBox;
public var iconTextBox:UITextBox;
public var iconSprite:HealthIcon;
public var gameOverCharTextBox:UITextBox;
public var antialiasingCheckbox:UICheckbox;
public var flipXCheckbox:UICheckbox;
public var iconColorWheel:UIColorwheel;
public var useDurationCheckbox:UICheckbox;
@@ -101,17 +109,58 @@ class CharacterInfoScreen extends UISubstateWindow {
closeButton.color = 0xFFFF0000;
}
public function saveCharacterInfo() {
function updateIcon(icon:String) {
if (iconSprite == null) add(iconSprite = new HealthIcon());
iconSprite.setIcon(icon);
var size = Std.int(150 * 0.5);
iconSprite.setUnstretchedGraphicSize(size, size, true);
iconSprite.updateHitbox();
iconSprite.setPosition(iconTextBox.x + iconTextBox.bWidth + 8, iconTextBox.y + (iconTextBox.bHeight / 2) - (iconSprite.height / 2));
iconSprite.scrollFactor.set(1, 1);
}
function saveCharacterInfo() {
UIUtil.confirmUISelections(this);
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
]
});
//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);
}
}
@@ -318,7 +318,8 @@ class CompactCharacterButton extends UIButton {
autoAlpha = false;
charIcon = new HealthIcon(funkin.game.Character.getIconFromCharName(char));
charIcon.scale.set(0.2, 0.2);
var size = Std.int(150 * 0.2);
charIcon.setUnstretchedGraphicSize(size, size, true);
charIcon.updateHitbox();
charIcon.setPosition(10, bHeight/2 - charIcon.height / 2);
charIcon.scrollFactor.set(1,1);
@@ -330,11 +331,10 @@ class CompactCharacterButton extends UIButton {
textBox.antialiasing = true;
textBox.onChange = function(char:String) {
char = funkin.game.Character.getIconFromCharName(char);
var image = Paths.image("icons/" + char);
if(!Assets.exists(image))
image = Paths.image("icons/" + Flags.DEFAULT_HEALTH_ICON);
charIcon.loadGraphic(image, true, 150, 150);
charIcon.setIcon(char);
charIcon.setUnstretchedGraphicSize(size, size, true);
charIcon.updateHitbox();
charIcon.setPosition(10, bHeight/2 - charIcon.height / 2);
}
deleteButton = new UIButton(textBox.x + 115 + 16, bHeight/2 - (32/2), null, function () {
+5 -2
View File
@@ -1423,8 +1423,11 @@ class Charter extends UIState {
for(id=>str in strumLines.members)
if (str != null) str.y = strumlineInfoBG.y;
strumlineAddButton.x = 0;
strumlineAddButton.y = strumlineInfoBG.y;
strumlineLockButton.y = strumlineInfoBG.y;
strumlineLockButton.text.visible = strumlineLockButton.button.selectable = strumlineLockButton.button.visible = strumLines.members.length > 0;
}
public override function onResize(width:Int, height:Int) {
@@ -1558,7 +1561,7 @@ class Charter extends UIState {
#if REGION
function _file_exit(_) {
if (undos.unsaved) SaveWarning.triggerWarning();
else {undos = null; FlxG.switchState(new CharterSelection()); PlayState.resetSongInfos(); Charter.instance.__clearStatics();}
else {undos = null; FlxG.switchState(new CharterSelection()); Charter.instance.__clearStatics();}
}
function _file_save_all(_) saveEverything();
@@ -2145,7 +2148,7 @@ class Charter extends UIState {
}
@:noCompletion public function __clearStatics() {
selection = null; undos = null; clipboard = null; playtestInfo = null;
selection = null; undos = null; clipboard = null; playtestInfo = null;
waveformHandler.destroy(); Charter.waveformHandler = null; autoSaveTimer = 0;
}
@@ -105,7 +105,7 @@ class CharterAutoSaveUI extends UISliceSprite {
bar.follow(this, 10, bHeight-20);
bar.alpha = alpha;
}
progressBar.clipRect = progressBar.clipRect.set(0, 0, progressBar.width*progress,progressBar.height);
progressBar.rawClipRect = progressBar.rawClipRect.set(0, 0, progressBar.frameWidth*progress,progressBar.frameHeight);
}
public function appearAnimation() {
@@ -202,11 +202,11 @@ class CharterEventScreen extends UISubstateWindow {
public function saveCurTab() {
if (curEvent < 0) return;
events[curEvent].params = [
for(p in paramsFields) {
var dataParams = EventsData.getEventParams(events[curEvent].name);
var params:Array<Dynamic> = [
for (i => p in paramsFields) {
if (p is UIDropDown) {
var dataParams = EventsData.getEventParams(events[curEvent].name);
if (dataParams[paramsFields.indexOf(p)].type == TStrumLine) cast(p, UIDropDown).index;
if (dataParams[i].type == TStrumLine) cast(p, UIDropDown).index;
else cast(p, UIDropDown).label.text;
}
else if (p is UINumericStepper) {
@@ -227,6 +227,14 @@ class CharterEventScreen extends UISubstateWindow {
null;
}
];
while(dataParams.length > 0 && {
var index = params.length - 1;
var dataParam = dataParams[index];
dataParam.saveIfDefault == false && params[index] == dataParam.defValue;
}) params.pop();
events[curEvent].params = params;
}
}
@@ -3,6 +3,7 @@ package funkin.editors.charter;
import flixel.math.FlxPoint;
import funkin.backend.chart.ChartData.ChartMetaData;
import funkin.editors.extra.PropertyButton;
import funkin.game.HealthIcon;
using StringTools;
@@ -19,7 +20,7 @@ class CharterMetaDataScreen extends UISubstateWindow {
public var displayNameTextBox:UITextBox;
public var iconTextBox:UITextBox;
public var iconSprite:FlxSprite;
public var iconSprite:HealthIcon;
public var opponentModeCheckbox:UICheckbox;
public var coopAllowedCheckbox:UICheckbox;
public var colorWheel:UIColorwheel;
@@ -84,7 +85,7 @@ class CharterMetaDataScreen extends UISubstateWindow {
coopAllowedCheckbox = new UICheckbox(opponentModeCheckbox.x + 150 + 26, opponentModeCheckbox.y, "Co-op Mode", metadata.coopAllowed);
add(coopAllowedCheckbox);
colorWheel = new UIColorwheel(iconTextBox.x, coopAllowedCheckbox.y, metadata.parsedColor);
colorWheel = new UIColorwheel(iconTextBox.x, coopAllowedCheckbox.y, metadata.color);
add(colorWheel);
addLabelOn(colorWheel, "Color");
@@ -124,22 +125,14 @@ class CharterMetaDataScreen extends UISubstateWindow {
}
function updateIcon(icon:String) {
if (iconSprite == null) add(iconSprite = new FlxSprite());
if (iconSprite == null) add(iconSprite = new HealthIcon());
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.setIcon(icon);
var size = Std.int(150 * 0.5);
iconSprite.setUnstretchedGraphicSize(size, size, true);
iconSprite.updateHitbox();
iconSprite.setPosition(iconTextBox.x + 150 + 8, (iconTextBox.y + 16) - (iconSprite.height/2));
iconSprite.setPosition(iconTextBox.x + iconTextBox.bWidth + 8, iconTextBox.y + (iconTextBox.bHeight / 2) - (iconSprite.height / 2));
iconSprite.scrollFactor.set(1, 1);
}
public function saveMeta() {
@@ -157,8 +150,7 @@ class CharterMetaDataScreen extends UISubstateWindow {
stepsPerBeat: Std.int(stepsPerBeatStepper.value),
displayName: displayNameTextBox.label.text,
icon: iconTextBox.label.text,
color: colorWheel.curColorString,
parsedColor: colorWheel.curColor,
color: colorWheel.curColor,
opponentModeAllowed: opponentModeCheckbox.checked,
coopAllowed: coopAllowedCheckbox.checked,
difficulties: [for (diff in difficultiesTextBox.label.text.split(",")) diff.trim()],
@@ -1,5 +1,6 @@
package funkin.editors.charter;
import funkin.backend.chart.Chart;
import funkin.backend.chart.ChartData.ChartMetaData;
import funkin.backend.chart.ChartData;
import funkin.backend.system.framerate.Framerate;
@@ -34,16 +35,20 @@ class CharterSelection extends EditorTreeMenu {
FlxG.switchState(new Charter(s.name, d));
})
];
list.push(new NewOption("New Difficulty", "Press ACCEPT to create a new difficulty.", function() {
#if sys
list.push(new NewOption("New Difficulty", "New Difficulty", function() {
FlxG.state.openSubState(new ChartCreationScreen(saveChart));
}));
#end
optionsTree.add(new OptionsScreen(s.name, "Select a difficulty to continue.", list));
}, s.parsedColor.getDefault(0xFFFFFFFF))
}, s.color.getDefault(0xFFFFFFFF))
];
list.insert(0, new NewOption("New Song", "Press ACCEPT to create a new song.", function() {
#if sys
list.insert(0, new NewOption("New Song", "New Song", function() {
FlxG.state.openSubState(new SongCreationScreen(saveSong));
}));
#end
main = new OptionsScreen("Chart Editor", "Select a song to modify the charts from.", list);
@@ -88,11 +93,12 @@ class CharterSelection extends EditorTreeMenu {
}
}
public function saveSong(creation:SongCreationData) {
#if sys
public function saveSong(creation:SongCreationData, ?callback:String -> SongCreationData -> Void) {
var songAlreadyExists:Bool = [for (s in freeplayList.songs) s.name.toLowerCase()].contains(creation.meta.name.toLowerCase());
if (songAlreadyExists) {
openSubState(new UIWarningSubstate("Creating Song: Error!", "The song you are trying to create already exists, if you would like to override it delete the song first!", [
openSubState(new UIWarningSubstate("Creating Song: Error!", "The song you are trying to create Already exists, if you would like to override it delete the song first!", [
{label: "Ok", color: 0xFFFF0000, onClick: function(t) {}}
]));
return;
@@ -109,11 +115,17 @@ class CharterSelection extends EditorTreeMenu {
sys.FileSystem.createDirectory('$songFolder/charts');
// Save Files
CoolUtil.safeSaveFile('$songFolder/meta.json', Json.stringify(creation.meta, Flags.JSON_PRETTY_PRINT));
CoolUtil.safeSaveFile('$songFolder/meta.json', Chart.makeMetaSaveable(creation.meta));
if (creation.instBytes != null) sys.io.File.saveBytes('$songFolder/song/Inst.${Flags.SOUND_EXT}', creation.instBytes);
if (creation.voicesBytes != null) sys.io.File.saveBytes('$songFolder/song/Voices.${Flags.SOUND_EXT}', creation.voicesBytes);
if (creation.playerVocals != null) sys.io.File.saveBytes('$songFolder/song/Voices-Player.${Flags.SOUND_EXT}', creation.playerVocals);
if (creation.oppVocals != null) sys.io.File.saveBytes('$songFolder/song/Voices-Opponent.${Flags.SOUND_EXT}', creation.oppVocals);
#end
if (callback != null)
callback(songFolder, creation);
var option = new EditorIconOption(creation.meta.name, "Press ACCEPT to choose a difficulty to edit.", creation.meta.icon, function() {
curSong = creation.meta;
var list:Array<OptionType> = [
@@ -122,11 +134,11 @@ class CharterSelection extends EditorTreeMenu {
FlxG.switchState(new Charter(creation.meta.name, d));
})
];
list.push(new NewOption("New Difficulty", "Press ACCEPT to create a new difficulty.", function() {
list.push(new NewOption("New Difficulty", "New Difficulty", function() {
FlxG.state.openSubState(new ChartCreationScreen(saveChart));
}));
optionsTree.insert(1, new OptionsScreen(creation.meta.name, "Select a difficulty to continue.", list));
}, creation.meta.parsedColor.getDefault(0xFFFFFFFF));
}, creation.meta.color.getDefault(0xFFFFFFFF));
// Add to List
freeplayList.songs.insert(0, creation.meta);
@@ -160,7 +172,8 @@ class CharterSelection extends EditorTreeMenu {
var meta = Json.parse(sys.io.File.getContent('$songFolder/meta.json'));
if (meta.difficulties != null && !meta.difficulties.contains(name)) {
meta.difficulties.push(name);
CoolUtil.safeSaveFile('$songFolder/meta.json', Json.stringify(meta));
CoolUtil.safeSaveFile('$songFolder/meta.json', Chart.makeMetaSaveable(meta));
}
}
#end
}
@@ -88,8 +88,7 @@ class CharterStrumline extends UISprite {
button = new CharterStrumlineOptions(this);
members.push(button);
vocals = strumLine.vocalsSuffix.length > 0 ? FlxG.sound.load(Paths.voices(PlayState.SONG.meta.name, PlayState.difficulty, strumLine.vocalsSuffix)) : new FlxSound();
vocals.group = FlxG.sound.defaultMusicGroup;
updateInfo();
selectedWaveform = -1;
}
@@ -58,8 +58,8 @@ class CharterStrumlineButton extends UISprite {
super.update(elapsed);
button.updateSpriteRect();
if(UIState.state.curContextMenu == null && UIState.state.isOverlapping(button, button.__rect)) {
if (UIState.state.curContextMenu == null && UIState.state.isOverlapping(button, button.__rect) && button.visible) {
UIState.state.updateSpriteRect(button);
buttonScale.set(0.95, 0.95);
if (FlxG.mouse.justPressed && onClick != null) {
onClick();
@@ -242,7 +242,8 @@ class CharacterButton extends UIButton {
super(x, y, null, null, 250, 54);
charIcon = new HealthIcon(Character.getIconFromCharName(char));
charIcon.scale.set(0.3, 0.3);
var size = Std.int(150 * 0.3);
charIcon.setUnstretchedGraphicSize(size, size, true);
charIcon.updateHitbox();
charIcon.setPosition(x + 10, bHeight/2 - charIcon.height / 2);
charIcon.scrollFactor.set(1,1);
@@ -254,11 +255,10 @@ class CharacterButton extends UIButton {
textBox.antialiasing = true;
textBox.onChange = function(char:String) {
char = Character.getIconFromCharName(char);
var image = Paths.image("icons/" + char);
if(!Assets.exists(image))
image = Paths.image("icons/" + Flags.DEFAULT_HEALTH_ICON);
charIcon.loadGraphic(image, true, 150, 150);
charIcon.setIcon(char);
charIcon.setUnstretchedGraphicSize(size, size, true);
charIcon.updateHitbox();
charIcon.setPosition(x + 10, bHeight/2 - charIcon.height / 2);
}
deleteButton = new UIButton(textBox.x + 115 + 16, bHeight/2 - (32/2), null, function () {
@@ -3,17 +3,26 @@ package funkin.editors.charter;
import flixel.group.FlxGroup;
import flixel.text.FlxText.FlxTextFormat;
import flixel.text.FlxText.FlxTextFormatMarkerPair;
import funkin.backend.chart.ChartData.ChartMetaData;
import funkin.backend.chart.ChartData;
import funkin.backend.chart.FNFLegacyParser.SwagSong;
import funkin.backend.chart.PsychParser;
import funkin.backend.chart.VSliceParser;
import funkin.backend.utils.ZipUtil.ZipReader;
import funkin.game.HealthIcon;
import haxe.Json;
import haxe.io.Bytes;
import haxe.io.BytesInput;
typedef SongCreationData = {
var meta:ChartMetaData;
var instBytes:Bytes;
var voicesBytes:Bytes;
@:optional var playerVocals:Bytes;
@:optional var oppVocals:Bytes;
}
class SongCreationScreen extends UISubstateWindow {
private var onSave:Null<SongCreationData> -> Void = null;
private var onSave:Null<SongCreationData> -> Null<String -> SongCreationData -> Void> -> Void = null;
public var songNameTextBox:UITextBox;
public var bpmStepper:UINumericStepper;
@@ -21,15 +30,26 @@ class SongCreationScreen extends UISubstateWindow {
public var stepsPerBeatStepper :UINumericStepper;
public var instExplorer:UIFileExplorer;
public var voicesExplorer:UIFileExplorer;
public var importFrom:UIButton;
public var displayNameTextBox:UITextBox;
public var iconTextBox:UITextBox;
public var iconSprite:FlxSprite;
public var iconSprite:HealthIcon;
public var opponentModeCheckbox:UICheckbox;
public var coopAllowedCheckbox:UICheckbox;
public var colorWheel:UIColorwheel;
public var difficultiesTextBox:UITextBox;
public var engineDropdown:UIDropDown;
public var createSong:UIButton;
public var importInstExplorer:UIFileExplorer;
public var importVoicesExplorer:UIFileExplorer;
public var importIdTextBox:UITextBox;
public var importChartFile:UIFileExplorer;
public var importMetaFile:UIFileExplorer;
public var backButton:UIButton;
public var saveButton:UIButton;
public var closeButton:UIButton;
@@ -37,11 +57,21 @@ class SongCreationScreen extends UISubstateWindow {
public var songDataGroup:FlxGroup = new FlxGroup();
public var menuDataGroup:FlxGroup = new FlxGroup();
public var selectFormatGroup:FlxGroup = new FlxGroup();
public var importAudioGroup:FlxGroup = new FlxGroup();
public var importDataGroup:FlxGroup = new FlxGroup();
public var pages:Array<FlxGroup> = [];
public var pageSizes:Array<FlxPoint> = [];
public var importPages:Array<FlxGroup> = [];
public var importPageSizes:Array<FlxPoint> = [];
public var isImporting = false;
public var curPage:Int = 0;
public function new(?onSave:SongCreationData->Void) {
public function new(?onSave:SongCreationData -> Null<String -> SongCreationData -> Void> -> Void) {
super();
if (onSave != null) this.onSave = onSave;
}
@@ -80,10 +110,7 @@ class SongCreationScreen extends UISubstateWindow {
stepsPerBeatStepper = new UINumericStepper(beatsPerMeasureStepper.x + 30 + 24, beatsPerMeasureStepper.y, 4, 1, 0, 1, null, 54);
songDataGroup.add(stepsPerBeatStepper);
var voicesUIText:UIText = null;
instExplorer = new UIFileExplorer(songNameTextBox.x, songNameTextBox.y + 32 + 36, null, null, Flags.SOUND_EXT, function (path, res) {
if (path == null || res == null) return;
instExplorer = new UIFileExplorer(songNameTextBox.x, songNameTextBox.y + 32 + 36, null, null, Flags.SOUND_EXT, function (res) {
var audioPlayer:UIAudioPlayer = new UIAudioPlayer(instExplorer.x + 8, instExplorer.y + 8, res);
instExplorer.members.push(audioPlayer);
instExplorer.uiElement = audioPlayer;
@@ -102,6 +129,14 @@ class SongCreationScreen extends UISubstateWindow {
songDataGroup.add(voicesExplorer);
addLabelOn(voicesExplorer, "Vocal Audio File");
importFrom = new UIButton(windowSpr.x + 20, windowSpr.y + windowSpr.bHeight - 16 - 32, "Import From...", function() {
winTitle = "Importing Song";
isImporting = true;
updatePagesTexts();
refreshPages();
}, 150);
songDataGroup.add(importFrom);
var menuTitle:UIText;
menuDataGroup.add(menuTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Menus Data (Freeplay/Story)", 28));
@@ -134,7 +169,64 @@ class SongCreationScreen extends UISubstateWindow {
for (checkbox in [opponentModeCheckbox, coopAllowedCheckbox])
{checkbox.y += 6; checkbox.x += 4;}
var menuTitle:UIText;
selectFormatGroup.add(menuTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Import From:", 28));
selectFormatGroup.add(engineDropdown);
addLabelOn(engineDropdown, "Chart Format");
createSong = new UIButton(windowSpr.x + 20, windowSpr.y + windowSpr.bHeight - 16 - 32, "< Back", function() {
winTitle = "Creating New Song";
isImporting = false;
updatePagesTexts();
refreshPages();
}, 120);
selectFormatGroup.add(createSong);
var menuTitle:UIText;
importAudioGroup.add(menuTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Add Audios", 28));
importInstExplorer = new UIFileExplorer(menuTitle.x, menuTitle.y + menuTitle.height + 36, null, null, Flags.SOUND_EXT, function (res) {
var audioPlayer:UIAudioPlayer = new UIAudioPlayer(importInstExplorer.x + 8, importInstExplorer.y + 8, res);
importInstExplorer.members.push(audioPlayer);
importInstExplorer.uiElement = audioPlayer;
});
importAudioGroup.add(importInstExplorer);
addLabelOn(importInstExplorer, "Inst Audio File").applyMarkup(
"Inst Audio File $* Required$",
[new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFAD1212), "$")]);
importVoicesExplorer = new UIFileExplorer(importInstExplorer.x + 320 + 26, importInstExplorer.y, null, null, Flags.SOUND_EXT, function (res) {
var audioPlayer:UIAudioPlayer = new UIAudioPlayer(importVoicesExplorer.x + 8, importVoicesExplorer.y + 8, res);
importVoicesExplorer.members.push(audioPlayer);
importVoicesExplorer.uiElement = audioPlayer;
});
importAudioGroup.add(importVoicesExplorer);
addLabelOn(importVoicesExplorer, "Vocal Audio File");
var menuTitle:UIText;
importDataGroup.add(menuTitle = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Add Data", 28));
importDataGroup.add(importIdTextBox = new UITextBox(menuTitle.x, menuTitle.y + menuTitle.height + 36));
addLabelOn(importIdTextBox, "Song file name").applyMarkup(
"Song file name $* Required$",
[new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFAD1212), "$")]);
importChartFile = new UIFileExplorer(importIdTextBox.x, importIdTextBox.y + importIdTextBox.height + 56, null, null, "fnfc", function (_) importIdTextBox.label.text = new haxe.io.Path(importChartFile.filePath).file);
importDataGroup.add(importChartFile);
addLabelOn(importChartFile, "Data/Chart File").applyMarkup(
"Data/Chart File $* Required$",
[new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFAD1212), "$")]);
importMetaFile = new UIFileExplorer(importChartFile.x + 320 + 26, importChartFile.y, null, null, "json");
importDataGroup.add(importMetaFile);
addLabelOn(importMetaFile, "Meta File").applyMarkup(
"Meta File $* Required$",
[new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFAD1212), "$")]);
engineDropdown = new UIDropDown(menuTitle.x, menuTitle.y + menuTitle.height + 36, 480, 32, ["Psych/Legacy FNF", "V-Slice", "V-Slice Project (.fnfc)"]);
saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, "Save & Close", function() {
var pages = isImporting ? importPages : pages;
if (curPage == pages.length-1) {
saveSongInfo();
close();
@@ -167,17 +259,41 @@ class SongCreationScreen extends UISubstateWindow {
pages.push(cast add(menuDataGroup));
pageSizes.push(FlxPoint.get(748 - 32 + 40, 400));
importPages.push(cast add(selectFormatGroup));
importPageSizes.push(FlxPoint.get(500, 250));
importPages.push(cast add(importAudioGroup));
importPageSizes.push(FlxPoint.get(705, 250));
importPages.push(cast add(importDataGroup));
importPageSizes.push(FlxPoint.get(705, 300));
refreshPages();
updatePagesTexts();
}
public override function update(elapsed:Float) {
if (curPage == 0) {
if (instExplorer.file != null)
if (isImporting)
{
var name = engineDropdown.options[engineDropdown.index];
var project = name == "V-Slice Project (.fnfc)";
if (curPage == 1) {
importInstExplorer.selectable = importVoicesExplorer.selectable = !project;
saveButton.selectable = project ? true : (importInstExplorer.file != null);
} else if (curPage == 2) {
importIdTextBox.selectable = !project;
importChartFile.fileType = project ? "fnfc" : "json";
importMetaFile.selectable = name == "V-Slice";
saveButton.selectable = importChartFile.file != null && (!importMetaFile.selectable || importMetaFile.file != null) && (!importIdTextBox.selectable || importIdTextBox.label.text.trim().length > 0);
} else
saveButton.selectable = true;
else saveButton.selectable = false;
} else
saveButton.selectable = true;
} else {
if (curPage == 0)
saveButton.selectable = (instExplorer.file != null);
else
saveButton.selectable = true;
}
saveButton.alpha = saveButton.field.alpha = saveButton.selectable ? 1 : 0.4;
super.update(elapsed);
@@ -185,10 +301,15 @@ class SongCreationScreen extends UISubstateWindow {
function refreshPages() {
for (i=>page in pages)
page.visible = page.exists = i == curPage;
page.visible = page.exists = i == curPage && !isImporting;
for (i=>page in importPages)
page.visible = page.exists = i == curPage && isImporting;
}
function updatePagesTexts() {
var pageSizes = isImporting ? importPageSizes : pageSizes;
var pages = isImporting ? importPages : pages;
windowSpr.bWidth = Std.int(pageSizes[curPage].x);
windowSpr.bHeight = Std.int(pageSizes[curPage].y);
@@ -196,7 +317,7 @@ class SongCreationScreen extends UISubstateWindow {
titleSpr.y = windowSpr.y + ((30 - titleSpr.height) / 2);
saveButton.field.text = curPage == pages.length-1 ? "Save & Close" : 'Next >';
titleSpr.text = 'Creating New Song (${curPage+1}/${pages.length})';
titleSpr.text = '$winTitle (${curPage+1}/${pages.length})';
backButton.field.text = '< Back';
backButton.visible = backButton.exists = curPage > 0;
@@ -204,51 +325,121 @@ class SongCreationScreen extends UISubstateWindow {
backButton.x = (saveButton.x = windowSpr.x + windowSpr.bWidth - 20 - 125) - 20 - saveButton.bWidth;
closeButton.x = (curPage > 0 ? backButton : saveButton).x - 20 - saveButton.bWidth;
for (button in [saveButton, backButton, closeButton])
for (button in [saveButton, backButton, closeButton, importFrom, createSong])
button.y = windowSpr.y + windowSpr.bHeight - 16 - 32;
}
function updateIcon(icon:String) {
if (iconSprite == null) menuDataGroup.add(iconSprite = new FlxSprite());
if (iconSprite == null) menuDataGroup.add(iconSprite = new HealthIcon());
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.setIcon(icon);
var size = Std.int(150 * 0.5);
iconSprite.setUnstretchedGraphicSize(size, size, true);
iconSprite.updateHitbox();
iconSprite.setPosition(iconTextBox.x + 150 + 8, (iconTextBox.y + 16) - (iconSprite.height/2));
iconSprite.setPosition(iconTextBox.x + iconTextBox.bWidth + 8, iconTextBox.y + (iconTextBox.bHeight / 2) - (iconSprite.height / 2));
iconSprite.scrollFactor.set(1, 1);
}
function saveSongInfo() {
UIUtil.confirmUISelections(this);
if (isImporting)
{
try switch(engineDropdown.options[engineDropdown.index])
{
case "V-Slice Project (.fnfc)":
var files:Map<String, Any> = [];
for (field in new ZipReader(new BytesInput(importChartFile.file)).read()) {
var fileName = field.fileName;
var fileContent = field.data;
files.set(fileName, fileContent);
}
saveFromVSlice(files);
case "V-Slice":
var songId = importIdTextBox.label.text;
var files:Map<String, Any> = [];
files.set('${songId}-metadata.json', importMetaFile.file);
files.set('${songId}-chart.json', importChartFile.file);
files.set('Inst.${Flags.SOUND_EXT}', importInstExplorer.file);
files.set('Voices.${Flags.SOUND_EXT}', importVoicesExplorer.file);
saveFromVSlice(files, songId);
default /*"Psych/Legacy FNF"*/:
var songId = importIdTextBox.label.text;
var oldChart:SwagSong = Json.parse(cast importChartFile.file);
var base:ChartData = {
strumLines: [],
noteTypes: [],
events: [],
meta: {name: null},
scrollSpeed: Flags.DEFAULT_SCROLL_SPEED,
stage: Flags.DEFAULT_STAGE,
codenameChart: true
};
PsychParser.parse(oldChart, base);
if (onSave != null) onSave({
meta: {
name: songId,
difficulties: [songId],
bpm: oldChart.bpm,
beatsPerMeasure: oldChart.beatsPerMeasure,
stepsPerBeat: oldChart.stepsPerBeat,
displayName: oldChart.song
},
instBytes: importInstExplorer.file,
voicesBytes: importVoicesExplorer.file,
}, (songFolder:String, creation:SongCreationData) -> {
#if sys
CoolUtil.safeSaveFile('$songFolder/charts/${songId}.json', Json.stringify(base, Flags.JSON_PRETTY_PRINT));
#end
});
} catch (e) {
openSubState(new UIWarningSubstate("Importing Song/Charts: Error!", Std.string(e), [
{label: "Ok", color: 0xFFFF0000, onClick: function(t) {}}
]));
}
} else {
for (stepper in [bpmStepper, beatsPerMeasureStepper, stepsPerBeatStepper])
@:privateAccess stepper.__onChange(stepper.label.text);
var meta:ChartMetaData = {
name: songNameTextBox.label.text,
bpm: bpmStepper.value,
beatsPerMeasure: Std.int(beatsPerMeasureStepper.value),
stepsPerBeat: Std.int(stepsPerBeatStepper.value),
displayName: displayNameTextBox.label.text,
icon: iconTextBox.label.text,
color: colorWheel.curColorString,
parsedColor: colorWheel.curColor,
opponentModeAllowed: opponentModeCheckbox.checked,
coopAllowed: coopAllowedCheckbox.checked,
difficulties: [for (diff in difficultiesTextBox.label.text.split(",")) diff.trim()],
};
var meta:ChartMetaData = {
name: songNameTextBox.label.text,
bpm: bpmStepper.value,
beatsPerMeasure: Std.int(beatsPerMeasureStepper.value),
stepsPerBeat: Std.int(stepsPerBeatStepper.value),
displayName: displayNameTextBox.label.text,
icon: iconTextBox.label.text,
color: colorWheel.curColor,
opponentModeAllowed: opponentModeCheckbox.checked,
coopAllowed: coopAllowedCheckbox.checked,
difficulties: [for (diff in difficultiesTextBox.label.text.split(",")) diff.trim()],
};
if (onSave != null) onSave({
meta: meta,
instBytes: instExplorer.file,
voicesBytes: voicesExplorer.file
}, null);
}
}
function saveFromVSlice(files:Map<String, Any>, ?name:String) {
var songId = name == null && files.exists("manifest.json") ? Json.parse(files.get("manifest.json")).songId : name;
var vslicemeta:SwagMetadata = Json.parse(files.get('${songId}-metadata.json'));
var vslicechart:NewSwagSong = Json.parse(files.get('${songId}-chart.json'));
var playData = vslicemeta.playData;
var meta:ChartMetaData = {name: songId};
var diffCharts:Array<ChartDataWithInfo> = [];
VSliceParser.parse(vslicemeta, vslicechart, meta, diffCharts, songId);
if (onSave != null) onSave({
meta: meta,
instBytes: instExplorer.file,
voicesBytes: voicesExplorer.file
instBytes: files.get('Inst.${Flags.SOUND_EXT}'),
voicesBytes: files.get('Voices.${Flags.SOUND_EXT}'), //it may exist
playerVocals: files.get('Voices-${playData.characters.playerVocals != null ? playData.characters.playerVocals[0] : playData.characters.player}.${Flags.SOUND_EXT}'),
oppVocals: files.get('Voices-${playData.characters.opponentVocals != null ? playData.characters.opponentVocals[0] : playData.characters.opponent}.${Flags.SOUND_EXT}'),
}, (songFolder:String, creation:SongCreationData) -> {
#if sys
for (diff in diffCharts) CoolUtil.safeSaveFile('$songFolder/charts/${diff.diffName}.json', Json.stringify(diff.chart, Flags.JSON_PRETTY_PRINT));
#end
});
}
}
+6 -5
View File
@@ -54,6 +54,7 @@ class UIAudioPlayer extends UIButton {
timeBarPlayer = new FlxSprite(timeBar.x, timeBar.y).loadGraphic(Paths.image('editors/ui/audio-time-empty'));
timeBarPlayer.colorTransform.color = 0x440364;
timeBarPlayer.scale.y = (timeBarPlayer.frameHeight + 2) / timeBarPlayer.frameHeight;
timeBarPlayer.pixelPerfectRender = true;
members.push(timeBarPlayer);
timeBarSpr = cast new UISprite(timeBar.x, timeBar.y).makeSolid(timeBar.barWidth, timeBar.barHeight, 0x00FFFFFF);
@@ -87,13 +88,13 @@ class UIAudioPlayer extends UIButton {
playingSprite.animation.play(sound.playing ? "playing" : "paused");
timeText.text = '${FlxStringUtil.formatTime(sound.time/1000, true)} / ${FlxStringUtil.formatTime(sound.length/1000)}';
if(timeBarPlayer.clipRect == null)
timeBarPlayer.clipRect = new FlxRect(0, 0, timeBarPlayer.frameWidth, timeBarPlayer.frameHeight);
if(timeBarPlayer.rawClipRect == null)
timeBarPlayer.rawClipRect = new FlxRect(0, 0, timeBarPlayer.frameWidth, timeBarPlayer.frameHeight);
timeBarPlayer.clipRect.x = timeBarPlayer.frameWidth * (sound.time/sound.length);
timeBarPlayer.clipRect.width = 2;
timeBarPlayer.rawClipRect.x = timeBarPlayer.frameWidth * (sound.time/sound.length);
timeBarPlayer.rawClipRect.width = 2;
timeBarPlayer.clipRect = timeBarPlayer.clipRect;
timeBarPlayer.rawClipRect = timeBarPlayer.rawClipRect;
nextPlayerColor = sound.playing ? 0x732D95 : 0x440364;
timeBarPlayer.colorTransform.color = FlxColor.interpolate(timeBarPlayer.colorTransform.color, nextPlayerColor, 1/14);
+5 -1
View File
@@ -15,16 +15,19 @@ class UIFileExplorer extends UISliceSprite {
public var onFile:(String, Bytes)->Void;
public var uiElement:FlxSprite;
public var fileType:String = "txt";
public function new(x:Float, y:Float, ?w:Int, ?h:Int, fileType:String = "txt", ?onFile:(String, Bytes)->Void) {
super(x, y, (w != null ? w : 320), (h != null ? h : 58), 'editors/ui/inputbox');
this.fileType = fileType;
if (onFile != null) this.onFile = onFile;
uploadButton = new UIButton(x + 8, y+ 8, null, function () {
var fileDialog = new FileDialog();
fileDialog.onSelect.add(loadFile);
fileDialog.browse(OPEN, fileType);
fileDialog.browse(OPEN, this.fileType);
}, bWidth - 16, bHeight - 16);
members.push(uploadButton);
@@ -66,6 +69,7 @@ class UIFileExplorer extends UISliceSprite {
}
public function removeFile() {
if (!selectable) return;
if (uiElement != null) {
members.remove(uiElement);
uiElement.destroy();
+21 -24
View File
@@ -101,30 +101,8 @@ class GameOverSubstate extends MusicBeatSubstate
if (__cancelDefault)
return;
if (controls.ACCEPT)
{
endBullshit();
}
if (controls.BACK)
{
if (PlayState.chartingMode && Charter.undos.unsaved)
game.saveWarn(false);
else {
PlayState.resetSongInfos();
if (Charter.instance != null) Charter.instance.__clearStatics();
if (FlxG.sound.music != null)
FlxG.sound.music.stop();
FlxG.sound.music = null;
if (PlayState.isStoryMode)
FlxG.switchState(new StoryMenuState());
else
FlxG.switchState(new FreeplayState());
}
}
if (controls.ACCEPT) endBullshit();
if (controls.BACK) exit();
if (!isEnding && ((!lossSFX.playing) || (character.getAnimName() == "firstDeath" && character.isAnimFinished())) && (FlxG.sound.music == null || !FlxG.sound.music.playing))
{
@@ -193,6 +171,25 @@ class GameOverSubstate extends MusicBeatSubstate
});
}
function exit()
{
var event = new CancellableEvent();
gameoverScript.call('onReturnToMenu', [event]);
if (event.cancelled)
return;
if (PlayState.chartingMode && Charter.undos.unsaved) game.saveWarn(false);
else {
if (Charter.instance != null) Charter.instance.__clearStatics();
if (FlxG.sound.music != null) FlxG.sound.music.stop();
FlxG.sound.music = null;
FlxG.switchState(PlayState.isStoryMode ? new StoryMenuState() : new FreeplayState());
}
}
override function destroy()
{
gameoverScript.call("destroy");
+406 -50
View File
@@ -1,12 +1,32 @@
package funkin.game;
import flixel.graphics.FlxGraphic;
import flixel.math.FlxPoint;
import flixel.util.typeLimit.OneOfTwo;
import funkin.backend.scripting.events.healthicon.HealthIconChangeEvent;
class HealthIcon extends FlxSprite
class HealthIcon extends FunkinSprite
{
/**
* Used for FreeplayState! If you use it elsewhere, prob gonna annoying
* Attaches the icon to a sprite, following it's position
*/
public var sprTracker:FlxSprite;
/**
* Where to place the icon in relation to the sprite
*
* LEFT: Left of the sprite
*
* CENTER: Center of the sprite
*
* RIGHT: Right of the sprite
*/
public var sprTrackerAlignment:TrackerAlignment = RIGHT;
/**
* Offset of the icon in relation to the sprite
*
* By default it is set to (10, -30) and is intended to be used with Alphabet
*/
public var sprTrackerOffset:FlxPoint = new FlxPoint(10, -30);
/**
* The currently showing icon
@@ -20,31 +40,60 @@ class HealthIcon extends FlxSprite
/**
* Health steps in this format:
* Min Percentage => Frame Index
*
* Min Percentage => Frame Index / Animation Name
*/
public var healthSteps:Map<Int, Int> = null;
public var healthSteps:Map<Int, OneOfTwo<String, Int>> = null;
/**
* current animation state
* Current animation state
*/
public var curAnimState:Int = -1;
public var curAnimState:OneOfTwo<String, Int> = -1;
/**
* The Default Scale For The Icon
*
* This is what scale the icon should return to when its bump animation is finished
*/
public var defaultScale:Float = 1;
/**
* Whenever or not the icon is animated or not
*/
public var animated:Bool = false;
/**
* XML Animated Icon data
*
* `null` if the icon is not animated or its invalid
*/
public var xmlData:Xml;
/**
* Extra offsets to add when updating the hitbox
*/
public var extraOffsets:FlxPoint = FlxPoint.get();
/**
* Helper for HScript who can't make maps
* @param steps Something like this: `[[0, 1], [20, 0]]`
*
* THIS IS DEPRECATED AND WILL BE REMOVED IN THE FUTURE
*
* Please set the healthSteps directly in the script instead, hscript does support maps
*
* @param steps Something like this: `[[0, 1], [20, 0]]` or `[[0, "losing"], [20, "neutral"]]` for animated icons
*/
public function setHealthSteps(steps:Array<Array<Int>>) { // helper for hscript that can't do maps
@:dox(hide) @:noCompletion @:deprecated("use healthSteps instead")
public function setHealthSteps(steps:Array<Array<OneOfTwo<String, Int>>>):Void { // helper for hscript that can't do maps
if (steps == null) return;
healthSteps = [];
for(s in steps)
for (s in steps)
if (s.length > 1)
healthSteps[s[0]] = s[1];
var am = 0;
for(k=>e in healthSteps) am++;
if (am <= 0) healthSteps = [
0 => 1, // losing icon
20 => 0, // normal icon
if (Lambda.count(healthSteps) <= 0) healthSteps = [
0 => (this.animated ? "losing" : 1), // losing icon
20 => (this.animated ? "neutral" : 0), // normal icon
];
}
@@ -58,50 +107,357 @@ class HealthIcon extends FlxSprite
scrollFactor.set();
}
public function setIcon(char:String, width:Int = 150, height:Int = 150) {
if(curCharacter != char || this.width != width || this.height != height) {
curCharacter = char;
var path = Paths.image('icons/$char');
if (!Assets.exists(path)) path = Paths.image('icons/' + Flags.DEFAULT_HEALTH_ICON);
loadGraphic(path, true, width, height);
animation.add(char, [for(i in 0...frames.frames.length) i], 0, false, isPlayer);
antialiasing = true;
animation.play(char);
healthSteps = [
0 => 1, // losing icon
20 => 0, // normal icon
];
if (frames.frames.length >= 3)
healthSteps[80] = 2; // winning icon
}
/**
* Called every beat, and causes the icon to become bigger
**/
public dynamic function bump():Void {
var iconScale = Flags.BOP_ICON_SCALE;
scale.set(defaultScale * iconScale, defaultScale * iconScale);
updateHitbox();
}
override function update(elapsed:Float)
{
/**
* Called every frame and causes the icon to become smaller
**/
public dynamic function updateBump():Void {
var iconLerp = Flags.ICON_LERP;
scale.set(CoolUtil.fpsLerp(scale.x, defaultScale, iconLerp), CoolUtil.fpsLerp(scale.y, defaultScale, iconLerp));
updateHitbox();
}
/**
* Sets the icon to the specified character
* @param char Character to set the icon to
* @param allowAnimated Whenever the icon can be animated
**/
public function setIcon(char:String, allowAnimated:Bool = true):Void {
if (curCharacter == char) return;
var oldIconPath = 'icons/$char';
var newIconPath = 'icons/$char/icon';
if (!Assets.exists(Paths.image(oldIconPath)) && !Assets.exists(Paths.image(newIconPath))) {
char = 'face';
oldIconPath = 'icons/$char';
newIconPath = 'icons/$char/icon';
}
curCharacter = char;
var iconPath = Assets.exists(Paths.image(oldIconPath)) ? oldIconPath : newIconPath;
var iconXmlPath = Paths.getPath('images/icons/$char/data.xml');
var iconFoundData = Assets.exists(iconXmlPath);
try {
xmlData = iconFoundData ? Xml.parse(Assets.getText(iconXmlPath)) : null;
} catch(e) {
Logs.trace('Error while parsing icon data for $char: ${e.message}', ERROR);
xmlData = null;
}
var xmlValid = xmlData != null && (xmlData = xmlData.firstElement()) != null;
this.animated = allowAnimated && iconFoundData && xmlValid && Paths.framesExists(Paths.image(newIconPath), true, true, true);
var iconAmt:Int = 0;
var iconSize:Int = 0;
var iconIsPlayer = xmlValid ? xmlData.get("facing").getDefault("right").toLowerCase() == "left" : false;
animateAtlas = null; // reset
if (this.animated)
loadSprite(Paths.image(newIconPath));
else {
var iconAsset:FlxGraphic = FlxG.bitmap.add(Paths.image(iconPath));
var assetW:Float = iconAsset.width;
var assetH:Float = iconAsset.height;
iconAmt = Math.round(assetW / assetH); // Just in case the icon is in a weird aspect ratio
iconSize = Math.floor(assetW / iconAmt);
if (iconSize * iconAmt > assetW) {
iconSize = Math.floor(assetW / iconAmt);
iconAmt = Math.floor(assetW / iconSize);
}
loadGraphic(iconAsset, true, Std.int(Math.min(iconSize, assetW)), Std.int(Math.min(iconSize, assetH)));
setGraphicSize(150);
updateHitbox();
animation.add(char, [for(i in 0...iconAmt) i], 0, false, isPlayer != iconIsPlayer);
animation.play(char);
}
if(!animation.onFinishEnd.has(animFinishCallback))
animation.onFinishEnd.add(animFinishCallback);
if(animateAtlas != null && !animateAtlas.anim.onFinishEnd.has(animFinishCallback))
animateAtlas.anim.onFinishEnd.add(animFinishCallback);
var parsedSteps:Map<Int, String> = [];
antialiasing = true;
if (xmlValid) {
if (xmlData.exists("antialiasing"))
antialiasing = xmlData.get("antialiasing").toLowerCase() == "true";
if (xmlData.exists("offsetX"))
extraOffsets.x = Std.parseFloat(xmlData.get("offsetX")).getDefault(0);
if (xmlData.exists("offsetY"))
extraOffsets.y = Std.parseFloat(xmlData.get("offsetY")).getDefault(0);
for (node in xmlData.elements())
switch(node.nodeName) {
case "transition":
if (this.animated == false) {
Logs.trace('Icon ${char} data <transition> is not allowed when not animated', WARNING);
continue;
}
if (!node.exists("anim")) {
Logs.trace('Icon ${char} data <transition> is missing anim', WARNING);
continue;
}
if (!node.exists("to")) {
Logs.trace('Icon ${char} data <transition> is missing to', WARNING);
continue;
}
if (!node.exists("from")) {
Logs.trace('Icon ${char} data <transition> is missing from', WARNING);
continue;
}
var animName = 'from-${node.get("from")}-to-${node.get("to")}';
var offsetX:Float = 0;
var offsetY:Float = 0;
if (node.exists("offsetX"))
offsetX = Std.parseFloat(node.get("offsetX")).getDefault(0);
else if (node.exists("offsetx"))
offsetX = Std.parseFloat(node.get("offsetx")).getDefault(0);
if (node.exists("offsetY"))
offsetY = Std.parseFloat(node.get("offsetY")).getDefault(0);
else if (node.exists("offsety"))
offsetY = Std.parseFloat(node.get("offsety")).getDefault(0);
addOffset(animName, offsetX, offsetY);
addAnim(animName, node.get("anim"), Std.parseInt(node.get("fps")).getDefault(24), false); // don't allow looping for transitions
if (animateAtlas == null && animation.exists(animName))
animation.getByName(animName).flipX = isPlayer != iconIsPlayer;
case "anim":
if (this.animated == false) {
Logs.trace('Icon ${char} data <anim> is not allowed when not animated', WARNING);
continue;
}
if (!node.exists("name")) {
Logs.trace('Icon ${char} data <anim> is missing name', WARNING);
continue;
}
if (!node.exists("anim")) {
Logs.trace('Icon ${char} data <anim> is missing anim', WARNING);
continue;
}
var animName = node.get("name");
var offsetX:Float = 0;
var offsetY:Float = 0;
if (node.exists("offsetX"))
offsetX = Std.parseFloat(node.get("offsetX")).getDefault(0);
else if (node.exists("offsetx"))
offsetX = Std.parseFloat(node.get("offsetx")).getDefault(0);
if (node.exists("offsetY"))
offsetY = Std.parseFloat(node.get("offsetY")).getDefault(0);
else if (node.exists("offsety"))
offsetY = Std.parseFloat(node.get("offsety")).getDefault(0);
addOffset(animName, offsetX, offsetY);
var looped:Bool = false;
if (node.exists("looped"))
looped = node.get("looped").toLowerCase() == "true";
else if (node.exists("loop"))
looped = node.get("loop").toLowerCase() == "true";
addAnim(animName, node.get("anim"), Std.parseInt(node.get("fps")).getDefault(24), looped);
if (animateAtlas == null && animation.exists(animName))
animation.getByName(animName).flipX = isPlayer != iconIsPlayer;
case "step":
if (!node.exists("percent")) {
Logs.trace('Icon ${char} data <step> is missing percent', WARNING);
continue;
}
if (!node.exists("name")) {
Logs.trace('Icon ${char} data <step> is missing name', WARNING);
continue;
}
parsedSteps.set(Std.parseInt(node.get("percent")).getDefault(0), node.get("name"));
}
}
if (Lambda.count(parsedSteps) > 0) {
healthSteps = parsedSteps;
} else {
var hasLosing = this.animated ? hasAnim("losing") : iconAmt >= 2;
var hasWinning = this.animated ? hasAnim("winning") : iconAmt >= 3;
if (hasLosing) {
healthSteps = [
0 => (this.animated ? "losing" : 1), // losing icon
20 => (this.animated ? "neutral" : 0), // normal icon
];
} else {
healthSteps = [
0 => (this.animated ? "neutral" : 0), // normal icon
];
}
if (hasWinning)
healthSteps.set(80, this.animated ? "winning" : 2); // winning icon
}
var data = getIconAnim(health);
if (data.isValid) {
if (this.animated)
playAnim(data.animState);
else
animation.curAnim.curFrame = data.animState;
curAnimState = data.animState;
}
if (animateAtlas != null) {
@:bypassAccessor
frameWidth = 150;
@:bypassAccessor
frameHeight = 150;
extraOffsets.x -= frameWidth / 2;
extraOffsets.y -= frameHeight / 2;
updateHitbox();
} else {
setGraphicSize(150);
updateHitbox();
}
defaultScale = (xmlValid && xmlData.exists("scale")) ? Std.parseFloat(xmlData.get("scale")).getDefault(scale.x) : scale.x;
scale.set(defaultScale, defaultScale);
updateHitbox();
}
var normalizedNames = ["neutral", "losing", "winning"];
private function normalizeAnim(anim:OneOfTwo<String, Int>):OneOfTwo<String, Int> {
if(this.animated) {
if (anim is Int) {
var _:Int = cast anim;
if(_ >= 0 && _ < normalizedNames.length)
anim = normalizedNames[anim];
}
} else {
if (anim is String) {
var _ = normalizedNames.indexOf(cast anim);
if(_ >= 0)
anim = _;
}
}
return anim;
}
/**
* Gets the animation data for a specific health percentage
* @param health Health percentage
* @return Animation data (-1 if invalid)
*/
public dynamic function getIconAnim(health:Float):IconAnimData {
var i:OneOfTwo<String, Int> = -1;
var oldKey:Int = -1;
var isValid = false;
for (k=>icon in healthSteps) if (k > oldKey && k <= health * 100) {
oldKey = k;
i = icon;
isValid = true;
}
i = normalizeAnim(i);
return new IconAnimData(i, isValid);
}
override function update(elapsed:Float):Void {
super.update(elapsed);
if (sprTracker != null)
setPosition(sprTracker.x + sprTracker.width + 10, sprTracker.y - 30);
if (sprTracker != null) {
setPosition(sprTracker.x + (switch sprTrackerAlignment {
case LEFT: -width;
case CENTER: (sprTracker.width - width) / 2;
case RIGHT: sprTracker.width;
}) + sprTrackerOffset.x, sprTracker.y + sprTrackerOffset.y);
}
if (animation.curAnim != null) {
var i:Int = -1;
var oldKey:Int = -1;
for (k=>icon in healthSteps) if (k > oldKey && k <= health * 100) {
oldKey = k;
i = icon;
}
if (i >= 0 && curAnimState != i) {
var event = EventManager.get(funkin.backend.scripting.events.healthicon.HealthIconChangeEvent).recycle(i, this);
if (animation.curAnim != null || (this.animated && animateAtlas != null)) {
var data = getIconAnim(health);
var localAnimState = data.animState;
if (data.isValid && curAnimState != localAnimState) {
var event = EventManager.get(HealthIconChangeEvent).recycle(localAnimState, this);
funkin.backend.scripting.GlobalScript.event("onHealthIconAnimChange", event);
if (!event.cancelled)
animation.curAnim.curFrame = event.amount;
if (!event.cancelled) {
if (this.animated) {
var transAnim = 'from-$curAnimState-to-${event.anim}';
playAnim(hasAnim(transAnim) ? transAnim : event.anim);
} else {
if(animation.curAnim != null)
animation.curAnim.curFrame = event.anim;
}
}
curAnimState = event.amount;
curAnimState = localAnimState;
}
}
}
function animFinishCallback(anim:String):Void {
if (this.animated)
if (anim.startsWith("from-"))
playAnim(anim.substr(anim.lastIndexOf('-') + 1));
}
override function updateHitbox():Void {
super.updateHitbox();
offset += extraOffsets;
}
}
@:dox(hide)
class IconAnimData {
public var animState:OneOfTwo<String, Int>;
public var isValid:Bool;
public function new(animState:OneOfTwo<String, Int>, isValid:Bool) {
this.animState = animState;
this.isValid = isValid;
}
public function toString():String {
return '$animState (Valid: $isValid)';
}
}
/**
* Used for `funkin.game.HealthIcon.sprTrackerAlignment`.
* This determines the position of the icon in relation to the sprite tracker.
**/
enum abstract TrackerAlignment(Int) {
/**
* Left of the sprite tracker
*
* Mathematically: `tracker.x - icon.width`
*/
var LEFT = 0;
/**
* Center of the sprite tracker
*
* Mathematically: `tracker.x + (tracker.width - icon.width) / 2`
*/
var CENTER = 1;
/**
* Right of the sprite tracker
*
* Mathematically: `tracker.x + tracker.width`
*/
var RIGHT = 2;
}
+23 -22
View File
@@ -75,6 +75,11 @@ class Note extends FlxSprite
public var scrollSpeed:Null<Float> = null;
public var noteAngle:Null<Float> = null;
public var copyStrumAngle:Bool = true;
public var updateNotesPosX:Bool = true;
public var updateNotesPosY:Bool = true;
public var updateFlipY:Bool = true;
public var noteType(get, never):String;
@:dox(hide) public var __strumCameras:Array<FlxCamera> = null;
@@ -146,7 +151,9 @@ class Note extends FlxSprite
case 0:
animation.addByPrefix('scroll', 'purple0');
animation.addByPrefix('hold', 'purple hold piece');
animation.addByPrefix('holdend', 'pruple end hold');
animation.addByPrefix("holdend", "pruple end hold");
if (animation.exists("holdend") != true) // null or false
animation.addByPrefix('holdend', 'purple hold end');
case 1:
animation.addByPrefix('scroll', 'blue0');
animation.addByPrefix('hold', 'blue hold piece');
@@ -191,7 +198,7 @@ class Note extends FlxSprite
}
public var lastScrollSpeed:Null<Float> = null;
public var gapFix:Single = 0;
public var gapFix:SingleOrFloat = 0;
public var useAntialiasingFix(get, set):Bool;
inline function set_useAntialiasingFix(v:Bool) {
if(v != useAntialiasingFix) {
@@ -211,7 +218,7 @@ class Note extends FlxSprite
override function drawComplex(camera:FlxCamera) {
var downscrollCam = (camera is HudCamera ? ({var _:HudCamera=cast camera;_;}).downscroll : false);
flipY = (isSustainNote && flipSustain) && (downscrollCam != (__strum != null && __strum.getScrollSpeed(this) < 0));
if (updateFlipY) flipY = (isSustainNote && flipSustain) && (downscrollCam != (__strum != null && __strum.getScrollSpeed(this) < 0));
if (downscrollCam) {
frameOffset.y += __notePosFrameOffset.y * 2;
super.drawComplex(camera);
@@ -267,30 +274,22 @@ class Note extends FlxSprite
public function updateSustain(strum:Strum) {
var scrollSpeed = strum.getScrollSpeed(this);
var len = 0.45 * CoolUtil.quantize(scrollSpeed, 100);
if (nextSustain != null && lastScrollSpeed != scrollSpeed) {
// is long sustain
if (lastScrollSpeed != scrollSpeed) {
lastScrollSpeed = scrollSpeed;
scale.y = (sustainLength * len) / frameHeight;
updateHitbox();
scale.y += gapFix / frameHeight;
if (nextSustain != null) {
scale.y = (sustainLength * 0.45 * scrollSpeed) / frameHeight;
updateHitbox();
scale.y += gapFix / frameHeight;
}
}
if (!wasGoodHit) return;
var t = CoolUtil.bound((Conductor.songPosition - strumTime) / (height) * len, 0, 1);
var swagRect = this.clipRect == null ? new FlxRect() : this.clipRect;
swagRect.x = 0;
swagRect.y = t * frameHeight;
swagRect.width = frameWidth;
swagRect.height = frameHeight;
setClipRect(swagRect);
updateSustainClip();
}
public inline function setClipRect(rect:FlxRect) {
this.clipRect = rect;
public function updateSustainClip() if (wasGoodHit) {
var t = FlxMath.bound((Conductor.songPosition - strumTime) / height * 0.45 * lastScrollSpeed, 0, 1);
var rect = clipRect == null ? FlxRect.get() : clipRect;
clipRect = rect.set(0, frameHeight * t, frameWidth, frameHeight * (1 - t));
}
@:noCompletion
@@ -306,5 +305,7 @@ class Note extends FlxSprite
public override function destroy() {
super.destroy();
clipRect = FlxDestroyUtil.put(clipRect);
}
}
+278 -134
View File
@@ -7,6 +7,7 @@ import flixel.math.FlxPoint;
import flixel.sound.FlxSound;
import flixel.text.FlxText;
import flixel.tweens.FlxTween;
import flixel.tweens.misc.VarTween;
import flixel.ui.FlxBar;
import flixel.util.FlxColor;
import flixel.util.FlxSort;
@@ -18,9 +19,9 @@ import funkin.backend.chart.EventsData;
import funkin.backend.scripting.DummyScript;
import funkin.backend.scripting.Script;
import funkin.backend.scripting.ScriptPack;
import funkin.backend.scripting.events.*;
import funkin.backend.scripting.events.gameplay.*;
import funkin.backend.scripting.events.note.*;
import funkin.backend.scripting.events.*;
import funkin.backend.system.Conductor;
import funkin.backend.system.RotatingSpriteGroup;
import funkin.editors.SaveWarning;
@@ -29,7 +30,7 @@ import funkin.editors.charter.CharterSelection;
import funkin.game.SplashHandler;
import funkin.game.cutscenes.*;
import funkin.menus.*;
import funkin.menus.StoryMenuState.WeekData;
import funkin.backend.week.WeekData;
import funkin.savedata.FunkinSave;
import haxe.io.Path;
@@ -69,15 +70,15 @@ class PlayState extends MusicBeatState
*/
public static var fromMods:Bool = false;
/**
* Whenever Charting Mode has been enabled for this song.
* Whenever Charting Mode is enabled for this song.
*/
public static var chartingMode:Bool = false;
/**
* Whenever the song has been started with opponent mode on.
* Whenever the song is started with opponent mode on.
*/
public static var opponentMode:Bool = Flags.DEFAULT_OPPONENT_MODE;
/**
* Whenever the song has been started with co-op mode on.
* Whenever the song is started with co-op mode on.
*/
public static var coopMode:Bool = Flags.DEFAULT_COOP_MODE;
@@ -138,8 +139,8 @@ class PlayState extends MusicBeatState
*/
public var downscroll(get, set):Bool;
@:dox(hide) private function set_downscroll(v:Bool) {return camHUD.downscroll = v;}
@:dox(hide) private function get_downscroll():Bool {return camHUD.downscroll;}
private inline function set_downscroll(v:Bool) return camHUD.downscroll = v;
private inline function get_downscroll():Bool return camHUD.downscroll;
/**
* Instrumental sound (Inst.ogg).
@@ -225,23 +226,6 @@ class PlayState extends MusicBeatState
*/
public var doIconBop:Bool = true;
/**
* Whenever cam zooming is enabled, enables on a note hit if not cancelled.
*/
public var camZooming:Bool = false;
/**
* Interval of cam zooming (beats).
* For example: if set to 4, the camera will zoom every 4 beats.
*/
public var camZoomingInterval:Int = Flags.DEFAULT_CAM_ZOOM_INTERVAL;
/**
* How strong the cam zooms should be (defaults to 1)
*/
public var camZoomingStrength:Float = Flags.DEFAULT_CAM_ZOOM_STRENGTH;
/**
* Maximum amount of zoom for the camera.
*/
public var maxCamZoom:Float = Flags.MAX_CAMERA_ZOOM;
/**
* Current song name (lowercase)
*/
@@ -263,12 +247,12 @@ class PlayState extends MusicBeatState
/**
* Current health. Goes from 0 to maxHealth (defaults to 2)
*/
public var health:Float = 1;
public var health(default, set):Float = 1;
/**
* Maximum health the player can have. Defaults to 2.
*/
@:isVar public var maxHealth(get, set):Float = Flags.DEFAULT_MAX_HEALTH;
public var maxHealth(default, set):Float = Flags.DEFAULT_MAX_HEALTH;
/**
* Current combo.
*/
@@ -304,6 +288,11 @@ class PlayState extends MusicBeatState
* Opponent's icon
*/
public var iconP2:HealthIcon;
/**
* Every active icon that will be updated during gameplay (defaults to `iconP1` and `iconP1` between `create` and `postCreate` in scripts)
*/
public var iconArray:Array<HealthIcon> = [];
/**
* Camera for the HUD (notes, misses).
*/
@@ -383,6 +372,30 @@ class PlayState extends MusicBeatState
*/
public var camHUDZoomLerp:Float = Flags.DEFAULT_HUD_ZOOM_LERP;
/**
* Whenever cam zooming is enabled, enables on a note hit if not cancelled.
*/
public var camZooming:Bool = false;
/**
* Interval of cam zooming (beats).
* For example: if set to 4, the camera will zoom every 4 beats.
*/
public var camZoomingInterval:Int = Flags.DEFAULT_CAM_ZOOM_INTERVAL;
/**
* How strong the cam zooms should be (defaults to 1)
*/
public var camZoomingStrength:Float = Flags.DEFAULT_CAM_ZOOM_STRENGTH;
/**
* Default multiplier for `maxCamZoom`.
*/
public var maxCamZoomMult:Float = Flags.MAX_CAMERA_ZOOM_MULT;
/**
* Maximum amount of zoom for the camera (based on `maxCamZoomMult` and the camera's zoom IF not set).
*/
public var maxCamZoom(get, default):Float = Math.NaN;
private inline function get_maxCamZoom() return Math.isNaN(maxCamZoom) ? maxCamZoomMult * defaultCamZoom : maxCamZoom;
/**
* Zoom for the pixel assets.
*/
@@ -532,10 +545,10 @@ class PlayState extends MusicBeatState
*/
public function updateRating() {
var rating = null;
var acc = get_accuracy();
var acc = accuracy; // caching since it has a getter with an operation - Nex
for(e in comboRatings)
if (e.percent <= acc && (rating == null || rating.percent < e.percent))
if (comboRatings != null && comboRatings.length > 0) for (e in comboRatings)
if ((e.percent <= acc && e.maxMisses >= misses) && (rating == null || (rating.percent < e.percent && e.maxMisses >= misses)))
rating = e;
var event = gameAndCharsEvent("onRatingUpdate", EventManager.get(RatingUpdateEvent).recycle(rating, curRating));
@@ -543,13 +556,13 @@ class PlayState extends MusicBeatState
curRating = event.rating;
}
private inline function get_maxHealth()
return this.maxHealth;
private function set_maxHealth(v:Float) {
if (healthBar != null && healthBar.max == this.maxHealth) {
healthBar.setRange(healthBar.min, v);
}
return this.maxHealth = v;
private inline function set_health(v:Float)
return health = FlxMath.bound(v, 0, maxHealth);
private inline function set_maxHealth(v:Float) {
if (healthBar != null && healthBar.max == maxHealth) healthBar.setRange(healthBar.min, v);
maxHealth = v;
health = health; // running the setter - Nex
return maxHealth;
}
private inline function get_curStage()
@@ -581,6 +594,7 @@ class PlayState extends MusicBeatState
@:dox(hide) override public function create()
{
Note.__customNoteTypeExists = [];
// SCRIPTING & DATA INITIALIZATION
#if REGION
instance = this;
@@ -609,8 +623,8 @@ class PlayState extends MusicBeatState
detailsText = isStoryMode ? ("Story Mode: " + storyWeek.name) : "Freeplay";
// Checks if cutscene files exists
var cutscenePath = Paths.script('songs/${SONG.meta.name.toLowerCase()}/cutscene');
var endCutscenePath = Paths.script('songs/${SONG.meta.name.toLowerCase()}/cutscene-end');
var cutscenePath = Paths.script('songs/${SONG.meta.name}/cutscene');
var endCutscenePath = Paths.script('songs/${SONG.meta.name}/cutscene-end');
if (Assets.exists(cutscenePath)) cutscene = cutscenePath;
if (Assets.exists(endCutscenePath)) endCutscene = endCutscenePath;
@@ -637,10 +651,11 @@ class PlayState extends MusicBeatState
// case "":
// ADD YOUR HARDCODED SCRIPTS HERE!
default:
var scriptsFolders:Array<String> = ['songs/${SONG.meta.name.toLowerCase()}/scripts', 'data/charts/', 'songs/'];
var normal = 'songs/${SONG.meta.name}/scripts';
var scriptsFolders:Array<String> = [normal, normal + '/$difficulty/', 'data/charts/', 'songs/'];
for(folder in scriptsFolders) {
for(file in Paths.getFolderContent(folder, true, fromMods ? MODS : BOTH)) {
for (folder in scriptsFolders) {
for (file in Paths.getFolderContent(folder, true, fromMods ? MODS : BOTH)) {
if (folder == 'data/charts/')
Logs.warn('data/charts/ is deprecated and will be removed in the future. Please move script $file to songs/', DARKYELLOW, "PlayState");
@@ -701,7 +716,7 @@ class PlayState extends MusicBeatState
case 2: "girlfriend";
}) : strumLine.position;
if (strumLine.characters != null) for(k=>charName in strumLine.characters) {
var char = new Character(0, 0, charName, stage.isCharFlipped(charPosName, strumLine.type == 1));
var char = new Character(0, 0, charName, stage.isCharFlipped(stage.characterPoses[charName] != null ? charName : charPosName, strumLine.type == 1));
stage.applyCharStuff(char, charPosName, k);
chars.push(char);
}
@@ -782,9 +797,9 @@ class PlayState extends MusicBeatState
if (Flags.DEFAULT_HEALTH != null) health = Flags.DEFAULT_HEALTH;
else health = maxHealth / 2;
iconP1 = new HealthIcon(boyfriend != null ? boyfriend.getIcon() : Flags.DEFAULT_HEALTH_ICON, true);
iconP2 = new HealthIcon(dad != null ? dad.getIcon() : Flags.DEFAULT_HEALTH_ICON, false);
for(icon in [iconP1, iconP2]) {
iconArray.push(iconP1 = new HealthIcon(boyfriend != null ? boyfriend.getIcon() : Flags.DEFAULT_HEALTH_ICON, true));
iconArray.push(iconP2 = new HealthIcon(dad != null ? dad.getIcon() : Flags.DEFAULT_HEALTH_ICON, false));
for (icon in iconArray) {
icon.y = healthBar.y - (icon.height / 2);
add(icon);
}
@@ -801,7 +816,8 @@ class PlayState extends MusicBeatState
scoreTxt.alignment = RIGHT;
missesTxt.alignment = CENTER;
accuracyTxt.alignment = LEFT;
updateRatingStuff();
if (updateRatingStuff != null)
updateRatingStuff();
for(e in [healthBar, healthBarBG, iconP1, iconP2, scoreTxt, missesTxt, accuracyTxt])
e.cameras = [camHUD];
@@ -838,7 +854,8 @@ class PlayState extends MusicBeatState
// Make icons appear in the correct spot during cutscenes
healthBar.update(0);
updateIconPositions();
if (updateIconPositions != null)
updateIconPositions();
__updateNote_event = EventManager.get(NoteUpdateEvent);
@@ -847,6 +864,7 @@ class PlayState extends MusicBeatState
/**
* Function used to update Discord Presence.
*
* This function is dynamic, which means you can do `updateDiscordPresence = function() {}` in scripts.
*/
public dynamic function updateDiscordPresence()
@@ -867,7 +885,7 @@ class PlayState extends MusicBeatState
return;
}
var songName = SONG.meta.name.toLowerCase();
var songName = SONG.meta.name;
if (cutsceneScriptPath == null)
cutsceneScriptPath = Paths.script('songs/$songName/${prefix}cutscene');
@@ -1027,10 +1045,11 @@ class PlayState extends MusicBeatState
Note.__customNoteTypeExists = [];
}
public static function resetSongInfos() {
deathCounter = 0;
seenCutscene = false;
}
@:dox(hide) @:deprecated("scrollSpeedTween is deprecated, use eventsTween['scrollSpeed'] instead")
public var scrollSpeedTween(get, set):FlxTween;
private inline function get_scrollSpeedTween() return eventsTween.get("scrollSpeed");
private inline function set_scrollSpeedTween(val:FlxTween) {eventsTween.set("scrollSpeed", val); return val;}
// End of backwards compat
@:dox(hide) private function generateSong(?songData:ChartData):Void
{
@@ -1223,7 +1242,7 @@ class PlayState extends MusicBeatState
if (SaveWarning.selectionClass != null) FlxG.switchState(Type.createInstance(SaveWarning.selectionClass, []));
} else {
WindowUtils.preventClosing = false; WindowUtils.resetClosing();
Sys.exit(0);
openfl.system.System.exit(0);
}
}
},
@@ -1236,7 +1255,7 @@ class PlayState extends MusicBeatState
if (SaveWarning.selectionClass != null) FlxG.switchState(Type.createInstance(SaveWarning.selectionClass, []));
} else {
WindowUtils.preventClosing = false; WindowUtils.resetClosing();
Sys.exit(0);
openfl.system.System.exit(0);
}
}
},
@@ -1250,21 +1269,20 @@ class PlayState extends MusicBeatState
]));
}
function updateIconPositions() {
dynamic function updateIconPositions() {
var iconOffset = Flags.ICON_OFFSET;
var healthBarPercent = healthBar.percent;
var center:Float = healthBar.x + healthBar.width * FlxMath.remapToRange(healthBar.percent, 0, 100, 1, 0);
var center:Float = healthBar.x + healthBar.width * FlxMath.remapToRange(healthBarPercent, 0, 100, 1, 0);
iconP1.x = center - iconOffset;
iconP2.x = center - (iconP2.width - iconOffset);
health = CoolUtil.bound(health, 0, maxHealth);
iconP1.health = healthBar.percent / 100;
iconP2.health = 1 - (healthBar.percent / 100);
iconP1.health = healthBarPercent / 100;
iconP2.health = 1 - (healthBarPercent / 100);
}
function updateRatingStuff() {
dynamic function updateRatingStuff() {
scoreTxt.text = 'Score:$songScore';
missesTxt.text = '${comboBreaks ? "Combo Breaks" : "Misses"}:$misses';
@@ -1294,45 +1312,37 @@ class PlayState extends MusicBeatState
return;
}
updateRatingStuff();
if (updateRatingStuff != null)
updateRatingStuff();
if (controls.PAUSE && startedCountdown && canPause)
pauseGame();
if (canAccessDebugMenus && chartingMode && controls.DEV_ACCESS)
FlxG.switchState(new funkin.editors.charter.Charter(SONG.meta.name, difficulty, false));
if (canAccessDebugMenus) {
if (chartingMode && FlxG.keys.justPressed.SEVEN) {
FlxG.switchState(new funkin.editors.charter.Charter(SONG.meta.name, difficulty, false));
}
#if EXPERMENTAL_SCRIPT_RELOADING
if (FlxG.keys.justPressed.F5) {
Logs.warn('Reloading scripts...', "PlayState");
scripts.reload();
Logs.warn('Song scripts successfully reloaded.', GREEN, "PlayState");
}
#end
}
if (doIconBop)
for (icon in iconArray)
if (icon.updateBump != null)
icon.updateBump();
if (doIconBop) {
var iconLerp = Flags.ICON_LERP;
iconP1.scale.set(lerp(iconP1.scale.x, 1, iconLerp), lerp(iconP1.scale.y, 1, iconLerp));
iconP2.scale.set(lerp(iconP2.scale.x, 1, iconLerp), lerp(iconP2.scale.y, 1, iconLerp));
if (updateIconPositions != null)
updateIconPositions();
iconP1.updateHitbox();
iconP2.updateHitbox();
}
updateIconPositions();
if (startingSong)
{
if (startedCountdown)
{
if (startingSong) {
if (startedCountdown) {
Conductor.songPosition += Conductor.songOffset + elapsed * 1000;
if (Conductor.songPosition >= 0)
startSong();
}
} else if (FlxG.sound.music != null) {
var instTime = FlxG.sound.music.time;
var isOffsync = vocals.time != instTime || [for(strumLine in strumLines.members) strumLine.vocals.time != instTime].contains(true);
var isOffsync:Bool = vocals.time != instTime;
if(!isOffsync) {
for(strumLine in strumLines.members) {
if(strumLine.vocals.time != instTime) {
isOffsync = true;
break;
}
}
}
__vocalOffsetViolation = Math.max(0, __vocalOffsetViolation + (isOffsync ? elapsed : -elapsed / 2));
if (__vocalOffsetViolation > Flags.VOCAL_OFFSET_VIOLATION_THRESHOLD) {
resyncVocals();
@@ -1343,31 +1353,13 @@ class PlayState extends MusicBeatState
while(events.length > 0 && events.last().time <= Conductor.songPosition)
executeEvent(events.pop());
if (generatedMusic && strumLines.members[curCameraTarget] != null)
{
var pos = FlxPoint.get();
var r = 0;
for(c in strumLines.members[curCameraTarget].characters) {
if (c == null || !c.visible) continue;
var cpos = c.getCameraPosition();
pos.x += cpos.x;
pos.y += cpos.y;
r++;
//cpos.put();
}
if (r > 0) {
pos.x /= r;
pos.y /= r;
if (controls.PAUSE && startedCountdown && canPause)
pauseGame();
var event = gameAndCharsEvent("onCameraMove", EventManager.get(CamMoveEvent).recycle(pos, strumLines.members[curCameraTarget], r));
if (!event.cancelled)
camFollow.setPosition(pos.x, pos.y);
}
pos.put();
}
if (generatedMusic)
moveCamera();
if (camZooming)
{
if (camZooming) {
FlxG.camera.zoom = lerp(FlxG.camera.zoom, defaultCamZoom, camGameZoomLerp);
camHUD.zoom = lerp(camHUD.zoom, defaultHudZoom, camHUDZoomLerp);
}
@@ -1401,7 +1393,52 @@ class PlayState extends MusicBeatState
scripts.event("postDraw", e);
}
public var scrollSpeedTween:FlxTween = null;
public function moveCamera() if (strumLines.members[curCameraTarget] != null) {
var data:CamPosData = getStrumlineCamPos(curCameraTarget);
if (data.amount > 0) {
var event = scripts.event("onCameraMove", EventManager.get(CamMoveEvent).recycle(data.pos, strumLines.members[curCameraTarget], data.amount));
if (!event.cancelled)
camFollow.setPosition(event.position.x, event.position.y);
}
data.put();
}
/**
* Returns the camera position of the specified strumline.
* @param strumLine The strumline to get the camera position of.
* @param pos The position to put the camera position in. If `null`, a new FlxPoint will be created.
* @param ignoreInvisible Whenever invisible characters should be ignored.
**/
public inline function getStrumlineCamPos(strumLine:Int, ?pos:FlxPoint = null, ?ignoreInvisible:Bool = true):CamPosData {
return getCharactersCamPos(strumLines.members[strumLine].characters, pos, ignoreInvisible);
}
/**
* Returns the camera position of the specified characters.
* @param chars The characters to get the camera position of.
* @param pos The position to put the camera position in. If `null`, a new FlxPoint will be created.
* @param ignoreInvisible Whenever invisible characters should be ignored.
**/
public dynamic function getCharactersCamPos(chars:Array<Character>, ?pos:FlxPoint = null, ?ignoreInvisible:Bool = true):CamPosData {
if (pos == null) pos = FlxPoint.get();
var amount = 0;
for(c in chars) {
if (c == null || (ignoreInvisible && !c.visible)) continue;
var cpos = c.getCameraPosition();
pos.x += cpos.x;
pos.y += cpos.y;
amount++;
//cpos.put(); // not actually in the pool, so no need
}
if (amount > 0) {
pos.x /= amount;
pos.y /= amount;
}
return new CamPosData(pos, amount);
}
public var eventsTween:Map<String, FlxTween> = [];
public function executeEvent(event:ChartEvent) @:privateAccess {
if (event == null || event.params == null) return;
@@ -1413,12 +1450,92 @@ class PlayState extends MusicBeatState
switch(event.name) {
case "HScript Call":
scripts.call(event.params[0], event.params[1].split(','));
var scriptPacks:Array<ScriptPack> = [scripts, stateScripts];
for (strLine in strumLines.members) for (char in strLine.characters) scriptPacks.push(char.scripts);
var args:Array<String> = event.params[1].split(',');
for (pack in scriptPacks) {
pack.call(event.params[0], args);
//public functions
if (pack.publicVariables.exists(event.params[0])) {
var func = pack.publicVariables.get(event.params[0]);
if (func != null && Reflect.isFunction(func))
Reflect.callMethod(null, func, args);
}
}
//static functions
if (Script.staticVariables.exists(event.params[0])) {
var func = Script.staticVariables.get(event.params[0]);
if (func != null && Reflect.isFunction(func))
Reflect.callMethod(null, func, args);
}
case "Camera Movement":
var tween = eventsTween.get("cameraMovement");
if (tween != null) {
if (tween.onComplete != null) tween.onComplete(tween);
tween.cancel();
}
curCameraTarget = event.params[0];
moveCamera();
if (strumLines.members[curCameraTarget] != null) {
if (event.params[1] == false) FlxG.camera.snapToTarget();
else if (event.params[3] != null && event.params[3] != "CLASSIC") { // making more nullchecks in this event because of the default save value being false - Nex
var oldFollow = FlxG.camera.followEnabled;
FlxG.camera.followEnabled = false;
eventsTween.set("cameraMovement", FlxTween.tween(FlxG.camera.scroll, {x: camFollow.x - FlxG.camera.width * 0.5, y: camFollow.y - FlxG.camera.height * 0.5},
(Conductor.stepCrochet / 1000) * (event.params[2] == null ? 4 : event.params[2]), {
ease: CoolUtil.flxeaseFromString(event.params[3], event.params[4]),
onComplete: (_) -> FlxG.camera.followEnabled = oldFollow
})
);
}
}
case "Camera Position":
var tween = eventsTween.get("cameraMovement");
if (tween != null) {
if (tween.onComplete != null) tween.onComplete(tween);
tween.cancel();
}
curCameraTarget = -1;
var isOffset = event.params[6] == true;
camFollow.setPosition(isOffset ? (camFollow.x + event.params[0]) : event.params[0], isOffset ? (camFollow.y + event.params[1]) : event.params[1]);
if (event.params[2] == false) FlxG.camera.snapToTarget();
else if (event.params[4] != null && event.params[4] != "CLASSIC") {
var oldFollow = FlxG.camera.followEnabled;
FlxG.camera.followEnabled = false;
eventsTween.set("cameraMovement", FlxTween.tween(FlxG.camera.scroll, {x: camFollow.x - FlxG.camera.width * 0.5, y: camFollow.y - FlxG.camera.height * 0.5},
(Conductor.stepCrochet / 1000) * (event.params[3] == null ? 4 : event.params[3]), {
ease: CoolUtil.flxeaseFromString(event.params[4], event.params[5]),
onComplete: (_) -> FlxG.camera.followEnabled = oldFollow
})
);
}
case "Add Camera Zoom":
var camera:FlxCamera = event.params[1] == "camHUD" ? camHUD : camGame;
camera.zoom += event.params[0];
case "Camera Zoom":
var cam = event.params[2] == "camHUD" ? camHUD : camGame;
var name = (event.params[2] == "camHUD" ? "camHUD" : "camGame") + ".zoom"; // avoiding having different values from these 2 - Nex
var tween = eventsTween.get(name);
if (tween != null) tween.cancel();
var finalZoom:Float = event.params[1] * (event.params[6] == 'direct' ? FlxCamera.defaultZoom : stage.defaultZoom);
if (event.params[7] == true) finalZoom *= cam.zoom;
if (event.params[0] == false) {
cam.zoom = finalZoom;
if (cam == camHUD) defaultHudZoom = finalZoom;
else defaultCamZoom = finalZoom;
} else
eventsTween.set(name, FlxTween.tween(cam, {zoom: finalZoom}, (Conductor.stepCrochet / 1000) * event.params[3], {ease: CoolUtil.flxeaseFromString(event.params[4], event.params[5]), onUpdate: function(_) {
if (cam == camHUD) defaultHudZoom = cam.zoom;
else defaultCamZoom = cam.zoom;
}}));
case "Camera Modulo Change":
camZoomingInterval = event.params[0];
camZoomingStrength = event.params[1];
@@ -1431,12 +1548,16 @@ class PlayState extends MusicBeatState
camera.flash(event.params[1], (Conductor.stepCrochet / 1000) * event.params[2], null, true);
case "BPM Change": // automatically handled by conductor
case "Scroll Speed Change":
if (scrollSpeedTween != null) scrollSpeedTween.cancel();
var tween = eventsTween.get("scrollSpeedTween");
if (tween != null) tween.cancel();
var finalScroll:Float = event.params[1];
if (event.params[5] == true) finalScroll *= scrollSpeed;
if (event.params[0] == false)
scrollSpeed = event.params[1];
scrollSpeed = finalScroll;
else
scrollSpeedTween = FlxTween.tween(this, {scrollSpeed: event.params[1]}, (Conductor.stepCrochet / 1000) * event.params[2], {ease: CoolUtil.flxeaseFromString(event.params[3], event.params[4])});
eventsTween.set("scrollSpeedTween", FlxTween.tween(this, {scrollSpeed: finalScroll}, (Conductor.stepCrochet / 1000) * event.params[2], {ease: CoolUtil.flxeaseFromString(event.params[3], event.params[4])}));
case "Alt Animation Toggle":
var strLine = strumLines.members[event.params[2]];
if (strLine != null) {
@@ -1451,7 +1572,7 @@ class PlayState extends MusicBeatState
case "Play Animation":
if (strumLines.members[event.params[0]] != null && strumLines.members[event.params[0]].characters != null)
for (char in strumLines.members[event.params[0]].characters)
if (char != null) char.playAnim(event.params[1], event.params[2], null);
if (char != null) char.playAnim(event.params[1], event.params[2], event.params[3] == "NONE" ? null : event.params[3]);
case "Unknown": // nothing
}
}
@@ -1506,6 +1627,7 @@ class PlayState extends MusicBeatState
*/
public function endSong():Void
{
endingSong = true;
gameAndCharsCall("onSongEnd");
canPause = false;
inst.volume = 0;
@@ -1546,8 +1668,6 @@ class PlayState extends MusicBeatState
* Immediately switches to the next song, or goes back to the Story/Freeplay menu.
*/
public function nextSong() {
resetSongInfos();
if (isStoryMode)
{
campaignScore += songScore;
@@ -1581,7 +1701,7 @@ class PlayState extends MusicBeatState
FlxG.sound.music.stop();
__loadSong(storyPlaylist[0].toLowerCase(), difficulty);
__loadSong(storyPlaylist[0], difficulty);
FlxG.switchState(new PlayState());
}
@@ -1711,9 +1831,9 @@ class PlayState extends MusicBeatState
var event:NoteHitEvent;
if (strumLine != null && !strumLine.cpu)
event = EventManager.get(NoteHitEvent).recycle(false, !note.isSustainNote, !note.isSustainNote, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, true, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, score, note.isSustainNote ? null : accuracy, 0.023, daRating, Options.splashesEnabled && !note.isSustainNote && daRating == "sick");
event = EventManager.get(NoteHitEvent).recycle(false, !note.isSustainNote, !note.isSustainNote, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, true, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, score, note.isSustainNote ? null : accuracy, 0.023, daRating, Options.splashesEnabled && !note.isSustainNote && daRating == "sick", 0.5, true, 0.7, true, true, iconP1);
else
event = EventManager.get(NoteHitEvent).recycle(false, false, false, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, false, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, 0, null, 0, daRating, false);
event = EventManager.get(NoteHitEvent).recycle(false, false, false, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, false, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, 0, null, 0, daRating, false, 0.5, true, 0.7, true, true, iconP2);
event.deleteNote = !note.isSustainNote; // work around, to allow sustain notes to be deleted
event = scripts.event(strumLine != null && !strumLine.cpu ? "onPlayerHit" : "onDadHit", event);
strumLine.onHit.dispatch(event);
@@ -1876,14 +1996,9 @@ class PlayState extends MusicBeatState
}
if (doIconBop)
{
var iconScale = Flags.BOP_ICON_SCALE;
iconP1.scale.set(iconScale, iconScale);
iconP2.scale.set(iconScale, iconScale);
iconP1.updateHitbox();
iconP2.updateHitbox();
}
for (icon in iconArray)
if (icon.bump != null)
icon.bump();
scripts.call("beatHit", [curBeat]);
}
@@ -1974,7 +2089,7 @@ class PlayState extends MusicBeatState
public static function loadWeek(weekData:WeekData, ?difficulty:String) {
if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY;
storyWeek = weekData;
storyPlaylist = [for(e in weekData.songs) e.name];
storyPlaylist = [for (e in weekData.songs) e.name];
isStoryMode = true;
campaignScore = 0;
campaignMisses = 0;
@@ -2008,6 +2123,8 @@ class PlayState extends MusicBeatState
*/
public static function __loadSong(_name:String, _difficulty:String) {
difficulty = _difficulty;
seenCutscene = false;
deathCounter = 0;
SONG = Chart.parse(_name, _difficulty);
fromMods = SONG.fromMods;
@@ -2018,8 +2135,10 @@ final class ComboRating {
public var percent:Float;
public var rating:String;
public var color:FlxColor;
public var maxMisses:Float; // Float since it could be Math.POSITIVE_INFINITY - Nex
public function new(percent:Float, rating:String, color:FlxColor) {
public function new(?percent:Float, ?rating:String, ?color:FlxColor, ?misses:Float) {
maxMisses = misses == null || Math.isNaN(misses) ? Math.POSITIVE_INFINITY : misses;
this.percent = percent;
this.rating = rating;
this.color = color;
@@ -2034,3 +2153,28 @@ typedef PlayStateTransitionData = {
var camFollowY:Float;
var camZoom:Float;
}
class CamPosData {
/**
* The camera position.
**/
public var pos:FlxPoint;
/**
* The amount of characters that was involved in the calculation.
**/
public var amount:Int;
public function new(pos:FlxPoint, amount:Int) {
this.pos = pos;
this.amount = amount;
}
/**
* Puts the position back into the pool, making it reusable.
**/
public function put() {
if(pos == null) return;
pos.put();
pos = null;
}
}
+39
View File
@@ -0,0 +1,39 @@
package funkin.game;
class Splash extends FunkinSprite
{
/**
* The current splash strum
* WARNING: It can be null
*/
public var strum:Null<Strum>;
/**
* Shortcut to `strum.ID`
* WARNING: It can be null
*/
public var strumID:Null<Int>;
public static function copyFrom(source:Splash):Splash
{
var spr = new Splash();
@:privateAccess {
spr.setPosition(source.x, source.y);
spr.frames = source.frames;
if (source.animateAtlas != null && source.atlasPath != null)
spr.loadSprite(source.atlasPath);
spr.animation.copyFrom(source.animation);
spr.visible = source.visible;
spr.alpha = source.alpha;
spr.antialiasing = source.antialiasing;
spr.scale.set(source.scale.x, source.scale.y);
spr.scrollFactor.set(source.scrollFactor.x, source.scrollFactor.y);
spr.skew.set(source.skew.x, source.skew.y);
spr.transformMatrix = source.transformMatrix;
spr.matrixExposed = source.matrixExposed;
spr.zoomFactor = source.zoomFactor;
spr.animOffsets = source.animOffsets.copy();
}
return spr;
}
}
+11 -6
View File
@@ -2,7 +2,7 @@ package funkin.game;
import haxe.xml.Access;
class SplashGroup extends FlxTypedGroup<FunkinSprite> {
class SplashGroup extends FlxTypedGroup<Splash> {
/**
* Whenever the splash group has successfully loaded or not.
*/
@@ -51,7 +51,7 @@ class SplashGroup extends FlxTypedGroup<FunkinSprite> {
var _antialiasing:Bool = true;
function createSplash(imagePath:String) {
var splash = new FunkinSprite();
var splash = new Splash();
splash.antialiasing = true;
splash.active = splash.visible = false;
splash.loadSprite(Paths.image(imagePath));
@@ -64,7 +64,7 @@ class SplashGroup extends FlxTypedGroup<FunkinSprite> {
return splash;
}
function setupAnims(xml:Access, splash:FunkinSprite) {
function setupAnims(xml:Access, splash:Splash) {
for(strum in xml.nodes.strum) {
var id:Null<Int> = Std.parseInt(strum.att.id);
if (id != null) {
@@ -93,12 +93,14 @@ class SplashGroup extends FlxTypedGroup<FunkinSprite> {
};
}
function pregenerateSplashes(splash:FunkinSprite) {
function pregenerateSplashes(splash:Splash) {
// make 7 additional splashes
for(i in 0...7) {
var spr = FunkinSprite.copyFrom(splash);
var spr = Splash.copyFrom(splash);
spr.animation.finishCallback = function(name:String) {
spr.active = spr.visible = false;
spr.strum = null;
spr.strumID = null;
};
add(spr);
}
@@ -112,11 +114,14 @@ class SplashGroup extends FlxTypedGroup<FunkinSprite> {
return animNames[FlxG.random.int(0, animNames.length - 1)];
}
var __splash:FunkinSprite;
var __splash:Splash;
public function showOnStrum(strum:Strum) {
if (!valid) return null;
__splash = recycle();
__splash.strum = strum;
__splash.strumID = strum.ID;
@:privateAccess
__splash.scale.x = __splash.scale.y = _scale * strum.strumLine.strumScale;
__splash.alpha = _alpha;
+1 -1
View File
@@ -2,7 +2,7 @@ package funkin.game;
import funkin.backend.scripting.events.splash.*;
class SplashHandler extends FlxTypedGroup<FunkinSprite> {
class SplashHandler extends FlxTypedGroup<Splash> {
/**
* Map containing all of the splashes group.
*/
+13 -7
View File
@@ -130,18 +130,19 @@ class Stage extends FlxBasic implements IBeatReceiver {
case "box" | "solid":
if (!node.has.name || !node.has.width || !node.has.height) continue;
var spr = new FunkinSprite(
(node.has.x) ? Std.parseFloat(node.att.x).getDefault(0) : 0,
(node.has.y) ? Std.parseFloat(node.att.y).getDefault(0) : 0
);
spr.name = node.getAtt("name");
var isSolid = node.name == "solid";
(node.name == "solid" ? spr.makeSolid : spr.makeGraphic)(
var spr = new FunkinSprite();
(isSolid ? spr.makeSolid : spr.makeGraphic)(
Std.parseInt(node.att.width),
Std.parseInt(node.att.height),
(node.has.color) ? CoolUtil.getColorFromDynamic(node.att.color) : -1
);
if (isSolid) node.x.remove("updateHitbox");
for (a in ["width", "height", "color"]) node.x.remove(a);
XMLUtil.loadSpriteFromXML(spr, node, "", NONE, false);
stageSprites.set(spr.name, spr);
addSprite(spr);
case "boyfriend" | "bf" | "player":
@@ -353,6 +354,11 @@ class Stage extends FlxBasic implements IBeatReceiver {
public function measureHit(curMeasure:Int) {}
public override function destroy() {
startCam.put();
super.destroy();
}
/**
* Gets a list of stages that are available to be used.
* @param mods Whenever only the mods folder should be checked
@@ -469,4 +475,4 @@ typedef OldCharInfo = {
var alpha:Float;
var zoomFactor:Float;
var angle:Float;
}
}
+58 -32
View File
@@ -42,6 +42,18 @@ class Strum extends FlxSprite {
public var lastDrawCameras(default, null):Array<FlxCamera> = [];
// Copy fields
public var copyStrumCamera:Bool = true;
public var copyStrumScrollX:Bool = true;
public var copyStrumScrollY:Bool = true;
public var copyStrumAngle:Bool = true;
public var updateNotesPosX:Bool = true;
public var updateNotesPosY:Bool = true;
public var extraCopyFields(default, set):Array<String> = [];
private inline function set_extraCopyFields(val:Array<String>)
return extraCopyFields = val == null ? [] : val;
/**
* Whenever the strum is pressed.
**/
@@ -130,47 +142,61 @@ class Strum extends FlxSprite {
public function updateNotePosition(daNote:Note) {
if (!daNote.exists) return;
daNote.__strumCameras = lastDrawCameras;
daNote.__strum = this;
daNote.scrollFactor.set(scrollFactor.x, scrollFactor.y);
daNote.__noteAngle = getNotesAngle(daNote);
daNote.angle = daNote.isSustainNote ? daNote.__noteAngle : angle;
if (copyStrumCamera) daNote.__strumCameras = lastDrawCameras;
if (copyStrumScrollX) daNote.scrollFactor.x = scrollFactor.x;
if (copyStrumScrollY) daNote.scrollFactor.y = scrollFactor.y;
if (copyStrumAngle && daNote.copyStrumAngle) {
daNote.__noteAngle = getNotesAngle(daNote);
daNote.angle = daNote.isSustainNote ? daNote.__noteAngle : angle;
}
updateNotePos(daNote);
for (field in extraCopyFields)
CoolUtil.cloneProperty(daNote, field, this); // TODO: make this cached to reduce the reflection calls - Neo
}
private inline function updateNotePos(daNote:Note) {
if (daNote.strumRelativePos) {
daNote.setPosition((this.width - daNote.width) / 2, (daNote.strumTime - Conductor.songPosition) * (0.45 * CoolUtil.quantize(getScrollSpeed(daNote), 100)));
if (daNote.isSustainNote) daNote.y += N_WIDTHDIV2 * (height / Note.swagWidth);
} else {
var offset = FlxPoint.get(0, (Conductor.songPosition - daNote.strumTime) * (0.45 * CoolUtil.quantize(getScrollSpeed(daNote), 100)));
var realOffset = FlxPoint.get(0, 0);
var shouldX = updateNotesPosX && daNote.updateNotesPosX;
var shouldY = updateNotesPosY && daNote.updateNotesPosY;
if (daNote.isSustainNote) offset.y -= N_WIDTHDIV2 * (height / Note.swagWidth);
if (Std.int(daNote.__noteAngle % 360) != 0) {
var noteAngleCos = FlxMath.fastCos(daNote.__noteAngle / PIX180);
var noteAngleSin = FlxMath.fastSin(daNote.__noteAngle / PIX180);
var aOffset:FlxPoint = FlxPoint.get(
(daNote.origin.x / daNote.scale.x) - daNote.offset.x,
(daNote.origin.y / daNote.scale.y) - daNote.offset.y
);
realOffset.x = -aOffset.x + (noteAngleCos * (offset.x + aOffset.x)) + (noteAngleSin * (offset.y + aOffset.y));
realOffset.y = -aOffset.y + (noteAngleSin * (offset.x + aOffset.x)) + (noteAngleCos * (offset.y + aOffset.y));
aOffset.put();
if (shouldX || shouldY) {
if (daNote.strumRelativePos) {
if (shouldX) daNote.x = (this.width - daNote.width) / 2;
if (shouldY) {
daNote.y = (daNote.strumTime - Conductor.songPosition) * (0.45 * CoolUtil.quantize(getScrollSpeed(daNote), 100));
if (daNote.isSustainNote) daNote.y += N_WIDTHDIV2;
}
} else {
realOffset.x = offset.x;
realOffset.y = offset.y;
var offset = FlxPoint.get(0, (Conductor.songPosition - daNote.strumTime) * (0.45 * CoolUtil.quantize(getScrollSpeed(daNote), 100)));
var realOffset = FlxPoint.get(0, 0);
if (daNote.isSustainNote) offset.y -= N_WIDTHDIV2;
if (Std.int(daNote.__noteAngle % 360) != 0) {
var noteAngleCos = FlxMath.fastCos(daNote.__noteAngle / PIX180);
var noteAngleSin = FlxMath.fastSin(daNote.__noteAngle / PIX180);
var aOffset:FlxPoint = FlxPoint.get(
(daNote.origin.x / daNote.scale.x) - daNote.offset.x,
(daNote.origin.y / daNote.scale.y) - daNote.offset.y
);
realOffset.x = -aOffset.x + (noteAngleCos * (offset.x + aOffset.x)) + (noteAngleSin * (offset.y + aOffset.y));
realOffset.y = -aOffset.y + (noteAngleSin * (offset.x + aOffset.x)) + (noteAngleCos * (offset.y + aOffset.y));
aOffset.put();
} else {
realOffset.x = offset.x;
realOffset.y = offset.y;
}
realOffset.y *= -1;
if (shouldX) daNote.x = x + realOffset.x;
if (shouldY) daNote.y = y + realOffset.y;
offset.put();
realOffset.put();
}
realOffset.y *= -1;
daNote.setPosition(x + realOffset.x, y + realOffset.y);
offset.put();
realOffset.put();
}
}
+8 -4
View File
@@ -221,7 +221,7 @@ class StrumLine extends FlxTypedGroup<Strum> {
if (cpu && __updateNote_event.__autoCPUHit && !daNote.avoid && !daNote.wasGoodHit && daNote.strumTime < __updateNote_songPos) PlayState.instance.goodNoteHit(this, daNote);
if (daNote.wasGoodHit && daNote.isSustainNote && daNote.strumTime + (daNote.sustainLength) < __updateNote_songPos) {
if (daNote.wasGoodHit && daNote.isSustainNote && daNote.strumTime + daNote.sustainLength < __updateNote_songPos) {
deleteNote(daNote);
return;
}
@@ -247,8 +247,9 @@ class StrumLine extends FlxTypedGroup<Strum> {
var __notePerStrum:Array<Note> = [];
function __inputProcessPressed(note:Note) {
if (__pressed[note.strumID] && note.isSustainNote && note.canBeHit && !note.wasGoodHit) {
if (__pressed[note.strumID] && note.isSustainNote && note.strumTime < __updateNote_songPos && !note.wasGoodHit) {
PlayState.instance.goodNoteHit(this, note);
note.updateSustainClip();
}
}
function __inputProcessJustPressed(note:Note) {
@@ -347,8 +348,10 @@ class StrumLine extends FlxTypedGroup<Strum> {
* Creates a strum and returns the created strum (needs to be added manually).
* @param i Index of the strum
* @param animPrefix (Optional) Animation prefix (`left` = `arrowLEFT`, `left press`, `left confirm`).
* @param spritePath (Optional) The sprite's graphic path if you don't want the default one.
* @param playIntroAnimation (Optional) Whenever the intro animation should be played, by default might be `true` under certain conditions.
*/
public function createStrum(i:Int, ?animPrefix:String) {
public function createStrum(i:Int, ?animPrefix:String, ?spritePath:String, ?playIntroAnimation:Bool) {
if (animPrefix == null)
animPrefix = strumAnimPrefix[i % strumAnimPrefix.length];
var babyArrow:Strum = new Strum(startingPos.x + (Note.swagWidth * strumScale * (data.strumSpacing != null ? data.strumSpacing : 1) * i), startingPos.y + (Note.swagWidth*0.5) - (Note.swagWidth * strumScale * 0.5));
@@ -359,7 +362,8 @@ class StrumLine extends FlxTypedGroup<Strum> {
babyArrow.scrollSpeed = data.scrollSpeed;
var event = EventManager.get(StrumCreationEvent).recycle(babyArrow, PlayState.instance.strumLines.members.indexOf(this), i, animPrefix);
event.__doAnimation = !MusicBeatState.skipTransIn && (PlayState.instance != null ? PlayState.instance.introLength > 0 : true);
event.__doAnimation = playIntroAnimation == null ? (!MusicBeatState.skipTransIn && (PlayState.instance != null ? PlayState.instance.introLength > 0 : true)) : playIntroAnimation;
if (spritePath != null) event.sprite = spritePath;
if (PlayState.instance != null) event = PlayState.instance.gameAndCharsEvent("onStrumCreation", event);
if (!event.cancelled) {
+3 -3
View File
@@ -8,11 +8,11 @@ import funkin.menus.PauseSubState;
*/
class Cutscene extends MusicBeatSubstate {
var __callback:Void->Void;
var __pausable:Bool;
var game:PlayState = PlayState.instance;
public var paused:Bool = false;
public var pauseItems:Array<String>;
public var pausable:Bool;
public var skippable(default, set):Bool;
public function set_skippable(val:Bool):Bool {
@@ -26,9 +26,9 @@ class Cutscene extends MusicBeatSubstate {
canOpenCustomTransition = true;
__callback = callback;
__pausable = allowPause;
pauseItems = Flags.DEFAULT_CUTSCENE_PAUSE_ITEMS;
pausable = allowPause;
skippable = canSkip;
}
@@ -38,7 +38,7 @@ class Cutscene extends MusicBeatSubstate {
}
public function pauseCheck():Bool
return controls.PAUSE && __pausable;
return controls.PAUSE && pausable;
var _before:Array<Bool> = [false, true];
public function pauseCutscene() {
@@ -92,7 +92,7 @@ class DialogueCutscene extends ScriptedCutscene {
callback: node.getAtt('callback'),
changeDefAnim: node.getAtt('changeDefAnim'),
speed: Std.parseFloat(node.getAtt("speed")).getDefault(0.05),
musicVolume: node.has.musicVolume ? (volume = Std.parseFloat(node.att.speed).getDefault(0.8)) : null,
musicVolume: node.has.musicVolume ? (volume = Std.parseFloat(node.att.musicVolume).getDefault(0.8)) : null,
changeMusic: node.has.changeMusic ? FlxG.sound.load(Paths.music(node.att.changeMusic), volume, true) : null,
playSound: node.has.playSound ? FlxG.sound.load(Paths.sound(node.att.playSound)) : null,
nextSound: node.has.nextSound ? FlxG.sound.load(Paths.sound(node.att.nextSound)) : null,
@@ -200,7 +200,7 @@ class DialogueCutscene extends ScriptedCutscene {
if (curMusic != null && !curMusic.persist) curMusic.destroy();
super.destroy();
cutscene = null;
if (cutscene == this) cutscene = null;
FlxG.cameras.remove(dialogueCamera);
}
}
@@ -186,7 +186,7 @@ class VideoCutscene extends Cutscene {
text.destroy();
}});
__pausable = __loaded = true;
pausable = __loaded = true;
video.play();
}
+1
View File
@@ -23,6 +23,7 @@ import flixel.tweens.FlxEase;
import flixel.util.FlxDestroyUtil;
import funkin.backend.system.Flags;
import funkin.Types;
import funkin.menus.ui.Alphabet;
import flixel.group.FlxGroup.FlxTypedGroup;
-57
View File
@@ -1,57 +0,0 @@
package funkin.menus;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.backend.FunkinText;
class BetaWarningState extends MusicBeatState {
var titleAlphabet:Alphabet;
var disclaimer:FunkinText;
var transitioning:Bool = false;
public override function create() {
super.create();
titleAlphabet = new Alphabet(0, 0, "WARNING", true);
titleAlphabet.screenCenter(X);
add(titleAlphabet);
disclaimer = new FunkinText(16, titleAlphabet.y + titleAlphabet.height + 10, FlxG.width - 32, "", 32);
disclaimer.alignment = CENTER;
disclaimer.applyMarkup('This engine is still in a *${Flags.RELEASE_CYCLE}* state. That means *majority of the features* are either *buggy* or *non finished*. If you find any bugs, please report them to the Codename Engine GitHub.\n\nPress ENTER to continue',
[
new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFFF4444), "*")
]
);
add(disclaimer);
var off = Std.int((FlxG.height - (disclaimer.y + disclaimer.height)) / 2);
disclaimer.y += off;
titleAlphabet.y += off;
DiscordUtil.call("onMenuLoaded", ["Beta Warning"]);
}
public override function update(elapsed:Float) {
super.update(elapsed);
if (controls.ACCEPT && transitioning) {
FlxG.camera.stopFX(); FlxG.camera.visible = false;
goToTitle();
}
if (controls.ACCEPT && !transitioning) {
transitioning = true;
CoolUtil.playMenuSFX(CONFIRM);
FlxG.camera.flash(FlxColor.WHITE, 1, function() {
FlxG.camera.fade(FlxColor.BLACK, 2.5, false, goToTitle);
});
}
}
private function goToTitle() {
MusicBeatState.skipTransIn = MusicBeatState.skipTransOut = true;
FlxG.switchState(new TitleState());
}
}
+11 -16
View File
@@ -139,6 +139,8 @@ class FreeplayState extends MusicBeatState
var icon:HealthIcon = new HealthIcon(songs[i].icon);
icon.sprTracker = songText;
icon.setUnstretchedGraphicSize(150, 150, true);
icon.updateHitbox();
// using a FlxGroup is too much fuss!
iconArray.push(icon);
@@ -234,7 +236,7 @@ class FreeplayState extends MusicBeatState
scoreText.x = coopText.x = scoreBG.x + 4;
diffText.x = Std.int(scoreBG.x + ((scoreBG.width - diffText.width) / 2));
interpColor.fpsLerpTo(songs[curSelected].parsedColor, 0.0625);
interpColor.fpsLerpTo(songs[curSelected].color, 0.0625);
bg.color = interpColor.color;
#if PRELOAD_ALL
@@ -320,10 +322,9 @@ class FreeplayState extends MusicBeatState
}
public function convertChart() {
var curSong = songs[curSelected];
trace('Converting ${curSong.name} (${curSong.difficulties[curDifficulty]}) to Codename format...');
var chart = Chart.parse(curSong.name, curSong.difficulties[curDifficulty]);
Chart.save('${Main.pathBack}assets/songs/${curSong.name}', chart, curSong.difficulties[curDifficulty].toLowerCase());
trace('Converting ${songs[curSelected].name} (${songs[curSelected].difficulties[curDifficulty]}) to Codename format...');
var chart = Chart.parse(songs[curSelected].name, songs[curSelected].difficulties[curDifficulty]);
Chart.save('${Main.pathBack}assets/songs/${songs[curSelected].name}', chart, songs[curSelected].difficulties[curDifficulty]);
}
/**
@@ -346,9 +347,9 @@ class FreeplayState extends MusicBeatState
updateScore();
if (curSong.difficulties.length > 1)
diffText.text = '< ${curSong.difficulties[curDifficulty]} >';
diffText.text = '< ${curSong.difficulties[curDifficulty].toUpperCase()} >';
else
diffText.text = validDifficulties ? curSong.difficulties[curDifficulty] : "-";
diffText.text = validDifficulties ? curSong.difficulties[curDifficulty].toUpperCase() : "-";
}
function updateScore() {
@@ -453,16 +454,10 @@ class FreeplaySonglist {
public function getSongsFromSource(source:funkin.backend.assets.AssetsLibraryList.AssetSource, useTxt:Bool = true) {
var path:String = Paths.txt('freeplaySonglist');
var songsFound:Array<String> = [];
if (useTxt && Paths.assetsTree.existsSpecific(path, "TEXT", source)) {
songsFound = CoolUtil.coolTextFile(Paths.txt('freeplaySonglist'));
} else {
songsFound = Paths.getFolderDirectories('songs', false, source);
}
var songsFound:Array<String> = useTxt && Paths.assetsTree.existsSpecific(path, "TEXT", source) ? CoolUtil.coolTextFile(path) : Paths.getFolderDirectories('songs', false, source);
if (songsFound.length > 0) {
for(s in songsFound)
songs.push(Chart.loadChartMeta(s, Flags.DEFAULT_DIFFICULTY, source == MODS));
for (s in songsFound) songs.push(Chart.loadChartMeta(s, Flags.DEFAULT_DIFFICULTY, source == MODS));
return false;
}
return true;
@@ -485,4 +480,4 @@ class FreeplaySonglist {
return songList;
}
}
}
-2
View File
@@ -64,9 +64,7 @@ class GitarooPause extends MusicBeatState
}
else
{
PlayState.resetSongInfos();
if (Charter.instance != null) Charter.instance.__clearStatics();
FlxG.switchState(new MainMenuState());
}
}
+1 -1
View File
@@ -92,7 +92,7 @@ class MainMenuState extends MusicBeatState
if (!selectedSomethin)
{
if (canAccessDebugMenus) {
if (FlxG.keys.justPressed.SEVEN) {
if (controls.DEV_ACCESS) {
persistentUpdate = false;
persistentDraw = true;
openSubState(new funkin.editors.EditorPicker());
+13
View File
@@ -12,9 +12,15 @@ class ModSwitchMenu extends MusicBeatSubstate {
var alphabets:FlxTypedGroup<Alphabet>;
var curSelected:Int = 0;
var subCam:FlxCamera;
public override function create() {
super.create();
camera = subCam = new FlxCamera();
subCam.bgColor = 0;
FlxG.cameras.add(subCam, false);
var bg = new FlxSprite(0, 0).makeSolid(FlxG.width, FlxG.height, 0xFF000000);
bg.updateHitbox();
bg.scrollFactor.set();
@@ -66,5 +72,12 @@ class ModSwitchMenu extends MusicBeatSubstate {
}
alphabets.members[curSelected].alpha = 1;
}
override function destroy() {
super.destroy();
if (FlxG.cameras.list.contains(subCam))
FlxG.cameras.remove(subCam);
}
}
#end
-1
View File
@@ -165,7 +165,6 @@ class PauseSubState extends MusicBeatSubstate
if (PlayState.chartingMode && Charter.undos.unsaved)
game.saveWarn(false);
else {
PlayState.resetSongInfos();
if (Charter.instance != null) Charter.instance.__clearStatics();
// prevents certain notes to disappear early when exiting - Nex
+94 -159
View File
@@ -5,17 +5,20 @@ import flixel.text.FlxText;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import flixel.util.typeLimit.OneOfTwo;
import funkin.backend.FunkinText;
import funkin.backend.scripting.events.CancellableEvent;
import funkin.backend.scripting.events.menu.MenuChangeEvent;
import funkin.backend.scripting.events.menu.storymenu.*;
import funkin.backend.scripting.events.CancellableEvent;
import funkin.backend.week.*;
import funkin.savedata.FunkinSave;
import haxe.io.Path;
import haxe.xml.Access;
class StoryMenuState extends MusicBeatState {
public var characters:Map<String, Access> = [];
public var characters:Map<String, WeekData.WeekCharacter> = [];
public var weeks:Array<WeekData> = [];
public var weekList:StoryWeeklist;
// yes it supports parameters - Nex
public var scoreMessage:String = "WEEK SCORE:{0}";
@@ -33,7 +36,6 @@ class StoryMenuState extends MusicBeatState {
public var blackBar:FlxSprite;
public var weekBG:FlxSprite;
public var defColor:String = "#F9CF51";
public var interpColor:FlxInterpolateColor;
public var lerpScore:Float = 0;
@@ -64,7 +66,7 @@ class StoryMenuState extends MusicBeatState {
weekTitle.alpha = 0.7;
weekBG = new FlxSprite(0, 56).makeSolid(FlxG.width, 400, 0xFFFFFFFF);
weekBG.color = weeks.length > 0 ? weeks[0].bgColor : FlxColor.fromString(defColor);
weekBG.color = weeks.length > 0 ? weeks[0].bgColor : Flags.DEFAULT_WEEK_COLOR;
weekBG.updateHitbox();
weekSprites = new FlxTypedGroup<MenuItem>();
@@ -92,19 +94,18 @@ class StoryMenuState extends MusicBeatState {
tracklist.color = 0xFFE55777;
add(weekSprites);
for(e in [blackBar, scoreText, weekTitle, weekBG, tracklist]) {
e.antialiasing = true;
for (e in [blackBar, scoreText, weekTitle, weekBG, tracklist]) {
e.scrollFactor.set();
add(e);
}
add(characterSprites = new FlxTypedGroup<FunkinSprite>());
for(i=>week in weeks) {
for (i=>week in weeks) {
var spr:MenuItem = new MenuItem(0, (i * 120) + 480, 'menus/storymenu/weeks/${week.sprite}');
weekSprites.add(spr);
for(e in week.difficulties) {
for (e in week.difficulties) {
var le = e.toLowerCase();
if (difficultySprites[le] == null) {
var diffSprite = new FlxSprite(leftArrow.x + leftArrow.width, leftArrow.y);
@@ -163,6 +164,12 @@ class StoryMenuState extends MusicBeatState {
weekBG.color = interpColor.color;
}
public override function beatHit(curBeat:Int) {
super.beatHit(curBeat);
if (characterSprites != null) // reason why I wanted to use a MusicBeatGroup, but eh, whatever - Nex
characterSprites.forEachAlive(function(spr) spr.beatHit(curBeat));
}
public function goBack() {
var event = event("onGoBack", new CancellableEvent());
if (!event.cancelled)
@@ -181,13 +188,16 @@ class StoryMenuState extends MusicBeatState {
e.targetY = k - curWeek;
e.alpha = k == curWeek ? 1.0 : 0.6;
}
tracklist.text = 'TRACKS\n\n${[for(e in weeks[curWeek].songs) if (!e.hide) e.name].join('\n')}';
tracklist.text = 'TRACKS\n\n${[for(e in weeks[curWeek].songs) if (!e.hide) e.displayName.getDefault(e.name).toUpperCase()].join('\n')}';
weekTitle.text = weeks[curWeek].name.getDefault("");
if(characterSprites != null) for(i in 0...3) {
var curChar:FunkinSprite; var newChar:Access = characters[weeks[curWeek].chars[i]];
if(newChar == null) modifyCharacterAt(i);
else if((curChar = characterSprites.members[i]) == null || newChar.getAtt("name") != curChar.name) modifyCharacterAt(i, newChar);
if (characterSprites != null) for (i in 0...3) {
var char = weeks[curWeek].chars[i];
var curChar:FunkinSprite = null;
var newChar = null;
if (char == null || (newChar = characters[char.name]) == null) modifyCharacterAt(i);
else if ((curChar = cast characterSprites.members[i]) == null || newChar.name != curChar.name) modifyCharacterAt(i, newChar); // forcing the sprites to be FunkinSprite basically - Nex
}
changeDifficulty(0, true);
@@ -195,27 +205,6 @@ class StoryMenuState extends MusicBeatState {
MemoryUtil.clearMinor();
}
public function modifyCharacterAt(i:Int, ?node:Access):FunkinSprite {
var old = characterSprites.members[i];
if(old != null) {
characterSprites.remove(old);
old.destroy();
}
if(node == null) return null;
var curChar:FunkinSprite = XMLUtil.createSpriteFromXML(node, "", BEAT);
curChar.offset.x += curChar.x; curChar.offset.y += curChar.y;
curChar.setPosition((FlxG.width * 0.25) * (1 + i) - 150, 70);
if(characterSprites != null) characterSprites.insert(i, curChar); // Making so many null checks abt this group just in case if mods destroy it - Nex
curChar.playAnim("idle", true, DANCE);
return curChar;
}
public override function beatHit(curBeat:Int) {
super.beatHit(curBeat);
if(characterSprites != null) characterSprites.forEachAlive(function(spr) spr.beatHit(curBeat));
}
var __oldDiffName = null;
public function changeDifficulty(change:Int, force:Bool = false) {
if (change == 0 && !force) return;
@@ -244,122 +233,44 @@ class StoryMenuState extends MusicBeatState {
}
public function loadXMLs() {
// CoolUtil.coolTextFile(Paths.txt('freeplaySonglist'));
var weeks:Array<String> = [];
switch(Flags.WEEKS_LIST_MOD_MODE) {
case 'prepend':
getWeeksFromSource(weeks, MODS);
getWeeksFromSource(weeks, SOURCE);
case 'append':
getWeeksFromSource(weeks, SOURCE);
getWeeksFromSource(weeks, MODS);
default /*case 'override'*/:
if (getWeeksFromSource(weeks, MODS))
getWeeksFromSource(weeks, SOURCE);
}
for(k=>weekName in weeks) {
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) continue;
if (!week.has.name) {
Logs.trace('Story Menu: Week at index ${k} has no name. Skipping...', WARNING);
continue;
}
var weekObj:WeekData = {
name: week.att.name,
id: weekName,
sprite: week.getAtt('sprite').getDefault(weekName),
chars: [null, null, null],
songs: [],
difficulties: ['easy', 'normal', 'hard'],
bgColor: FlxColor.fromString(week.getAtt("bgColor").getDefault(defColor))
};
var diffNodes = week.nodes.difficulty;
if (diffNodes.length > 0) {
var diffs:Array<String> = [];
for(e in diffNodes) {
if (e.has.name) diffs.push(e.att.name);
}
if (diffs.length > 0)
weekObj.difficulties = diffs;
}
if (week.has.chars) {
for(k=>e in week.att.chars.split(",")) {
if (e.trim() == "" || e == "none" || e == "null")
weekObj.chars[k] = null;
else {
addCharacter(weekObj.chars[k] = e.trim());
}
}
}
for(k2=>song in week.nodes.song) {
if (song == null) continue;
try {
var name = song.innerData.trim();
if (name == "") {
Logs.trace('Story Menu: Song at index ${k2} in week ${weekObj.name} has no name. Skipping...', WARNING);
continue;
}
weekObj.songs.push({
name: name,
hide: song.getAtt('hide').getDefault('false') == "true"
});
} catch(e) {
Logs.trace('Story Menu: Song at index ${k2} in week ${weekObj.name} cannot contain any other XML nodes in its name.', WARNING);
continue;
}
}
if (weekObj.songs.length <= 0) {
Logs.trace('Story Menu: Week ${weekObj.name} has no songs. Skipping...', WARNING);
continue;
}
this.weeks.push(weekObj);
}
weekList = StoryWeeklist.get(true, false); // will only load week files AND NOT characters too (we will load them later only if needed)!! - Nex
weeks = weekList.weeks;
for (week in weeks) for (char in week.chars) if (char != null)
addCharacter(char.name);
}
public function addCharacter(charName:String) {
var char:Access = null;
try {
char = new Access(Xml.parse(Assets.getText(Paths.xml('weeks/characters/$charName'))).firstElement());
} catch(e) {
Logs.trace('Story Menu: Cannot parse character "$charName.xml": ${Std.string(e)}`', ERROR);
}
public function addCharacter(char:OneOfTwo<String, WeekData.WeekCharacter>) {
// better to use unsafe casts for going fast - Nex
var charObj:WeekData.WeekCharacter = null;
var charName:String;
if(char != null && characters[charName] == null) {
if(!char.x.exists("name")) char.x.set("name", charName);
if(!char.x.exists("sprite")) char.x.set("sprite", 'menus/storymenu/characters/${charName}');
if(!char.x.exists("beatInterval")) char.x.set("beatInterval", "1");
if(!char.x.exists("updateHitbox")) char.x.set("updateHitbox", "true");
characters[charName] = char;
}
charName = char is String ? cast char : (charObj = cast char).name;
if (characters[charName] != null) return; // will load only if it can be saved inside the map - Nex
characters[charName] = charObj == null ? Week.loadWeekCharacter(charName) : charObj;
}
public function getWeeksFromSource(weeks:Array<String>, source:funkin.backend.assets.AssetsLibraryList.AssetSource) {
var path:String = Paths.txt('freeplaySonglist');
var weeksFound:Array<String> = [];
if (Paths.assetsTree.existsSpecific(path, "TEXT", source)) {
var trim = "";
weeksFound = CoolUtil.coolTextFile(Paths.txt('weeks/weeks'));
} else {
weeksFound = [for(c in Paths.getFolderContent('data/weeks/weeks/', false, source)) if (Path.extension(c).toLowerCase() == "xml") Path.withoutExtension(c)];
}
public function modifyCharacterAt(i:Int, ?data:WeekData.WeekCharacter):FunkinSprite {
var curChar:FunkinSprite = null;
if (weeksFound.length > 0) {
for(s in weeksFound)
weeks.push(s);
return false;
if (characterSprites != null) {
var old = characterSprites.members[i];
if (old != null) {
characterSprites.remove(old);
old.destroy();
}
if (data != null) {
curChar = XMLUtil.createSpriteFromXML(data.xml, "", BEAT);
curChar.offset.x += curChar.x; curChar.offset.y += curChar.y;
curChar.setPosition((FlxG.width * 0.25) * (1 + i) - 150, 70);
curChar.playAnim("idle", true, DANCE);
characterSprites.insert(i, curChar);
} else {
//characterSprites.members[i] = null; // cant add nulls to flxgroups, so - Nex
characterSprites.insert(i, new FunkinSprite()).visible = false;
}
}
return true;
return curChar;
}
public function selectWeek() {
@@ -369,7 +280,7 @@ class StoryMenuState extends MusicBeatState {
canSelect = false;
CoolUtil.playMenuSFX(CONFIRM);
if(characterSprites != null)
if (characterSprites != null)
characterSprites.forEachAlive(function(spr) spr.playAnim("confirm", true, LOCK));
PlayState.loadWeek(event.week, event.difficulty);
@@ -382,21 +293,6 @@ class StoryMenuState extends MusicBeatState {
}
}
typedef WeekData = {
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<String>;
var songs:Array<WeekSong>;
var difficulties:Array<String>;
var bgColor:FlxColor;
}
typedef WeekSong = {
var name:String;
var hide:Bool;
}
class MenuItem extends FlxSprite
{
public var targetY:Float = 0;
@@ -438,3 +334,42 @@ class MenuItem extends FlxSprite
color = (time % 0.1 > 0.05) ? FlxColor.WHITE : 0xFF33ffff;
}
}
class StoryWeeklist {
public var weeks:Array<WeekData> = [];
public function new() {}
public function getWeeksFromSource(source:funkin.backend.assets.AssetsLibraryList.AssetSource, useTxt:Bool = true, loadCharactersData:Bool = true) {
var path:String = Paths.txt('weeks/weeks');
var weeksFound:Array<String> = useTxt && Paths.assetsTree.existsSpecific(path, "TEXT", source) ? CoolUtil.coolTextFile(path) :
[for (c in Paths.getFolderContent('data/weeks/weeks/', false, source)) if (Path.extension(c).toLowerCase() == "xml") Path.withoutExtension(c)];
if (weeksFound.length > 0) {
for (w in weeksFound) {
var week = Week.loadWeek(w, loadCharactersData);
if (week != null) weeks.push(week);
}
return false;
}
return true;
}
public static function get(useTxt:Bool = true, loadCharactersData:Bool = true) {
var weekList = new StoryWeeklist();
switch(Flags.WEEKS_LIST_MOD_MODE) {
case 'prepend':
weekList.getWeeksFromSource(MODS, useTxt, loadCharactersData);
weekList.getWeeksFromSource(SOURCE, useTxt, loadCharactersData);
case 'append':
weekList.getWeeksFromSource(SOURCE, useTxt, loadCharactersData);
weekList.getWeeksFromSource(MODS, useTxt, loadCharactersData);
default /*case 'override'*/:
if (weekList.getWeeksFromSource(MODS, useTxt, loadCharactersData))
weekList.getWeeksFromSource(SOURCE, useTxt, loadCharactersData);
}
return weekList;
}
}
+12 -15
View File
@@ -1,11 +1,12 @@
package funkin.menus.credits;
import flixel.util.FlxColor;
import funkin.backend.assets.AssetsLibraryList.AssetSource;
import funkin.backend.system.github.GitHubContributor.CreditsGitHubContributor;
import funkin.options.OptionsScreen;
import funkin.options.TreeMenu;
import funkin.backend.system.github.GitHubContributor.CreditsGitHubContributor;
import funkin.options.type.*;
import haxe.xml.Access;
import flixel.util.FlxColor;
class CreditsMain extends TreeMenu {
var bg:FlxSprite;
@@ -21,21 +22,17 @@ class CreditsMain extends TreeMenu {
bg.antialiasing = true;
add(bg);
var xmlPath = Paths.xml('config/credits');
for(source in [funkin.backend.assets.AssetsLibraryList.AssetSource.SOURCE, funkin.backend.assets.AssetsLibraryList.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)));
} catch(e) {
Logs.trace('[CreditsMain] Error while parsing credits.xml: ${Std.string(e)}', ERROR);
}
for (i in funkin.backend.assets.ModsFolder.getLoadedMods()) {
var xmlPath = Paths.xml('config/credits/LIB_$i');
if (access != null)
for(c in parseCreditsFromXML(access, source))
items.push(c);
if (Paths.assetsTree.existsSpecific(xmlPath, "TEXT")) {
var access:Access = null;
try access = new Access(Xml.parse(Paths.assetsTree.getSpecificAsset(xmlPath, "TEXT")))
catch(e) Logs.trace('[CreditsMain] Error while parsing credits.xml: ${Std.string(e)}', ERROR);
if (access != null) for (c in parseCreditsFromXML(access)) items.push(c);
}
}
items.push(new TextOption("Codename Engine >", "Select this to see all the contributors of the engine!", function() {
optionsTree.add(Type.createInstance(CreditsCodename, []));
}));
@@ -52,7 +49,7 @@ class CreditsMain extends TreeMenu {
/**
* XML STUFF
*/
public function parseCreditsFromXML(xml:Access, source:funkin.backend.assets.AssetsLibraryList.AssetSource):Array<OptionType> {
public function parseCreditsFromXML(xml:Access, source:AssetSource = BOTH):Array<OptionType> {
var credsMenus:Array<OptionType> = [];
for(node in xml.elements) {
+3 -1
View File
@@ -52,7 +52,9 @@ class Alphabet extends FlxSpriteGroup
public function refreshAlphabetXML(path:String) {
AlphaCharacter.__alphaPath = Paths.getAssetsRoot() + path;
try {
var xml = new Access(Xml.parse(Assets.getText(path)).firstElement());
var file = Assets.getText(path);
if(file == null) return;
var xml = new Access(Xml.parse(file).firstElement());
AlphaCharacter.boldAnims = [];
AlphaCharacter.letterAnims = [];
AlphaCharacter.boldAlphabetPath = AlphaCharacter.letterAlphabetPath = 'ui/alphabet';
+49 -4
View File
@@ -33,6 +33,7 @@ class Options
public static var gameplayShaders:Bool = true;
public static var colorHealthBar:Bool = true;
public static var lowMemoryMode:Bool = false;
public static var devMode:Bool = false;
public static var betaUpdates:Bool = false;
public static var splashesEnabled:Bool = true;
public static var hitWindow:Float = 250;
@@ -94,6 +95,8 @@ class Options
public static var P1_NOTE_DOWN:Array<FlxKey> = [S];
public static var P1_NOTE_UP:Array<FlxKey> = [W];
public static var P1_NOTE_RIGHT:Array<FlxKey> = [D];
// Menus
public static var P1_LEFT:Array<FlxKey> = [A];
public static var P1_DOWN:Array<FlxKey> = [S];
public static var P1_UP:Array<FlxKey> = [W];
@@ -101,16 +104,30 @@ class Options
public static var P1_ACCEPT:Array<FlxKey> = [ENTER];
public static var P1_BACK:Array<FlxKey> = [BACKSPACE];
public static var P1_PAUSE:Array<FlxKey> = [ENTER];
// Misc
public static var P1_RESET:Array<FlxKey> = [R];
public static var P1_SWITCHMOD:Array<FlxKey> = [TAB];
public static var P1_VOLUME_UP:Array<FlxKey> = [];
public static var P1_VOLUME_DOWN:Array<FlxKey> = [];
public static var P1_VOLUME_MUTE:Array<FlxKey> = [];
// Debugs
public static var P1_DEV_ACCESS:Array<FlxKey> = [SEVEN];
public static var P1_DEV_CONSOLE:Array<FlxKey> = [F2];
public static var P1_DEV_RELOAD:Array<FlxKey> = [F5];
/**
* PLAYER 2 CONTROLS (ALT)
*/
* PLAYER 2 CONTROLS (ALT)
*/
// Notes
public static var P2_NOTE_LEFT:Array<FlxKey> = [LEFT];
public static var P2_NOTE_DOWN:Array<FlxKey> = [DOWN];
public static var P2_NOTE_UP:Array<FlxKey> = [UP];
public static var P2_NOTE_RIGHT:Array<FlxKey> = [RIGHT];
// Menus
public static var P2_LEFT:Array<FlxKey> = [LEFT];
public static var P2_DOWN:Array<FlxKey> = [DOWN];
public static var P2_UP:Array<FlxKey> = [UP];
@@ -118,16 +135,30 @@ class Options
public static var P2_ACCEPT:Array<FlxKey> = [SPACE];
public static var P2_BACK:Array<FlxKey> = [ESCAPE];
public static var P2_PAUSE:Array<FlxKey> = [ESCAPE];
// Misc
public static var P2_RESET:Array<FlxKey> = [];
public static var P2_SWITCHMOD:Array<FlxKey> = [];
public static var P2_VOLUME_UP:Array<FlxKey> = [PLUS];
public static var P2_VOLUME_DOWN:Array<FlxKey> = [MINUS];
public static var P2_VOLUME_MUTE:Array<FlxKey> = [ZERO];
// Debugs
public static var P2_DEV_ACCESS:Array<FlxKey> = [];
public static var P2_DEV_CONSOLE:Array<FlxKey> = [];
public static var P2_DEV_RELOAD:Array<FlxKey> = [];
/**
* SOLO GETTERS
*/
* SOLO GETTERS
*/
// Notes
public static var SOLO_NOTE_LEFT(get, null):Array<FlxKey>;
public static var SOLO_NOTE_DOWN(get, null):Array<FlxKey>;
public static var SOLO_NOTE_UP(get, null):Array<FlxKey>;
public static var SOLO_NOTE_RIGHT(get, null):Array<FlxKey>;
// Menus
public static var SOLO_LEFT(get, null):Array<FlxKey>;
public static var SOLO_DOWN(get, null):Array<FlxKey>;
public static var SOLO_UP(get, null):Array<FlxKey>;
@@ -135,8 +166,18 @@ class Options
public static var SOLO_ACCEPT(get, null):Array<FlxKey>;
public static var SOLO_BACK(get, null):Array<FlxKey>;
public static var SOLO_PAUSE(get, null):Array<FlxKey>;
// Misc
public static var SOLO_RESET(get, null):Array<FlxKey>;
public static var SOLO_SWITCHMOD(get, null):Array<FlxKey>;
public static var SOLO_VOLUME_UP(get, null):Array<FlxKey>;
public static var SOLO_VOLUME_DOWN(get, null):Array<FlxKey>;
public static var SOLO_VOLUME_MUTE(get, null):Array<FlxKey>;
// Debugs
public static var SOLO_DEV_ACCESS(get, null):Array<FlxKey>;
public static var SOLO_DEV_CONSOLE(get, null):Array<FlxKey>;
public static var SOLO_DEV_RELOAD(get, null):Array<FlxKey>;
public static function load() {
if (__save == null) __save = new FlxSave();
@@ -165,6 +206,10 @@ class Options
PlayerSettings.solo.setKeyboardScheme(Solo);
PlayerSettings.player1.setKeyboardScheme(Duo(true));
PlayerSettings.player2.setKeyboardScheme(Duo(false));
FlxG.sound.volumeUpKeys = SOLO_VOLUME_UP;
FlxG.sound.volumeDownKeys = SOLO_VOLUME_DOWN;
FlxG.sound.muteKeys = SOLO_VOLUME_MUTE;
}
public static function save() {
+11 -23
View File
@@ -54,36 +54,24 @@ class OptionsMenu extends TreeMenu {
if (o.substate != null) {
persistentUpdate = false;
persistentDraw = true;
if (o.substate is MusicBeatSubstate) {
openSubState(o.substate);
} else {
openSubState(Type.createInstance(o.substate, []));
}
if (o.substate is MusicBeatSubstate) openSubState(o.substate);
else openSubState(Type.createInstance(o.substate, []));
} else {
if (o.state is OptionsScreen) {
optionsTree.add(o.state);
} else {
optionsTree.add(Type.createInstance(o.state, []));
}
if (o.state is OptionsScreen) optionsTree.add(o.state);
else optionsTree.add(Type.createInstance(o.state, []));
}
})]);
var xmlPath = Paths.xml("config/options");
for(source in [funkin.backend.assets.AssetsLibraryList.AssetSource.SOURCE, funkin.backend.assets.AssetsLibraryList.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)));
} catch(e) {
Logs.trace('Error while parsing options.xml: ${Std.string(e)}', ERROR);
}
for (i in funkin.backend.assets.ModsFolder.getLoadedMods()) {
var xmlPath = Paths.xml('config/options/LIB_$i');
if (access != null)
for(o in parseOptionsFromXML(access))
main.add(o);
if (Paths.assetsTree.existsSpecific(xmlPath, "TEXT")) {
var access:Access = null;
try access = new Access(Xml.parse(Paths.assetsTree.getSpecificAsset(xmlPath, "TEXT")))
catch(e) Logs.trace('Error while parsing options.xml: ${Std.string(e)}', ERROR);
if (access != null) for (o in parseOptionsFromXML(access)) main.add(o);
}
}
}
public override function exit() {
@@ -1,9 +1,12 @@
package funkin.options.categories;
class MiscOptions extends OptionsScreen {
public override function new() {
super("Miscellaneous", "Use this menu to reset save data or engine settings.");
super("Miscellaneous", "Change some other options like enabling Developer Mode, resetting data...");
add(new Checkbox(
"Developer Mode",
"If checked, you will be able to access developer features like certain keybinds, editors, developer options, state reloads, console and more.",
"devMode"));
#if UPDATE_CHECKING
add(new Checkbox(
"Enable Nightly Updates",
@@ -67,6 +67,23 @@ class KeybindsOptions extends MusicBeatSubstate {
},
]
},
{
name: 'Volume',
settings: [
{
name: 'Up',
control: 'VOLUME_UP'
},
{
name: 'Down',
control: 'VOLUME_DOWN'
},
{
name: 'Mute',
control: 'VOLUME_MUTE'
},
]
},
{
name: 'Engine',
settings: [
@@ -75,6 +92,26 @@ class KeybindsOptions extends MusicBeatSubstate {
control: 'SWITCHMOD'
},
]
},
{
name: 'Developer',
devModeOnly: true,
settings: [
{
name: 'Developer Menus',
control: 'DEV_ACCESS'
},
#if GLOBAL_SCRIPT // since theyre integrated into global script - Nex
{
name: 'Open Console',
control: 'DEV_CONSOLE'
},
{
name: 'Reload State',
control: 'DEV_RELOAD'
},
#end
]
}
];
@@ -132,7 +169,9 @@ class KeybindsOptions extends MusicBeatSubstate {
for (i in customCategories) categories.push(i);
var k:Int = 0;
for(category in categories) {
for (category in categories) {
if (category.devModeOnly && !Options.devMode) continue;
k++;
var title = new Alphabet(0, k * 75, category.name, true);
title.screenCenter(X);
@@ -168,6 +207,10 @@ class KeybindsOptions extends MusicBeatSubstate {
}
add(alphabets);
add(camFollow);
FlxG.sound.volumeUpKeys = [];
FlxG.sound.volumeDownKeys = [];
FlxG.sound.muteKeys = [];
}
public override function destroy() {
@@ -181,10 +224,8 @@ class KeybindsOptions extends MusicBeatSubstate {
public override function update(elapsed:Float) {
super.update(elapsed);
if (isSubState) {
bg.alpha = lerp(bg.alpha, 0.1, 0.125);
} else {
if (isSubState) bg.alpha = lerp(bg.alpha, 0.1, 0.125);
else {
if (curSelected < 4) {
if (coloredBG.alpha == 0)
coloredBG.color = noteColors[curSelected];
@@ -200,11 +241,11 @@ class KeybindsOptions extends MusicBeatSubstate {
changeSelection((controls.UP_P ? -1 : 0) + (controls.DOWN_P ? 1 : 0));
if (controls.BACK) {
MusicBeatState.skipTransIn = true;
if (isSubState)
close();
else
if (isSubState) close();
else {
MusicBeatState.skipTransIn = true;
FlxG.switchState(new OptionsMenu());
}
ControlsUtil.resetCustomControls();
Options.applyKeybinds();
ControlsUtil.loadCustomControls();
@@ -251,7 +292,7 @@ class KeybindsOptions extends MusicBeatSubstate {
var minH = FlxG.height / 2;
var maxH = alphabets.members[alphabets.length-1].y + alphabets.members[alphabets.length-1].height - (FlxG.height / 2);
if (minH < maxH)
camFollow.setPosition(FlxG.width / 2, CoolUtil.bound(alphabet.y + (alphabet.height / 2) - (35), minH, maxH));
camFollow.setPosition(FlxG.width / 2, CoolUtil.bound(alphabet.y + (alphabet.height / 2) - 35, minH, maxH));
else
camFollow.setPosition(FlxG.width / 2, FlxG.height / 2);
}
+18 -4
View File
@@ -49,7 +49,21 @@ class GithubUserIcon extends FlxSprite
super.update(elapsed);
}
#if target.threaded
final mutex = new sys.thread.Mutex();
#end
inline function acquireMutex() {
#if target.threaded
mutex.acquire();
#end
}
inline function releaseMutex() {
#if target.threaded
mutex.release();
#end
}
override function drawComplex(camera:FlxCamera):Void { // Making the image download only if the player actually sees it on the screen - Nex
if(waitUntilLoad <= 0) {
waitUntilLoad = null;
@@ -82,19 +96,19 @@ class GithubUserIcon extends FlxSprite
}
if(bmap != null) try {
mutex.acquire(); // Avoiding critical section - Nex
acquireMutex(); // Avoiding critical section - Nex
var leGraphic:FlxGraphic = FlxG.bitmap.add(bmap, false, key);
leGraphic.persist = true;
updateDaFunni(leGraphic);
bmap = null;
mutex.release();
releaseMutex();
} catch(e) {
Logs.traceColored([Logs.logText('Failed to update the pfp for ${user.login}: ${e.message}', RED)], ERROR);
}
} else {
mutex.acquire();
acquireMutex();
updateDaFunni(bmap);
mutex.release();
releaseMutex();
}
});
}
+4
View File
@@ -14,6 +14,10 @@ class IconOption extends TextOption {
iconSpr = new HealthIcon(icon, false);
iconSpr.setPosition(90 - iconSpr.width, (__text.height - iconSpr.height) / 2);
iconSpr.setUnstretchedGraphicSize(150, 150, true);
iconSpr.updateHitbox();
iconSpr.sprTracker = __text;
iconSpr.sprTrackerAlignment = LEFT;
add(iconSpr);
}
}
-1
View File
@@ -1,7 +1,6 @@
package funkin.options.type;
import flixel.util.FlxColor;
import funkin.game.HealthIcon;
/**
* Option type that has a new button, and is green.
+411
View File
@@ -0,0 +1,411 @@
package lime.ui;
import haxe.io.Bytes;
import haxe.io.Path;
import lime._internal.backend.native.NativeCFFI;
import lime.app.Event;
import lime.graphics.Image;
import lime.system.BackgroundWorker;
import lime.utils.ArrayBuffer;
import lime.utils.Resource;
#if hl
import hl.Bytes as HLBytes;
import hl.NativeArray;
#end
#if sys
import sys.io.File;
#end
#if (js && html5)
import js.html.Blob;
#end
/**
Simple file dialog used for asking user where to save a file, or select files to open.
Example usage:
```haxe
var fileDialog = new FileDialog();
fileDialog.onCancel.add( () -> trace("Canceled.") );
fileDialog.onSave.add( path -> trace("File saved in " + path) );
fileDialog.onOpen.add( res -> trace("Size of the file = " + (res:haxe.io.Bytes).length) );
if ( fileDialog.open("jpg", null, "Load file") )
trace("File dialog opened, waiting for selection...");
else
trace("This dialog is unsupported.");
```
Availability note: most file dialog operations are only available on desktop targets, though
`save()` is also available in HTML5.
**/
#if !lime_debug
@:fileXml('tags="haxe,release"')
@:noDebug
#end
@:access(lime._internal.backend.native.NativeCFFI)
@:access(lime.graphics.Image)
class FileDialog
{
/**
Triggers when the user clicks "Cancel" during any operation, or when a function is unsupported
(such as `open()` on HTML5).
**/
public var onCancel = new Event<Void->Void>();
/**
Triggers when `open()` is successful. The `lime.utils.Resource` contains the file's data, and can
be implicitly cast to `haxe.io.Bytes`.
**/
public var onOpen = new Event<Resource->Void>();
/**
Triggers when `open()` is successful. The `lime.utils.Resource` contains the file's data, and can
be implicitly cast to `haxe.io.Bytes`, the String is the path to the file.
**/
public var onOpenFile = new Event<(Resource, String)->Void>(); // Added by @NeeEoo
/**
Triggers when `save()` is successful. The `String` is the path to the saved file.
**/
public var onSave = new Event<String->Void>();
/**
Triggers when `browse()` is successful and `type` is anything other than
`FileDialogType.OPEN_MULTIPLE`. The `String` is the path to the selected file.
**/
public var onSelect = new Event<String->Void>();
/**
Triggers when `browse()` is successful and `type` is `FileDialogType.OPEN_MULTIPLE`. The
`Array<String>` contains all selected file paths.
**/
public var onSelectMultiple = new Event<Array<String>->Void>();
public function new() {}
/**
Opens a file selection dialog. If successful, either `onSelect` or `onSelectMultiple` will trigger
with the result(s).
This function only works on desktop targets, and will return `false` otherwise.
@param type Type of the file dialog: `OPEN`, `SAVE`, `OPEN_DIRECTORY` or `OPEN_MULTIPLE`.
@param filter A filter to use when browsing. Asterisks are treated as wildcards. For example,
`"*.jpg"` will match any file ending in `.jpg`.
@param defaultPath The directory in which to start browsing and/or the default filename to
suggest. Defaults to `Sys.getCwd()`, with no default filename.
@param title The title to give the dialog window.
@return Whether `browse()` is supported on this target.
**/
public function browse(type:FileDialogType = null, filter:String = null, defaultPath:String = null, title:String = null):Bool
{
if (type == null) type = FileDialogType.OPEN;
#if desktop
var worker = new BackgroundWorker();
worker.doWork.add(function(_)
{
switch (type)
{
case OPEN:
#if linux
if (title == null) title = "Open File";
#end
var path = null;
#if (!macro && lime_cffi)
#if hl
var bytes = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath);
if (bytes != null)
{
path = @:privateAccess String.fromUTF8(cast bytes);
}
#else
path = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath);
#end
#end
worker.sendComplete(path);
case OPEN_MULTIPLE:
#if linux
if (title == null) title = "Open Files";
#end
var paths = null;
#if (!macro && lime_cffi)
#if hl
var bytes:NativeArray<HLBytes> = cast NativeCFFI.lime_file_dialog_open_files(title, filter, defaultPath);
if (bytes != null)
{
paths = [];
for (i in 0...bytes.length)
{
paths[i] = @:privateAccess String.fromUTF8(bytes[i]);
}
}
#else
paths = NativeCFFI.lime_file_dialog_open_files(title, filter, defaultPath);
#end
#end
worker.sendComplete(paths);
case OPEN_DIRECTORY:
#if linux
if (title == null) title = "Open Directory";
#end
var path = null;
#if (!macro && lime_cffi)
#if hl
var bytes = NativeCFFI.lime_file_dialog_open_directory(title, filter, defaultPath);
if (bytes != null)
{
path = @:privateAccess String.fromUTF8(cast bytes);
}
#else
path = NativeCFFI.lime_file_dialog_open_directory(title, filter, defaultPath);
#end
#end
worker.sendComplete(path);
case SAVE:
#if linux
if (title == null) title = "Save File";
#end
var path = null;
#if (!macro && lime_cffi)
#if hl
var bytes = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath);
if (bytes != null)
{
path = @:privateAccess String.fromUTF8(cast bytes);
}
#else
path = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath);
#end
#end
worker.sendComplete(path);
}
});
worker.onComplete.add(function(result)
{
switch (type)
{
case OPEN, OPEN_DIRECTORY, SAVE:
var path:String = cast result;
if (path != null)
{
// Makes sure the filename ends with extension
if (type == SAVE && filter != null && path.indexOf(".") == -1)
{
path += "." + filter;
}
onSelect.dispatch(path);
}
else
{
onCancel.dispatch();
}
case OPEN_MULTIPLE:
var paths:Array<String> = cast result;
if (paths != null && paths.length > 0)
{
onSelectMultiple.dispatch(paths);
}
else
{
onCancel.dispatch();
}
}
});
worker.run();
return true;
#else
onCancel.dispatch();
return false;
#end
}
/**
Shows an open file dialog. If successful, `onOpen` will trigger with the file contents.
This function only works on desktop targets, and will return `false` otherwise.
@param filter A filter to use when browsing. Asterisks are treated as wildcards. For example,
`"*.jpg"` will match any file ending in `.jpg`.
@param defaultPath The directory in which to start browsing and/or the default filename to
suggest. Defaults to `Sys.getCwd()`, with no default filename.
@param title The title to give the dialog window.
@return Whether `open()` is supported on this target.
**/
public function open(filter:String = null, defaultPath:String = null, title:String = null):Bool
{
#if (desktop && sys)
var worker = new BackgroundWorker();
worker.doWork.add(function(_)
{
#if linux
if (title == null) title = "Open File";
#end
var path = null;
#if (!macro && lime_cffi)
#if hl
var bytes = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath);
if (bytes != null) path = @:privateAccess String.fromUTF8(cast bytes);
#else
path = NativeCFFI.lime_file_dialog_open_file(title, filter, defaultPath);
#end
#end
worker.sendComplete(path);
});
worker.onComplete.add(function(path:String)
{
if (path != null)
{
try
{
var data = File.getBytes(path);
onOpen.dispatch(data);
onOpenFile.dispatch(data, path); // Added by @NeeEoo
return;
}
catch (e:Dynamic) {}
}
onCancel.dispatch();
});
worker.run();
return true;
#else
onCancel.dispatch();
return false;
#end
}
/**
Shows an open file dialog. If successful, `onSave` will trigger with the selected path.
This function only works on desktop and HMTL5 targets, and will return `false` otherwise.
@param data The file contents, in `haxe.io.Bytes` format. (Implicit casting possible.)
@param filter A filter to use when browsing. Asterisks are treated as wildcards. For example,
`"*.jpg"` will match any file ending in `.jpg`. Used only if targeting deskop.
@param defaultPath The directory in which to start browsing and/or the default filename to
suggest. When targeting destkop, this defaults to `Sys.getCwd()` with no default filename. When targeting
HTML5, this defaults to the browser's download directory, with a default filename based on the MIME type.
@param title The title to give the dialog window.
@param type The default MIME type of the file, in case the type can't be determined from the
file data. Used only if targeting HTML5.
@return Whether `save()` is supported on this target.
**/
public function save(data:Resource, filter:String = null, defaultPath:String = null, title:String = null, type:String = "application/octet-stream"):Bool
{
if (data == null)
{
onCancel.dispatch();
return false;
}
#if (desktop && sys)
var worker = new BackgroundWorker();
worker.doWork.add(function(_)
{
#if linux
if (title == null) title = "Save File";
#end
var path = null;
#if (!macro && lime_cffi)
#if hl
var bytes = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath);
path = @:privateAccess String.fromUTF8(cast bytes);
#else
path = NativeCFFI.lime_file_dialog_save_file(title, filter, defaultPath);
#end
#end
worker.sendComplete(path);
});
worker.onComplete.add(function(path:String)
{
if (path != null)
{
try
{
File.saveBytes(path, data);
onSave.dispatch(path);
return;
}
catch (e:Dynamic) {}
}
onCancel.dispatch();
});
worker.run();
return true;
#elseif (js && html5)
// TODO: Cleaner API for mimeType detection
var defaultExtension = "";
if (Image.__isPNG(data))
{
type = "image/png";
defaultExtension = ".png";
}
else if (Image.__isJPG(data))
{
type = "image/jpeg";
defaultExtension = ".jpg";
}
else if (Image.__isGIF(data))
{
type = "image/gif";
defaultExtension = ".gif";
}
else if (Image.__isWebP(data))
{
type = "image/webp";
defaultExtension = ".webp";
}
var path = defaultPath != null ? Path.withoutDirectory(defaultPath) : "download" + defaultExtension;
var buffer = (data : Bytes).getData();
buffer = buffer.slice(0, (data : Bytes).length);
#if commonjs
untyped #if haxe4 js.Syntax.code #else __js__ #end ("require ('file-saver')")(new Blob([buffer], {type: type}), path, true);
#else
untyped window.saveAs(new Blob([buffer], {type: type}), path, true);
#end
onSave.dispatch(path);
return true;
#else
onCancel.dispatch();
return false;
#end
}
}
+2 -1
View File
@@ -695,7 +695,8 @@ class Sound extends EventDispatcher
// var samples = (__buffer.data.length * 8) / (__buffer.channels * __buffer.bitsPerSample);
// return Std.int(samples / __buffer.sampleRate * 1000);
var samples = (Int64.make(0, __buffer.data.length) * Int64.ofInt(8)) / Int64.ofInt(__buffer.channels * __buffer.bitsPerSample);
var value = samples / Int64.ofInt(__buffer.sampleRate) * Int64.ofInt(1000);
var div = Int64.divMod(samples * Int64.ofInt(1000), Int64.ofInt(__buffer.sampleRate));
var value = (div.quotient + (div.modulus / __buffer.sampleRate));
return Int64.toInt(value);
}
else if (__buffer.__srcVorbisFile != null)