1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-12-08 04:58:48 +00:00

Compare commits

...

17 commits

Author SHA1 Message Date
Kolo ffd84abd66
Merge d06c25a4b1 into 758f712eb5 2025-12-07 12:36:05 -05:00
AbnormalPoof 758f712eb5 Add global offsets for Pico and Nene's pixel variants 2025-12-07 01:40:24 -07:00
Furo 01029817c0 Added funnyColor backwards compatibility 2025-12-07 01:40:20 -07:00
Furo 8a31e12d10 Change BGScrollingText to a single FlxText
BGScrollingText is now a single FlxText drawn multiple time in a single frame which is more optimized that a FlxSpriteGroup with multiple FlxSprites taking the text's graphic
2025-12-07 01:40:20 -07:00
EliteMasterEric 90ab04caa1 Tweak vertical offset on the measure numbers. 2025-12-07 01:40:20 -07:00
EliteMasterEric ba7f89b9a2 Redo the backing color of the measure ticks. 2025-12-07 01:40:20 -07:00
EliteMasterEric ee36cbbbcf Rewrite measure ticks to not get redrawn EVERY FRAME GODAM 2025-12-07 01:40:20 -07:00
Hyper_ d74b8fb4ca fix: Cursor not properly fading out when exiting CharSelect with backspace 2025-12-07 01:40:20 -07:00
Cameron Taylor 8fa622e147 remove unused pitch stuff in CharSelect 2025-12-07 01:40:20 -07:00
Cameron Taylor a12950d5a7 fix char select inputs for keyb and controllers 2025-12-07 01:40:20 -07:00
EliteMasterEric 71bc847698 Clean up the Lime target config dropdown by removing unused targets like AIR and Flash. 2025-12-07 01:39:25 -07:00
AbnormalPoof a560bf51a9 Re-export Pico's PERFECT rank animation with BTA 2025-12-07 01:39:25 -07:00
AbnormalPoof 866e5aa008 Fix the cursor lerping from the top left in character select 2025-12-07 01:39:22 -07:00
AbnormalPoof 3c213ad45c Add getFramesWithKeyword() 2025-12-07 01:39:22 -07:00
anysad a0b933ce8f Remove unused Chart Editor Context Menu code 2025-12-07 01:39:22 -07:00
Kolo d06c25a4b1 use sha1 encoding 2025-11-09 22:03:43 +01:00
Kolo e0f0c81086 use macro to detect if the chart has been tampered with on runtime 2025-11-09 18:53:43 +01:00
16 changed files with 643 additions and 461 deletions

112
.vscode/settings.json vendored
View file

@ -1,31 +1,22 @@
{
"[haxe]": {
// Automatically keep Haxe files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
},
"editor.codeActionsOnSave": { "source.organizeImports": "never" },
"editor.defaultFormatter": "nadako.vshaxe",
"editor.tabSize": 2
},
"[json]": {
// Automatically keep JSON files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
// Automatically keep JSONC files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"prettier.tabWidth": 2,
// XML formatting style configuration
// XML Formatting
"xml.format.enabled": true,
"xml.format.legacy": false,
"xml.format.emptyElements": "collapse",
@ -33,7 +24,7 @@
"xml.format.enforceQuoteStyle": "preferred",
"xml.format.preserveAttributeLineBreaks": false,
"xml.format.preservedNewlines": 0,
"xml.format.splitAttributes": false,
"xml.format.splitAttributes": "preserve",
"xml.format.joinCDATALines": true,
"xml.format.preserveEmptyContent": false,
"xml.format.joinCommentLines": false,
@ -56,26 +47,19 @@
"xml.format.maxLineWidth": 0,
"xml.format.grammarAwareFormatting": true,
// Generic file formatting style configuration
// General formatting
"files.insertFinalNewline": true,
"files.trimFinalNewlines": false,
"files.trimTrailingWhitespace": true,
// Automatically detect indentation.
"editor.detectIndentation": true,
"editor.insertSpaces": true,
"editor.tabSize": 2,
// Automatically enforce Linux style line endings.
"prettier.tabWidth": 2,
"files.eol": "\n",
"haxe.displayPort": "auto",
"haxe.enableCompilationServer": true,
"haxe.enableServerView": true,
"haxe.displayServer": {
"arguments": ["-v"]
},
// Fix file associations for HScript.
"haxe.displayServer": { "arguments": ["-v"] },
"files.associations": {
"*.hxp": "haxe",
"*.hscript": "haxe",
@ -84,14 +68,27 @@
"*.hxc": "haxe"
},
"projectManager.git.baseFolders": ["./"],
"haxecheckstyle.sourceFolders": ["src", "source"],
"haxecheckstyle.externalSourceRoots": [],
"haxecheckstyle.configurationFile": "checkstyle.json",
"haxecheckstyle.codeSimilarityBufferSize": 100,
"lime.projectFile": "project.hxp",
"lime.targets": [
{ "name": "windows", "enabled": true, "label": "Windows" },
{ "name": "mac", "enabled": true, "label": "macOS" },
{ "name": "linux", "enabled": true, "label": "Linux" },
{ "name": "html5", "enabled": true, "label": "HTML5" },
{ "name": "android", "enabled": true, "label": "Android" },
{ "name": "ios", "enabled": true, "label": "iOS" },
// Disabled targets
{ "name": "hl", "enabled": false, "label": "HashLink" },
{ "name": "air", "enabled": false },
{ "name": "electron", "enabled": false },
{ "name": "flash", "enabled": false },
{ "name": "neko", "enabled": false },
{ "name": "tvos", "enabled": false }
],
"lime.defaultTargetConfiguration": "Windows / Debug",
"lime.targetConfigurations": [
{
"label": "Windows / Debug (Discord)",
@ -103,21 +100,11 @@
"target": "windows",
"args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (FlxAnimate Test)",
"target": "hl",
"args": ["-debug", "-DANIMATE"]
},
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
"args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Freeplay)",
"target": "hl",
"args": ["-debug", "-DFREEPLAY"]
},
{
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
"target": "windows",
@ -132,26 +119,6 @@
"target": "windows",
"args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
"target": "hl",
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
},
{
"label": "Windows / Debug (Conversation Test)",
"target": "windows",
"args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Conversation Test)",
"target": "hl",
"args": ["-debug", "-DDIALOGUE"]
},
{
"label": "Windows / Debug (Results Screen Test)",
"target": "windows",
"args": ["-debug", "-DRESULTS"]
},
{
"label": "Windows / Debug (Straight to Stage Editor)",
"target": "windows",
@ -172,36 +139,16 @@
"target": "windows",
"args": ["-debug", "-DHXVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
"target": "hl",
"args": ["-debug", "-DANIMDEBUG"]
},
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
"args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Latency Test)",
"target": "hl",
"args": ["-debug", "-DLATENCY"]
},
{
"label": "Windows / Debug (Waveform Test)",
"target": "windows",
"args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Release (GitHub Actions)",
"target": "windows",
"args": ["-release", "-DGITHUB_BUILD"]
},
{
"label": "HashLink / Debug (Waveform Test)",
"target": "hl",
"args": ["-debug", "-DWAVEFORM"]
},
{
"label": "HTML5 / Debug (Watch)",
"target": "html5",
@ -209,10 +156,7 @@
}
],
"lime.buildTypes": [
{
"label": "Debug",
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{ "label": "Debug", "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"] },
{
"label": "Debug (Unlock Everything)",
"args": ["-debug", "-DUNLOCK_EVERYTHING"]
@ -225,10 +169,7 @@
"label": "Debug (Straight to Chart Editor)",
"args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Release",
"args": ["-release"]
},
{ "label": "Release", "args": ["-release"] },
{
"label": "Release (GitHub Actions)",
"args": ["-release", "-DGITHUB_BUILD"]
@ -244,9 +185,6 @@
],
"vscord.app.privacyMode.enable": true,
"json.schemas": [
{
"fileMatch": ["/hmm.json"],
"url": "./.vscode/schema/hmm.json"
}
{ "fileMatch": ["/hmm.json"], "url": "./.vscode/schema/hmm.json" }
]
}

2
assets

@ -1 +1 @@
Subproject commit 1374c558b577ce64e205cf838e2ee6f0311a7a7c
Subproject commit 1a961f111381eb3bfc452166c4e4b5a18b409781

View file

@ -1117,6 +1117,9 @@ class Project extends HXProject
{
// This macro allows addition of new functionality to existing Flixel. -->
addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')");
// This macro will go over every song in the assets folder and store them in an array to check for cheated scores.
addHaxeMacro("funkin.util.macro.SongDataValidator.loadSongData()");
}
function configureOutputDir()

View file

@ -10,6 +10,7 @@ import funkin.play.song.ScriptedSong;
import funkin.play.song.Song;
import funkin.util.assets.DataAssets;
import funkin.util.VersionUtil;
import funkin.util.macro.SongDataValidator;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;
@ -51,6 +52,7 @@ using funkin.data.song.migrator.SongDataMigrator;
public override function loadEntries():Void
{
clearEntries();
SongDataValidator.clearLists();
//
// SCRIPTED ENTRIES
@ -499,11 +501,16 @@ using funkin.data.song.migrator.SongDataMigrator;
function loadEntryChartFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
if (!openfl.Assets.exists(entryFilePath)) return null;
var rawJson:String = openfl.Assets.getText(entryFilePath);
if (rawJson == null) return null;
rawJson = rawJson.trim();
SongDataValidator.checkChartValidity(rawJson, id, variation);
return {fileName: entryFilePath, contents: rawJson};
}

View file

@ -451,6 +451,46 @@ class FunkinSprite extends FlxAnimate
return false;
}
/**
* Gets every frame on every symbol that starts with the given keyword.
* @param keyword The keyword to search for.
* @return An array of frames.
*/
public function getFramesWithKeyword(keyword:String):Array<animate.internal.Frame>
{
if (!this.isAnimate)
{
trace('WARNING: getFramesWithKeyword() only works texture atlases!');
return [];
}
var symbolItems:Array<animate.internal.SymbolItem> = [];
var frames:Array<animate.internal.Frame> = [];
@:privateAccess
for (symbol in this.library.dictionary.keys())
{
var symbolItem:Null<animate.internal.SymbolItem> = this.library.getSymbol(symbol);
if (symbolItem == null) continue;
if (symbolItem.name.contains(keyword))
{
symbolItems.push(symbolItem);
}
}
for (symbolItem in symbolItems)
{
symbolItem.timeline.forEachLayer((layer) -> {
layer.forEachFrame((frame) -> {
frames.push(frame);
});
});
}
return frames;
}
/**
* Gets the current animation ID.
*/

View file

@ -1267,6 +1267,8 @@ class FunkinAction extends FlxActionDigital
public var namePressed(default, null):Null<String>;
public var nameReleased(default, null):Null<String>;
var cache:Map<String, {timestamp:Int, value:Bool}> = [];
public function new(?name:String = "", ?namePressed:String, ?nameReleased:String)
{
super(name);
@ -1386,7 +1388,13 @@ class FunkinAction extends FlxActionDigital
public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool
{
// Make sure we only update the inputs once per frame.
if (_timestamp == FlxG.game.ticks) return triggered; // run no more than once per frame
var key = '${filterTrigger}:${filterDevice}';
var cacheEntry = cache.get(key);
if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks)
{
return cacheEntry.value;
}
_x = null;
_y = null;
@ -1428,6 +1436,8 @@ class FunkinAction extends FlxActionDigital
}
}
cache.set(key, {timestamp: FlxG.game.ticks, value: triggered});
return triggered;
}
}

View file

@ -3380,8 +3380,9 @@ class PlayState extends MusicBeatSubState
var isNewHighscore = false;
var prevScoreData:Null<SaveScoreData> = Save.instance.getSongScore(currentSong.id, suffixedDifficulty);
var isChartValid:Bool = funkin.util.macro.SongDataValidator.isChartValid(currentSong.id, currentVariation);
if (currentSong != null && currentSong.validScore)
if (currentSong != null && currentSong.validScore && isChartValid)
{
// crackhead double thingie, sets whether was new highscore, AND saves the song!
var data =

View file

@ -569,7 +569,8 @@ class ResultState extends MusicBeatSubState
clearPercentCounter.curNumber = clearPercentTarget;
#if FEATURE_NEWGROUNDS
var isScoreValid = !(params?.isPracticeMode ?? false) && !(params?.isBotPlayMode ?? false);
var isChartValid:Bool = funkin.util.macro.SongDataValidator.isChartValid(params?.songId ?? "", params?.variationId ?? Constants.DEFAULT_VARIATION);
var isScoreValid = !(params?.isPracticeMode ?? false) && !(params?.isBotPlayMode ?? false) && isChartValid;
// This is the easiest spot to do the medal calculation lol.
if (isScoreValid && clearPercentTarget == 69) Medals.award(Nice);
#end

View file

@ -10,6 +10,9 @@ import funkin.util.MathUtil;
class CharSelectCursors extends FlxTypedSpriteContainer<FunkinSprite>
{
/**
* The main cursor sprite for this class.
*/
public var main:FunkinSprite;
var lightBlue:FunkinSprite;
@ -56,6 +59,7 @@ class CharSelectCursors extends FlxTypedSpriteContainer<FunkinSprite>
add(cursorDenied);
scrollFactor.set();
directAlpha = true;
}
public function confirm():Void
@ -86,6 +90,32 @@ class CharSelectCursors extends FlxTypedSpriteContainer<FunkinSprite>
main.visible = lightBlue.visible = darkBlue.visible = true;
}
/**
* Snaps the cursors to the given position.
* @param intendedPosition The position to snap to as a `FlxPoint`.
*/
public function snapToLocation(intendedPosition:FlxPoint):Void
{
main.x = intendedPosition.x;
main.y = intendedPosition.y;
lightBlue.x = main.x;
lightBlue.y = main.y;
darkBlue.x = intendedPosition.x;
darkBlue.y = intendedPosition.y;
cursorConfirmed.x = main.x - 2;
cursorConfirmed.y = main.y - 4;
cursorDenied.x = main.x - 2;
cursorDenied.y = main.y - 4;
}
/**
* Lerps the cursors to the given position.
* @param intendedPosition The position to lerp to as a `FlxPoint`.
*/
public function lerpToLocation(intendedPosition:FlxPoint):Void
{
main.x = MathUtil.snap(MathUtil.smoothLerpPrecision(main.x, intendedPosition.x, FlxG.elapsed, 0.1), intendedPosition.x, 1);

View file

@ -43,6 +43,11 @@ import funkin.util.TouchUtil;
@:nullSafety
class CharSelectSubState extends MusicBeatSubState
{
/**
* The default index for the cursor.
*/
final DEFAULT_CURSOR_INDEX:Int = 4;
var cursors:CharSelectCursors;
var cursorX:Int = 0;
@ -235,14 +240,17 @@ class CharSelectSubState extends MusicBeatSubState
{
if (charId == rememberedChar)
{
setCursorPosition(pos);
setCursorPosition(pos, true);
break;
}
}
@:bypassAccessor curChar = rememberedChar;
}
else
{
setupPlayerChill(Constants.DEFAULT_CHARACTER);
setCursorPosition(DEFAULT_CURSOR_INDEX, true);
}
var speakers:FunkinSprite = FunkinSprite.createTextureAtlas(cutoutSize - 10, 0, "charSelect/charSelectSpeakers",
{
@ -307,15 +315,12 @@ class CharSelectSubState extends MusicBeatSubState
charHitbox.scrollFactor.set();
selectSound.loadEmbedded(Paths.sound('CS_select'));
selectSound.pitch = 1;
selectSound.volume = 0.7;
FlxG.sound.defaultSoundGroup.add(selectSound);
FlxG.sound.list.add(selectSound);
unlockSound.loadEmbedded(Paths.sound('CS_unlock'));
unlockSound.pitch = 1;
unlockSound.volume = 0;
unlockSound.play(true);
@ -323,18 +328,13 @@ class CharSelectSubState extends MusicBeatSubState
FlxG.sound.list.add(unlockSound);
lockedSound.loadEmbedded(Paths.sound('CS_locked'));
lockedSound.pitch = 1;
lockedSound.volume = 1.;
FlxG.sound.defaultSoundGroup.add(lockedSound);
FlxG.sound.list.add(lockedSound);
staticSound.loadEmbedded(Paths.sound('static loop'));
staticSound.pitch = 1;
staticSound.looped = true;
staticSound.volume = 0.6;
FlxG.sound.defaultSoundGroup.add(staticSound);
@ -415,7 +415,6 @@ class CharSelectSubState extends MusicBeatSubState
introSound = new FunkinSound();
introSound.loadEmbedded(Paths.sound('CS_Lights'));
introSound.pitch = 1;
introSound.volume = 0;
FlxG.sound.defaultSoundGroup.add(introSound);
@ -722,6 +721,7 @@ class CharSelectSubState extends MusicBeatSubState
var holdTmrLeft:Float = 0;
var holdTmrRight:Float = 0;
var spamDirections:FlxDirectionFlags = NONE;
var initSpam = 0.5;
var mobileDeny:Bool = false;
var mobileAccept:Bool = false;
@ -736,9 +736,6 @@ class CharSelectSubState extends MusicBeatSubState
mobileAccept = false;
if (controls.UI_UP_R || controls.UI_DOWN_R || controls.UI_LEFT_R || controls.UI_RIGHT_R #if FEATURE_TOUCH_CONTROLS || TouchUtil.justReleased #end)
selectSound.pitch = 1;
if (allowInput && !pressedSelect)
{
#if FEATURE_TOUCH_CONTROLS
@ -774,41 +771,6 @@ class CharSelectSubState extends MusicBeatSubState
}
#end
if (controls.UI_UP) holdTmrUp += elapsed;
if (controls.UI_UP_R)
{
holdTmrUp = 0;
spamDirections = spamDirections.without(UP);
}
if (controls.UI_DOWN) holdTmrDown += elapsed;
if (controls.UI_DOWN_R)
{
holdTmrDown = 0;
spamDirections = spamDirections.without(DOWN);
}
if (controls.UI_LEFT) holdTmrLeft += elapsed;
if (controls.UI_LEFT_R)
{
holdTmrLeft = 0;
spamDirections = spamDirections.without(LEFT);
}
if (controls.UI_RIGHT) holdTmrRight += elapsed;
if (controls.UI_RIGHT_R)
{
holdTmrRight = 0;
spamDirections = spamDirections.without(RIGHT);
}
var initSpam = 0.5;
if (holdTmrUp >= initSpam) spamDirections = spamDirections.with(UP);
if (holdTmrDown >= initSpam) spamDirections = spamDirections.with(DOWN);
if (holdTmrLeft >= initSpam) spamDirections = spamDirections.with(LEFT);
if (holdTmrRight >= initSpam) spamDirections = spamDirections.with(RIGHT);
if (controls.UI_UP_P)
{
cursorY -= 1;
@ -841,6 +803,39 @@ class CharSelectSubState extends MusicBeatSubState
selectSound.play(true);
}
if (controls.UI_UP) holdTmrUp += elapsed;
if (controls.UI_UP_R || !controls.UI_UP)
{
holdTmrUp = 0;
spamDirections = spamDirections.without(UP);
}
if (controls.UI_DOWN) holdTmrDown += elapsed;
if (controls.UI_DOWN_R || !controls.UI_DOWN)
{
holdTmrDown = 0;
spamDirections = spamDirections.without(DOWN);
}
if (controls.UI_LEFT) holdTmrLeft += elapsed;
if (controls.UI_LEFT_R || !controls.UI_LEFT)
{
holdTmrLeft = 0;
spamDirections = spamDirections.without(LEFT);
}
if (controls.UI_RIGHT) holdTmrRight += elapsed;
if (controls.UI_RIGHT_R || !controls.UI_RIGHT)
{
holdTmrRight = 0;
spamDirections = spamDirections.without(RIGHT);
}
if (holdTmrUp >= initSpam) spamDirections = spamDirections.with(UP);
if (holdTmrDown >= initSpam) spamDirections = spamDirections.with(DOWN);
if (holdTmrLeft >= initSpam) spamDirections = spamDirections.with(LEFT);
if (holdTmrRight >= initSpam) spamDirections = spamDirections.with(RIGHT);
if (controls.BACK_P) goBack();
}
@ -895,7 +890,7 @@ class CharSelectSubState extends MusicBeatSubState
cursors.confirm();
FlxG.sound.play(Paths.sound('CS_confirm'));
FunkinSound.playOnce(Paths.sound('CS_confirm'));
dispatchEvent(new CharacterSelectScriptEvent(CHARACTER_CONFIRMED, curChar));
@ -1124,8 +1119,7 @@ class CharSelectSubState extends MusicBeatSubState
return gridPosition;
}
// Moved this code into a function because is now used twice
function setCursorPosition(index:Int)
function setCursorPosition(index:Int, instant:Bool = false):Void
{
var copy = 3;
var yThing = -1;
@ -1141,6 +1135,17 @@ class CharSelectSubState extends MusicBeatSubState
// Look, I'd write better code but I had better aneurysms, my bad - Cheems
cursorY = yThing;
cursorX = xThing;
if (instant)
{
cursorLocIntended.x = (cursorFactor * cursorX) + (FlxG.width / 2) - cursors.main.width / 2;
cursorLocIntended.y = (cursorFactor * cursorY) + (FlxG.height / 2) - cursors.main.height / 2;
cursorLocIntended.x += cursorOffsetX;
cursorLocIntended.y += cursorOffsetY;
cursors.snapToLocation(cursorLocIntended);
}
}
function set_curChar(value:String):String

View file

@ -386,12 +386,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (isViewDownscroll)
{
gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS);
measureTicks.y = gridTiledSprite.y;
}
else
{
gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS);
measureTicks.y = gridTiledSprite.y;
for (member in audioWaveforms.members)
{
@ -2173,11 +2171,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var notePreviewViewportBitmap:Null<BitmapData> = null;
/**
* The IMAGE used for the measure ticks. Updated by ChartEditorThemeHandler.
*/
var measureTickBitmap:Null<BitmapData> = null;
/**
* The IMAGE used for the offset ticks. Updated by ChartEditorThemeHandler.
*/
@ -2408,7 +2401,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Setup the onClick listeners for the UI after it's been created.
setupUIListeners();
setupContextMenu();
setupTurboKeyHandlers();
setupAutoSave();
@ -2736,10 +2728,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
measureTicks = new ChartEditorMeasureTicks(this);
var measureTicksWidth = (GRID_SIZE);
measureTicks.x = gridTiledSprite.x - measureTicksWidth;
measureTicks.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
measureTicks.zIndex = 20;
add(measureTicks);
handleMeasureTickPosition();
}
function buildNotePreview():Void
@ -3376,21 +3368,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// registerContextMenu(null, Paths.ui('chart-editor/context/test'));
}
function setupContextMenu():Void
{
Screen.instance.registerEvent(MouseEvent.RIGHT_MOUSE_UP, function(e:MouseEvent) {
var xPos = e.screenX;
var yPos = e.screenY;
onContextMenu(xPos, yPos);
});
}
function onContextMenu(xPos:Float, yPos:Float)
{
trace('User right clicked to open menu at (${xPos}, ${yPos})');
// this.openDefaultContextMenu(xPos, yPos);
}
function copySelection():Void
{
// Doesn't use a command because it's not undoable.
@ -6447,6 +6424,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (gridTiledSprite != null)
{
gridTiledSprite.height = songLengthInPixels;
measureTicks.setHeight(gridTiledSprite.height);
}
// Remove any notes past the end of the song.
@ -6802,32 +6780,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/**
* Updates the measure tick bitmap forcibly to make sure it's correct.
*/
function updateTimeSignature():Void
{
this.updateMeasureTicks(true);
gridTiledSprite.loadGraphic(gridBitmap);
}
function updateTimeSignature():Void {}
/**
* Handle positioning the measure ticks sprite.
*/
function handleMeasureTickPosition():Void
{
this.updateMeasureTicks();
var currentMeasureTime = Conductor.instance.getMeasureTimeInMs(Math.floor(Conductor.instance.getTimeInMeasures(scrollPositionInMs)));
var currentMeasurePos = currentMeasureTime < 0 ? 0 : Conductor.instance.getTimeInSteps(currentMeasureTime) * GRID_SIZE;
measureTicks.y = gridTiledSprite?.y + currentMeasurePos;
// Make sure the measure tick bitmap does not past the grid itself.
var totalMeasureTicksHeight:Float = currentMeasurePos + measureTickBitmap.height;
if (totalMeasureTicksHeight >= songLengthInPixels)
{
var spillOver:Float = totalMeasureTicksHeight - songLengthInPixels;
measureTicks.setClipRect(new FlxRect(0, 0, measureTickBitmap.width, measureTickBitmap.height - spillOver));
}
else
{
measureTicks.setClipRect(null);
}
// var currentMeasureTime = Conductor.instance.getMeasureTimeInMs(Math.floor(Conductor.instance.getTimeInMeasures(scrollPositionInMs)));
// var currentMeasurePos = currentMeasureTime < 0 ? 0 : Conductor.instance.getTimeInSteps(currentMeasureTime) * GRID_SIZE;
measureTicks.y = gridTiledSprite?.y;
}
/**

View file

@ -1,29 +1,63 @@
package funkin.ui.debug.charting.components;
#if FEATURE_CHART_EDITOR
import flixel.addons.display.FlxTiledSprite;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxRect;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.graphics.FunkinSprite;
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
/**
* Handles the display of the measure ticks and numbers on the left side.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorMeasureTicks extends FlxTypedSpriteGroup<FlxSprite>
{
/**
* The owning ChartEditorState.
*/
var chartEditorState:ChartEditorState;
var measureTicksSprite:FlxSprite;
var measureNumbers:Array<FlxText> = [];
/**
* The measure ticks underneath the numbers.
*/
var measureTicksSprite:FlxTiledSprite = new FlxTiledSprite(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE * 16);
public var measureLengthsInPixels:Array<Int> = [];
/**
* The numbers that display the current measure number.
* This is a group so we can kill and recycle its members.
*/
var measureNumbers:FlxTypedSpriteGroup<FlxText> = new FlxTypedSpriteGroup<FlxText>();
/**
* The horizontal bars over the grid at each measure tick.
*/
var measureDividers:FlxTypedSpriteGroup<FlxSprite> = new FlxTypedSpriteGroup<FlxSprite>();
/**
* The positions of each measure tick, in pixels, relative to the start of the song.
*/
var measurePositions:Array<Float> = [];
/**
* A map of the
* @param value
* @return Float
*/
override function set_y(value:Float):Float
{
var result = super.set_y(value);
// Don't update if the value hasn't changed.
if (this.y == value) return value;
updateMeasureNumber();
super.set_y(value);
return result;
updateMeasureNumbers();
return this.y;
}
public function new(chartEditorState:ChartEditorState)
@ -32,59 +66,182 @@ class ChartEditorMeasureTicks extends FlxTypedSpriteGroup<FlxSprite>
this.chartEditorState = chartEditorState;
measureTicksSprite = new FlxSprite(0, 0);
add(measureTicksSprite);
add(measureNumbers);
add(measureDividers);
for (i in 0...5)
{
var measureNumber = new FlxText(0, 0, ChartEditorState.GRID_SIZE, "1");
measureNumber.setFormat(Paths.font('vcr.ttf'), 20, FlxColor.WHITE);
measureNumber.borderStyle = FlxTextBorderStyle.OUTLINE;
measureNumber.borderColor = FlxColor.BLACK;
add(measureNumber);
measureNumbers.push(measureNumber);
}
// Need these two lines or the ticks don't render before loading a chart!
chartEditorState.updateMeasureTicks(true);
reloadTickBitmap();
}
public function reloadTickBitmap():Void
{
measureTicksSprite.loadGraphic(chartEditorState.measureTickBitmap);
}
public function setClipRect(rect:Null<FlxRect>):Void
{
measureTicksSprite.clipRect = rect;
buildMeasureTicksSprite();
updateMeasureNumbers(true);
}
/**
* Update all 5 measure numbers, since that's the most we can really see at a time, even if barely.
* Please excuse the horror you're about to witness.
* Welp, you gotta go back in commits to see it now.
* Set the overall height of the measure ticks.
* @param height The desired height in pixels.
*/
function updateMeasureNumber()
public function setHeight(height:Float):Void
{
if (measureLengthsInPixels.length == 0 || measureLengthsInPixels == null) return;
measureTicksSprite.height = height;
}
var currentMeasure:Int = Math.floor(Conductor.instance?.getTimeInMeasures(chartEditorState.scrollPositionInMs));
for (i in 0...measureNumbers.length)
public function updateTheme():Void
{
buildMeasureTicksSprite();
updateMeasureNumbers(true);
}
function buildMeasureTicksSprite():Void
{
var backingColor:FlxColor = switch (chartEditorState.currentTheme)
{
var measureNumber:FlxText = measureNumbers[i];
if (measureNumber == null) continue;
case Light: ChartEditorThemeHandler.MEASTURE_TICKS_BACKING_COLOR_LIGHT;
case Dark: ChartEditorThemeHandler.MEASTURE_TICKS_BACKING_COLOR_DARK;
default: ChartEditorThemeHandler.MEASTURE_TICKS_BACKING_COLOR_LIGHT;
};
var dividerColor:FlxColor = switch (chartEditorState.currentTheme)
{
case Light: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_LIGHT;
case Dark: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_DARK;
default: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_LIGHT;
};
var measureNumberPosition = measureLengthsInPixels[i];
measureNumber.y = this.y + measureNumberPosition;
// TODO: This does NOT account for time signature, and always assumes 4/4!
// Better to have the little lines not line up than be required to redraw the image every frame,
// but we need to fix this eventually.
var stepsPerMeasure:Int = Constants.STEPS_PER_BEAT * 4;
// Show the measure number only if it isn't beneath the end of the note grid.
// Using measureNumber + 1 because the cut-off bar at the bottom is technically a bar, but it looks bad if a measure number shows up there.
var fixedMeasureNumberValue = currentMeasure + i + 1;
if (fixedMeasureNumberValue < Math.ceil(Conductor.instance?.getTimeInMeasures(chartEditorState.songLengthInMs)))
measureNumber.text = '${fixedMeasureNumberValue}';
// Start the bitmap with the basic gray color.
var measureTickBitmap = new BitmapData(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE * 16, true, backingColor);
// Draw the measure ticks at the top and bottom.
measureTickBitmap.fillRect(new Rectangle(0, 0, ChartEditorState.GRID_SIZE, ChartEditorThemeHandler.MEASURE_TICKS_MEASURE_WIDTH / 2), dividerColor);
var bottomTickY:Float = measureTickBitmap.height - (ChartEditorThemeHandler.MEASURE_TICKS_MEASURE_WIDTH / 2);
measureTickBitmap.fillRect(new Rectangle(0, bottomTickY, ChartEditorState.GRID_SIZE, ChartEditorThemeHandler.MEASURE_TICKS_MEASURE_WIDTH / 2),
dividerColor);
// Draw the beat ticks and dividers, and step ticks. No need for two seperate loops thankfully.
for (i in 1...stepsPerMeasure)
{
if ((i % Constants.STEPS_PER_BEAT) == 0) // If we're on a beat, draw a beat tick and divider.
{
var beatTickY:Float = ChartEditorState.GRID_SIZE * i - (ChartEditorThemeHandler.MEASURE_TICKS_BEAT_WIDTH / 2);
var beatTickLength:Float = ChartEditorState.GRID_SIZE * 2 / 3;
measureTickBitmap.fillRect(new Rectangle(0, beatTickY, beatTickLength, ChartEditorThemeHandler.MEASURE_TICKS_BEAT_WIDTH), dividerColor);
}
else
measureNumber.text = '';
{
// Draw a step tick.
var stepTickY:Float = ChartEditorState.GRID_SIZE * i - (ChartEditorThemeHandler.MEASURE_TICKS_STEP_WIDTH / 2);
var stepTickLength:Float = ChartEditorState.GRID_SIZE * 1 / 3;
measureTickBitmap.fillRect(new Rectangle(0, stepTickY, stepTickLength, ChartEditorThemeHandler.MEASURE_TICKS_STEP_WIDTH), dividerColor);
}
}
// Finally, set the sprite to use the image.
measureTicksSprite.loadGraphic(measureTickBitmap);
// Destroy these so they get rebuilt with the right theme later.
measureNumbers.forEach(function(measureNumber:FlxText) {
measureNumber.destroy();
});
measureNumbers.clear();
// Destroy these so they get rebuilt with the right theme later.
measureDividers.forEach(function(measureDivider:FlxSprite) {
measureDivider.destroy();
});
measureDividers.clear();
}
// The last measure number we updated the ticks on.
var previousMeasure:Int = 0;
function updateMeasureNumbers(force:Bool = false):Void
{
if (chartEditorState == null || Conductor.instance == null) return;
// Get the time at the top of the screen, in measures, rounded down.
// This is the earliest measure we'll need to display a tick for.
var currentMeasure:Int = Math.floor(Conductor.instance.getTimeInMeasures(chartEditorState.scrollPositionInMs));
if (previousMeasure == currentMeasure && !force) return;
if (currentMeasure < 0) currentMeasure = previousMeasure = 0;
// Remove existing measure numbers.
measureNumbers.forEachAlive(function(measureNumber:FlxText) {
measureNumber.kill();
});
measureDividers.forEachAlive(function(measureDivider:FlxSprite) {
measureDivider.kill();
});
final ARBITRARY_LIMIT = 5;
for (i in 0...ARBITRARY_LIMIT)
{
var targetMeasure:Int = currentMeasure + i - 1;
if (targetMeasure < 0) continue;
// TODO: This math is kinda awkward but DOES account for time signatures,
// might want some cleanup though? Maybe add a `getMeasureTimeInSteps` method?
var measureTimeInMs:Float = Conductor.instance.getMeasureTimeInMs(targetMeasure);
var measureTimeInSteps:Float = Conductor.instance.getTimeInSteps(measureTimeInMs);
var measureTimeInPixels:Float = measureTimeInSteps * ChartEditorState.GRID_SIZE;
var relativeMeasureTimeInPixels:Float = measureTimeInPixels + this.y;
final SCREEN_PADDING:Float = ChartEditorState.GRID_SIZE / 2;
// Above the visible area, keep going.
if (relativeMeasureTimeInPixels < 0 - SCREEN_PADDING)
{
continue;
}
// Below the visible area, quit it.
if (relativeMeasureTimeInPixels > FlxG.height + SCREEN_PADDING)
{
break;
}
// Else, display a number.
// Reuse an existing number. If we need a new number, create one with makeMeasureNumber().
final REVIVE:Bool = true;
var measureNumber = measureNumbers.recycle(makeMeasureNumber, false, REVIVE);
trace('Placing measure number. $relativeMeasureTimeInPixels -> $targetMeasure');
// Measures are base ZERO gah!
final OFFSET = 2;
measureNumber.text = '${targetMeasure + 1}';
measureNumber.y = relativeMeasureTimeInPixels + OFFSET;
measureNumber.x = this.x;
// Place a measure divider too.
var measureDivider = measureDividers.recycle(makeMeasureDivider, false, REVIVE);
measureDivider.y = relativeMeasureTimeInPixels - (ChartEditorThemeHandler.MEASURE_TICKS_MEASURE_WIDTH / 2);
measureDivider.x = this.x + (measureTicksSprite.width);
}
}
function makeMeasureNumber():FlxText
{
var measureNumber = new FlxText(0, 0, ChartEditorState.GRID_SIZE, "1");
measureNumber.setFormat(Paths.font('vcr.ttf'), 20, FlxColor.WHITE);
measureNumber.borderStyle = FlxTextBorderStyle.OUTLINE;
measureNumber.borderColor = FlxColor.BLACK;
return measureNumber;
}
function makeMeasureDivider():FlxSprite
{
var dividerColor:FlxColor = switch (chartEditorState.currentTheme)
{
case Light: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_LIGHT;
case Dark: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_DARK;
default: ChartEditorThemeHandler.GRID_MEASURE_DIVIDER_COLOR_LIGHT;
};
var measureDivider = new FunkinSprite().makeSolidColor(ChartEditorState.GRID_SIZE * ChartEditorThemeHandler.TOTAL_COLUMN_COUNT,
ChartEditorThemeHandler.MEASURE_TICKS_MEASURE_WIDTH, dividerColor);
return measureDivider;
}
}
#end

View file

@ -25,35 +25,37 @@ class ChartEditorThemeHandler
static final BACKGROUND_COLOR_DARK:FlxColor = 0xFF361E60;
// Color 1 of the grid pattern. Alternates with Color 2.
static final GRID_COLOR_1_LIGHT:FlxColor = 0xFFE7E6E6;
static final GRID_COLOR_1_DARK:FlxColor = 0xFF181919;
public static final GRID_COLOR_1_LIGHT:FlxColor = 0xFFE7E6E6;
public static final GRID_COLOR_1_DARK:FlxColor = 0xFF181919;
// Color 2 of the grid pattern. Alternates with Color 1.
static final GRID_COLOR_2_LIGHT:FlxColor = 0xFFF8F8F8;
static final GRID_COLOR_2_DARK:FlxColor = 0xFF202020;
public static final GRID_COLOR_2_LIGHT:FlxColor = 0xFFF8F8F8;
public static final GRID_COLOR_2_DARK:FlxColor = 0xFF202020;
// Color 3 of the grid pattern. Borders the other colors.
static final GRID_COLOR_3_LIGHT:FlxColor = 0xFFD9D5D5;
static final GRID_COLOR_3_DARK:FlxColor = 0xFF262A2A;
public static final GRID_COLOR_3_LIGHT:FlxColor = 0xFFD9D5D5;
public static final GRID_COLOR_3_DARK:FlxColor = 0xFF262A2A;
// Vertical divider between characters.
static final GRID_STRUMLINE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
static final GRID_STRUMLINE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
public static final GRID_STRUMLINE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
public static final GRID_STRUMLINE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
// Horizontal divider between measures.
static final GRID_MEASURE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
static final GRID_MEASURE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
public static final GRID_MEASURE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
public static final GRID_MEASURE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
static final GRID_MEASURE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
// Horizontal divider between beats.
static final GRID_BEAT_DIVIDER_COLOR_LIGHT:FlxColor = 0xFFC1C1C1;
static final GRID_BEAT_DIVIDER_COLOR_DARK:FlxColor = 0xFF848484;
static final GRID_BEAT_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
public static final MEASTURE_TICKS_BACKING_COLOR_LIGHT:FlxColor = 0xFFC1C1C1;
public static final MEASTURE_TICKS_BACKING_COLOR_DARK:FlxColor = 0xFF484848;
// Border on the square highlighting selected notes.
static final SELECTION_SQUARE_BORDER_COLOR_LIGHT:FlxColor = 0xFF339933;
static final SELECTION_SQUARE_BORDER_COLOR_DARK:FlxColor = 0xFF339933;
/**
* The width of the opaque border around the square highlighting selected notes.
*/
public static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1;
// Fill on the square highlighting selected notes.
@ -65,6 +67,11 @@ class ChartEditorThemeHandler
static final PLAYHEAD_BLOCK_BORDER_COLOR:FlxColor = 0xFF9D0011;
static final PLAYHEAD_BLOCK_FILL_COLOR:FlxColor = 0xFFBD0231;
// Lines on the measure ticks.
public static final MEASURE_TICKS_MEASURE_WIDTH:Int = 6;
public static final MEASURE_TICKS_BEAT_WIDTH:Int = 4;
public static final MEASURE_TICKS_STEP_WIDTH:Int = 2;
// Border on the square over the note preview.
static final NOTE_PREVIEW_VIEWPORT_BORDER_COLOR_LIGHT = 0xFFF8A657;
static final NOTE_PREVIEW_VIEWPORT_BORDER_COLOR_DARK = 0xFFF8A657;
@ -73,10 +80,7 @@ class ChartEditorThemeHandler
static final NOTE_PREVIEW_VIEWPORT_FILL_COLOR_LIGHT = 0x80F8A657;
static final NOTE_PREVIEW_VIEWPORT_FILL_COLOR_DARK = 0x80F8A657;
static final TOTAL_COLUMN_COUNT:Int = ChartEditorState.STRUMLINE_SIZE * 2 + 1;
// Used for checking in updateMeasureTicks() so we don't have to redraw it everytime if the current measure is the same as before.
static var previousMeasure:Int = -69;
public static final TOTAL_COLUMN_COUNT:Int = ChartEditorState.STRUMLINE_SIZE * 2 + 1;
/**
* When the theme is changed, this function updates all of the UI elements to match the new theme.
@ -86,10 +90,10 @@ class ChartEditorThemeHandler
{
updateBackground(state);
updateGridBitmap(state);
updateMeasureTicks(state);
updateOffsetTicks(state);
updateSelectionSquare(state);
updateNotePreview(state);
updateMeasureTicks(state);
}
/**
@ -127,6 +131,13 @@ class ChartEditorThemeHandler
default: GRID_COLOR_2_LIGHT;
};
var dividerColor:FlxColor = switch (state.currentTheme)
{
case Light: GRID_STRUMLINE_DIVIDER_COLOR_LIGHT;
case Dark: GRID_STRUMLINE_DIVIDER_COLOR_DARK;
default: GRID_STRUMLINE_DIVIDER_COLOR_LIGHT;
}
// Draw the base grid.
// 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall.
@ -161,7 +172,7 @@ class ChartEditorThemeHandler
ChartEditorState.GRID_SELECTION_BORDER_WIDTH),
selectionBorderColor);
// Selection border at left.
// Selection border at far left.
state.gridBitmap.fillRect(new Rectangle(-(ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0, ChartEditorState.GRID_SELECTION_BORDER_WIDTH,
state.gridBitmap.height),
selectionBorderColor);
@ -169,12 +180,14 @@ class ChartEditorThemeHandler
// Selection borders vertically along the middle.
for (i in 1...TOTAL_COLUMN_COUNT)
{
var isStrumlineColumn:Bool = (i % ChartEditorState.STRUMLINE_SIZE == 0) && (i > 0);
state.gridBitmap.fillRect(new Rectangle((ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0,
ChartEditorState.GRID_SELECTION_BORDER_WIDTH, state.gridBitmap.height),
selectionBorderColor);
isStrumlineColumn ? dividerColor : selectionBorderColor);
}
// Selection border at right.
// Selection border at far right.
state.gridBitmap.fillRect(new Rectangle(state.gridBitmap.width - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0,
ChartEditorState.GRID_SELECTION_BORDER_WIDTH, state.gridBitmap.height),
selectionBorderColor);
@ -186,115 +199,6 @@ class ChartEditorThemeHandler
// Else, gridTiledSprite will be built later.
}
/**
* Draw the measure ticks left of the grid. This also includes the horizontal beat and measure dividers as well as the vertical strumline dividers.
*/
public static function updateMeasureTicks(state:ChartEditorState, force:Bool = false):Void
{
var measureTickWidth:Int = 6;
var beatTickWidth:Int = 4;
var stepTickWidth:Int = 2;
var measuresToCheck:Int = 5;
var currentMeasure:Int = Math.floor(Conductor.instance.getTimeInMeasures(state.scrollPositionInMs));
if (previousMeasure == currentMeasure && !force) return;
if (currentMeasure < 0) currentMeasure = previousMeasure = 0;
// Make the bitmap.
var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE);
var linesWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT);
state.measureTickBitmap = new BitmapData(ticksWidth + linesWidth, 6144, true, 0x00FFFFFF);
// Draw the vertical grey sidebar.
state.measureTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, 6144), GRID_BEAT_DIVIDER_COLOR_DARK);
// Then draw the ticks and dividers themselves.
var totalTicksHeight:Int = 0;
var measureLengthsInPixels:Array<Int> = [];
for (measure in 0...measuresToCheck)
{
var currentTimeInMs:Float = Conductor.instance.getMeasureTimeInMs(currentMeasure + measure)
+ 10; // Manual adjustment as it can get the wrong time change without this.
var currentTimeChange:SongTimeChange = Conductor.instance.getTimeChange(currentTimeInMs);
var stepsPerMeasure:Int = Constants.STEPS_PER_BEAT * currentTimeChange.timeSignatureNum;
var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE * stepsPerMeasure);
// Draw horizontal dividers between the measures.
var gridMeasureDividerColor:FlxColor = switch (state.currentTheme)
{
case Light: GRID_MEASURE_DIVIDER_COLOR_LIGHT;
case Dark: GRID_MEASURE_DIVIDER_COLOR_DARK;
default: GRID_MEASURE_DIVIDER_COLOR_LIGHT;
};
// Divider at top
state.measureTickBitmap.fillRect(new Rectangle(ticksWidth, totalTicksHeight, linesWidth, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
// Divider at bottom
var dividerLineBY:Float = ticksHeight - (GRID_MEASURE_DIVIDER_WIDTH / 2);
state.measureTickBitmap.fillRect(new Rectangle(ticksWidth, totalTicksHeight + dividerLineBY, linesWidth, GRID_MEASURE_DIVIDER_WIDTH / 2),
gridMeasureDividerColor);
// Measure ticks.
state.measureTickBitmap.fillRect(new Rectangle(0, totalTicksHeight, ticksWidth, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
var bottomTickY:Float = ticksHeight - (measureTickWidth / 2);
state.measureTickBitmap.fillRect(new Rectangle(0, totalTicksHeight + bottomTickY, ticksWidth, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
// Draw horizontal dividers between the beats, this is done inside the following loop.
var gridBeatDividerColor:FlxColor = switch (state.currentTheme)
{
case Light: GRID_BEAT_DIVIDER_COLOR_LIGHT;
case Dark: GRID_BEAT_DIVIDER_COLOR_DARK;
default: GRID_BEAT_DIVIDER_COLOR_LIGHT;
};
// Draw the beat ticks and dividers, and step ticks. No need for two seperate loops thankfully.
for (i in 1...stepsPerMeasure)
{
if ((i % Constants.STEPS_PER_BEAT) == 0) // If we're on a beat, draw a beat tick and divider.
{
var beatTickY:Float = totalTicksHeight + ChartEditorState.GRID_SIZE * i - (beatTickWidth / 2);
var beatTickLength:Float = ticksWidth * 2 / 3;
state.measureTickBitmap.fillRect(new Rectangle(0, beatTickY, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
// Horizontal divider.
state.measureTickBitmap.fillRect(new Rectangle(ticksWidth, totalTicksHeight + (ChartEditorState.GRID_SIZE * i) - (GRID_BEAT_DIVIDER_WIDTH / 2),
linesWidth, GRID_BEAT_DIVIDER_WIDTH),
gridBeatDividerColor);
}
else // Else, draw a step tick.
{
var stepTickY:Float = totalTicksHeight + ChartEditorState.GRID_SIZE * i - (stepTickWidth / 2);
var stepTickLength:Float = ticksWidth * 1 / 3;
state.measureTickBitmap.fillRect(new Rectangle(0, stepTickY, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
}
}
measureLengthsInPixels.push(totalTicksHeight);
totalTicksHeight += ticksHeight;
}
previousMeasure = currentMeasure;
// Finally, draw vertical dividers between the strumlines.
var gridStrumlineDividerColor:FlxColor = switch (state.currentTheme)
{
case Light: GRID_STRUMLINE_DIVIDER_COLOR_LIGHT;
case Dark: GRID_STRUMLINE_DIVIDER_COLOR_DARK;
default: GRID_STRUMLINE_DIVIDER_COLOR_LIGHT;
};
// Divider at 1 * (Strumline Size)
var dividerLineAX:Float = ticksWidth + ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
state.measureTickBitmap.fillRect(new Rectangle(dividerLineAX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.measureTickBitmap.height), gridStrumlineDividerColor);
// Divider at 2 * (Strumline Size)
var dividerLineBX:Float = ticksWidth + ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
state.measureTickBitmap.fillRect(new Rectangle(dividerLineBX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.measureTickBitmap.height), gridStrumlineDividerColor);
if (state.measureTicks != null)
{
state.measureTicks.measureLengthsInPixels = measureLengthsInPixels;
state.measureTicks.reloadTickBitmap();
}
}
/**
* Horizontal offset ticks.
*/
@ -306,7 +210,7 @@ class ChartEditorThemeHandler
var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure); // 10 minor ticks wide.
var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE); // 1 grid squares tall.
state.offsetTickBitmap = new BitmapData(ticksWidth, ticksHeight, true);
state.offsetTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), GRID_BEAT_DIVIDER_COLOR_DARK);
state.offsetTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), 0xFFC4C4C4);
// Draw the major ticks.
var leftTickX:Float = 0;
@ -420,6 +324,11 @@ class ChartEditorThemeHandler
}
}
static function updateMeasureTicks(state:ChartEditorState):Void
{
state.measureTicks?.updateTheme();
}
public static function buildPlayheadBlock():FlxSprite
{
var playheadBlock:FlxSprite = new FlxSprite();

View file

@ -1,125 +1,95 @@
package funkin.ui.freeplay;
import flixel.FlxObject;
import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint;
import flixel.text.FlxText;
import flixel.util.FlxSort;
import flixel.FlxSprite;
import flixel.util.FlxDestroyUtil;
import flixel.util.FlxColor;
import flixel.util.FlxSort;
// its kinda like marqeee html lol!
@:nullSafety
class BGScrollingText extends FlxSpriteGroup
class BGScrollingText extends FlxText
{
var grpTexts:FlxTypedSpriteGroup<FlxSprite>;
var sourceText:FlxText;
var _textPositions:Array<FlxPoint> = [];
var _positionCache:FlxPoint = FlxPoint.get();
public var widthShit:Float = FlxG.width;
public var placementOffset:Float = 20;
public var speed:Float = 1;
public var size(default, set):Int = 48;
public var funnyColor(default, set):FlxColor = 0xFFFFFFFF;
@:deprecated("Use color instead")
public var funnyColor(get, set):FlxColor;
function get_funnyColor():FlxColor
return color;
function set_funnyColor(c:FlxColor):FlxColor
{
return color = c;
}
public function new(x:Float, y:Float, text:String, widthShit:Float = 100, ?bold:Bool = false, ?size:Int = 48)
{
super(x, y);
grpTexts = new FlxTypedSpriteGroup<FlxSprite>();
super(x, y, 0, text, size);
_positionCache = FlxPoint.get(x, y);
font = "5by7";
this.bold = bold ?? false;
this.widthShit = widthShit;
// Only keep one FlxText graphic at a time for batching
sourceText = new FlxText(0, 0, 0, text, size ?? this.size);
sourceText.font = "5by7";
sourceText.bold = bold ?? false;
@:privateAccess
sourceText.regenGraphic();
regenGraphic();
var needed:Int = Math.ceil(widthShit / sourceText.frameWidth) + 1;
var needed:Int = Math.ceil(widthShit / frameWidth) + 1;
for (i in 0...needed)
{
var coolText = new FlxSprite((i * sourceText.frameWidth) + (i * 20), 0);
grpTexts.add(coolText);
}
if (size != null) this.size = size;
add(grpTexts);
}
function reloadGraphics()
{
if (grpTexts != null)
{
@:privateAccess
sourceText.regenGraphic();
grpTexts.forEach(function(txt:FlxSprite) {
txt.loadGraphic(sourceText.graphic);
txt.updateHitbox();
});
_textPositions.push(FlxPoint.get((i * frameWidth) + (i * 20), 0));
}
}
function set_size(value:Int):Int
override public function update(elapsed:Float):Void
{
sourceText.size = value;
reloadGraphics();
this.size = value;
return value;
}
function set_funnyColor(value:FlxColor):FlxColor
{
sourceText.color = value;
reloadGraphics();
this.funnyColor = value;
return value;
}
override public function update(elapsed:Float)
{
for (txt in grpTexts.group)
{
if (txt == null) continue;
txt.x -= 1 * (speed * (elapsed / (1 / 60)));
if (speed > 0)
{
if (txt.x < -txt.frameWidth)
{
txt.x = grpTexts.group.members[grpTexts.length - 1].x + grpTexts.group.members[grpTexts.length - 1].frameWidth + placementOffset;
sortTextShit();
}
}
else
{
if (txt.x > txt.frameWidth * 2)
{
txt.x = grpTexts.group.members[0].x - grpTexts.group.members[0].frameWidth - placementOffset;
sortTextShit();
}
}
}
super.update(elapsed);
for (txtPosition in _textPositions)
{
if (txtPosition == null) continue;
txtPosition.x -= 1 * (speed * (elapsed / (1 / 60)));
if (speed > 0) // Going left
{
if (txtPosition.x < -frameWidth)
{
txtPosition.x = _textPositions[_textPositions.length - 1].x + frameWidth + placementOffset;
sortTextShit();
}
}
else // Going right
{
if (txtPosition.x > frameWidth * 2)
{
txtPosition.x = _textPositions[0].x - frameWidth - placementOffset;
sortTextShit();
}
}
}
}
override public function draw():Void
{
_positionCache.set(x, y);
for (position in _textPositions)
{
setPosition(_positionCache.x + position.x, _positionCache.y + position.y);
super.draw();
}
setPosition(_positionCache.x, _positionCache.y);
}
function sortTextShit():Void
{
grpTexts.sort(function(Order:Int, Obj1:FlxObject, Obj2:FlxObject) {
return FlxSort.byValues(Order, Obj1.x, Obj2.x);
_textPositions.sort(function(Obj1:FlxPoint, Obj2:FlxPoint) {
return FlxSort.byValues(FlxSort.ASCENDING, Obj1.x, Obj2.x);
});
}
override function destroy():Void
{
super.destroy();
sourceText = FlxDestroyUtil.destroy(sourceText);
}
}

View file

@ -131,27 +131,27 @@ class NewCharacterCard extends BackingCard
darkBg = new FlxSprite(0, 0).loadGraphic(bitmap);
add(darkBg);
friendFoe.funnyColor = 0xFF139376;
friendFoe.color = 0xFF139376;
friendFoe.speed = -4;
add(friendFoe);
newUnlock1.funnyColor = 0xFF99BDF2;
newUnlock1.color = 0xFF99BDF2;
newUnlock1.speed = 2;
add(newUnlock1);
waiting.funnyColor = 0xFF40EA84;
waiting.color = 0xFF40EA84;
waiting.speed = -2;
add(waiting);
newUnlock2.funnyColor = 0xFF99BDF2;
newUnlock2.color = 0xFF99BDF2;
newUnlock2.speed = 2;
add(newUnlock2);
friendFoe2.funnyColor = 0xFF139376;
friendFoe2.color = 0xFF139376;
friendFoe2.speed = -4;
add(friendFoe2);
newUnlock3.funnyColor = 0xFF99BDF2;
newUnlock3.color = 0xFF99BDF2;
newUnlock3.speed = 2;
add(newUnlock3);

View file

@ -0,0 +1,149 @@
package funkin.util.macro;
import haxe.crypto.Sha1;
import haxe.rtti.Meta;
#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.io.Path;
import sys.FileSystem;
#end
class SongDataValidator
{
static var _allCharts:Map<String, String> = null;
static var _checkedCharts:Array<String> = [];
static var _invalidCharts:Array<String> = [];
/**
* See if the chart for the variation is valid, i.e. if the chart content differs from the compilation-time one.
* If it isn't, add it to an array of invalid charts.
* @param chartContent The content of the chart file.
*/
public static function checkChartValidity(chartContent:String, songId:String, variation:String = "default"):Void
{
var songFormat:String = '${songId}::${variation}';
// If the chart is already checked, do nothing.
if (_checkedCharts.contains(songFormat)) return;
// If the all charts list is null, fetch it from the class' type.
if (_allCharts == null)
{
var metaData:Dynamic = Meta.getType(SongDataValidator);
if (metaData.charts != null)
{
_allCharts = [];
for (element in (metaData.charts ?? []))
{
if (element.length != 2) throw 'Malformed element in chart datas: ' + element;
var song:String = element[0];
var data:String = element[1];
_allCharts.set(song, data);
}
}
else
{
throw 'No chart datas found in SongDataValidator';
}
}
var isValid:Bool = false;
// If there is no chart found for the song and variation, it's a custom song and it should always be valid.
if (!_allCharts.exists(songFormat))
{
isValid = true;
}
else
{
// Check if the content matches.
var chartClean:String = Sha1.encode(chartContent);
if (chartClean == _allCharts.get(songFormat)) isValid = true;
}
// Add to an array if the chart is invalid.
if (!isValid)
{
trace(' [WARN] The chart file for the song $songId and variation $variation has been tampered with.');
_invalidCharts.push(songFormat);
}
// Add the song to the checked charts so that we don't have to run checks again.
_checkedCharts.push(songFormat);
}
/**
* Returns true if the chart isn't in the invalid charts list.
*/
public static function isChartValid(songId:String, variation:String = "default"):Bool
{
return !_invalidCharts.contains('${songId}::${variation}');
}
/**
* Clear the lists so we can check for songs again.
*/
public static function clearLists():Void
{
_checkedCharts = [];
_invalidCharts = [];
}
#if macro
public static inline final BASE_PATH:String = "assets/preload/data/songs";
static var calledBefore:Bool = false;
#end
public static macro function loadSongData():Void
{
Context.onAfterTyping(function(_) {
if (calledBefore) return;
calledBefore = true;
var allCharts:Array<Expr> = [];
// Load songs from the assets folder.
var songs:Array<String> = FileSystem.readDirectory(BASE_PATH);
for (song in songs)
{
var songFiles:Array<String> = FileSystem.readDirectory(Path.join([BASE_PATH, song]));
for (file in songFiles)
{
if (!StringTools.endsWith(file, ".json")) continue; // Exclude non-json files.
var splitter:Array<String> = StringTools.replace(file, ".json", "").split("-");
if (splitter[1] != "chart") continue; // Exclude non-chart files.
var variation:String = splitter[2] ?? "default";
var chart:String = sys.io.File.getContent(Path.join([BASE_PATH, song, file]));
chart = Sha1.encode(StringTools.trim(chart));
var entry = [macro $v{'${song}::${variation}'}, macro $v{chart}];
allCharts.push(macro $a{entry});
}
}
// Add the chart data to the class.
var dataClass = Context.getType('funkin.util.macro.SongDataValidator');
switch (dataClass)
{
case TInst(t, _):
var dataClassType = t.get();
dataClassType.meta.remove('charts');
dataClassType.meta.add('charts', allCharts, Context.currentPos());
default:
throw 'Could not find SongDataValidator type';
}
});
}
}