diff --git a/FEATURES.md b/FEATURES.md
index caa854171..ae8daf07e 100644
--- a/FEATURES.md
+++ b/FEATURES.md
@@ -79,4 +79,5 @@ _**QOL = Quality of Life**_
- Features not found in other editors!
- Every single state & substate can be modified via HScript (`data/states/StateName.hx`)
- **Instances launched via `lime test windows` will automatically use assets from source.**
-
\ No newline at end of file
+- Modcharting features powered by [FunkinModchart](https://lib.haxe.org/p/funkin-modchart/).
+
diff --git a/README.md b/README.md
index 2d95c17b2..d5ba590c9 100644
--- a/README.md
+++ b/README.md
@@ -86,4 +86,5 @@ In the future (when the engine won't be a WIP anymore) we're gonna also publish
- Credits to the [FlxAnimate](https://github.com/Dot-Stuff/flxanimate) team for the Animate Atlas support
- Credits to Smokey555 for the backup Animate Atlas to spritesheet code
- Credits to MAJigsaw77 for [hxvlc](https://github.com/MAJigsaw77/hxvlc) (video cutscene/mp4 support) and [hxdiscord_rpc](https://github.com/MAJigsaw77/hxdiscord_rpc) (discord rpc integration)
+- Credits to [TheoDev](https://github.com/TheoDevelops) for [FunkinModchart](https://lib.haxe.org/p/funkin-modchart/). ***(library used for modcharting features)***
diff --git a/libs.xml b/libs.xml
index 570b451c2..8903b8399 100644
--- a/libs.xml
+++ b/libs.xml
@@ -13,6 +13,7 @@
+
diff --git a/project.xml b/project.xml
index 382ed9f65..4cf4523eb 100644
--- a/project.xml
+++ b/project.xml
@@ -159,6 +159,9 @@
+
+
+
@@ -195,6 +198,14 @@
+
+
diff --git a/source/funkin/game/Note.hx b/source/funkin/game/Note.hx
index 9b06fc819..dc909f0e3 100644
--- a/source/funkin/game/Note.hx
+++ b/source/funkin/game/Note.hx
@@ -53,6 +53,13 @@ class Note extends FlxSprite
*/
public var nextSustain:Note;
+ /**
+ * The parent of the sustain.
+ *
+ * If this note is not sustain, this will be null.
+ */
+ public var sustainParent:Null;
+
/**
* Name of the splash.
*/
@@ -123,6 +130,10 @@ class Note extends FlxSprite
}
}
+ // work around to set the `sustainParent`
+ if (isSustainNote)
+ sustainParent = prevNote.isSustainNote ? prevNote.sustainParent : prevNote;
+
x += 50;
// MAKE SURE ITS DEFINITELY OFF SCREEN?
y -= 2000;
diff --git a/source/funkin/game/Strum.hx b/source/funkin/game/Strum.hx
index cbce2da40..ffd34b714 100644
--- a/source/funkin/game/Strum.hx
+++ b/source/funkin/game/Strum.hx
@@ -11,6 +11,11 @@ class Strum extends FlxSprite {
*/
public var animSuffix:String = "";
+ /**
+ * This strum's StrumLine
+ */
+ public var strumLine:StrumLine;
+
public var cpu = false; // Unused
public var lastHit:Float = -5000;
diff --git a/source/funkin/game/StrumLine.hx b/source/funkin/game/StrumLine.hx
index 1b0780ddd..5bb2b7340 100644
--- a/source/funkin/game/StrumLine.hx
+++ b/source/funkin/game/StrumLine.hx
@@ -307,6 +307,7 @@ class StrumLine extends FlxTypedGroup {
animPrefix = strumAnimPrefix[i % strumAnimPrefix.length];
var babyArrow:Strum = new Strum(startingPos.x + ((Note.swagWidth * strumScale) * i), startingPos.y);
babyArrow.ID = i;
+ babyArrow.strumLine = this;
if(data.scrollSpeed != null)
babyArrow.scrollSpeed = data.scrollSpeed;
diff --git a/source/funkin/options/Options.hx b/source/funkin/options/Options.hx
index a9bf72ac1..b7b74876d 100644
--- a/source/funkin/options/Options.hx
+++ b/source/funkin/options/Options.hx
@@ -35,6 +35,9 @@ class Options
public static var songOffset:Float = 0;
public static var framerate:Int = 120;
public static var gpuOnlyBitmaps:Bool = #if (mac || web) false #else true #end; // causes issues on mac and web
+ #if MODCHARTING_FEATURES
+ public static var modchartHoldSubdivisions:Int = 4;
+ #end
public static var lastLoadedMod:String = null;
diff --git a/source/funkin/options/OptionsMenu.hx b/source/funkin/options/OptionsMenu.hx
index 306621362..68d732cd2 100644
--- a/source/funkin/options/OptionsMenu.hx
+++ b/source/funkin/options/OptionsMenu.hx
@@ -24,6 +24,13 @@ class OptionsMenu extends TreeMenu {
desc: 'Change Appearance options such as Flashing menus...',
state: AppearanceOptions
},
+ #if MODCHARTING_FEATURES
+ {
+ name: 'Modchart Settings >',
+ desc: 'Customize your modcharting experience...',
+ state: ModchartingOptions
+ },
+ #end
{
name: 'Miscellaneous >',
desc: 'Use this menu to reset save data or engine settings.',
diff --git a/source/funkin/options/categories/AppearanceOptions.hx b/source/funkin/options/categories/AppearanceOptions.hx
index 926213431..a6a458393 100644
--- a/source/funkin/options/categories/AppearanceOptions.hx
+++ b/source/funkin/options/categories/AppearanceOptions.hx
@@ -56,4 +56,4 @@ class AppearanceOptions extends OptionsScreen {
else
FlxG.updateFramerate = FlxG.drawFramerate = Std.int(change);
}
-}
\ No newline at end of file
+}
diff --git a/source/funkin/options/categories/ModchartingOptions.hx b/source/funkin/options/categories/ModchartingOptions.hx
new file mode 100644
index 000000000..e5850198f
--- /dev/null
+++ b/source/funkin/options/categories/ModchartingOptions.hx
@@ -0,0 +1,17 @@
+package funkin.options.categories;
+
+#if MODCHARTING_FEATURES
+class ModchartingOptions extends OptionsScreen {
+ public override function new() {
+ super("Modcharting Options", "Customize your modcharting experience.");
+ add(new NumOption(
+ "Hold Subdivisions",
+ "Softens the tail/hold/sustain of the arrows by subdividing it, giving them greater quality. By higher the subdivisions number is, performance will be affected.",
+ 1, // minimum
+ 128, // maximum
+ 1, // change
+ "modchartHoldSubdivisions" // save name
+ )); // callback
+ }
+}
+#end
\ No newline at end of file
diff --git a/source/modchart/standalone/adapters/codename/Codename.hx b/source/modchart/standalone/adapters/codename/Codename.hx
new file mode 100644
index 000000000..40ca6b30d
--- /dev/null
+++ b/source/modchart/standalone/adapters/codename/Codename.hx
@@ -0,0 +1,163 @@
+package modchart.standalone.adapters.codename;
+
+import flixel.FlxCamera;
+import flixel.FlxSprite;
+import funkin.backend.system.Conductor;
+import funkin.game.Note;
+import funkin.game.PlayState;
+import funkin.game.Strum;
+import funkin.options.Options;
+import modchart.standalone.IAdapter;
+
+class Codename implements IAdapter {
+ private var beatCrochet:Float = 0;
+
+ public function onModchartingInitialization() {
+ beatCrochet = Conductor.crochet;
+ }
+
+ public function isTapNote(sprite:FlxSprite) {
+ return sprite is Note;
+ }
+
+ // Song related
+ public function getSongPosition():Float {
+ return Conductor.songPosition;
+ }
+
+ public function getCurrentBeat():Float {
+ return Conductor.curBeatFloat;
+ }
+
+ public function getStaticCrochet():Float {
+ return beatCrochet;
+ }
+
+ public function getBeatFromStep(step:Float):Float {
+ return step * Conductor.stepsPerBeat;
+ }
+
+ public function arrowHit(arrow:FlxSprite) {
+ if (arrow is Note) {
+ final note:Note = cast arrow;
+ return note.wasGoodHit;
+ }
+ return false;
+ }
+
+ public function isHoldEnd(arrow:FlxSprite) {
+ if (arrow is Note) {
+ final note:Note = cast arrow;
+ return note.nextSustain == null;
+ }
+ return false;
+ }
+
+ public function getLaneFromArrow(arrow:FlxSprite) {
+ if (arrow is Note) {
+ final note:Note = cast arrow;
+ return note.strumID;
+ } else if (arrow is Strum) {
+ final strum:Strum = cast arrow;
+ return strum.ID;
+ }
+ return 0;
+ }
+
+ public function getPlayerFromArrow(arrow:FlxSprite) {
+ if (arrow is Note) {
+ final note:Note = cast arrow;
+ return note.strumLine.ID;
+ } else if (arrow is Strum) {
+ final strum:Strum = cast arrow;
+ return strum.strumLine.ID;
+ }
+
+ return 0;
+ }
+
+ public function getHoldParentTime(arrow:FlxSprite) {
+ final note:Note = cast arrow;
+ return note.sustainParent.strumTime;
+ }
+
+ // im so fucking sorry for those conditionals
+ public function getKeyCount(?player:Int = 0):Int {
+ return PlayState.instance != null
+ && PlayState.instance.strumLines != null
+ && PlayState.instance.strumLines.members != null
+ && PlayState.instance.strumLines.members[player] != null
+ && PlayState.instance.strumLines.members[player].members != null ? PlayState.instance.strumLines.members[player].members.length : 4;
+ }
+
+ public function getPlayerCount():Int {
+ return PlayState.instance != null && PlayState.instance.strumLines != null ? PlayState.instance.strumLines.length : 2;
+ }
+
+ public function getTimeFromArrow(arrow:FlxSprite) {
+ if (arrow is Note) {
+ final note:Note = cast arrow;
+ return note.strumTime;
+ }
+
+ return 0;
+ }
+
+ public function getHoldSubdivisions():Int
+ return Options.modchartHoldSubdivisions;
+
+ public function getDownscroll():Bool
+ return Options.downscroll;
+
+ public function getDefaultReceptorX(lane:Int, player:Int):Float {
+ @:privateAccess
+ return PlayState.instance.strumLines.members[player].members[lane].x;
+ }
+
+ public function getDefaultReceptorY(lane:Int, player:Int):Float {
+ @:privateAccess
+ return PlayState.instance.strumLines.members[player].members[lane].y;
+ }
+
+ public function getArrowCamera():Array
+ return [PlayState.instance.camHUD];
+
+ public function getCurrentScrollSpeed():Float {
+ return PlayState.instance.scrollSpeed;
+ }
+
+ public function getArrowItems() {
+ var drawMembers:Array>> = [];
+ var strumLineMembers = PlayState.instance.strumLines.members;
+
+ for (i in 0...strumLineMembers.length) {
+ final sl = strumLineMembers[i];
+
+ if (!sl.visible)
+ continue;
+
+ // setup list
+ drawMembers[i] = [
+ cast sl.members.copy(),
+ [],
+ []
+ ];
+
+ // preallocating first
+ var st = 0;
+ var nt = 0;
+ sl.notes.forEachAlive((spr) -> {
+ spr.isSustainNote ? st++ : nt++;
+ });
+
+ drawMembers[i][1].resize(nt);
+ drawMembers[i][2].resize(st);
+
+ var si = 0;
+ var ni = 0;
+ sl.notes.forEachAlive((spr) -> drawMembers[i][spr.isSustainNote ? 2 : 1][spr.isSustainNote ? si++ : ni++] = spr);
+ }
+
+ return drawMembers;
+ }
+}