Merge branch 'main' into new-editors-conductor

# Conflicts:
#	source/funkin/backend/utils/CoolUtil.hx
#	source/funkin/backend/utils/IniUtil.hx
#	source/funkin/editors/character/CharacterEditor.hx
#	source/funkin/editors/ui/UIWarningSubstate.hx
This commit is contained in:
Ralty
2025-07-19 23:51:52 +07:00
9 changed files with 184 additions and 113 deletions
-25
View File
@@ -1,25 +0,0 @@
[Common]
Name="baconeggandcheese"
Description="who gaf"
APIVersion="uhmmmm probs like 2"
DownloadedFrom="https://yourmom.org"
Dependencies=[
"linkA",
"linkB",
"linkC"
]
Icon64="path/to/icon64.png"
Icon32="path/to/icon32.png"
Icon16="path/to/icon16.png"
Icon="path/to/icon.ico"
[Flags]
UseNoteRendererV2=false
UseAway3D=false
UseDiscordRPCIntegration=true
UseLocalization=false
UseLegacyFlixelAnimateHitbox=false
[Discord]
ClientID="1027994136193810442",
LogoKey="icon"
+46 -33
View File
@@ -1,9 +1,11 @@
package funkin.backend.system;
import flixel.util.FlxColor;
import funkin.backend.assets.ModsFolder;
import funkin.backend.assets.IModsAssetLibrary;
import funkin.backend.assets.ScriptedAssetLibrary;
import funkin.backend.system.macros.GitCommitMacro;
import funkin.backend.utils.IniUtil;
import lime.app.Application;
import lime.utils.AssetLibrary as LimeAssetLibrary;
import lime.utils.AssetType;
@@ -13,11 +15,14 @@ import lime.utils.AssetType;
*/
@:build(funkin.backend.system.macros.FlagMacro.build())
class Flags {
// -- Codename's Addon Config --
@:bypass public static var addonFlags:Map<String, Dynamic> = [];
// -- Codename's Mod Config --
public static var MOD_NAME:String = "";
public static var MOD_DESCRIPTION:String = "";
public static var MOD_API_VERSION:Int = 1;
public static var MOD_DOWNLOADED_LINK:String = "";
public static var MOD_DOWNLOAD_LINK:String = "";
public static var MOD_DEPENDENCIES:Array<String> = [];
public static var MOD_ICON64:String = "";
@@ -27,7 +32,9 @@ class Flags {
public static var MOD_DISCORD_CLIENT_ID:String = "";
public static var MOD_DISCORD_LOGO_KEY:String = "";
public static var MOD_DISCORD_LOGO_TEXT:String = "";
// -- Codename's Default Flags --
public static var CURRENT_API_VERSION:Int = 1;
public static var COMMIT_NUMBER:Int = GitCommitMacro.commitNumber;
public static var COMMIT_HASH:String = GitCommitMacro.commitHash;
public static var COMMIT_MESSAGE:String = 'Commit $COMMIT_NUMBER ($COMMIT_HASH)';
@@ -175,29 +182,9 @@ class Flags {
@:bypass public static var customFlags:Map<String, String> = [];
public static function loadFromData(flags:Map<String, String>, data:String) {
var trimmed:String;
var splitContent = [for(e in data.split("\n")) if ((trimmed = e.trim()) != "") trimmed];
var res = IniUtil.parseString(data);
for(line in splitContent) {
if(line.startsWith(";")) continue;
if(line.startsWith("#")) continue;
if(line.startsWith("//")) continue;
if(line.length == 0) continue;
if(line.charAt(0) == "[" && line.charAt(line.length-1) == "]") continue;
var index = line.indexOf("=");
if(index == -1) continue;
var name = line.substr(0, index).trim();
var value = line.substr(index+1).trim();
var wasQuoted = value.length > 1 && value.charCodeAt(0) == '"'.code && value.charCodeAt(value.length-1) == '"'.code;
if(wasQuoted) value = value.substr(1, value.length - 2);
if((!wasQuoted && value.length == 0) || name.length == 0)
continue;
if(!flags.exists(name))
flags[name] = value;
}
for (section in res) for (key => value in section) flags[key] = value;
}
public static function loadFromDatas(datas:Array<String>) {
@@ -221,16 +208,42 @@ class Flags {
* Loads the flags from the assets.
**/
public static function load(?libs:Array<LimeAssetLibrary> = null) {
if (libs == null)
libs = Paths.assetsTree.libraries;
final flagsPath = Paths.getPath("flags.ini");
var datas:Array<String> = [
for(lib in libs)
if(lib.exists(flagsPath, AssetType.TEXT))
lib.getAsset(flagsPath, AssetType.TEXT)
];
if (libs == null) {
libs = Paths.assetsTree.libraries.copy();
libs.reverse();
}
for(lib in libs) {
var l = lib;
if (l is openfl.utils.AssetLibrary) {
@:privateAccess
l = cast(l, openfl.utils.AssetLibrary).__proxy;
}
var flags:Map<String, String> = loadFromDatas(datas);
parseFlags(flags);
if (l is IModsAssetLibrary) {
var flagsTxt = "";
if (l.exists(Paths.ini("config/modpack"), AssetType.TEXT))
flagsTxt = l.getAsset(Paths.ini("config/modpack"), AssetType.TEXT);
if (cast(l, IModsAssetLibrary).modName == "assets") continue;
if (cast(l, IModsAssetLibrary).modName == ModsFolder.currentModFolder) {
var flags:Map<String, String> = [];
loadFromData(flags, flagsTxt);
parseFlags(flags);
}
else {
var flags:Map<String, String> = [];
loadFromData(flags, flagsTxt);
addonFlags.set(cast(l, IModsAssetLibrary).modName.toLowerCase().replace(" ", "").trim(), flags);
}
}
else {
var flagsTxt = "";
if (l.exists(Paths.getPath("data/config/flags.ini"), AssetType.TEXT))
flagsTxt = l.getAsset(Paths.getPath("data/config/flags.ini"), AssetType.TEXT);
var flags:Map<String, String> = [];
loadFromData(flags, flagsTxt);
parseFlags(flags);
}
}
}
}
+17 -2
View File
@@ -5,8 +5,10 @@ import sys.FileSystem;
#end
import flixel.FlxState;
import funkin.backend.assets.ModsFolder;
import funkin.backend.assets.ModsFolderLibrary;
import funkin.backend.chart.EventsData;
import funkin.backend.system.framerate.Framerate;
import funkin.editors.ModConfigWarning;
import funkin.menus.TitleState;
import haxe.io.Path;
@@ -114,10 +116,23 @@ class MainState extends FlxState {
if (Framerate.isLoaded)
Framerate.instance.reload();
FlxG.switchState(new TitleState());
#if sys
CoolUtil.safeAddAttributes('./.temp/', NativeAPI.FileAttribute.HIDDEN);
#end
if (Options.devMode) {
var lib:ModsFolderLibrary;
for (e in Paths.assetsTree.libraries) {
@:privateAccess if (!(e is openfl.utils.AssetLibrary) || !((lib = cast cast(e, openfl.utils.AssetLibrary).__proxy) is ModsFolderLibrary)) continue;
if (lib.modName == ModsFolder.currentModFolder) {
if (lib.exists(Paths.ini("config/modpack"), lime.utils.AssetType.TEXT)) break;
FlxG.switchState(new ModConfigWarning(lib));
return;
}
}
}
FlxG.switchState(new TitleState());
}
}
+13 -21
View File
@@ -449,30 +449,22 @@ final class CoolUtil
FlxG.sound.music.persist = Persist;
}
var infoPath = '${Path.withoutExtension(path)}.ini';
if (Assets.exists(infoPath)) {
var musicInfo = IniUtil.parseAsset(infoPath, [
"BPM" => null,
"TimeSignature" => Flags.DEFAULT_BEATS_PER_MEASURE + "/" + Flags.DEFAULT_STEPS_PER_BEAT,
"LoopTime" => '${Flags.DEFAULT_LOOP_TIME}'
]);
var iniPath = '${Path.withoutExtension(path)}.ini';
var musicData = Assets.exists(iniPath) ? IniUtil.parseAsset(iniPath)["Global"] : null;
if (musicData != null) {
if (musicData["LoopTime"] != null) FlxG.sound.music.loopTime = Std.parseFloat(musicData["LoopTime"]) * 1000;
var timeSignParsed:Array<Null<Float>> = musicData["TimeSignature"] == null ? [] : [for(s in musicData["TimeSignature"].split("/")) Std.parseFloat(s)];
var beatsPerMeasure:Float = Flags.DEFAULT_BEATS_PER_MEASURE, stepsPerBeat:Float = Flags.DEFAULT_STEPS_PER_BEAT;
if (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;
// Check later, i don't think timeSignParsed can contain null, only nan
if (timeSignParsed.length == 2 && !timeSignParsed.contains(null)) {
beatsPerMeasure = timeSignParsed[0] == null || timeSignParsed[0] <= 0 ? Flags.DEFAULT_BEATS_PER_MEASURE : cast timeSignParsed[0];
stepsPerBeat = timeSignParsed[1] == null || timeSignParsed[1] <= 0 ? Flags.DEFAULT_STEPS_PER_BEAT : cast timeSignParsed[1];
if (timeSignParsed.length == 2) {
beatsPerMeasure = Math.isNaN(timeSignParsed[0]) ? Flags.DEFAULT_BEATS_PER_MEASURE : timeSignParsed[0];
stepsPerBeat = Math.isNaN(timeSignParsed[1]) ? Flags.DEFAULT_STEPS_PER_BEAT : (16 / timeSignParsed[1]); // from denominator
}
var bpm:Null<Float> = Std.parseFloat(musicInfo["BPM"]).getDefault(DefaultBPM);
Conductor.changeBPM(bpm, beatsPerMeasure, floorInt(stepsPerBeat));
} else
var bpm:Float = Std.parseFloat(musicData["BPM"]).getDefault(DefaultBPM);
Conductor.changeBPM(bpm, beatsPerMeasure, Math.floor(stepsPerBeat));
}
else
Conductor.changeBPM(DefaultBPM);
}
@@ -89,6 +89,10 @@ final class DiscordUtil
config.logoKey = config.logoKey.getDefault(Flags.DEFAULT_DISCORD_LOGO_KEY);
config.logoText = config.logoText.getDefault(Flags.DEFAULT_DISCORD_LOGO_TEXT);
config.clientID = config.clientID.getDefault(Flags.DEFAULT_DISCORD_CLIENT_ID);
if (Flags.MOD_DISCORD_CLIENT_ID.length > 0) config.clientID = Flags.MOD_DISCORD_CLIENT_ID;
if (Flags.MOD_DISCORD_LOGO_KEY.length > 0) config.logoKey = Flags.MOD_DISCORD_LOGO_KEY;
if (Flags.MOD_DISCORD_LOGO_TEXT.length > 0) config.logoText = Flags.MOD_DISCORD_LOGO_TEXT;
currentID = config.clientID;
#end
}
+25 -31
View File
@@ -1,39 +1,33 @@
package funkin.backend.utils;
/**
* DOESN'T SUPPORT CATEGORIES YET!!
*/
final class IniUtil {
public static inline function parseAsset(assetPath:String, ?defaultVariables:Map<String, String>)
return parseString(Assets.getText(assetPath), defaultVariables);
class IniUtil {
public static inline function parseAsset(assetPath:String)
return parseString(Assets.getText(assetPath));
public static function parseString(data:String, ?defaultVariables:Map<String, String>) {
var trimmed:String;
var splitContent = [for(e in data.split("\n")) if ((trimmed = e.trim()) != "") trimmed];
public static function parseString(data:String):Map<String, Map<String, String>> {
var res:Map<String, Map<String, String>> = [];
parseStringToMap(res, data);
return res;
}
var finalMap:Map<String, String> = [];
if (defaultVariables != null)
for(k=>e in defaultVariables)
finalMap[k] = e;
public static function parseStringToMap(map:Map<String, Map<String, String>>, data:String) {
var section:Map<String, String> = null;
for(line in splitContent) {
// comment
if (line.startsWith(";")) continue;
// categories; not supported yet
if (line.startsWith("[") && line.endsWith("]")) continue;
var regexSec = ~/\\[(.+)\\]/g, regexVal = ~/(.+)=(.+)/g, quote = ~/[\\'"](.+)[\\'"]/g, iln = 0;
do {
var line = data.substring(iln, ((iln = data.indexOf('\n', iln) + 1) == 0 ? data.length : iln - 1));
if (regexSec.match(line)) {
if ((section = map.get(regexSec.matched(1))) == null) map.set(regexSec.matched(1), section = []);
}
else if (regexVal.match(line)) {
var s = regexVal.matched(2).trim();
if (quote.match(s)) s = quote.matched(1);
var index = line.indexOf("=");
var name = line.substr(0, index).trim();
var value = line.substr(index+1).trim();
if (value.length > 1 && value.charCodeAt(0) == '"'.code && value.charCodeAt(value.length-1) == '"'.code)
value = value.substr(1, value.length - 2);
if (value.length == 0 || name.length == 0)
continue;
finalMap[name] = value;
}
return finalMap;
if (section == null) {
if ((section = map.get("Global")) == null) map.set("Global", section = []);
}
section.set(regexVal.matched(1).trim(), s);
}
} while (iln != 0);
}
}
+75
View File
@@ -0,0 +1,75 @@
package funkin.editors;
import funkin.backend.assets.ModsFolderLibrary;
import flixel.FlxState;
class ModConfigWarning extends UIState {
public static var hadPopup:Bool = false;
var library:ModsFolderLibrary = null;
var goToState:Class<FlxState>;
public static inline var defaultModConfigText =
'[Common]
MOD_NAME="YOUR MOD NAME HERE"
MOD_DESCRIPTION="YOUR MOD DESCRIPTION HERE"
# DO NOT EDIT!! this is used to check for version compatibility!
MOD_API_VERSION=1
MOD_DOWNLOAD_LINK="YOUR MOD PAGE LINK HERE"
# Not supported yet
;MOD_ICON64="path/to/icon64.png"
;MOD_ICON32="path/to/icon32.png"
;MOD_ICON16="path/to/icon16.png"
;MOD_ICON="path/to/icon.ico"
[Flags]
DISABLE_BETA_WARNING_SCREEN=true
[Discord]
MOD_DISCORD_CLIENT_ID=""
MOD_DISCORD_LOGO_KEY=""
MOD_DISCORD_LOGO_TEXT=""';
public function new(library:ModsFolderLibrary, ?goToState:Class<FlxState>) {
super();
this.library = library;
this.goToState = goToState != null ? goToState : funkin.menus.TitleState;
}
override function createPost() {
super.createPost();
hadPopup = true;
var substate = new UIWarningSubstate("Missing mod folder configuration!", "Your mod is currently missing a mod config file!\n\n\nWould you like to automatically generate one?\n\n(PS: If this is not your mod please disable Developer mode to stop this popup from appearing.)", [
{
label: "Not Now",
color: 0x969533,
onClick: function (_) {
MusicBeatState.skipTransOut = MusicBeatState.skipTransIn = false;
FlxG.switchState(cast Type.createInstance(goToState, []));
}
},
{
label: "Yes",
onClick: function(_) {
var path = '${library.folderPath}/data/config/modpack.ini';
CoolUtil.safeSaveFile(path, defaultModConfigText);
openSubState(new UIWarningSubstate("Created mod config!", "Your mod config file has been created at " + path + "!", [
{
label: "Ok",
onClick: function (_) {
MusicBeatState.skipTransOut = MusicBeatState.skipTransIn = false;
FlxG.switchState(cast Type.createInstance(goToState, []));
}
},
], false));
}
}
], false);
substate.bHeight = 300;
openSubState(substate);
}
}
@@ -22,6 +22,8 @@ class UIWarningSubstate extends MusicBeatSubstate {
_;
};
public var bHeight:Int = 232;
var title:String;
var message:String;
var buttons:Array<WarningButton>;
@@ -70,7 +72,7 @@ class UIWarningSubstate extends MusicBeatSubstate {
FlxG.cameras.add(warnCam, false);
var spr = new UISliceSprite(0, 0, CoolUtil.maxInt(560, 30 + (170 * buttons.length)), 232, 'editors/ui/${isError ? "normal" : "grayscale"}-popup');
var spr = new UISliceSprite(0, 0, CoolUtil.maxInt(560, 30 + (170 * buttons.length)), bHeight, 'editors/ui/${isError ? "normal" : "grayscale"}-popup');
var sprIcon:FlxSprite = new FlxSprite(spr.x + 18, spr.y + 28 + 26).loadGraphic(Paths.image('editors/warnings/${isError ? "error" : "warning"}'));
sprIcon.scale.set(1.4, 1.4);
+1
View File
@@ -29,6 +29,7 @@ class MainMenuState extends MusicBeatState
override function create()
{
super.create();
DiscordUtil.call("onMenuLoaded", ["Main Menu"]);