1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-08-31 19:04:55 +00:00

Compare commits

...

65 commits

Author SHA1 Message Date
Lasercar 2f2efead77 Fix volumes being reset to 100% after playtest 2025-06-18 14:59:34 +10:00
Lasercar 7415cc4069 Set playspeed after variation load 2025-06-18 14:58:39 +10:00
Lasercar f597a87d10 I.. quit (to the menu)! 2025-06-18 11:13:12 +08:00
Lasercar 15f07e8e26 There's a reason it's called the MAIN menu 2025-06-18 11:13:12 +08:00
Lasercar cb60bb9b68 Stage editor - Ctrl+N new stage
Also windows target configuration preset for straight to stage editor (not to be confused with the stage builder)
2025-06-18 11:13:12 +08:00
Kolo e563c2c59d
2bugs2fix (#5245)
Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 23:18:26 +00:00
Lasercar 65a2673e38
[ENHANCEMENT] Stage Editor - Closeable toolboxes (#5238)
* Close the toolboxes

* Get deselected
2025-06-17 21:52:10 +00:00
Lasercar 8fcc841e79 Fix Are you sure?
Also they now dance to Artistic Expression
2025-06-18 04:50:56 +08:00
Hyper_ 9626f354ef
The sounds PR fixening (#5235)
* aaaaAAAAAAAAAAAAAAAA

Actually fix vocals (and other sounds) playing in the chart editor......
...I think

* forgot vocals aren't included
2025-06-17 18:50:44 +00:00
Abnormal 57ee401f8e now it builds??????????????????? what the fUCKKKKKKKK 2025-06-16 23:44:00 -05:00
Abnormal a4ef2514c1 hyper........................................................................ 2025-06-16 23:37:33 -05:00
Abnormal dcc989782e fix a build error 2025-06-16 23:15:25 -05:00
Abnormal 6b712e71e6 update assets submod for stage editor files 2025-06-16 23:10:49 -05:00
100ec bbcc5ab81e Bump assets submod for develop testing 2025-06-17 11:38:39 +08:00
VioletSnowLeopard 1c59256871
[BUGFIX] Fix Two Issues With Song Text on Freeplay Capsules (#5036)
* fix song text remaining highlighted

unintentionally fixes song text squishing

* Fix squashed text for real this time
2025-06-17 01:14:32 +00:00
CrusherNotDrip 3bb5d7c12f
Fix crash when mashing I or D on title state. (#5160) 2025-06-17 01:14:21 +00:00
Hyper_ cb9e335d79
fix: Cleanup on LatencyState not being performed when closed/destroyed by state switch (#5085) 2025-06-17 01:14:08 +00:00
Lasercar c59fb53142
no one was around to fix this bug (#4128) 2025-06-17 01:13:56 +00:00
Lasercar afb90615e5
Cancel state change on debug menu (#4211) 2025-06-17 01:13:36 +00:00
Lasercar 5333cdf0bc
ctrl click on hold note null reference fix (#4203) 2025-06-17 01:13:07 +00:00
Hyper_ 36cef9f915
Fix possible crash when trying to open nonexistent folders (#4940) 2025-06-17 01:12:50 +00:00
Lasercar cc8aff94a8
Chart editor state: open target song difficulty & variation (#4116)
* chart editor target song difficulty parameter

* chart editor target song variation parameter

* fixed success message (also forgor playstate change)

* Difficulty no longer always set to normal
2025-06-17 01:12:38 +00:00
Hundrec 9420842f5e
[ENHANCEMENT] Raise maximum FPS cap to 500 (#5044)
* Raise max fps cap to 360

* Make it 500, actually
2025-06-17 01:11:20 +00:00
Hundrec 63da7fcab1
Fix chart editor playbar ms display (#4257) 2025-06-17 01:11:02 +00:00
anysad 456596a34b
fix playhead width (#5090) 2025-06-17 01:10:40 +00:00
Hundrec e640ba1274
Prevent the playhead from scrolling before song start (#5024) 2025-06-17 01:10:29 +00:00
Lasercar 42226e32f2
Remove JPEG (#4895) 2025-06-17 01:10:01 +00:00
charlesisfeline 08d1eff7af
silly typo... (#4773) 2025-06-17 01:09:33 +00:00
anysad 3d65a18c00
bye bye trace! (#5097) 2025-06-17 01:09:23 +00:00
Hyper_ 568e57c32f
Remove V-Sync option from PreferencesMenu on web builds (#5062) 2025-06-17 01:09:06 +00:00
Abnormal c0f9281ee4
chore: Format the project.hxp file (#4762) 2025-06-17 01:08:28 +00:00
Hyper_ 621b4b8ccc
Polymod: Blacklist funkin.util.macro.* (#5185)
It has `CompiledClassList` which allows access to `sys` and Newgrounds API functions.
2025-06-17 01:08:14 +00:00
JackXson 940ece9c07
[BUGFIX] Fix Latency State Exiting to Main Menu Instead of Options Menu (#5076)
* latency state exits to options state

* OptionsState now remembers selection

---------

Co-authored-by: Abnormal <86753001+AbnormalPoof@users.noreply.github.com>
2025-06-17 01:07:37 +00:00
Hundrec c9405ec609
Always display charSelectHint after unlocking character (#5023) 2025-06-17 01:05:38 +00:00
VioletSnowLeopard 62058ee971
Remove this unnecessary line (#5043)
`curSelected` is always set to 0 directly above this
2025-06-17 01:04:52 +00:00
Lasercar 9a01083d9f
no character? (#5008) 2025-06-17 01:03:23 +00:00
Lasercar 64f25395c5
Fake rank now visible (#4986)
Also fixes the song text clipping the rank area when getting a new rank
2025-06-17 01:01:56 +00:00
VioletSnowLeopard bdc044ce04
Fix combo drop animations (#4968) 2025-06-17 01:01:42 +00:00
Abnormal 9cffb3fd6b
[BUGFIX] Fix the game crashing when hot-reloading with F5 (#5065)
* fix: Fix the game crashing when hot reloading with F5

* NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO

Co-authored-by: Hundrec <hundrecard@gmail.com>

---------

Co-authored-by: Hundrec <hundrecard@gmail.com>
2025-06-17 01:01:19 +00:00
Lasercar 50a8e47293
Clear un/redo history on song load (#4308) 2025-06-17 00:52:23 +00:00
Hyper_ 923078bd57
Remove unnecessary Tracy frame mark (#5004)
This is already done by OpenFL
2025-06-17 00:51:16 +00:00
Lasercar 7c136ff38f
Add all tween types (#4249) 2025-06-17 00:50:45 +00:00
zackaryowo 01b8f69553
[BUGFIX] Fix Audio/Visual Offset causing skips on song start (#3732)
* Fix audio offset interactions with song start

Don't use combined offset here-- using it will cause the instrumental to skip forwards due to your offset. Just use instrumental offset, and don't play it when the song starts-- let resyncVocals do that

* Alter countdown + conductor behavior

Conductor's minimum songPosition when music is playing is now combinedOffset.

resyncVocals is also no longer used when the song starts, as it complicates matters and causes weird double-upping whatever due to the song being played, paused, and then played again

* Update source/funkin/Conductor.hx

thank you https://github.com/cyn0x8 for reminding me FlxMath.bound exists

Co-Authored-By: cyn <cyn0x8+git@gmail.com>

* Oops, don't need this here

Thank you @NotHyper-474!

* Fixed instrumentalOffset goofiness :D

---------

Co-authored-by: cyn <cyn0x8+git@gmail.com>
Co-authored-by: Abnormal <86753001+AbnormalPoof@users.noreply.github.com>
2025-06-17 00:48:23 +00:00
Lasercar fd53ff89a4
Open chart editor on selected song in freeplay state (#4114)
* Open chart editor in freeplay

* idk

* Load a random song when opened on random
2025-06-17 00:44:05 +00:00
Kolo 657564322d
another 20 trillion sandboxed classes (#5040)
Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 00:42:46 +00:00
Hundrec 605b5ba5c2
Remove alphabetical sort from Favorites (#3609) 2025-06-17 00:42:28 +00:00
Hundrec 5a4d8344d2
Hide cursor when Title Screen starts (#4520) 2025-06-17 00:42:18 +00:00
Hundrec 1794a9e57e
[CHORE] Fix some typos in PlayState.hx (#4333)
* change -> chance

* Remove extra "lose"

* Fix two more misspellings in PlayState

Co-Authored-By: VioletSnowLeopard <202548129+violetsnowleopard@users.noreply.github.com>

* Finish Eric's sentences in PlayState.hx

---------

Co-authored-by: VioletSnowLeopard <202548129+violetsnowleopard@users.noreply.github.com>
Co-authored-by: Abnormal <86753001+AbnormalPoof@users.noreply.github.com>
2025-06-17 00:41:11 +00:00
Ralty 39c2e7136d
Fix Lit Up being impossible to submit score into (#4577) 2025-06-17 00:40:18 +00:00
Kolo d1d96f58e2
move da hold note trails (#4127)
Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 00:40:09 +00:00
Kolo 3b667044e1
save ALL params grrrrr (#4956)
Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 00:38:28 +00:00
Lasercar 726cda48c9
Use first difficulty as fallback (#4949) 2025-06-17 00:38:09 +00:00
Hyper_ e736229024
[BUGFIX] Fix Countdown Stacking upon Restarting (#4875)
* run checks if timer's running

* Improve vwoosh timer behaviour (no freezing the whole game this time)

* Prevent vwoosh timer from running outside PlayState

---------

Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 00:35:19 +00:00
Lasercar 1fecb6ea3e
Chart Editor - Save all audio settings (#4149)
* Save chart editor vocal volume and playback speed
Also opponent hitsounds

* Whoops, didn't save and load the stuff properly

* I sawed this playbackspeed in half!
2025-06-17 00:35:03 +00:00
VioletSnowLeopard 4294e9a6ec
set selected after changing difficulties or unfavoriting a song (#4677) 2025-06-17 00:34:40 +00:00
Abnormal a73be72ba0
fix: Remove spammy trace calls from DiscordClient (#4207) 2025-06-17 00:33:33 +00:00
Hyper_ 7947ecaa00
Fix implementation of fastIndexOf causing duplicate notes in displayedNoteData (#5073) 2025-06-17 00:33:18 +00:00
Abnormal 5801411be0
feat: Options Menu scrolling (#4706) 2025-06-17 00:28:03 +00:00
Hyper_ f16e42368b
Make sound effects pause with the game (#4058) 2025-06-17 00:27:52 +00:00
Hyper_ a9bd80d5ec
[ENHANCEMENT] Prevent Stacked Notes (#3574)
* Initial commit

* use chunking

i tested it out, and it is faster than the previous implementation

* WIP implementation of no stacking when pasting

* Better method name and remove unnecessary variable

* Fix wrong notes being removed from notesB

* Hopefully temporary implementation of note overwrite

* Small cleanup and refactors

* Note overwrite on paste again

* Small refactor; reduce default threshold

* Hopefully should be good to go now.

* Add support for overlapping notes in chart editor preview
Also fixes some inconsistencies in the documentation

* Respect style guide

Optional parameters and parameters with default values should be mutually exclusive.

* Fix scrolling causing performance loss

* Switch to snap-based threshold instead
(No idea if this was implemented correctly)

* Shift + Delete for deleting all stacked notes in chart or selection

* Ignore events when wanting to delete stacked notes

* aaa this just feels wrong

* Don't make event selection box red if there's a stacked note selected

* Fix deleting all stacked notes action not working

* Add 'Edit -> Delete Stacked' menu item and some fixes

* Make undo feel less weird
Actually correct implementation of  threshold (maybe)
Minor bugfixes

* Some stuff I forgor 💀

* use only when ONLY only is only

Co-authored-by: Hundrec <hundrecard@gmail.com>

* This minor spelling mistake will be the bane of me

Co-authored-by: Hundrec <hundrecard@gmail.com>

* Hundrecard's paste note text suggestion

* Fix a minor bug with RemoveStackedNotesCommand

* Singular text for singular note

* Use dropdown for threshold (thanks Hundrec for the suggestion!)

Also R.I.P True...

* - Revert snaps order
- Actually change default threshold to match default dropdown value
- Increase epsilon on threshold check

* Clarify how threshold works differently in the UI

Co-authored-by: Hundrec <hundrecard@gmail.com>

* Fix `Exact` threshold check

* Fix a bug with RemoveStackedNotesCommand, properly this time
Also fixes bug where listStackedNotes could include a stacked note twice
Also minor optimization?

* Warn user about stacked notes when opening song

* Change back to fastContains since the PR that fixes `fastIndexOf` was accepted

---------

Co-authored-by: lemz1 <ismael.amjad07@gmail.com>
Co-authored-by: Hundrec <hundrecard@gmail.com>
2025-06-17 00:27:30 +00:00
Lasercar 908d6ca834
Chart editor - Load meta/chartdata fix (#4278)
* Load meta/chartdata fix
Deletes the song serialiser class, it has no use anymore
Also fixes the BPM changes
Note style is properly set when metadata toolbox is refreshed
Add variation dialog now fills note style dropdown with note styles and sets it properly afterwards
Also makes a few optimisations for the chart editor

* Try push unique the difficulties for the notes
2025-06-17 00:27:08 +00:00
Kolo 7be65fc2bd
[BUGFIX/ENHANCEMENT] Miscellaneous Stage Editor Bugfixes and Missing Features (#3974)
* stage editor bugfixes + features :D

* Merge branch 'develop' of https://github.com/FunkinCrew/Funkin into stage-editor-bugfix-n-stuff

* even more fixes and missing features

* delete logic fix + 2 new feats

feat 1: new objects now have the zIndex 1 higher than the last one (thanks hundrec)
feat 2: chars to test as are now saved (thanks imverybad)

* compilation fix

---------

Co-authored-by: Kolo <67389779+JustKolosaki@users.noreply.github.com>
2025-06-17 00:26:06 +00:00
KutikiPlayz 62b2815d04
notes move freaking normally (#3544) 2025-06-17 00:25:39 +00:00
Hyper_ caf56d496c
fix: Clear waveform data when destroying audio (#5231)
This fixes an issue where recycled sounds would use the previous sound's waveform data.
2025-06-17 00:25:24 +00:00
Lasercar 2a815e91db
Charselect remember character (#4072) 2025-06-17 00:24:54 +00:00
70 changed files with 3812 additions and 2051 deletions

10
.vscode/settings.json vendored
View file

@ -150,6 +150,16 @@
"target": "windows",
"args": ["-debug", "-DRESULTS"]
},
{
"label": "Windows / Debug (Straight to Stage Editor)",
"target": "windows",
"args": ["-debug", "-DSTAGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Debug (Straight to Stage Builder)",
"target": "windows",
"args": ["-debug", "-DSTAGEBUILD", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",

2
assets

@ -1 +1 @@
Subproject commit c108a7ff0d11bf328e7b232160b8f68c71e21bca
Subproject commit d3bca00d0619c7cd0de4343049762dfba1e0fe23

File diff suppressed because it is too large Load diff

View file

@ -92,6 +92,12 @@ class Conductor
*/
public var songPosition(default, null):Float = 0;
/**
* The offset between frame time and music time.
* Used in `getTimeWithDelta()` to get a more accurate music time when on higher framerates.
*/
var songPositionDelta(default, null):Float = 0;
var prevTimestamp:Float = 0;
var prevTime:Float = 0;
@ -422,7 +428,8 @@ class Conductor
// If the song is playing, limit the song position to the length of the song or beginning of the song.
if (FlxG.sound.music != null && FlxG.sound.music.playing)
{
this.songPosition = Math.min(currentLength, Math.max(0, songPos));
this.songPosition = FlxMath.bound(Math.min(this.combinedOffset, 0), songPos, currentLength);
this.songPositionDelta += FlxG.elapsed * 1000 * FlxG.sound.music.pitch;
}
else
{
@ -488,12 +495,23 @@ class Conductor
// which it doesn't do every frame!
if (prevTime != this.songPosition)
{
this.songPositionDelta = 0;
// Update the timestamp for use in-between frames
prevTime = this.songPosition;
prevTimestamp = Std.int(Timer.stamp() * 1000);
}
}
/**
* Returns a more accurate music time for higher framerates.
* @return Float
*/
public function getTimeWithDelta():Float
{
return this.songPosition + this.songPositionDelta;
}
/**
* Can be called in-between frames, usually for input related things
* that can potentially get processed on exact milliseconds/timestmaps.

View file

@ -27,6 +27,7 @@ import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.play.PlayStatePlaylist;
import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.debug.stageeditor.StageEditorState;
import funkin.ui.title.TitleState;
import funkin.ui.transition.LoadingState;
import funkin.util.CLIUtil;
@ -241,6 +242,9 @@ class InitState extends FlxState
#elseif CHARTING
// -DCHARTING
FlxG.switchState(() -> new funkin.ui.debug.charting.ChartEditorState());
#elseif STAGING
// -DSTAGING
FlxG.switchState(() -> new funkin.ui.debug.stageeditor.StageEditorState());
#elseif STAGEBUILD
// -DSTAGEBUILD
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
@ -303,6 +307,13 @@ class InitState extends FlxState
fnfcTargetPath: params.chart.chartPath,
}));
}
else if (params.stage.shouldLoadStage)
{
FlxG.switchState(() -> new StageEditorState(
{
fnfsTargetPath: params.stage.stagePath,
}));
}
else
{
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));

View file

@ -343,44 +343,6 @@ class Preferences
return value;
}
/**
* The game will save any screenshots taken to this format.
* @default `PNG`
*/
public static var saveFormat(get, set):Any;
static function get_saveFormat():Any
{
return Save?.instance?.options?.screenshot?.saveFormat ?? 'PNG';
}
static function set_saveFormat(value):Any
{
var save:Save = Save.instance;
save.options.screenshot.saveFormat = value;
save.flush();
return value;
}
/**
* The game will save JPEG screenshots with this quality percentage.
* @default `80`
*/
public static var jpegQuality(get, set):Int;
static function get_jpegQuality():Int
{
return Save?.instance?.options?.screenshot?.jpegQuality ?? 80;
}
static function set_jpegQuality(value:Int):Int
{
var save:Save = Save.instance;
save.options.screenshot.jpegQuality = value;
save.flush();
return value;
}
/**
* Loads the user's preferences from the save data and apply them.
*/

View file

@ -56,8 +56,6 @@ class DiscordClient
{
while (true)
{
trace('[DISCORD] Performing client update...');
#if DISCORD_DISABLE_IO_THREAD
Discord.updateConnection();
#end
@ -76,8 +74,6 @@ class DiscordClient
public function setPresence(params:DiscordClientPresenceParams):Void
{
trace('[DISCORD] Updating presence... (${params})');
Discord.updatePresence(buildPresence(params));
}
@ -101,8 +97,6 @@ class DiscordClient
// IMPORTANT NOTE: This can be an asset key uploaded to Discord's developer panel OR any URL you like.
presence.largeImageKey = cast(params.largeImageKey, Null<String>) ?? "album-volume1";
trace('[DISCORD] largeImageKey: ${presence.largeImageKey}');
// TODO: Make this use the song's album art.
// presence.largeImageKey = "icon";
// presence.largeImageKey = "https://f4.bcbits.com/img/a0746694746_16.jpg";
@ -204,4 +198,17 @@ typedef DiscordClientPresenceParams =
*/
var ?smallImageKey:String;
}
class DiscordClientSandboxed
{
public static function setPresence(params:DiscordClientPresenceParams)
{
return DiscordClient.instance.setPresence(params);
}
public static function shutdown()
{
DiscordClient.instance.shutdown();
}
}
#end

View file

@ -2,7 +2,10 @@ package funkin.api.newgrounds;
#if FEATURE_NEWGROUNDS
import io.newgrounds.Call.CallError;
import io.newgrounds.components.ScoreBoardComponent;
import io.newgrounds.objects.Score;
import io.newgrounds.objects.ScoreBoard as LeaderboardData;
import io.newgrounds.objects.User;
import io.newgrounds.objects.events.Outcome;
import io.newgrounds.utils.ScoreBoardList;
@ -66,6 +69,41 @@ class Leaderboards
}
}
/**
* Request to receive scores from Newgrounds.
* @param leaderboard The leaderboard to fetch scores from.
* @param params Additional parameters for fetching the score.
*/
public static function requestScores(leaderboard:Leaderboard, params:RequestScoresParams)
{
// Silently reject retrieving scores from unknown leaderboards.
if (leaderboard == Leaderboard.Unknown) return;
var leaderboardList = NewgroundsClient.instance.leaderboards;
if (leaderboardList == null) return;
var leaderboardData:Null<LeaderboardData> = leaderboardList.get(leaderboard.getId());
if (leaderboardData == null) return;
var user:Null<User> = null;
if ((params?.useCurrentUser ?? false) && NewgroundsClient.instance.isLoggedIn()) user = NewgroundsClient.instance.user;
leaderboardData.requestScores(params?.limit ?? 10, params?.skip ?? 0, params?.period ?? ALL, params?.social ?? false, params?.tag, user,
function(outcome:Outcome<CallError>):Void {
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Fetched scores!');
if (params?.onComplete != null) params.onComplete(leaderboardData.scores);
case FAIL(error):
trace('[NEWGROUNDS] Failed to fetch scores!');
trace(error);
if (params?.onFail != null) params.onFail();
}
});
}
/**
* Submit a score for a Story Level to Newgrounds.
*/
@ -84,6 +122,74 @@ class Leaderboards
Leaderboards.submitScore(Leaderboard.getLeaderboardBySong(songId, difficultyId), score, tag);
}
}
/**
* Wrapper for `Leaderboards` that prevents submitting scores.
*/
@:nullSafety
class LeaderboardsSandboxed
{
public static function getLeaderboardBySong(songId:String, difficultyId:String)
{
return Leaderboard.getLeaderboardBySong(songId, difficultyId);
}
public static function getLeaderboardByLevel(levelId:String)
{
return Leaderboard.getLeaderboardByLevel(levelId);
}
public function requestScores(leaderboard:Leaderboard, params:RequestScoresParams)
{
Leaderboards.requestScores(leaderboard, params);
}
}
/**
* Additional parameters for `Leaderboards.requestScores()`
*/
typedef RequestScoresParams =
{
/**
* How many scores to include in a list.
* @default `10`
*/
var ?limit:Int;
/**
* How many scores to skip before starting the list.
* @default `0`
*/
var ?skip:Int;
/**
* The time-frame to pull the scores from.
* @default `Period.ALL`
*/
var ?period:Period;
/**
* If true, only scores by the user and their friends will be loaded. Ignored if no user is set.
* @default `false`
*/
var ?social:Bool;
/**
* An optional tag to filter the results by.
* @default `null`
*/
var ?tag:String;
/**
* If true, only the scores from the currently logged in user will be loaded.
* Additionally, if `social` is set to true, the scores of the user's friend will be loaded.
* @default `false`
*/
var ?useCurrentUser:Bool;
var ?onComplete:Array<Score>->Void;
var ?onFail:Void->Void;
}
#end
enum abstract Leaderboard(Int)
@ -285,7 +391,7 @@ enum abstract Leaderboard(Int)
{
case "darnell":
return DarnellBFMix;
case "litup":
case "lit-up":
return LitUpBFMix;
default:
return Unknown;
@ -379,7 +485,7 @@ enum abstract Leaderboard(Int)
return Stress;
case "darnell":
return Darnell;
case "litup":
case "lit-up":
return LitUp;
case "2hot":
return TwoHot;

View file

@ -131,32 +131,78 @@ class Medals
}
}
public static function awardStoryLevel(id:String):Void
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
{
switch (id)
var medalList = NewgroundsClient.instance.medals;
@:privateAccess
if (medalList == null || medalList._map == null) return null;
var medalData:Null<MedalData> = medalList.get(medal.getId());
@:privateAccess
if (medalData == null || medalData._data == null)
{
case 'tutorial':
Medals.award(Medal.StoryTutorial);
case 'week1':
Medals.award(Medal.StoryWeek1);
case 'week2':
Medals.award(Medal.StoryWeek2);
case 'week3':
Medals.award(Medal.StoryWeek3);
case 'week4':
Medals.award(Medal.StoryWeek4);
case 'week5':
Medals.award(Medal.StoryWeek5);
case 'week6':
Medals.award(Medal.StoryWeek6);
case 'week7':
Medals.award(Medal.StoryWeek7);
case 'weekend1':
Medals.award(Medal.StoryWeekend1);
default:
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
return null;
}
return {
id: medalData.id,
name: medalData.name,
description: medalData.description,
icon: medalData.icon,
value: medalData.value,
difficulty: medalData.difficulty,
secret: medalData.secret,
unlocked: medalData.unlocked
}
}
public static function awardStoryLevel(id:String):Void
{
var medal:Medal = Medal.getMedalByStoryLevel(id);
if (medal == Medal.Unknown)
{
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
return;
}
Medals.award(medal);
}
}
/**
* Wrapper for `Medals` that prevents awarding medals.
*/
class MedalsSandboxed
{
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
{
return Medals.fetchMedalData(medal);
}
public static function getMedalByStoryLevel(id:String):Medal
{
return Medal.getMedalByStoryLevel(id);
}
public static function getAllMedals():Array<Medal>
{
return Medal.getAllMedals();
}
}
/**
* Contains data for a Medal, but excludes functions like `sendUnlock()`.
*/
typedef FetchedMedalData =
{
var id:Int;
var name:String;
var description:String;
var icon:String;
var value:Int;
var difficulty:Int;
var secret:Bool;
var unlocked:Bool;
}
#end
@ -324,6 +370,8 @@ enum abstract Medal(Int) from Int to Int
{
switch (levelId)
{
case "tutorial":
return StoryTutorial;
case "week1":
return StoryWeek1;
case "week2":
@ -344,4 +392,33 @@ enum abstract Medal(Int) from Int to Int
return Unknown;
}
}
/**
* Lists all medals aside from the `Unknown` one.
*/
public static function getAllMedals()
{
return [
StartGame,
StoryTutorial,
StoryWeek1,
StoryWeek2,
StoryWeek3,
StoryWeek4,
StoryWeek5,
StoryWeek6,
StoryWeek7,
StoryWeekend1,
CharSelect,
FreeplayPicoMix,
FreeplayStressPico,
LossRating,
PerfectRatingHard,
GoldPerfectRatingHard,
ErectDifficulty,
GoldPerfectRatingNightmare,
FridayNight,
Nice
];
}
}

View file

@ -331,4 +331,22 @@ class NewgroundsClient
return Save.instance.ngSessionId;
}
}
/**
* Wrapper for `NewgroundsClient` that prevents submitting cheated data.
*/
class NewgroundsClientSandboxed
{
public static var user(get, never):Null<User>;
static function get_user()
{
return NewgroundsClient.instance.user;
}
public static function isLoggedIn()
{
return NewgroundsClient.instance.isLoggedIn();
}
}
#end

View file

@ -551,6 +551,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
}
FlxTween.cancelTweensOf(this);
this._label = 'unknown';
this._waveformData = null;
}
@:access(openfl.media.Sound)

View file

@ -5,7 +5,6 @@ import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.util.ClipboardUtil;
import funkin.util.SerializerUtil;
using Lambda;

View file

@ -0,0 +1,138 @@
package funkin.data.song;
using SongData.SongNoteData;
/**
* Utility class for extra handling of song notes
*/
@:nullSafety
class SongNoteDataUtils
{
static final CHUNK_INTERVAL_MS:Float = 2500;
/**
* Retrieves all stacked notes. It does this by cycling through "chunks" of notes within a certain interval.
*
* @param notes Sorted notes by time.
* @param threshold The note stack threshold. Refer to `doNotesStack` for more details.
* @param includeOverlapped (Optional) If overlapped notes should be included.
* @param overlapped (Optional) An array that gets populated with overlapped notes.
* Note that it's only guaranteed to work properly if the provided notes are sorted.
* @return Stacked notes.
*/
public static function listStackedNotes(notes:Array<SongNoteData>, threshold:Float, includeOverlapped:Bool = true,
?overlapped:Array<SongNoteData>):Array<SongNoteData>
{
var stackedNotes:Array<SongNoteData> = [];
var chunkTime:Float = 0;
var chunks:Array<Array<SongNoteData>> = [[]];
for (note in notes)
{
if (note == null)
{
continue;
}
while (note.time >= chunkTime + CHUNK_INTERVAL_MS)
{
chunkTime += CHUNK_INTERVAL_MS;
chunks.push([]);
}
chunks[chunks.length - 1].push(note);
}
for (chunk in chunks)
{
for (i in 0...(chunk.length - 1))
{
for (j in (i + 1)...chunk.length)
{
var noteI:SongNoteData = chunk[i];
var noteJ:SongNoteData = chunk[j];
if (doNotesStack(noteI, noteJ, threshold))
{
if (!stackedNotes.fastContains(noteI))
{
if (includeOverlapped) stackedNotes.push(noteI);
if (overlapped != null && !overlapped.contains(noteI)) overlapped.push(noteI);
}
if (!stackedNotes.fastContains(noteJ))
{
stackedNotes.push(noteJ);
}
}
}
}
}
return stackedNotes;
}
/**
* Concatenates two arrays of notes but overwrites notes in `lhs` that are overlapped by notes in `rhs`.
* Hold notes are only overwritten by longer hold notes.
* This operation only modifies the second array and `overwrittenNotes`.
*
* @param lhs An array of notes
* @param rhs An array of notes to concatenate into `lhs`
* @param overwrittenNotes An optional array that is modified in-place with the notes in `lhs` that were overwritten.
* @param threshold The note stack threshold. Refer to `doNotesStack` for more details.
* @return The unsorted resulting array.
*/
public static function concatOverwrite(lhs:Array<SongNoteData>, rhs:Array<SongNoteData>, ?overwrittenNotes:Array<SongNoteData>,
threshold:Float = 0):Array<SongNoteData>
{
if (lhs == null || rhs == null || rhs.length == 0) return lhs;
if (lhs.length == 0) return rhs;
var result = lhs.copy();
for (i in 0...rhs.length)
{
var noteB:SongNoteData = rhs[i];
var hasOverlap:Bool = false;
for (j in 0...lhs.length)
{
var noteA:SongNoteData = lhs[j];
if (doNotesStack(noteA, noteB, threshold))
{
// Long hold notes should have priority over shorter hold notes
if (noteA.length <= noteB.length)
{
overwrittenNotes?.push(result[j].clone());
result[j] = noteB;
}
hasOverlap = true;
break;
}
}
if (!hasOverlap) result.push(noteB);
}
return result;
}
/**
* @param noteA First note.
* @param noteB Second note.
* @param threshold The note stack threshold, in steps.
* @return Returns `true` if both notes are on the same strumline, have the same direction
* and their time difference in steps is less than the step-based threshold.
* A threshold of 0 will return `true` if notes are nearly perfectly aligned.
*/
public static function doNotesStack(noteA:SongNoteData, noteB:SongNoteData, threshold:Float = 0):Bool
{
if (noteA.data != noteB.data) return false;
else if (threshold == 0) return Math.ffloor(Math.abs(noteA.time - noteB.time)) < 1;
final stepDiff:Float = Math.abs(noteA.getStepTime() - noteB.getStepTime());
return stepDiff <= threshold + 0.001;
}
}

View file

@ -324,7 +324,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleto
}
else
{
throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.';
}
}
@ -337,7 +337,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleto
}
else
{
throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.';
}
}

View file

@ -258,6 +258,21 @@ class PolymodHandler
// `funkin.util.FileUtil` has unrestricted access to the file system.
Polymod.addImportAlias('funkin.util.FileUtil', funkin.util.FileUtilSandboxed);
#if FEATURE_NEWGROUNDS
// `funkin.api.newgrounds.Leaderboards` allows for submitting cheated scores.
Polymod.addImportAlias('funkin.api.newgrounds.Leaderboards', funkin.api.newgrounds.Leaderboards.LeaderboardsSandboxed);
// `funkin.api.newgrounds.Medals` allows for unfair granting of medals.
Polymod.addImportAlias('funkin.api.newgrounds.Medals', funkin.api.newgrounds.Medals.MedalsSandboxed);
// `funkin.api.newgrounds.NewgroundsClientSandboxed` allows for submitting cheated data.
Polymod.addImportAlias('funkin.api.newgrounds.NewgroundsClient', funkin.api.newgrounds.NewgroundsClient.NewgroundsClientSandboxed);
#end
#if FEATURE_DISCORD_RPC
Polymod.addImportAlias('funkin.api.discord.DiscordClient', funkin.api.discord.DiscordClient.DiscordClientSandboxed);
#end
// Add blacklisting for prohibited classes and packages.
// `Sys`
@ -276,9 +291,9 @@ class PolymodHandler
// Lib.load() can load malicious DLLs
Polymod.blacklistImport('cpp.Lib');
// `Unserializer`
// `haxe.Unserializer`
// Unserializer.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
Polymod.blacklistImport('Unserializer');
Polymod.blacklistImport('haxe.Unserializer');
// `lime.system.CFFI`
// Can load and execute compiled binaries.
@ -310,6 +325,7 @@ class PolymodHandler
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
if (polymod.hscript._internal.PolymodScriptClass.importOverrides.exists(className)) continue;
Polymod.blacklistImport(className);
}
@ -322,15 +338,6 @@ class PolymodHandler
Polymod.blacklistImport(className);
}
// `funkin.api.newgrounds.*`
// Contains functions which allow for cheating medals and leaderboards.
for (cls in ClassMacro.listClassesInPackage('funkin.api.newgrounds'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// `io.newgrounds.*`
// Contains functions which allow for cheating medals and leaderboards.
for (cls in ClassMacro.listClassesInPackage('io.newgrounds'))
@ -348,6 +355,16 @@ class PolymodHandler
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
// `funkin.util.macro.*`
// CompiledClassList's get function allows access to sys and Newgrounds classes
// None of the classes are suitable for mods anyway
for (cls in ClassMacro.listClassesInPackage('funkin.util.macro'))
{
if (cls == null) continue;
var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
}
/**

View file

@ -255,7 +255,7 @@ class HoldNoteScriptEvent extends NoteScriptEvent
*/
public var doesNotesplash:Bool = false;
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, cancelable:Bool = false):Void
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, comboCount:Int = 0, cancelable:Bool = false):Void
{
super(type, null, healthChange, comboCount, true);
this.holdNote = holdNote;

View file

@ -766,11 +766,15 @@ class PauseSubState extends MusicBeatSubState
* Quit the game and return to the chart editor.
* @param state The current PauseSubState.
*/
@:access(funkin.play.PlayState)
static function quitToChartEditor(state:PauseSubState):Void
{
// This should come first because the sounds list gets cleared!
PlayState.instance?.forEachPausedSound(s -> s.destroy());
state.close();
if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
PlayState.instance.close(); // This only works because PlayState is a substate!
FlxG.sound.music?.pause(); // Don't reset song position!
PlayState.instance?.vocals?.pause();
PlayState.instance?.close(); // This only works because PlayState is a substate!
}
}

View file

@ -7,6 +7,7 @@ import flixel.FlxObject;
import flixel.FlxSubState;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.sound.FlxSound;
import flixel.text.FlxText;
import flixel.tweens.FlxTween;
import flixel.ui.FlxBar;
@ -185,6 +186,11 @@ class PlayState extends MusicBeatSubState
*/
public var needsReset:Bool = false;
/**
* A timer that gets active once resetting happens. Used to vwoosh in notes.
*/
public var vwooshTimer:FlxTimer = new FlxTimer();
/**
* The current 'Blueball Counter' to display in the pause menu.
* Resets when you beat a song or go back to the main menu.
@ -304,13 +310,13 @@ class PlayState extends MusicBeatSubState
/**
* Whether the game is currently in Practice Mode.
* If true, player will not lose gain or lose score from notes.
* If true, player will not gain or lose score from notes.
*/
public var isPracticeMode:Bool = false;
/**
* Whether the game is currently in Bot Play Mode.
* If true, player will not lose gain or lose score from notes.
* If true, player will not gain or lose score from notes.
*/
public var isBotPlayMode:Bool = false;
@ -432,6 +438,11 @@ class PlayState extends MusicBeatSubState
*/
var cameraTweensPausedBySubState:List<FlxTween> = new List<FlxTween>();
/**
* Track any sounds we've paused for a Pause substate, so we can unpause them when we return.
*/
var soundsPausedBySubState:List<FlxSound> = new List<FlxSound>();
/**
* False until `create()` has completed.
*/
@ -807,7 +818,6 @@ class PlayState extends MusicBeatSubState
super.update(elapsed);
var list = FlxG.sound.list;
updateHealthBar();
updateScoreText();
@ -837,9 +847,9 @@ class PlayState extends MusicBeatSubState
// Reset music properly.
if (FlxG.sound.music != null)
{
FlxG.sound.music.time = startTimestamp - Conductor.instance.combinedOffset;
FlxG.sound.music.pitch = playbackRate;
FlxG.sound.music.pause();
FlxG.sound.music.time = startTimestamp;
FlxG.sound.music.pitch = playbackRate;
}
if (!overrideMusic)
@ -854,7 +864,7 @@ class PlayState extends MusicBeatSubState
}
}
vocals.pause();
vocals.time = 0 - Conductor.instance.combinedOffset;
vocals.time = startTimestamp - Conductor.instance.instrumentalOffset;
if (FlxG.sound.music != null) FlxG.sound.music.volume = 1;
vocals.volume = 1;
@ -875,9 +885,6 @@ class PlayState extends MusicBeatSubState
// Delete all notes and reset the arrays.
regenNoteData();
// so the song doesn't start too early :D
Conductor.instance.update(-5000, false);
// Reset camera zooming
cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY;
hudCameraZoomIntensity = (cameraBopIntensity - 1.0) * 2.0;
@ -886,10 +893,13 @@ class PlayState extends MusicBeatSubState
health = Constants.HEALTH_STARTING;
songScore = 0;
Highscore.tallies.combo = 0;
// so the song doesn't start too early :D
var vwooshDelay:Float = 0.5;
Conductor.instance.update(-vwooshDelay * 1000 + startTimestamp + Conductor.instance.beatLengthMs * -5);
// timer for vwoosh
var vwooshTimer = new FlxTimer();
vwooshTimer.start(0.5, function(t:FlxTimer) {
Conductor.instance.update(startTimestamp - Conductor.instance.combinedOffset, false);
vwooshTimer.start(vwooshDelay, function(_) {
if (playerStrumline.notes.length == 0) playerStrumline.updateNotes();
if (opponentStrumline.notes.length == 0) opponentStrumline.updateNotes();
playerStrumline.vwooshInNotes();
@ -897,6 +907,9 @@ class PlayState extends MusicBeatSubState
Countdown.performCountdown();
});
// Stops any existing countdown.
Countdown.stopCountdown();
// Reset the health icons.
currentStage?.getBoyfriend()?.initHealthIcon(false);
currentStage?.getDad()?.initHealthIcon(true);
@ -959,18 +972,16 @@ class PlayState extends MusicBeatSubState
// Enable drawing while the substate is open, allowing the game state to be shown behind the pause menu.
persistentDraw = true;
// There is a 1/1000 change to use a special pause menu.
// Prevent vwoosh timer from starting countdown in pause menu
vwooshTimer.active = false;
// There is a 1/1000 chance to use a special pause menu.
// This prevents the player from resuming, but that's the point.
// It's a reference to Gitaroo Man, which doesn't let you pause the game.
if (!isSubState && event.gitaroo)
{
this.remove(currentStage);
FlxG.switchState(() -> new GitarooPause(
{
targetSong: currentSong,
targetDifficulty: currentDifficulty,
targetVariation: currentVariation,
}));
FlxG.switchState(() -> new GitarooPause(lastParams));
}
else
{
@ -1127,6 +1138,8 @@ class PlayState extends MusicBeatSubState
playerStrumline.clean();
opponentStrumline.clean();
vwooshTimer.cancel();
songScore = 0;
updateScoreText();
@ -1230,9 +1243,29 @@ class PlayState extends MusicBeatSubState
musicPausedBySubState = true;
}
// Pause vocals.
// Not tracking that we've done this via a bool because vocal re-syncing involves pausing the vocals anyway.
if (vocals != null) vocals.pause();
// Pause any sounds that are playing and keep track of them.
// Vocals are also paused here but are not included as they are handled separately.
if (Std.isOfType(subState, PauseSubState))
{
FlxG.sound.list.forEachAlive(function(sound:FlxSound) {
if (!sound.active || sound == FlxG.sound.music) return;
// In case it's a scheduled sound
var funkinSound:FunkinSound = cast sound;
if (funkinSound != null && !funkinSound.isPlaying) return;
if (!sound.playing && sound.time >= 0) return;
sound.pause();
soundsPausedBySubState.add(sound);
});
vocals?.forEach(function(voice:FunkinSound) {
soundsPausedBySubState.remove(voice);
});
}
else
{
vocals?.pause();
}
}
// Pause camera tweening, and keep track of which tweens we pause.
@ -1281,6 +1314,9 @@ class PlayState extends MusicBeatSubState
if (event.eventCanceled) return;
// Resume vwooshTimer
if (!vwooshTimer.finished) vwooshTimer.active = true;
// Resume music if we paused it.
if (musicPausedBySubState)
{
@ -1288,6 +1324,8 @@ class PlayState extends MusicBeatSubState
musicPausedBySubState = false;
}
forEachPausedSound((s) -> needsReset ? s.destroy() : s.resume());
// Resume camera tweens if we paused any.
for (camTween in cameraTweensPausedBySubState)
{
@ -1423,6 +1461,10 @@ class PlayState extends MusicBeatSubState
{
performCleanup();
// `performCleanup()` clears the static reference to this state
// scripts might still need it, so we set it back to `this`
instance = this;
funkin.modding.PolymodHandler.forceReloadAssets();
lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id);
LoadingState.loadPlayState(lastParams);
@ -2056,9 +2098,9 @@ class PlayState extends MusicBeatSubState
FlxG.sound.music.onComplete = function() {
if (mayPauseGame) endSong(skipEndingTransition);
};
// A negative instrumental offset means the song skips the first few milliseconds of the track.
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
FlxG.sound.music.play(true, Math.max(0, startTimestamp - Conductor.instance.combinedOffset));
FlxG.sound.music.pause();
FlxG.sound.music.time = startTimestamp;
FlxG.sound.music.pitch = playbackRate;
// Prevent the volume from being wrong.
@ -2067,13 +2109,17 @@ class PlayState extends MusicBeatSubState
trace('Playing vocals...');
add(vocals);
vocals.play();
vocals.volume = 1.0;
vocals.time = startTimestamp - Conductor.instance.instrumentalOffset;
vocals.pitch = playbackRate;
vocals.time = FlxG.sound.music.time;
vocals.volume = 1.0;
// trace('STARTING SONG AT:');
// trace('${FlxG.sound.music.time}');
// trace('${vocals.time}');
resyncVocals();
FlxG.sound.music.play();
vocals.play();
#if FEATURE_DISCORD_RPC
// Updating Discord Rich Presence (with Time Left)
@ -2102,7 +2148,7 @@ class PlayState extends MusicBeatSubState
}
/**
* Resyncronize the vocal tracks if they have become offset from the instrumental.
* Resynchronize the vocal tracks if they have become offset from the instrumental.
*/
function resyncVocals():Void
{
@ -2111,8 +2157,10 @@ class PlayState extends MusicBeatSubState
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
if (!(FlxG.sound.music?.playing ?? false)) return;
var timeToPlayAt:Float = Math.min(FlxG.sound.music.length, Math.max(0, Conductor.instance.songPosition - Conductor.instance.combinedOffset));
var timeToPlayAt:Float = Math.min(FlxG.sound.music.length,
Math.max(Math.min(Conductor.instance.combinedOffset, 0), Conductor.instance.songPosition) - Conductor.instance.combinedOffset);
trace('Resyncing vocals to ${timeToPlayAt}');
FlxG.sound.music.pause();
vocals.pause();
@ -2347,7 +2395,7 @@ class PlayState extends MusicBeatSubState
{
// Call an event to allow canceling the note miss.
// NOTE: This is what handles the character animations!
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Constants.HEALTH_MISS_PENALTY, 0, true);
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Constants.HEALTH_MISS_PENALTY, Highscore.tallies.combo, true);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@ -2414,7 +2462,7 @@ class PlayState extends MusicBeatSubState
var healthChange = healthChangeUncapped.clamp(healthChangeMax, 0);
var scoreChange = Std.int(Constants.SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec);
var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true);
var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true, Highscore.tallies.combo);
dispatchEvent(event);
trace('Penalizing score by ${event.score} and health by ${event.healthChange} for dropping hold note (is combo break: ${event.isComboBreak})!');
@ -2448,7 +2496,7 @@ class PlayState extends MusicBeatSubState
}
}
// Respawns notes that were b
// Respawns notes that were between the previous time and the current time when skipping backward, or destroy notes between the previous time and the current time when skipping forward.
playerStrumline.handleSkippedNotes();
opponentStrumline.handleSkippedNotes();
}
@ -2709,6 +2757,8 @@ class PlayState extends MusicBeatSubState
FlxG.switchState(() -> new ChartEditorState(
{
targetSongId: currentSong.id,
targetSongDifficulty: currentDifficulty,
targetSongVariation: currentVariation,
}));
}
}
@ -3154,6 +3204,9 @@ class PlayState extends MusicBeatSubState
// TODO: Uncache the song.
}
// Prevent vwoosh timer from running outside PlayState (e.g Chart Editor)
vwooshTimer.cancel();
if (overrideMusic)
{
// Stop the music. Do NOT destroy it, something still references it!
@ -3175,6 +3228,8 @@ class PlayState extends MusicBeatSubState
}
}
forEachPausedSound((s) -> s.destroy());
// Remove reference to stage and remove sprites from it to save memory.
if (currentStage != null)
{
@ -3429,7 +3484,7 @@ class PlayState extends MusicBeatSubState
cancelCameraZoomTween();
}
var prevScrollTargets:Array<Dynamic> = []; // used to snap scroll speed when things go unruely
var prevScrollTargets:Array<Dynamic> = []; // used to snap scroll speed when things go unruly
/**
* The magical function that shall tween the scroll speed.
@ -3483,6 +3538,15 @@ class PlayState extends MusicBeatSubState
scrollSpeedTweens = [];
}
function forEachPausedSound(f:FlxSound->Void):Void
{
for (sound in soundsPausedBySubState)
{
f(sound);
}
soundsPausedBySubState.clear();
}
#if FEATURE_DEBUG_FUNCTIONS
/**
* Jumps forward or backward a number of sections in the song.

View file

@ -218,9 +218,21 @@ class FocusCameraSongEvent extends SongEvent
'Smooth Step In' => 'smoothStepIn',
'Smooth Step Out' => 'smoothStepOut',
'Smooth Step In/Out' => 'smoothStepInOut',
'Smoother Step In' => 'smootherStepIn',
'Smoother Step Out' => 'smootherStepOut',
'Smoother Step In/Out' => 'smootherStepInOut',
'Elastic In' => 'elasticIn',
'Elastic Out' => 'elasticOut',
'Elastic In/Out' => 'elasticInOut',
'Back In' => 'backIn',
'Back Out' => 'backOut',
'Back In/Out' => 'backInOut',
'Bounce In' => 'bounceIn',
'Bounce Out' => 'bounceOut',
'Bounce In/Out' => 'bounceInOut',
'Circ In' => 'circIn',
'Circ Out' => 'circOut',
'Circ In/Out' => 'circInOut',
'Instant (Ignores duration)' => 'INSTANT',
'Classic (Ignores duration)' => 'CLASSIC'
]

View file

@ -149,9 +149,21 @@ class ScrollSpeedEvent extends SongEvent
'Smooth Step In' => 'smoothStepIn',
'Smooth Step Out' => 'smoothStepOut',
'Smooth Step In/Out' => 'smoothStepInOut',
'Smoother Step In' => 'smootherStepIn',
'Smoother Step Out' => 'smootherStepOut',
'Smoother Step In/Out' => 'smootherStepInOut',
'Elastic In' => 'elasticIn',
'Elastic Out' => 'elasticOut',
'Elastic In/Out' => 'elasticInOut'
'Elastic In/Out' => 'elasticInOut',
'Back In' => 'backIn',
'Back Out' => 'backOut',
'Back In/Out' => 'backInOut',
'Bounce In' => 'bounceIn',
'Bounce Out' => 'bounceOut',
'Bounce In/Out' => 'bounceInOut',
'Circ In' => 'circIn',
'Circ Out' => 'circOut',
'Circ In/Out' => 'circInOut'
]
},
{

View file

@ -158,9 +158,21 @@ class ZoomCameraSongEvent extends SongEvent
'Smooth Step In' => 'smoothStepIn',
'Smooth Step Out' => 'smoothStepOut',
'Smooth Step In/Out' => 'smoothStepInOut',
'Smoother Step In' => 'smootherStepIn',
'Smoother Step Out' => 'smootherStepOut',
'Smoother Step In/Out' => 'smootherStepInOut',
'Elastic In' => 'elasticIn',
'Elastic Out' => 'elasticOut',
'Elastic In/Out' => 'elasticInOut'
'Elastic In/Out' => 'elasticInOut',
'Back In' => 'backIn',
'Back Out' => 'backOut',
'Back In/Out' => 'backInOut',
'Bounce In' => 'bounceIn',
'Bounce Out' => 'bounceOut',
'Bounce In/Out' => 'bounceInOut',
'Circ In' => 'circIn',
'Circ Out' => 'circOut',
'Circ In/Out' => 'circInOut'
]
}
]);

View file

@ -448,7 +448,6 @@ class Strumline extends FlxSpriteGroup
}
}
/**
* For a note's strumTime, calculate its Y position relative to the strumline.
* NOTE: Assumes Conductor and PlayState are both initialized.
@ -458,7 +457,7 @@ class Strumline extends FlxSpriteGroup
public function calculateNoteYPos(strumTime:Float):Float
{
return
Constants.PIXELS_PER_MS * (conductorInUse.songPosition - strumTime - Conductor.instance.inputOffset) * scrollSpeed * (Preferences.downscroll ? 1 : -1);
Constants.PIXELS_PER_MS * (conductorInUse.getTimeWithDelta() - strumTime - Conductor.instance.inputOffset) * scrollSpeed * (Preferences.downscroll ? 1 : -1);
}
public function updateNotes():Void

View file

@ -1,76 +0,0 @@
package funkin.play.song;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongMetadata;
import funkin.util.FileUtil;
import openfl.net.FileReference;
/**
* TODO: Refactor and remove this.
*/
class SongSerializer
{
/**
* Access a SongChartData JSON file from a specific path, then load it.
* @param path The file path to read from.
*/
public static function importSongChartDataSync(path:String):SongChartData
{
var fileData = FileUtil.readStringFromPath(path);
if (fileData == null) return null;
var songChartData:SongChartData = fileData.parseJSON();
return songChartData;
}
/**
* Access a SongMetadata JSON file from a specific path, then load it.
* @param path The file path to read from.
*/
public static function importSongMetadataSync(path:String):SongMetadata
{
var fileData = FileUtil.readStringFromPath(path);
if (fileData == null) return null;
var songMetadata:SongMetadata = fileData.parseJSON();
return songMetadata;
}
/**
* Prompt the user to browse for a SongChartData JSON file path, then load it.
* @param callback The function to call when the file is loaded.
*/
public static function importSongChartDataAsync(callback:SongChartData->Void):Void
{
FileUtil.browseFileReference(function(fileReference:FileReference) {
var data = fileReference.data.toString();
if (data == null) return;
var songChartData:SongChartData = data.parseJSON();
if (songChartData != null) callback(songChartData);
});
}
/**
* Prompt the user to browse for a SongMetadata JSON file path, then load it.
* @param callback The function to call when the file is loaded.
*/
public static function importSongMetadataAsync(callback:SongMetadata->Void):Void
{
FileUtil.browseFileReference(function(fileReference:FileReference) {
var data = fileReference.data.toString();
if (data == null) return;
var songMetadata:SongMetadata = data.parseJSON();
if (songMetadata != null) callback(songMetadata);
});
}
}

View file

@ -248,8 +248,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
propSprite.scrollFactor.y = dataProp.scroll[1];
propSprite.angle = dataProp.angle;
propSprite.color = FlxColor.fromString(dataProp.color);
@:privateAccess if (!isSolidColor) propSprite.blend = BlendMode.fromString(dataProp.blend);
if (!isSolidColor) propSprite.color = FlxColor.fromString(dataProp.color);
@:privateAccess propSprite.blend = BlendMode.fromString(dataProp.blend);
propSprite.zIndex = dataProp.zIndex;

View file

@ -2,6 +2,7 @@ package funkin.save;
import flixel.util.FlxSave;
import funkin.input.Controls.Device;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.scoring.Scoring;
import funkin.play.scoring.Scoring.ScoringRank;
import funkin.save.migrator.RawSaveData_v1_0_0;
@ -127,8 +128,6 @@ class Save
shouldHideMouse: true,
fancyPreview: true,
previewOnSave: true,
saveFormat: 'PNG',
jpegQuality: 80,
},
controls:
@ -173,6 +172,10 @@ class Save
metronomeVolume: 1.0,
hitsoundVolumePlayer: 1.0,
hitsoundVolumeOpponent: 1.0,
instVolume: 1.0,
playerVoiceVolume: 1.0,
opponentVoiceVolume: 1.0,
playbackSpeed: 0.5,
themeMusic: true
},
@ -181,7 +184,10 @@ class Save
previousFiles: [],
moveStep: "1px",
angleStep: 5,
theme: StageEditorTheme.Light
theme: StageEditorTheme.Light,
bfChar: "bf",
gfChar: "gf",
dadChar: "dad"
}
};
}
@ -407,6 +413,57 @@ class Save
return data.optionsChartEditor.hitsoundVolumeOpponent;
}
public var chartEditorInstVolume(get, set):Float;
function get_chartEditorInstVolume():Float
{
if (data.optionsChartEditor.instVolume == null) data.optionsChartEditor.instVolume = 1.0;
return data.optionsChartEditor.instVolume;
}
function set_chartEditorInstVolume(value:Float):Float
{
// Set and apply.
data.optionsChartEditor.instVolume = value;
flush();
return data.optionsChartEditor.instVolume;
}
public var chartEditorPlayerVoiceVolume(get, set):Float;
function get_chartEditorPlayerVoiceVolume():Float
{
if (data.optionsChartEditor.playerVoiceVolume == null) data.optionsChartEditor.playerVoiceVolume = 1.0;
return data.optionsChartEditor.playerVoiceVolume;
}
function set_chartEditorPlayerVoiceVolume(value:Float):Float
{
// Set and apply.
data.optionsChartEditor.playerVoiceVolume = value;
flush();
return data.optionsChartEditor.playerVoiceVolume;
}
public var chartEditorOpponentVoiceVolume(get, set):Float;
function get_chartEditorOpponentVoiceVolume():Float
{
if (data.optionsChartEditor.opponentVoiceVolume == null) data.optionsChartEditor.opponentVoiceVolume = 1.0;
return data.optionsChartEditor.opponentVoiceVolume;
}
function set_chartEditorOpponentVoiceVolume(value:Float):Float
{
// Set and apply.
data.optionsChartEditor.opponentVoiceVolume = value;
flush();
return data.optionsChartEditor.opponentVoiceVolume;
}
public var chartEditorThemeMusic(get, set):Bool;
function get_chartEditorThemeMusic():Bool
@ -428,7 +485,7 @@ class Save
function get_chartEditorPlaybackSpeed():Float
{
if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 1.0;
if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 0.5;
return data.optionsChartEditor.playbackSpeed;
}
@ -550,6 +607,60 @@ class Save
return data.optionsStageEditor.theme;
}
public var stageBoyfriendChar(get, set):String;
function get_stageBoyfriendChar():String
{
if (data.optionsStageEditor.bfChar == null
|| CharacterDataParser.fetchCharacterData(data.optionsStageEditor.bfChar) == null) data.optionsStageEditor.bfChar = "bf";
return data.optionsStageEditor.bfChar;
}
function set_stageBoyfriendChar(value:String):String
{
// Set and apply.
data.optionsStageEditor.bfChar = value;
flush();
return data.optionsStageEditor.bfChar;
}
public var stageGirlfriendChar(get, set):String;
function get_stageGirlfriendChar():String
{
if (data.optionsStageEditor.gfChar == null
|| CharacterDataParser.fetchCharacterData(data.optionsStageEditor.gfChar ?? "") == null) data.optionsStageEditor.gfChar = "gf";
return data.optionsStageEditor.gfChar;
}
function set_stageGirlfriendChar(value:String):String
{
// Set and apply.
data.optionsStageEditor.gfChar = value;
flush();
return data.optionsStageEditor.gfChar;
}
public var stageDadChar(get, set):String;
function get_stageDadChar():String
{
if (data.optionsStageEditor.dadChar == null
|| CharacterDataParser.fetchCharacterData(data.optionsStageEditor.dadChar ?? "") == null) data.optionsStageEditor.dadChar = "dad";
return data.optionsStageEditor.dadChar;
}
function set_stageDadChar(value:String):String
{
// Set and apply.
data.optionsStageEditor.dadChar = value;
flush();
return data.optionsStageEditor.dadChar;
}
/**
* When we've seen a character unlock, add it to the list of characters seen.
* @param character
@ -1441,16 +1552,12 @@ typedef SaveDataOptions =
* @param shouldHideMouse Should the mouse be hidden when taking a screenshot? Default: `true`
* @param fancyPreview Show a fancy preview? Default: `true`
* @param previewOnSave Only show the fancy preview after a screenshot is saved? Default: `true`
* @param saveFormat The save format of the screenshot, PNG or JPEG. Default: `PNG`
* @param jpegQuality The JPEG Quality, if we're saving to the format. Default: `80`
*/
var screenshot:
{
var shouldHideMouse:Bool;
var fancyPreview:Bool;
var previewOnSave:Bool;
var saveFormat:String;
var jpegQuality:Int;
};
var controls:
@ -1653,10 +1760,16 @@ typedef SaveDataChartEditorOptions =
var ?instVolume:Float;
/**
* Voices volume in the Chart Editor.
* Player voice volume in the Chart Editor.
* @default `1.0`
*/
var ?voicesVolume:Float;
var ?playerVoiceVolume:Float;
/**
* Opponent voice volume in the Chart Editor.
* @default `1.0`
*/
var ?opponentVoiceVolume:Float;
/**
* Playback speed in the Chart Editor.
@ -1699,4 +1812,22 @@ typedef SaveDataStageEditorOptions =
* @default `StageEditorTheme.Light`
*/
var ?theme:StageEditorTheme;
/**
* The BF character ID used in testing stages.
* @default bf
*/
var ?bfChar:String;
/**
* The GF character ID used in testing stages.
* @default gf
*/
var ?gfChar:String;
/**
* The Dad character ID used in testing stages.
* @default dad
*/
var ?dadChar:String;
};

View file

@ -173,6 +173,12 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
}
}
public function cancelAccept()
{
FlxFlicker.stopFlickering(members[selectedIndex]);
busy = false;
}
public function selectItem(index:Int)
{
members[selectedIndex].idle();

View file

@ -62,7 +62,8 @@ class CharSelectSubState extends MusicBeatSubState
var chooseDipshit:FlxSprite;
var dipshitBlur:FlxSprite;
var transitionGradient:FlxSprite;
var curChar(default, set):String = "pico";
var curChar(default, set):String = Constants.DEFAULT_CHARACTER;
var rememberedChar:String;
var nametag:Nametag;
var camFollow:FlxObject;
var autoFollow:Bool = false;
@ -87,9 +88,10 @@ class CharSelectSubState extends MusicBeatSubState
var bopInfo:FramesJSFLInfo;
var blackScreen:FunkinSprite;
public function new()
public function new(?params:CharSelectSubStateParams)
{
super();
rememberedChar = params?.character;
loadAvailableCharacters();
}
@ -167,18 +169,39 @@ class CharSelectSubState extends MusicBeatSubState
charLightGF.loadGraphic(Paths.image('charSelect/charLight'));
add(charLightGF);
gfChill = new CharSelectGF();
gfChill.switchGF("bf");
add(gfChill);
function setupPlayerChill(character:String)
{
gfChill = new CharSelectGF();
gfChill.switchGF(character);
add(gfChill);
playerChillOut = new CharSelectPlayer(0, 0);
playerChillOut.switchChar("bf");
playerChillOut.visible = false;
add(playerChillOut);
playerChillOut = new CharSelectPlayer(0, 0);
playerChillOut.switchChar(character);
playerChillOut.visible = false;
add(playerChillOut);
playerChill = new CharSelectPlayer(0, 0);
playerChill.switchChar("bf");
add(playerChill);
playerChill = new CharSelectPlayer(0, 0);
playerChill.switchChar(character);
add(playerChill);
}
// I think I can do the character preselect thing here? This better work
// Edit: [UH-OH!] yes! It does!
if (rememberedChar != null && rememberedChar != Constants.DEFAULT_CHARACTER)
{
setupPlayerChill(rememberedChar);
for (pos => charId in availableChars)
{
if (charId == rememberedChar)
{
setCursorPosition(pos);
break;
}
}
@:bypassAccessor curChar = rememberedChar;
}
else
setupPlayerChill(Constants.DEFAULT_CHARACTER);
var speakers:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/charSelectSpeakers"));
speakers.anim.play("");
@ -224,7 +247,7 @@ class CharSelectSubState extends MusicBeatSubState
dipshitBacking.scrollFactor.set();
dipshitBlur.scrollFactor.set();
nametag = new Nametag();
nametag = new Nametag(curChar);
add(nametag);
nametag.scrollFactor.set();
@ -1063,6 +1086,25 @@ class CharSelectSubState extends MusicBeatSubState
return gridPosition;
}
// Moved this code into a function because is now used twice
function setCursorPosition(index:Int)
{
var copy = 3;
var yThing = -1;
while ((index + 1) > copy)
{
yThing++;
copy += 3;
}
var xThing = (copy - index - 2) * -1;
// Look, I'd write better code but I had better aneurysms, my bad - Cheems
cursorY = yThing;
cursorX = xThing;
}
function set_curChar(value:String):String
{
if (curChar == value) return value;
@ -1113,3 +1155,11 @@ class CharSelectSubState extends MusicBeatSubState
return value;
}
}
/**
* Parameters used to initialize the CharSelectSubState.
*/
typedef CharSelectSubStateParams =
{
?character:String, // ?fromFreeplaySelect:Bool,
};

View file

@ -10,14 +10,17 @@ class Nametag extends FlxSprite
var midpointY(default, set):Float = 100;
var mosaicShader:MosaicEffect;
public function new(?x:Float = 0, ?y:Float = 0)
public function new(?x:Float = 0, ?y:Float = 0, character:String)
{
super(x, y);
mosaicShader = new MosaicEffect();
shader = mosaicShader;
switchChar("bf");
// So that's why there was that cursed sight (originally defaulted to bf)
if (character != null) switchChar(character);
else
switchChar(Constants.DEFAULT_CHARACTER);
}
public function updatePosition():Void

View file

@ -30,6 +30,7 @@ import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongOffsets;
import funkin.data.song.SongData.NoteParamData;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongNoteDataUtils;
import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite;
import funkin.input.Cursor;
@ -61,6 +62,7 @@ import funkin.ui.debug.charting.commands.PasteItemsCommand;
import funkin.ui.debug.charting.commands.RemoveEventsCommand;
import funkin.ui.debug.charting.commands.RemoveItemsCommand;
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
import funkin.ui.debug.charting.commands.RemoveStackedNotesCommand;
import funkin.ui.debug.charting.commands.SelectAllItemsCommand;
import funkin.ui.debug.charting.commands.SelectItemsCommand;
import funkin.ui.debug.charting.commands.SetItemSelectionCommand;
@ -88,6 +90,7 @@ import haxe.io.Bytes;
import haxe.io.Path;
import haxe.ui.backend.flixel.UIState;
import haxe.ui.components.Button;
import haxe.ui.components.DropDown;
import haxe.ui.components.Label;
import haxe.ui.components.Slider;
import haxe.ui.containers.dialogs.CollapsibleDialog;
@ -355,6 +358,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
value = 0;
}
// Make sure playhead doesn't scroll outside the song.
if (value + playheadPositionInPixels < 0) playheadPositionInPixels = -value;
if (value + playheadPositionInPixels > songLengthInPixels) playheadPositionInPixels = songLengthInPixels - value;
if (value > songLengthInPixels) value = songLengthInPixels;
if (value == scrollPositionInPixels) return value;
@ -779,6 +786,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var currentLiveInputPlaceNoteData:Array<SongNoteData> = [];
/**
* Defines how "close" two notes must be to be considered stacked, based on steps.
* For example, setting this to `0.5` (16/32) will highlight notes half a step apart.
* Setting it to `0` only highlights notes that are nearly perfectly aligned.
* In the dropdown menu, the threshold is based on note snaps instead.
* For example, `0.5` would be displayed as `1/32`, and `0` would show as `Exact`.
*/
public static var stackedNoteThreshold:Float = 0;
// Note Movement
/**
@ -851,6 +867,32 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return currentNoteSelection;
}
var currentOverlappingNotes(default, set):Array<SongNoteData> = [];
function set_currentOverlappingNotes(value:Array<SongNoteData>):Array<SongNoteData>
{
// This value is true if all elements of the current overlapping array are also in the new array.
var isSuperset:Bool = currentOverlappingNotes.isSubset(value);
var isEqual:Bool = currentOverlappingNotes.isEqualUnordered(value);
currentOverlappingNotes = value;
if (!isEqual)
{
if (currentOverlappingNotes.length > 0 && isSuperset)
{
notePreview.addOverlappingNotes(currentOverlappingNotes, Std.int(songLengthInMs));
}
else
{
// The new array might add or remove elements from the old array, so we have to redraw the note preview.
notePreviewDirty = true;
}
}
return currentOverlappingNotes;
}
/**
* The events which are currently in the user's selection.
*/
@ -1294,6 +1336,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function set_currentSongChartData(value:SongChartData):SongChartData
{
songChartData.set(selectedVariation, value);
var variationMetadata:Null<SongMetadata> = songMetadata.get(selectedVariation);
if (variationMetadata != null)
{
// Add the chartdata difficulties to the metadata difficulties if they don't exist so that the editor properly loads them
var keys:Array<String> = [for (x in songChartData.get(selectedVariation).notes.keys()) x];
for (key in keys)
{
variationMetadata.playData.difficulties.pushUnique(key);
}
}
return value;
}
@ -1742,6 +1794,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var menubarItemDelete:MenuItem;
/**
* The `Edit -> Delete Stacked Notes` menu item.
*/
var menubarItemDeleteStacked:MenuItem;
/**
* The `Edit -> Flip Notes` menu item.
*/
@ -1787,6 +1844,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var menuBarItemNoteSnapIncrease:MenuItem;
/**
* The `Edit -> Stacked Note Threshold` menu dropdown
*/
var menuBarStackedNoteThreshold:DropDown;
/**
* The `View -> Downscroll` menu item.
*/
@ -1978,9 +2040,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/**
* The IMAGE used for the selection squares. Updated by ChartEditorThemeHandler.
* Used two ways:
* Used three ways:
* 1. A sprite is given this bitmap and placed over selected notes.
* 2. The image is split and used for a 9-slice sprite for the selection box.
* 2. Same as above but for notes that are overlapped by another.
* 3. The image is split and used for a 9-slice sprite for the selection box.
*/
var selectionSquareBitmap:Null<BitmapData> = null;
@ -2231,7 +2294,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
else if (params != null && params.targetSongId != null)
{
this.loadSongAsTemplate(params.targetSongId);
var targetSongDifficulty = params.targetSongDifficulty ?? null;
var targetSongVariation = params.targetSongVariation ?? null;
this.loadSongAsTemplate(params.targetSongId, targetSongDifficulty, targetSongVariation);
}
else
{
@ -2286,13 +2351,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
currentTheme = save.chartEditorTheme;
metronomeVolume = save.chartEditorMetronomeVolume;
hitsoundVolumePlayer = save.chartEditorHitsoundVolumePlayer;
hitsoundVolumePlayer = save.chartEditorHitsoundVolumeOpponent;
hitsoundVolumeOpponent = save.chartEditorHitsoundVolumeOpponent;
this.welcomeMusic.active = save.chartEditorThemeMusic;
// audioInstTrack.volume = save.chartEditorInstVolume;
// audioInstTrack.pitch = save.chartEditorPlaybackSpeed;
// audioVocalTrackGroup.volume = save.chartEditorVoicesVolume;
// audioVocalTrackGroup.pitch = save.chartEditorPlaybackSpeed;
menubarItemVolumeInstrumental.value = Std.int(save.chartEditorInstVolume * 100);
menubarItemVolumeVocalsPlayer.value = Std.int(save.chartEditorPlayerVoiceVolume * 100);
menubarItemVolumeVocalsOpponent.value = Std.int(save.chartEditorOpponentVoiceVolume * 100);
menubarItemPlaybackSpeed.value = Std.int(save.chartEditorPlaybackSpeed * 100);
}
public function writePreferences(hasBackup:Bool):Void
@ -2318,9 +2383,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
save.chartEditorHitsoundVolumeOpponent = hitsoundVolumeOpponent;
save.chartEditorThemeMusic = this.welcomeMusic.active;
// save.chartEditorInstVolume = audioInstTrack.volume;
// save.chartEditorVoicesVolume = audioVocalTrackGroup.volume;
// save.chartEditorPlaybackSpeed = audioInstTrack.pitch;
save.chartEditorInstVolume = menubarItemVolumeInstrumental.value / 100.0;
save.chartEditorPlayerVoiceVolume = menubarItemVolumeVocalsPlayer.value / 100.0;
save.chartEditorOpponentVoiceVolume = menubarItemVolumeVocalsOpponent.value / 100.0;
save.chartEditorPlaybackSpeed = menubarItemPlaybackSpeed.value / 100.0;
}
public function populateOpenRecentMenu():Void
@ -2469,7 +2535,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
add(gridPlayhead);
gridPlayhead.zIndex = 30;
var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2);
var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + PLAYHEAD_SCROLL_AREA_WIDTH;
var playheadBaseYPos:Float = GRID_INITIAL_Y_POS;
gridPlayhead.setPosition(GRID_X_POS, playheadBaseYPos);
var playheadSprite:FunkinSprite = new FunkinSprite().makeSolidColor(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR);
@ -2936,6 +3002,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
};
menubarItemDeleteStacked.onClick = _ -> {
if (currentEventSelection.length > 0 && currentNoteSelection.length == 0)
{
performCommand(new RemoveEventsCommand(currentEventSelection));
}
else
{
performCommand(new RemoveStackedNotesCommand(currentNoteSelection.length > 0 ? currentNoteSelection : null));
}
};
menubarItemFlipNotes.onClick = _ -> performCommand(new FlipNotesCommand(currentNoteSelection));
menubarItemSelectAllNotes.onClick = _ -> performCommand(new SelectAllItemsCommand(true, false));
@ -2958,6 +3035,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
};
final REVERSE_SNAPS = SNAP_QUANTS.reversed();
for (snap in REVERSE_SNAPS)
{
menuBarStackedNoteThreshold.dataSource.add({text: '1/$snap'});
}
menuBarStackedNoteThreshold.onChange = event -> {
// NOTE: It needs to be offset by 1 because of the 'Exact' option
// -1 value means that it is the one selected
var selectedIdx:Int = menuBarStackedNoteThreshold.selectedIndex - 1;
stackedNoteThreshold = selectedIdx == -1 ? 0 : BASE_QUANT / REVERSE_SNAPS[selectedIdx];
noteDisplayDirty = true;
notePreviewDirty = true;
}
menuBarItemInputStyleNone.onClick = function(event:UIEvent) {
currentLiveInputStyle = None;
};
@ -2977,7 +3069,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
#if sys
menubarItemGoToBackupsFolder.onClick = _ -> this.openBackupsFolder();
#else
// Disable the menu item if we're not on a desktop platform.
// Disable the menu item if we're not on a native platform.
menubarItemGoToBackupsFolder.disabled = true;
#end
@ -3486,7 +3578,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
// This sprite is off-screen or was deleted.
// Kill the note sprite and recycle it.
noteSprite.noteData = null;
noteSprite.kill();
}
}
// Sort the note data array, using an algorithm that is fast on nearly-sorted data.
@ -3504,7 +3596,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// It will be displayed by gridGhostHoldNoteSprite instead.
holdNoteSprite.kill();
}
else if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD))
else if (!holdNoteSprite.isHoldNoteVisible(viewAreaBottomPixels, viewAreaTopPixels))
{
// This hold note is off-screen.
// Kill the hold note sprite and recycle it.
@ -3528,7 +3620,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Update the event sprite's height and position.
// var holdNoteHeight = holdNoteSprite.noteData.getStepLength() * GRID_SIZE;
// holdNoteSprite.setHeightDirectly(holdNoteHeight);
holdNoteSprite.updateHoldNotePosition(renderedNotes);
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
}
}
// Sort the note data array, using an algorithm that is fast on nearly-sorted data.
@ -3544,7 +3636,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Resolve an issue where dragging an event too far would cause it to be hidden.
var isSelectedAndDragged = currentEventSelection.fastContains(eventSprite.eventData) && (dragTargetCurrentStep != 0);
if ((eventSprite.isEventVisible(FlxG.height - PLAYBAR_HEIGHT, MENU_BAR_HEIGHT)
if ((eventSprite.isEventVisible(viewAreaBottomPixels, viewAreaTopPixels)
&& currentSongChartEventData.fastContains(eventSprite.eventData))
|| isSelectedAndDragged)
{
@ -3560,7 +3652,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
// This event was deleted.
// Kill the event sprite and recycle it.
eventSprite.eventData = null;
eventSprite.kill();
}
}
// Sort the note data array, using an algorithm that is fast on nearly-sorted data.
@ -3617,6 +3709,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var noteLengthPixels:Float = noteSprite.noteData.getStepLength() * GRID_SIZE;
holdNoteSprite.noteData = noteSprite.noteData;
holdNoteSprite.overrideStepTime = null;
holdNoteSprite.overrideData = null;
holdNoteSprite.noteDirection = noteSprite.noteData.getDirection();
holdNoteSprite.setHeightDirectly(noteLengthPixels);
@ -3684,6 +3778,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var noteLengthPixels:Float = noteData.getStepLength() * GRID_SIZE;
holdNoteSprite.noteData = noteData;
holdNoteSprite.overrideStepTime = null;
holdNoteSprite.overrideData = null;
holdNoteSprite.noteDirection = noteData.getDirection();
holdNoteSprite.setHeightDirectly(noteLengthPixels);
@ -3701,13 +3797,32 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
member.kill();
}
// Gather stacked notes to render later
// No need to update it every time we scroll
if (Math.abs(currentScrollEase - scrollPositionInPixels) < .0001)
{
currentOverlappingNotes = SongNoteDataUtils.listStackedNotes(currentSongChartNoteData, stackedNoteThreshold);
}
// Readd selection squares for selected notes.
// Recycle selection squares if possible.
for (noteSprite in renderedNotes.members)
{
if (noteSprite == null || noteSprite.noteData == null || !noteSprite.exists || !noteSprite.visible) continue;
// TODO: Handle selection of hold notes.
if (isNoteSelected(noteSprite.noteData))
{
var holdNoteSprite:ChartEditorHoldNoteSprite = null;
if (noteSprite.noteData != null && noteSprite.noteData.length > 0)
{
for (holdNote in renderedHoldNotes.members)
{
if (holdNote.noteData == noteSprite.noteData && holdNoteSprite == null) holdNoteSprite = holdNote;
}
}
// Determine if the note is being dragged and offset the vertical position accordingly.
if (dragTargetCurrentStep != 0.0)
{
@ -3716,6 +3831,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
noteSprite.overrideStepTime = (stepTime + dragTargetCurrentStep).clamp(0, songLengthInSteps - (1 * noteSnapRatio));
// Then reapply the note sprite's position relative to the grid.
noteSprite.updateNotePosition(renderedNotes);
// We only need to update the position of the hold note tails as we drag the note.
if (holdNoteSprite != null)
{
holdNoteSprite.overrideStepTime = noteSprite.overrideStepTime;
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
}
}
else
{
@ -3725,6 +3847,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
noteSprite.overrideStepTime = null;
// Then reapply the note sprite's position relative to the grid.
noteSprite.updateNotePosition(renderedNotes);
if (holdNoteSprite != null)
{
holdNoteSprite.overrideStepTime = null;
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
}
}
}
@ -3737,6 +3865,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
ChartEditorState.STRUMLINE_SIZE * 2 - 1));
// Then reapply the note sprite's position relative to the grid.
noteSprite.updateNotePosition(renderedNotes);
// We only need to update the position of the hold note tails as we drag the note.
if (holdNoteSprite != null)
{
holdNoteSprite.overrideData = noteSprite.overrideData;
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
}
}
else
{
@ -3746,6 +3881,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
noteSprite.overrideData = null;
// Then reapply the note sprite's position relative to the grid.
noteSprite.updateNotePosition(renderedNotes);
if (holdNoteSprite != null)
{
holdNoteSprite.overrideData = null;
holdNoteSprite.noteDirection = noteSprite.noteData.getDirection();
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
}
}
}
@ -3758,14 +3900,30 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
selectionSquare.x = noteSprite.x;
selectionSquare.y = noteSprite.y;
selectionSquare.width = GRID_SIZE;
selectionSquare.color = FlxColor.WHITE;
var stepLength = noteSprite.noteData.getStepLength();
selectionSquare.height = (stepLength <= 0) ? GRID_SIZE : ((stepLength + 1) * GRID_SIZE);
}
else if (doesNoteStack(noteSprite.noteData, currentOverlappingNotes))
{
// TODO: Maybe use another way to display these notes
var selectionSquare:ChartEditorSelectionSquareSprite = renderedSelectionSquares.recycle(buildSelectionSquare);
// Set the position and size (because we might be recycling one with bad values).
selectionSquare.noteData = noteSprite.noteData;
selectionSquare.eventData = null;
selectionSquare.x = noteSprite.x;
selectionSquare.y = noteSprite.y;
selectionSquare.width = selectionSquare.height = GRID_SIZE;
selectionSquare.color = FlxColor.RED;
}
}
for (eventSprite in renderedEvents.members)
{
if (eventSprite == null || eventSprite.eventData == null || !eventSprite.exists || !eventSprite.visible) continue;
if (isEventSelected(eventSprite.eventData))
{
// Determine if the note is being dragged and offset the position accordingly.
@ -3797,6 +3955,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
selectionSquare.y = eventSprite.y;
selectionSquare.width = eventSprite.width;
selectionSquare.height = eventSprite.height;
selectionSquare.color = FlxColor.WHITE;
}
// Additional cleanup on notes.
@ -4371,7 +4530,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
{
// Control click to select/deselect an individual note.
if (isNoteSelected(highlightedNote.noteData))
if (isNoteSelected(highlightedHoldNote.noteData))
{
performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], []));
}
@ -4663,7 +4822,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
{
if (isNoteSelected(highlightedNote.noteData))
if (isNoteSelected(highlightedHoldNote.noteData))
{
performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], []));
}
@ -5107,7 +5266,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
playbarHeadLayout.y = FlxG.height - 48 - 8;
var songPos:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset;
var songPosMilliseconds:String = Std.string(Math.floor(Math.abs(songPos) % 1000)).lpad('0', 2).substr(0, 2);
var songPosMilliseconds:String = Std.string(Math.floor(Math.abs(songPos) % 1000)).lpad('0', 3).substr(0, 2);
var songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2);
var songPosMinutes:String = Std.string(Math.floor((Math.abs(songPos) / 1000) / 60)).lpad('0', 2);
if (songPos < 0) songPosMinutes = '-' + songPosMinutes;
@ -5501,18 +5660,36 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (delete)
{
// Delete selected items.
if (currentNoteSelection.length > 0 && currentEventSelection.length > 0)
var noteSelection = currentNoteSelection.length > 0;
var eventSelection = currentEventSelection.length > 0;
// Shift to delete stacked notes
if (FlxG.keys.pressed.SHIFT)
{
performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection));
if (eventSelection && !noteSelection)
{
performCommand(new RemoveEventsCommand(currentEventSelection));
}
else
{
performCommand(new RemoveStackedNotesCommand(noteSelection ? currentNoteSelection : null));
}
}
else if (currentNoteSelection.length > 0)
else
{
performCommand(new RemoveNotesCommand(currentNoteSelection));
}
else if (currentEventSelection.length > 0)
{
performCommand(new RemoveEventsCommand(currentEventSelection));
// Delete selected items.
if (noteSelection && eventSelection)
{
performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection));
}
else if (noteSelection)
{
performCommand(new RemoveNotesCommand(currentNoteSelection));
}
else if (eventSelection)
{
performCommand(new RemoveEventsCommand(currentEventSelection));
}
}
}
@ -5673,7 +5850,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var startTimestamp:Float = 0;
if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs;
var playbackRate:Float = ((menubarItemPlaybackSpeed.value ?? 1.0) * 2.0) / 100.0;
var playbackRate:Float = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0;
playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
@ -6144,20 +6321,30 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION);
// Reapply the volume.
var instTargetVolume:Float = menubarItemVolumeInstrumental.value ?? 1.0;
var vocalPlayerTargetVolume:Float = menubarItemVolumeVocalsPlayer.value ?? 1.0;
var vocalOpponentTargetVolume:Float = menubarItemVolumeVocalsOpponent.value ?? 1.0;
// Reapply the volume and playback rate.
var instTargetVolume:Float = (menubarItemVolumeInstrumental.value / 100.0) ?? 1.0;
var vocalPlayerTargetVolume:Float = (menubarItemVolumeVocalsPlayer.value / 100.0) ?? 1.0;
var vocalOpponentTargetVolume:Float = (menubarItemVolumeVocalsOpponent.value / 100.0) ?? 1.0;
var playbackRate = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0;
playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
if (audioInstTrack != null)
{
audioInstTrack.volume = instTargetVolume;
#if FLX_PITCH
audioInstTrack.pitch = playbackRate;
#end
audioInstTrack.onComplete = null;
}
if (audioVocalTrackGroup != null)
{
audioVocalTrackGroup.playerVolume = vocalPlayerTargetVolume;
audioVocalTrackGroup.opponentVolume = vocalOpponentTargetVolume;
#if FLX_PITCH
audioVocalTrackGroup.pitch = playbackRate;
#end
}
}
@ -6188,6 +6375,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// TODO: Only update the notes that have changed.
notePreview.erase();
notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs));
notePreview.addOverlappingNotes(currentOverlappingNotes, Std.int(songLengthInMs));
notePreview.addSelectedNotes(currentNoteSelection, Std.int(songLengthInMs));
notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs));
}
@ -6311,6 +6499,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
public function postLoadInstrumental():Void
{
// Reapply the volume and playback rate.
var instTargetVolume:Float = ((menubarItemVolumeInstrumental.value / 100) ?? 1.0);
var playbackRate:Float = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0;
playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
if (audioInstTrack != null)
{
// Prevent the time from skipping back to 0 when the song ends.
@ -6323,6 +6516,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
audioVocalTrackGroup.pause();
};
audioInstTrack.volume = instTargetVolume;
#if FLX_PITCH
audioInstTrack.pitch = playbackRate;
#end
}
else
{
@ -6339,6 +6536,25 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
healthIconsDirty = true;
}
public function postLoadVocals():Void
{
// Reapply the volume and playback rate.
var vocalPlayerTargetVolume:Float = (menubarItemVolumeVocalsPlayer.value / 100.0) ?? 1.0;
var vocalOpponentTargetVolume:Float = (menubarItemVolumeVocalsOpponent.value / 100.0) ?? 1.0;
var playbackRate:Float = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0;
playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
if (audioVocalTrackGroup != null)
{
audioVocalTrackGroup.playerVolume = vocalPlayerTargetVolume;
audioVocalTrackGroup.opponentVolume = vocalOpponentTargetVolume;
#if FLX_PITCH
audioVocalTrackGroup.pitch = playbackRate;
#end
}
}
function hardRefreshOffsetsToolbox():Void
{
var offsetsToolbox:ChartEditorOffsetsToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT);
@ -6372,6 +6588,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return note != null && currentNoteSelection.indexOf(note) != -1;
}
function doesNoteStack(note:Null<SongNoteData>, curStackedNotes:Array<SongNoteData>):Bool
{
return note != null && curStackedNotes.contains(note);
}
override function destroy():Void
{
super.destroy();
@ -6521,6 +6742,16 @@ typedef ChartEditorParams =
* If non-null, load this song immediately instead of the welcome screen.
*/
var ?targetSongId:String;
/**
* If non-null, load this difficulty immediately instead of the default difficulty.
*/
var ?targetSongDifficulty:String;
/**
* If non-null, load this variation immediately instead of the default variation.
*/
var ?targetSongVariation:String;
};
/**

View file

@ -37,7 +37,6 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true;
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
@ -61,7 +60,6 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true;
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);

View file

@ -4,6 +4,8 @@ import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongDataUtils.SongClipboardItems;
import funkin.data.song.SongNoteDataUtils;
import funkin.ui.debug.charting.ChartEditorState;
/**
* A command which inserts the contents of the clipboard into the chart editor.
@ -13,9 +15,11 @@ import funkin.data.song.SongDataUtils.SongClipboardItems;
class PasteItemsCommand implements ChartEditorCommand
{
var targetTimestamp:Float;
// Notes we added with this command, for undo.
// Notes we added and removed with this command, for undo.
var addedNotes:Array<SongNoteData> = [];
var addedEvents:Array<SongEventData> = [];
var removedNotes:Array<SongNoteData> = [];
var isRedo:Bool = false;
public function new(targetTimestamp:Float)
{
@ -41,7 +45,10 @@ class PasteItemsCommand implements ChartEditorCommand
addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp));
addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff);
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes);
removedNotes.clear();
var mergedNotes:Array<SongNoteData> = SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, addedNotes, removedNotes);
state.currentSongChartNoteData = mergedNotes;
state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents);
state.currentNoteSelection = addedNotes.copy();
state.currentEventSelection = addedEvents.copy();
@ -52,29 +59,49 @@ class PasteItemsCommand implements ChartEditorCommand
state.sortChartData();
state.success('Paste Successful', 'Successfully pasted clipboard contents.');
var title = isRedo ? 'Redone Paste Successfully' : 'Paste Successful';
var msgType = removedNotes.length > 0 ? 'warning' : 'success';
var msg = if (removedNotes.length == 1)
{
'But 1 overlapped note was overwritten.';
}
else if (removedNotes.length > 1)
{
'But ${removedNotes.length} overlapped notes were overwritten.';
}
else if (isRedo)
{
'Successfully placed pasted note(s) back.';
}
else 'Successfully pasted clipboard contents.';
Reflect.callMethod(null, Reflect.field(ChartEditorNotificationHandler, msgType), [state, title, msg]);
isRedo = false;
}
public function undo(state:ChartEditorState):Void
{
state.playSound(Paths.sound('chartingSounds/undo'));
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes);
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes).concat(removedNotes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.performCommand(new SelectItemsCommand(removedNotes.copy()), false);
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
isRedo = true;
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return (addedNotes.length > 0 || addedEvents.length > 0);
return (addedNotes.length > 0 || addedEvents.length > 0 || removedNotes.length > 0);
}
public function toString():String

View file

@ -0,0 +1,82 @@
package funkin.ui.debug.charting.commands;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongNoteDataUtils;
/**
* Deletes the given notes from the current chart in the chart editor if any overlap another.
* Use when ONLY notes are being deleted.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class RemoveStackedNotesCommand implements ChartEditorCommand
{
var notes:Null<Array<SongNoteData>>;
var overlappedNotes:Array<SongNoteData>;
var removedNotes:Array<SongNoteData>;
public function new(?notes:Array<SongNoteData>)
{
this.notes = notes;
this.overlappedNotes = [];
this.removedNotes = [];
}
public function execute(state:ChartEditorState):Void
{
var isSelection:Bool = notes != null;
var notes:Array<SongNoteData> = notes ?? state.currentSongChartNoteData;
if (notes.length == 0) return;
overlappedNotes.clear();
removedNotes = SongNoteDataUtils.listStackedNotes(notes, ChartEditorState.stackedNoteThreshold, false, overlappedNotes);
if (removedNotes.length == 0) return;
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, removedNotes);
state.currentNoteSelection = isSelection ? overlappedNotes.copy() : [];
state.currentEventSelection = [];
state.playSound(Paths.sound('chartingSounds/noteErase'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function undo(state:ChartEditorState):Void
{
if (removedNotes.length == 0) return;
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(removedNotes);
state.currentNoteSelection = overlappedNotes.concat(removedNotes).copy();
state.currentEventSelection = [];
state.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
state.notePreviewDirty = true;
state.sortChartData();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
{
// This command is undoable. Add to the history if we actually performed an action.
return removedNotes.length > 0;
}
public function toString():String
{
if (removedNotes.length == 1 && removedNotes[0] != null)
{
var dir:String = removedNotes[0].getDirectionName();
return 'Remove $dir Stacked Note';
}
return 'Remove ${removedNotes.length} Stacked Notes';
}
}

View file

@ -123,7 +123,6 @@ class ChartEditorEventSprite extends FlxSprite
public function correctAnimationName(name:String):String
{
if (this.animation.exists(name)) return name;
trace('Warning: Invalid animation name "${name}" for song event. Using "${DEFAULT_EVENT}"');
return DEFAULT_EVENT;
}

View file

@ -37,6 +37,30 @@ class ChartEditorHoldNoteSprite extends SustainTrail
return value;
}
public var overrideStepTime(default, set):Null<Float> = null;
function set_overrideStepTime(value:Null<Float>):Null<Float>
{
if (overrideStepTime == value) return overrideStepTime;
overrideStepTime = value;
updateHoldNotePosition();
return overrideStepTime;
}
public var overrideData(default, set):Null<Int> = null;
function set_overrideData(value:Null<Int>):Null<Int>
{
if (overrideData == value) return overrideData;
overrideData = value;
if (overrideData != null) this.noteDirection = overrideData;
updateHoldNoteGraphic();
updateHoldNotePosition();
return overrideData;
}
public function new(parent:ChartEditorState)
{
var noteStyle = NoteStyleRegistry.instance.fetchDefault();
@ -211,7 +235,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail
{
if (this.noteData == null) return;
var cursorColumn:Int = this.noteData.data;
var cursorColumn:Int = (overrideData != null) ? overrideData : this.noteData.data;
if (cursorColumn < 0) cursorColumn = 0;
if (cursorColumn >= (ChartEditorState.STRUMLINE_SIZE * 2 + 1))
@ -232,10 +256,11 @@ class ChartEditorHoldNoteSprite extends SustainTrail
}
this.x = cursorColumn * ChartEditorState.GRID_SIZE;
updateHoldNoteGraphic();
// Notes far in the song will start far down, but the group they belong to will have a high negative offset.
// noteData.getStepTime() returns a calculated value which accounts for BPM changes
var stepTime:Float =
var stepTime:Float = (overrideStepTime != null) ? overrideStepTime :
inline this.noteData.getStepTime();
if (stepTime >= 0)
{

View file

@ -27,6 +27,7 @@ class ChartEditorNotePreview extends FlxSprite
static final RIGHT_COLOR:FlxColor = 0xFFCC1111;
static final EVENT_COLOR:FlxColor = 0xFF111111;
static final SELECTED_COLOR:FlxColor = 0xFFFFFF00;
static final OVERLAPPING_COLOR:FlxColor = 0xFF640000;
var previewHeight:Int;
@ -58,21 +59,22 @@ class ChartEditorNotePreview extends FlxSprite
* @param note The data for the note.
* @param songLengthInMs The total length of the song in milliseconds.
*/
public function addNote(note:SongNoteData, songLengthInMs:Int, ?isSelection:Bool = false):Void
public function addNote(note:SongNoteData, songLengthInMs:Int, previewType:NotePreviewType = None):Void
{
var noteDir:Int = note.getDirection();
var mustHit:Bool = note.getStrumlineIndex() == 0;
drawNote(noteDir, mustHit, Std.int(note.time), songLengthInMs, isSelection);
drawNote(noteDir, mustHit, Std.int(note.time), songLengthInMs, previewType);
}
/**
* Add a song event to the preview.
* @param event The data for the event.
* @param songLengthInMs The total length of the song in milliseconds.
* @param isSelection If current event is selected, which then it's forced to be yellow.
*/
public function addEvent(event:SongEventData, songLengthInMs:Int, ?isSelection:Bool = false):Void
public function addEvent(event:SongEventData, songLengthInMs:Int, isSelection:Bool = false):Void
{
drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection);
drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection ? Selection : None);
}
/**
@ -84,7 +86,7 @@ class ChartEditorNotePreview extends FlxSprite
{
for (note in notes)
{
addNote(note, songLengthInMs, false);
addNote(note, songLengthInMs, None);
}
}
@ -97,7 +99,20 @@ class ChartEditorNotePreview extends FlxSprite
{
for (note in notes)
{
addNote(note, songLengthInMs, true);
addNote(note, songLengthInMs, Selection);
}
}
/**
* Add an array of overlapping notes to the preview.
* @param notes The data for the notes
* @param songLengthInMs The total length of the song in milliseconds.
*/
public function addOverlappingNotes(notes:Array<SongNoteData>, songLengthInMs:Int):Void
{
for (note in notes)
{
addNote(note, songLengthInMs, Overlapping);
}
}
@ -133,9 +148,9 @@ class ChartEditorNotePreview extends FlxSprite
* @param mustHit False if opponent, true if player.
* @param strumTimeInMs Time in milliseconds to strum the note.
* @param songLengthInMs Length of the song in milliseconds.
* @param isSelection If current note is selected note, which then it's forced to be green
* @param previewType If the note should forcibly be colored as selected or overlapping.
*/
public function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int, ?isSelection:Bool = false):Void
public function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int, previewType:NotePreviewType = None):Void
{
var color:FlxColor = switch (dir)
{
@ -148,10 +163,15 @@ class ChartEditorNotePreview extends FlxSprite
var noteHeight:Int = NOTE_HEIGHT;
if (isSelection != null && isSelection)
switch (previewType)
{
color = SELECTED_COLOR;
noteHeight += 1;
case Selection:
color = SELECTED_COLOR;
noteHeight += 1;
case Overlapping:
color = OVERLAPPING_COLOR;
noteHeight += 2;
default:
}
var noteX:Float = NOTE_WIDTH * dir;
@ -178,3 +198,10 @@ class ChartEditorNotePreview extends FlxSprite
FlxSpriteUtil.drawRect(this, noteX, noteY, width, height, color);
}
}
enum NotePreviewType
{
None;
Selection;
Overlapping;
}

View file

@ -22,7 +22,8 @@ class ChartEditorCharacterIconSelectorMenu extends ChartEditorBaseMenu
public var charSelectScroll:ScrollView;
public var charIconName:Label;
var currentCharButton:Button;
var currentCharButton:Null<Button> = null;
var currentCharId:String = '';
public function new(chartEditorState2:ChartEditorState, charType:CharacterType, lockPosition:Bool = false)
{
@ -36,14 +37,16 @@ class ChartEditorCharacterIconSelectorMenu extends ChartEditorBaseMenu
ease: FlxEase.quartOut,
onComplete: function(_) {
// Just focus the button FFS. Idk why, but the scrollbar doesn't update until after the tween finishes with this????
currentCharButton.focus = true;
if (currentCharButton != null) currentCharButton.focus = true;
else
chartEditorState.error('Failure', 'Could not find character of ${currentCharId} in registry (Is the character in the registry?)');
}
});
}
function initialize(charType:CharacterType, lockPosition:Bool)
{
var currentCharId:String = switch (charType)
currentCharId = switch (charType)
{
case BF: chartEditorState.currentSongMetadata.playData.characters.player;
case GF: chartEditorState.currentSongMetadata.playData.characters.girlfriend;

View file

@ -146,6 +146,8 @@ class ChartEditorAudioHandler
result = playVocals(state, DAD, opponentId, instId);
// if (!result) return false;
state.postLoadVocals();
var perfE:Float = TimerUtil.start();
state.hardRefreshOffsetsToolbox();

View file

@ -1168,7 +1168,8 @@ class ChartEditorDialogHandler
var dialogNoteStyle:Null<DropDown> = dialog.findComponent('dialogNoteStyle', DropDown);
if (dialogNoteStyle == null) throw 'Could not locate dialogNoteStyle DropDown in Add Variation dialog';
dialogNoteStyle.value = state.currentSongMetadata.playData.noteStyle;
var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(dialogNoteStyle, state.currentSongMetadata.playData.noteStyle);
dialogNoteStyle.value = startingValueNoteStyle;
var dialogCharacterPlayer:Null<DropDown> = dialog.findComponent('dialogCharacterPlayer', DropDown);
if (dialogCharacterPlayer == null) throw 'Could not locate dialogCharacterPlayer DropDown in Add Variation dialog';
@ -1203,7 +1204,7 @@ class ChartEditorDialogHandler
var pendingVariation:SongMetadata = new SongMetadata(dialogSongName.text, dialogSongArtist.text, dialogVariationName.text.toLowerCase());
pendingVariation.playData.stage = dialogStage.value.id;
pendingVariation.playData.noteStyle = dialogNoteStyle.value;
pendingVariation.playData.noteStyle = dialogNoteStyle.value.id;
pendingVariation.timeChanges[0].bpm = dialogBPM.value;
state.songMetadata.set(pendingVariation.variation, pendingVariation);

View file

@ -1,5 +1,6 @@
package funkin.ui.debug.charting.handlers;
import funkin.data.song.SongNoteDataUtils;
import funkin.util.VersionUtil;
import funkin.util.DateUtil;
import haxe.io.Path;
@ -26,7 +27,7 @@ class ChartEditorImportExportHandler
/**
* Fetch's a song's existing chart and audio and loads it, replacing the current song.
*/
public static function loadSongAsTemplate(state:ChartEditorState, songId:String):Void
public static function loadSongAsTemplate(state:ChartEditorState, songId:String, targetSongDifficulty:String = null, targetSongVariation:String = null):Void
{
trace('===============START');
@ -92,6 +93,14 @@ class ChartEditorImportExportHandler
{
trace('[WARN] Strange quantity of voice paths for difficulty ${difficultyId}: ${voiceList.length}');
}
// Set the difficulty of the song if one was passed in the params, and it isn't the default
if (targetSongDifficulty != null
&& targetSongDifficulty != state.selectedDifficulty
&& targetSongDifficulty == diff.difficulty) state.selectedDifficulty = targetSongDifficulty;
// Set the variation of the song if one was passed in the params, and it isn't the default
if (targetSongVariation != null
&& targetSongVariation != state.selectedVariation
&& targetSongVariation == diff.variation) state.selectedVariation = targetSongVariation;
}
}
@ -103,7 +112,11 @@ class ChartEditorImportExportHandler
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
state.success('Success', 'Loaded song (${rawSongMetadata[0].songName})');
// Actually state the correct variation loaded
for (metadata in rawSongMetadata)
{
if (metadata.variation == state.selectedVariation) state.success('Success', 'Loaded song (${metadata.songName})');
}
trace('===============END');
}
@ -117,12 +130,48 @@ class ChartEditorImportExportHandler
{
state.songMetadata = newSongMetadata;
state.songChartData = newSongChartData;
state.selectedDifficulty = state.availableDifficulties[0];
if (!newSongMetadata.exists(state.selectedVariation))
if (!state.songMetadata.exists(state.selectedVariation))
{
state.selectedVariation = Constants.DEFAULT_VARIATION;
}
// Use the first available difficulty as a fallback if the currently selected one cannot be found.
if (state.availableDifficulties.indexOf(state.selectedDifficulty) < 0) state.selectedDifficulty = state.availableDifficulties[0];
var delay:Float = 0.5;
for (variation => chart in state.songChartData)
{
var metadata:SongMetadata = state.songMetadata[variation] ?? continue;
var stackedNotesCount:Int = 0;
var affectedDiffs:Array<String> = [];
for (diff => notes in chart.notes)
{
if (!metadata.playData.difficulties.contains(diff)) continue;
var count:Int = SongNoteDataUtils.listStackedNotes(notes, 0, false).length;
if (count > 0)
{
affectedDiffs.push(diff.toTitleCase());
stackedNotesCount += count;
}
}
if (stackedNotesCount > 0)
{
// Difficulty names might be out of order due to how maps work
affectedDiffs.sort(SortUtil.defaultsThenAlphabetically.bind(['Easy', 'Normal', 'Hard', 'Erect', 'Nightmare']));
// Delay it so it doesn't overlap other notifications
flixel.util.FlxTimer.wait(delay, () -> {
state.warning('Stacked Notes Detected',
'Found $stackedNotesCount stacked note(s) in \'${variation.toTitleCase()}\' variation, ' +
'on ${affectedDiffs.joinPlural()} difficult${affectedDiffs.length > 1 ? 'ies' : 'y'}.');
});
delay *= 1.5;
}
}
Conductor.instance.forceBPM(null); // Disable the forced BPM.
Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata.
@ -143,6 +192,11 @@ class ChartEditorImportExportHandler
}
state.audioVocalTrackGroup.stop();
state.audioVocalTrackGroup.clear();
// Clear the undo and redo history
state.undoHistory = [];
state.redoHistory = [];
state.commandHistoryDirty = true;
}
/**
@ -295,7 +349,7 @@ class ChartEditorImportExportHandler
public static function getLatestBackupPath():Null<String>
{
#if sys
if (!sys.FileSystem.exists(BACKUPS_PATH)) sys.FileSystem.createDirectory(BACKUPS_PATH);
FileUtil.createDirIfNotExists(BACKUPS_PATH);
var entries:Array<String> = sys.FileSystem.readDirectory(BACKUPS_PATH);
entries.sort(SortUtil.alphabetically);

View file

@ -113,7 +113,10 @@ class ChartEditorToolboxHandler
{
var toolbox:Null<ChartEditorBaseToolbox> = cast state.activeToolboxes.get(id);
if (toolbox == null) return;
if (toolbox == null)
{
toolbox = cast initToolbox(state, id);
}
if (toolbox != null)
{

View file

@ -1,12 +1,24 @@
package funkin.ui.debug.charting.toolboxes;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongRegistry;
import haxe.ui.components.Button;
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
import funkin.data.song.SongData.SongMetadata;
import haxe.ui.components.DropDown;
import haxe.ui.components.HorizontalSlider;
import funkin.util.VersionUtil;
import funkin.util.FileUtil;
import openfl.net.FileReference;
import haxe.ui.containers.dialogs.MessageBox.MessageBoxType;
import funkin.play.song.SongSerializer;
import haxe.ui.components.Label;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
import haxe.ui.components.TextField;
import funkin.play.stage.Stage;
import haxe.ui.containers.Box;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
import haxe.ui.events.UIEvent;
@ -81,26 +93,96 @@ class ChartEditorDifficultyToolbox extends ChartEditorBaseToolbox
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-metadata.json', chartEditorState.currentSongMetadata.serialize());
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-metadata.json', chartEditorState.currentSongMetadata.serialize(),
function(notification:String) {
switch (notification)
{
case "success":
chartEditorState.success("Saved Metadata", 'Successfully wrote file (${chartEditorState.currentSongId}$vari-metadata.json).');
case "info":
chartEditorState.info("Canceled Save Metadata", '(${chartEditorState.currentSongId}$vari-metadata.json)');
case "error":
chartEditorState.error("Failure", 'Failed to write file (${chartEditorState.currentSongId}$vari-metadata.json).');
}
});
};
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-chart.json', chartEditorState.currentSongChartData.serialize());
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-chart.json', chartEditorState.currentSongChartData.serialize(),
function(notification:String) {
switch (notification)
{
case "success":
chartEditorState.success("Saved Chart Data", 'Successfully wrote file (${chartEditorState.currentSongId}$vari-chart.json).');
case "info":
chartEditorState.info("Canceled Save Chart Data", '(${chartEditorState.currentSongId}$vari-chart.json)');
case "error":
chartEditorState.error("Failure", 'Failed to write file (${chartEditorState.currentSongId}$vari-chart.json).');
}
});
};
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
// Replace metadata for current variation.
SongSerializer.importSongMetadataAsync(function(songMetadata) {
chartEditorState.currentSongMetadata = songMetadata;
FileUtil.browseFileReference(function(fileReference:FileReference) {
var data = fileReference.data.toString();
if (data == null) return;
var songMetadataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(data);
var songMetadata:Null<SongMetadata> = null;
if (VersionUtil.validateVersion(songMetadataVersion,
SongRegistry.SONG_METADATA_VERSION_RULE)) songMetadata = SongRegistry.instance.parseEntryMetadataRawWithMigration(data, fileReference.name,
songMetadataVersion);
if (songMetadata != null)
{
chartEditorState.currentSongMetadata = songMetadata;
chartEditorState.healthIconsDirty = true;
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
chartEditorState.success('Replaced Metadata', 'Replaced metadata with file (${fileReference.name})');
}
else
{
chartEditorState.error('Failure', 'Failed to load metadata file (${fileReference.name})');
}
});
};
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
// Replace chart data for current variation.
SongSerializer.importSongChartDataAsync(function(songChartData) {
chartEditorState.currentSongChartData = songChartData;
chartEditorState.noteDisplayDirty = true;
FileUtil.browseFileReference(function(fileReference:FileReference) {
var data = fileReference.data.toString();
if (data == null) return;
var songChartDataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(data);
var songChartData:Null<SongChartData> = null;
if (VersionUtil.validateVersion(songChartDataVersion,
SongRegistry.SONG_CHART_DATA_VERSION_RULE)) songChartData = SongRegistry.instance.parseEntryChartDataRawWithMigration(data, fileReference.name,
songChartDataVersion);
if (songChartData != null)
{
chartEditorState.currentSongChartData = songChartData;
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
updateTree();
refresh();
chartEditorState.success('Loaded Chart Data', 'Loaded chart data file (${fileReference.name})');
if (chartEditorState.currentNoteSelection != []) chartEditorState.currentNoteSelection = [];
if (chartEditorState.currentEventSelection != []) chartEditorState.currentEventSelection = [];
chartEditorState.noteDisplayDirty = true;
chartEditorState.notePreviewDirty = true;
chartEditorState.noteTooltipsDirty = true;
chartEditorState.notePreviewViewportBoundsDirty = true;
}
else
{
chartEditorState.error('Failure', 'Failed to load chart data file (${fileReference.name})');
}
});
};

View file

@ -3,6 +3,8 @@ package funkin.ui.debug.charting.toolboxes;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData;
import funkin.data.stage.StageRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import haxe.ui.components.Button;
@ -112,8 +114,12 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
inputStage.value = startingValueStage;
inputNoteStyle.onChange = function(event:UIEvent) {
if (event.data?.id == null) return;
var valid:Bool = event.data != null && event.data.id != null;
if (valid)
{
chartEditorState.currentSongNoteStyle = event.data.id;
}
};
var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, chartEditorState.currentSongMetadata.playData.noteStyle);
inputNoteStyle.value = startingValueNoteStyle;
@ -122,8 +128,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
if (event.value == null || event.value <= 0) return;
// Use a command so we can undo/redo this action.
var startingBPM = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
if (event.value != startingBPM)
if (event.value != Conductor.instance.bpm)
{
chartEditorState.performCommand(new ChangeStartingBPMCommand(event.value));
}
@ -191,7 +196,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
inputSongArtist.value = chartEditorState.currentSongMetadata.artist;
inputSongCharter.value = chartEditorState.currentSongMetadata.charter;
inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle;
inputNoteStyle.value = chartEditorState.currentSongNoteStyle;
inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
inputDifficultyRating.value = chartEditorState.currentSongChartDifficultyRating;
inputScrollSpeed.value = chartEditorState.currentSongChartScrollSpeed;
@ -199,6 +204,11 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';
frameDifficulty.text = 'Difficulty: ${chartEditorState.selectedDifficulty.toTitleCase()}';
if (chartEditorState.currentSongMetadata.timeChanges[0].bpm != Conductor.instance.bpm)
{
chartEditorState.performCommand(new ChangeStartingBPMCommand(chartEditorState.currentSongMetadata.timeChanges[0].bpm));
}
var currentTimeSignature = '${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum}/${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen}';
trace('Setting time signature to ${currentTimeSignature}');
inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature};
@ -212,6 +222,15 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
{id: "mainStage", text: "Main Stage"};
}
var noteStyleId:String = chartEditorState.currentSongNoteStyle;
var noteStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (inputNoteStyle != null)
{
inputNoteStyle.value = (noteStyle != null) ?
{id: noteStyle.id, text: noteStyle.getName()} :
{id: "Funkin", text: "Funkin'"};
}
var LIMIT = 6;
var charDataOpponent:Null<CharacterData> = CharacterDataParser.fetchCharacterData(chartEditorState.currentSongMetadata.playData.characters.opponent);

View file

@ -179,10 +179,10 @@ class LatencyState extends MusicBeatSubState
strumLine.releaseKey(event.noteDirection);
}
override public function close():Void
override public function destroy():Void
{
cleanup();
super.close();
super.destroy();
}
function cleanup():Void
@ -314,7 +314,7 @@ class LatencyState extends MusicBeatSubState
{
// close();
cleanup();
FlxG.switchState(() -> new MainMenuState());
FlxG.switchState(() -> new funkin.ui.options.OptionsState());
}
super.update(elapsed);

View file

@ -23,12 +23,14 @@ import haxe.ui.containers.menus.MenuBar;
import haxe.ui.containers.menus.MenuOptionBox;
import haxe.ui.containers.menus.MenuCheckBox;
import funkin.util.FileUtil;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler.StageEditorObjectData;
import funkin.ui.debug.stageeditor.handlers.StageDataHandler;
import funkin.ui.debug.stageeditor.handlers.UndoRedoHandler.UndoAction;
import funkin.ui.debug.stageeditor.toolboxes.*;
import funkin.ui.debug.stageeditor.components.*;
import haxe.ui.containers.dialogs.Dialog;
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
import haxe.ui.containers.dialogs.MessageBox.MessageBoxType;
@ -45,6 +47,8 @@ import funkin.audio.FunkinSound;
import haxe.ui.notifications.NotificationType;
import haxe.ui.notifications.NotificationManager;
import funkin.util.logging.CrashHandler;
import funkin.graphics.shaders.Grayscale;
import funkin.data.stage.StageRegistry;
/**
* Da Stage Editor woo!!
@ -110,7 +114,9 @@ class StageEditorState extends UIState
var menubarItemViewCamBounds:MenuCheckBox; // view cam bounds check
var menubarMenuWindow:Menu;
var menubarItemWindowObject:MenuCheckBox;
var menubarItemWindowObjectGraphic:MenuCheckBox;
var menubarItemWindowObjectAnims:MenuCheckBox;
var menubarItemWindowObjectProps:MenuCheckBox;
var menubarItemWindowCharacter:MenuCheckBox;
var menubarItemWindowStage:MenuCheckBox;
@ -133,8 +139,11 @@ class StageEditorState extends UIState
function set_selectedSprite(value:StageEditorObject)
{
selectedSprite?.selectedShader.setAmount(0);
this.selectedSprite = value;
updateDialog(StageEditorDialogType.OBJECT);
updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
updateDialog(StageEditorDialogType.OBJECT_ANIMS);
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
if (selectedSprite != null)
{
@ -251,6 +260,7 @@ class StageEditorState extends UIState
public var bitmaps:Map<String, BitmapData> = []; // used for optimizing the file size!!!
var charDeselectShader:Grayscale = new Grayscale();
var floorLines:Array<FlxSprite> = [];
var posCircles:Array<FlxShapeCircle> = [];
var camFields:FlxTypedGroup<FlxSprite>;
@ -285,6 +295,17 @@ class StageEditorState extends UIState
return value;
}
/**
* The params which were passed in when the Stage Editor was initialized.
*/
var params:Null<StageEditorParams>;
public function new(?params:StageEditorParams)
{
super();
this.params = params;
}
override public function create():Void
{
WindowManager.instance.reset();
@ -304,8 +325,6 @@ class StageEditorState extends UIState
persistentUpdate = false;
// FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height);
bg = FlxGridOverlay.create(10, 10);
bg.scrollFactor.set();
add(bg);
@ -322,12 +341,12 @@ class StageEditorState extends UIState
WindowManager.instance.container = root;
Screen.instance.addComponent(root);
// group shit + assets
var gf = CharacterDataParser.fetchCharacter("gf", true);
// Characters setup.
var gf = CharacterDataParser.fetchCharacter(Save.instance.stageGirlfriendChar, true);
gf.characterType = CharacterType.GF;
var dad = CharacterDataParser.fetchCharacter("dad", true);
var dad = CharacterDataParser.fetchCharacter(Save.instance.stageDadChar, true);
dad.characterType = CharacterType.DAD;
var bf = CharacterDataParser.fetchCharacter("bf", true);
var bf = CharacterDataParser.fetchCharacter(Save.instance.stageBoyfriendChar, true);
bf.characterType = CharacterType.BF;
bf.flipX = !bf.getDataFlipX();
@ -338,17 +357,13 @@ class StageEditorState extends UIState
dad.updateHitbox();
bf.updateHitbox();
// only one char !!!
// Only one character per group allowed.
charGroups = [
CharacterType.BF => new FlxTypedGroup<BaseCharacter>(1),
CharacterType.GF => new FlxTypedGroup<BaseCharacter>(1),
CharacterType.DAD => new FlxTypedGroup<BaseCharacter>(1)
];
// this is the part where the stage generate function comes up
// apparently no, said the future me
// back to the regular program
gf.x = charPos[CharacterType.GF][0] - gf.characterOrigin.x + gf.globalOffsets[0];
gf.y = charPos[CharacterType.GF][1] - gf.characterOrigin.y + gf.globalOffsets[1];
dad.x = charPos[CharacterType.DAD][0] - dad.characterOrigin.x + dad.globalOffsets[0];
@ -366,12 +381,7 @@ class StageEditorState extends UIState
add(charGroups[CharacterType.DAD]);
add(charGroups[CharacterType.BF]);
// ui
// spriteMarker = new FlxSprite().makeGraphic(1, 1, FlxColor.CYAN);
// spriteMarker.alpha = 0.3;
// spriteMarker.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 3; // PLEASE
// add(spriteMarker);
// UI Sprites setup.
camFields = new FlxTypedGroup<FlxSprite>();
camFields.visible = false;
camFields.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 1;
@ -422,6 +432,7 @@ class StageEditorState extends UIState
addUI();
// Some callbacks.
findObjDialog = new FindObjDialog(this, selectedSprite == null ? "" : selectedSprite.name);
FlxG.stage.window.onDropFile.add(function(path:String):Void {
@ -442,29 +453,66 @@ class StageEditorState extends UIState
}
});
onMenuItemClick("new stage");
welcomeDialog.closable = false;
#if sys
if (Save.instance.stageEditorHasBackup)
if (params?.targetStageId != null && StageRegistry.instance.hasEntry(params?.targetStageId))
{
FileUtil.createDirIfNotExists(BACKUPS_PATH);
var stageData = StageRegistry.instance.parseEntryDataWithMigration(params.targetStageId, StageRegistry.instance.fetchEntryVersion(params.targetStageId));
var files = sys.FileSystem.readDirectory(BACKUPS_PATH);
if (files.length > 0)
if (stageData != null)
{
// ensures that the top most file is a backup
files.sort(funkin.util.SortUtil.alphabetically);
while (!files[files.length - 1].endsWith(FileUtil.FILE_EXTENSION_INFO_FNFS.extension)
|| !files[files.length - 1].startsWith("stage-editor-"))
files.pop();
// Load the stage data.
currentFile = "";
this.loadFromDataRaw(stageData);
}
else
{
// Notify the error and create a new stage.
notifyChange("Problem Loading the Stage", "The Stage File could not be loaded.", true);
onMenuItemClick("new stage");
}
if (files.length != 0) new BackupAvailableDialog(this, haxe.io.Path.join([BACKUPS_PATH, files[files.length - 1]])).showDialog(true);
}
#end
else if (params?.fnfsTargetPath != null)
{
var bytes = FileUtil.readBytesFromPath(params.fnfsTargetPath);
if (bytes != null)
{
// Open the stage file.
currentFile = params.fnfsTargetPath;
this.unpackShitFromZip(bytes);
}
else
{
// Notify the error and create a new stage.
notifyChange("Problem Loading the Stage", "The Stage File could not be loaded.", true);
onMenuItemClick("new stage");
}
}
else
{
onMenuItemClick("new stage");
welcomeDialog.closable = false;
#if sys
if (Save.instance.stageEditorHasBackup)
{
FileUtil.createDirIfNotExists(BACKUPS_PATH);
var files = sys.FileSystem.readDirectory(BACKUPS_PATH);
if (files.length > 0)
{
// ensures that the top most file is a backup
files.sort(funkin.util.SortUtil.alphabetically);
while (!files[files.length - 1].endsWith(FileUtil.FILE_EXTENSION_INFO_FNFS.extension)
|| !files[files.length - 1].startsWith("stage-editor-"))
files.pop();
}
if (files.length != 0) new BackupAvailableDialog(this, haxe.io.Path.join([BACKUPS_PATH, files[files.length - 1]])).showDialog(true);
}
#end
}
WindowUtil.windowExit.add(windowClose);
CrashHandler.errorSignal.add(autosavePerCrash);
@ -473,7 +521,10 @@ class StageEditorState extends UIState
Save.instance.stageEditorHasBackup = false;
Cursor.show();
FlxG.sound.playMusic(Paths.music('chartEditorLoop/chartEditorLoop'));
FunkinSound.playMusic('chartEditorLoop',
{
startingVolume: 0.0
});
FlxG.sound.music.fadeIn(10, 0, 1);
}
@ -502,6 +553,22 @@ class StageEditorState extends UIState
override public function update(elapsed:Float):Void
{
// Save the stage if exiting through the F4 keybind, as it moves you to the Main Menu.
if (FlxG.keys.justPressed.F4)
{
@:privateAccess
if (!autoSaveTimer.finished) autoSaveTimer.onLoopFinished();
resetWindowTitle();
WindowUtil.windowExit.remove(windowClose);
CrashHandler.errorSignal.remove(autosavePerCrash);
CrashHandler.criticalErrorSignal.remove(autosavePerCrash);
Cursor.hide();
FlxG.sound.music.stop();
return;
}
updateBGSize();
conductorInUse.update();
@ -517,7 +584,7 @@ class StageEditorState extends UIState
if (testingMode)
{
for (char in getCharacters())
char.alpha = 1;
char.shader = null;
// spriteMarker.visible = camMarker.visible = false;
findObjDialog.hideDialog(DialogButton.CANCEL);
@ -571,13 +638,15 @@ class StageEditorState extends UIState
if (FlxG.keys.justPressed.S) FlxG.keys.pressed.SHIFT ? onMenuItemClick("save stage as") : onMenuItemClick("save stage");
if (FlxG.keys.justPressed.F) onMenuItemClick("find object");
if (FlxG.keys.justPressed.O) onMenuItemClick("open stage");
if (FlxG.keys.justPressed.N) onMenuItemClick("new stage");
if (FlxG.keys.justPressed.Q) onMenuItemClick("exit");
}
if (FlxG.keys.justPressed.TAB) onMenuItemClick("switch mode");
if (FlxG.keys.justPressed.DELETE) onMenuItemClick("delete object");
if (FlxG.keys.justPressed.ENTER) onMenuItemClick("test stage");
if (FlxG.keys.justPressed.ESCAPE) onMenuItemClick("exit");
if (FlxG.keys.justPressed.F1) onMenuItemClick("user guide");
if (FlxG.keys.justPressed.F1 && welcomeDialog == null && userGuideDialog == null) onMenuItemClick("user guide");
if (FlxG.keys.justPressed.T)
{
@ -612,19 +681,20 @@ class StageEditorState extends UIState
if (moveMode == "assets")
{
if (selectedSprite != null && !FlxG.mouse.overlaps(selectedSprite) && FlxG.mouse.justPressed && !isCursorOverHaxeUI)
{
selectedSprite = null;
}
for (spr in spriteArray)
{
spr.active = spr.isOnScreen();
if (spr.pixelsOverlapPoint(FlxG.mouse.getWorldPosition()))
if (FlxG.mouse.overlaps(spr))
{
if (spr.visible && !FlxG.keys.pressed.SHIFT) nameTxt.text = spr.name;
if (FlxG.mouse.justPressed && allowInput && spr.visible && !FlxG.keys.pressed.SHIFT && !isCursorOverHaxeUI)
{
selectedSprite.selectedShader.setAmount(0);
selectedSprite = spr;
updateDialog(StageEditorDialogType.OBJECT);
}
}
@ -639,7 +709,7 @@ class StageEditorState extends UIState
if (FlxG.mouse.pressed && allowInput && selectedSprite != null && FlxG.mouse.overlaps(selectedSprite) && FlxG.mouse.justMoved && !isCursorOverHaxeUI)
{
saved = false;
updateDialog(StageEditorDialogType.OBJECT);
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
if (moveOffset.length == 0)
{
@ -671,15 +741,15 @@ class StageEditorState extends UIState
arrowMovement(selectedSprite);
for (char in getCharacters())
char.alpha = 1;
char.shader = null;
}
else
{
selectedChar.alpha = 1;
selectedChar.shader = null;
for (char in getCharacters())
{
if (char != selectedChar) char.alpha = 0.3;
if (char != selectedChar) char.shader = charDeselectShader;
if (char != null && checkCharOverlaps(char)) // flxg.mouse.overlaps crashes the game
{
@ -732,12 +802,10 @@ class StageEditorState extends UIState
nameTxt.x = FlxG.mouse.getScreenPosition(camHUD).x;
nameTxt.y = FlxG.mouse.getScreenPosition(camHUD).y - nameTxt.height;
// spriteMarker.visible = (moveMode == "assets" && selectedSprite != null);
camMarker.visible = moveMode == "chars";
// if (selectedSprite != null) spriteMarker.setPosition(selectedSprite.x, selectedSprite.y);
// for (item in sprDependant)
// item.disabled = !spriteMarker.visible;
for (item in sprDependant)
item.disabled = (moveMode != "assets" || selectedSprite == null);
menubarItemPaste.disabled = copiedSprite == null;
menubarItemFindObj.disabled = !(moveMode == "assets");
@ -755,22 +823,22 @@ class StageEditorState extends UIState
function autosavePerCrash(message:String)
{
trace("fuuuucckkkkk we crashed, reason: " + message);
trace("Crashed the game for the reason: " + message);
if (!saved)
{
trace("dw we're making a backup!!!");
trace("You haven't saved recently, so a backup will be made.");
autoSaveTimer.onComplete(autoSaveTimer);
}
}
function windowClose(exitCode:Int)
{
trace("closing da window ye");
trace("Closing the game window.");
if (!saved)
{
trace("dum dum why no save >:[");
trace("You haven't saved recently, so a backup will be made.");
autoSaveTimer.onComplete(autoSaveTimer);
}
}
@ -873,6 +941,7 @@ class StageEditorState extends UIState
public function updateArray()
{
sortAssets();
spriteArray = [];
for (thing in members)
@ -881,7 +950,6 @@ class StageEditorState extends UIState
}
findObjDialog.updateIndicator();
updateDialog(StageEditorDialogType.OBJECT);
}
public function sortAssets()
@ -967,7 +1035,7 @@ class StageEditorState extends UIState
menubarItemRedo.onClick = function(_) onMenuItemClick("redo");
menubarItemCopy.onClick = function(_) onMenuItemClick("copy object");
menubarItemCut.onClick = function(_) onMenuItemClick("cut object");
menubarItemPaste.onClick = function(_) onMenuItemClick("paste stage");
menubarItemPaste.onClick = function(_) onMenuItemClick("paste object");
menubarItemDelete.onClick = function(_) onMenuItemClick("delete object");
menubarItemNewObj.onClick = function(_) onMenuItemClick("new object");
menubarItemFindObj.onClick = function(_) onMenuItemClick("find object");
@ -993,7 +1061,7 @@ class StageEditorState extends UIState
var shit = Std.parseInt(StringTools.replace(bottomBarMoveStepText.text, "px", ""));
moveStep = shit;
updateDialog(StageEditorDialogType.OBJECT);
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
updateDialog(StageEditorDialogType.CHARACTER);
updateDialog(StageEditorDialogType.STAGE);
}
@ -1016,7 +1084,7 @@ class StageEditorState extends UIState
Save.instance.stageEditorAngleStep = angleOptions[id];
bottomBarAngleStepText.text = (angleOptions.contains(Save.instance.stageEditorAngleStep) ? Save.instance.stageEditorAngleStep : 5) + "°";
updateDialog(StageEditorDialogType.OBJECT);
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
}
bottomBarAngleStepText.onClick = function(_) changeAngle(1);
@ -1024,11 +1092,15 @@ class StageEditorState extends UIState
changeAngle(); // update
dialogs.set(StageEditorDialogType.OBJECT, new StageEditorObjectToolbox(this));
dialogs.set(StageEditorDialogType.OBJECT_GRAPHIC, new StageEditorObjectGraphicToolbox(this));
dialogs.set(StageEditorDialogType.OBJECT_ANIMS, new StageEditorObjectAnimsToolbox(this));
dialogs.set(StageEditorDialogType.OBJECT_PROPERTIES, new StageEditorObjectPropertiesToolbox(this));
dialogs.set(StageEditorDialogType.CHARACTER, new StageEditorCharacterToolbox(this));
dialogs.set(StageEditorDialogType.STAGE, new StageEditorStageToolbox(this));
menubarItemWindowObject.onChange = function(_) toggleDialog(StageEditorDialogType.OBJECT, menubarItemWindowObject.selected);
menubarItemWindowObjectGraphic.onChange = function(_) toggleDialog(StageEditorDialogType.OBJECT_GRAPHIC, menubarItemWindowObjectGraphic.selected);
menubarItemWindowObjectAnims.onChange = function(_) toggleDialog(StageEditorDialogType.OBJECT_ANIMS, menubarItemWindowObjectAnims.selected);
menubarItemWindowObjectProps.onChange = function(_) toggleDialog(StageEditorDialogType.OBJECT_PROPERTIES, menubarItemWindowObjectProps.selected);
menubarItemWindowCharacter.onChange = function(_) toggleDialog(StageEditorDialogType.CHARACTER, menubarItemWindowCharacter.selected);
menubarItemWindowStage.onChange = function(_) toggleDialog(StageEditorDialogType.STAGE, menubarItemWindowStage.selected);
@ -1111,6 +1183,7 @@ class StageEditorState extends UIState
public var userGuideDialog:UserGuideDialog;
public var aboutDialog:AboutDialog;
public var loadUrlDialog:LoadFromUrlDialog;
public var exitConfirmDialog:Dialog;
public function onMenuItemClick(item:String):Void
{
@ -1133,8 +1206,6 @@ class StageEditorState extends UIState
currentFile = path;
}, null, stageName + "." + FileUtil.FILE_EXTENSION_INFO_FNFS.extension);
bitmaps.clear();
case "save stage":
if (currentFile == "")
{
@ -1154,8 +1225,7 @@ class StageEditorState extends UIState
saved = true;
updateRecentFiles();
bitmaps.clear();
reloadRecentFiles();
case "open stage":
if (!saved)
@ -1172,26 +1242,35 @@ class StageEditorState extends UIState
return;
}
FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], function(path:String) {
FileUtil.browseForBinaryFile("Open Stage Data", [FileUtil.FILE_EXTENSION_INFO_FNFS], function(_) {
if (_?.fullPath == null) return;
clearAssets();
currentFile = path;
this.unpackShitFromZip(FileUtil.readBytesFromPath(path));
currentFile = _.fullPath;
this.unpackShitFromZip(FileUtil.readBytesFromPath(currentFile));
reloadRecentFiles();
}, null, null, "Open Stage Data");
}, function() {
// This function does nothing, it's there for crash prevention.
});
case "exit":
if (!saved)
{
Dialogs.messageBox("You are about to leave the Editor without Saving.\n\nAre you sure? ", "Leave Editor", MessageBoxType.TYPE_YESNO, true,
if (exitConfirmDialog == null)
{
exitConfirmDialog = Dialogs.messageBox("You are about to leave the Editor without Saving.\n\nAre you sure? ", "Leave Editor",
MessageBoxType.TYPE_YESNO, true,
function(btn:DialogButton) {
exitConfirmDialog = null;
if (btn == DialogButton.YES)
{
saved = true;
onMenuItemClick("exit");
}
});
}
return;
}
@ -1203,11 +1282,14 @@ class StageEditorState extends UIState
CrashHandler.criticalErrorSignal.remove(autosavePerCrash);
Cursor.hide();
FlxG.switchState(() -> new DebugMenuSubState());
FlxG.switchState(() -> new MainMenuState());
FlxG.sound.music.stop();
case "switch mode":
if (!testingMode) moveMode = (moveMode == "assets" ? "chars" : "assets");
if (testingMode) return;
moveMode = (moveMode == "assets" ? "chars" : "assets");
selectedSprite?.selectedShader.setAmount((moveMode == "assets" ? 1 : 0));
case "switch focus":
if (testingMode)
@ -1264,6 +1346,10 @@ class StageEditorState extends UIState
userGuideDialog = new UserGuideDialog();
userGuideDialog.showDialog();
userGuideDialog.onDialogClosed = function(_) {
userGuideDialog = null;
}
case "open folder":
#if sys
var absoluteBackupsPath:String = haxe.io.Path.join([Sys.getCwd(), BACKUPS_PATH]);
@ -1281,19 +1367,25 @@ class StageEditorState extends UIState
a.isDebugged = testingMode;
}
if (!testingMode) menubarItemWindowObject.selected = menubarItemWindowCharacter.selected = menubarItemWindowStage.selected = false;
if (!testingMode)
{
menubarItemWindowObjectGraphic.selected = menubarItemWindowObjectAnims.selected = menubarItemWindowObjectProps.selected = menubarItemWindowCharacter.selected = menubarItemWindowStage.selected = false;
}
selectedSprite?.selectedShader.setAmount((testingMode ? (moveMode == "assets" ? 1 : 0) : 0));
testingMode = !testingMode;
case "clear assets":
Dialogs.messageBox("This will destroy all the Objects in this Stage.\n\nAre you sure? This cannot be undone.", "Clear Assets",
MessageBoxType.TYPE_YESNO, true, function(btn:DialogButton) {
Dialogs.messageBox("This will destroy all Objects in this Stage.\n\nAre you sure? This cannot be undone.", "Clear Assets", MessageBoxType.TYPE_YESNO,
true, function(btn:DialogButton) {
if (btn == DialogButton.YES)
{
clearAssets();
saved = false;
updateDialog(StageEditorDialogType.OBJECT);
updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
updateDialog(StageEditorDialogType.OBJECT_ANIMS);
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
}
});
@ -1301,7 +1393,7 @@ class StageEditorState extends UIState
if (selectedSprite != null && moveMode == "assets")
{
selectedSprite.screenCenter();
updateDialog(StageEditorDialogType.OBJECT);
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
saved = false;
}
@ -1313,6 +1405,8 @@ class StageEditorState extends UIState
}
case "delete object":
if (selectedSprite == null) return;
this.createAndPushAction(OBJECT_DELETED);
spriteArray.remove(selectedSprite);
@ -1323,7 +1417,6 @@ class StageEditorState extends UIState
selectedSprite = null;
updateArray();
sortAssets();
case "copy object":
if (selectedSprite == null) return;
@ -1347,6 +1440,7 @@ class StageEditorState extends UIState
spr.name += " (" + i + ")";
}
add(spr);
selectedSprite = spr;
updateArray();
@ -1355,7 +1449,9 @@ class StageEditorState extends UIState
onMenuItemClick("delete object"); // already changes the saved var
case "new stage":
if (menubarItemWindowObject.selected) menubarItemWindowObject.selected = false;
if (menubarItemWindowObjectGraphic.selected) menubarItemWindowObjectGraphic.selected = false;
if (menubarItemWindowObjectAnims.selected) menubarItemWindowObjectAnims.selected = false;
if (menubarItemWindowObjectProps.selected) menubarItemWindowObjectProps.selected = false;
if (menubarItemWindowCharacter.selected) menubarItemWindowCharacter.selected = false;
if (menubarItemWindowStage.selected) menubarItemWindowStage.selected = false;
@ -1366,7 +1462,9 @@ class StageEditorState extends UIState
updateWindowTitle();
welcomeDialog = null;
updateDialog(StageEditorDialogType.OBJECT);
updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
updateDialog(StageEditorDialogType.OBJECT_ANIMS);
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
updateDialog(StageEditorDialogType.CHARACTER);
updateDialog(StageEditorDialogType.STAGE);
}
@ -1388,9 +1486,7 @@ class StageEditorState extends UIState
undoArray = [];
redoArray = [];
updateArray();
sortAssets();
removeUnusedBitmaps();
}
@ -1479,7 +1575,30 @@ enum StageEditorDialogType
CHARACTER;
/**
* The Object Options Dialog.
* The Object Graphic Options Dialog.
*/
OBJECT;
OBJECT_GRAPHIC;
/**
* The Object Animations Options Dialog.
*/
OBJECT_ANIMS;
/**
* The Object Properties Options Dialog.
*/
OBJECT_PROPERTIES;
}
typedef StageEditorParams =
{
/**
* If non-null, load this stage immediately instead of the welcome screen.
*/
var ?fnfsTargetPath:String;
/**
* If non-null, load this stage immediately instead of the welcome screen.
*/
var ?targetStageId:String;
};

View file

@ -11,7 +11,7 @@ using StringTools;
@:xml('
<dialog id="backupAvailableDialog" width="475" height="150" title="Hey! Listen!">
<vbox width="100%" height="100%">
<label text="There is a chart backup available, would you like to open it?\n" width="100%" textAlign="center" />
<label text="There is a stage backup available, would you like to open it?\n" width="100%" textAlign="center" />
<spacer height="6" />
<label id="backupTimeLabel" text="Jan 1, 1970 0:00" width="100%" textAlign="center" />
<spacer height="100%" />

View file

@ -62,7 +62,9 @@ class NewObjDialog extends Dialog
spr.name = field.text;
spr.screenCenter();
spr.zIndex = 0;
var sprArray = stageEditorState.spriteArray;
spr.zIndex = sprArray.length == 0 ? 0 : (sprArray[sprArray.length - 1].zIndex + 1);
stageEditorState.selectedSprite = spr;
stageEditorState.createAndPushAction(OBJECT_CREATED);

View file

@ -33,8 +33,6 @@ class WelcomeDialog extends Dialog
for (file in Save.instance.stageEditorPreviousFiles)
{
trace(file);
if (!FileUtil.fileExists(file)) continue; // whats the point of loading something that doesnt exist
var patj = new haxe.io.Path(file);
@ -56,7 +54,7 @@ class WelcomeDialog extends Dialog
boxDrag.onClick = function(_) FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], loadFromFilePath, null, null, "Open Stage Data");
var defaultStages:Array<String> = StageRegistry.instance.listBaseGameEntryIds();
var defaultStages = StageRegistry.instance.listEntryIds();
defaultStages.sort(funkin.util.SortUtil.alphabetically);
for (stage in defaultStages)
@ -132,7 +130,8 @@ class WelcomeDialog extends Dialog
function killDaDialog()
{
stageEditorState.updateDialog(StageEditorDialogType.OBJECT);
stageEditorState.updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
stageEditorState.updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
stageEditorState.updateDialog(StageEditorDialogType.CHARACTER);
stageEditorState.updateDialog(StageEditorDialogType.STAGE);

View file

@ -1,5 +1,6 @@
package funkin.ui.debug.stageeditor.handlers;
import flixel.graphics.frames.FlxAtlasFrames;
import openfl.display.BitmapData;
import flixel.FlxSprite;
import flixel.util.FlxColor;
@ -43,14 +44,17 @@ class AssetDataHandler
startingAnimation: obj.startingAnimation,
animType: "sparrow", // automatically making sparrow atlases yeah
angle: obj.angle,
flipX: obj.flipX,
flipY: obj.flipY,
blend: obj.blend == null ? "" : Std.string(obj.blend),
color: obj.color.toWebString(),
xmlData: obj.generateXML()
animData: ""
}
if (useBitmaps)
{
outputData.bitmap = obj.pixels.clone();
outputData.animData = obj.generateXML();
return outputData;
}
@ -59,6 +63,7 @@ class AssetDataHandler
if (areTheseBitmapsEqual(bit, obj.pixels))
{
outputData.assetPath = name;
outputData.animData = obj.generateXML(name);
return outputData;
}
}
@ -77,14 +82,33 @@ class AssetDataHandler
{
if (data.bitmap != null)
{
var bitToLoad = state.addBitmap(data.bitmap.clone());
object.loadGraphic(state.bitmaps[bitToLoad]);
if (data.animations != null && data.animations.length > 0)
{
var bitToLoad = state.addBitmap(data.bitmap.clone());
object.frames = FlxAtlasFrames.fromSparrow(state.bitmaps[bitToLoad], data.animData);
}
else if (areTheseBitmapsEqual(data.bitmap, getDefaultGraphic()))
{
object.loadGraphic(getDefaultGraphic());
}
else
{
var bitToLoad = state.addBitmap(data.bitmap.clone());
object.loadGraphic(state.bitmaps[bitToLoad]);
}
}
else
{
if (data.animations != null && data.animations.length > 0) // considering we're unpacking we might as well just do this instead of switch
{
object.frames = flixel.graphics.frames.FlxAtlasFrames.fromSparrow(state.bitmaps[data.assetPath].clone(), data.xmlData);
if (data.animData.contains("</TextureAtlas>"))
{
object.frames = FlxAtlasFrames.fromSparrow(state.bitmaps[data.assetPath].clone(), data.animData);
}
else
{
object.frames = FlxAtlasFrames.fromSpriteSheetPacker(state.bitmaps[data.assetPath].clone(), data.animData);
}
}
else if (data.assetPath.startsWith("#"))
{
@ -107,10 +131,6 @@ class AssetDataHandler
object.blend = blendFromString(data.blend);
if (!data.assetPath.startsWith("#")) object.color = FlxColor.fromString(data.color);
// yeah
object.pixelPerfectRender = data.isPixel;
object.pixelPerfectPosition = data.isPixel;
for (anim in data.animations)
{
object.addAnim(anim.name, anim.prefix, anim.offsets ?? [0, 0], anim.frameIndices ?? [], anim.frameRate ?? 24, anim.looped ?? false, anim.flipX ?? false,
@ -132,7 +152,7 @@ class AssetDataHandler
object.playAnim(object.startingAnimation);
flixel.util.FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() {
if (object != null && object.animation.curAnim != null) object.animation.stop();
if (object?.animation?.curAnim != null) object.animation.stop();
});
return object;
@ -159,15 +179,15 @@ class AssetDataHandler
return BlendMode.fromString(blend.toLowerCase().trim());
}
public static function generateXML(obj:StageEditorObject)
public static function generateXML(obj:StageEditorObject, bitmapName:String = "")
{
// the last check is for if the only frame is the standard graphic frame
if (obj == null || obj.frames.frames.length == 0 || obj.frames.frames[0].name == null) return "";
var xml = [
"<!--This XML File was automatically generated by StageEditorEngine, in order to make Funkin' be able to load it.-->",
"<!--This XML File was automatically generated by the Stage Editor.-->",
'<?xml version="1.0" encoding="UTF-8"?>',
'<TextureAtlas imagePath="${obj.toData(false).assetPath}.png" width="${obj.pixels.width}" height="${obj.pixels.height}">'
'<TextureAtlas imagePath="${haxe.io.Path.withoutDirectory(bitmapName)}.png" width="${obj.pixels.width}" height="${obj.pixels.height}">'
].join("\n");
for (daFrame in obj.frames.frames)
@ -184,11 +204,14 @@ class AssetDataHandler
{
if (bitmap1.width != bitmap2.width || bitmap1.height != bitmap2.height) return false;
for (px in 0...bitmap1.width)
var bytes1 = bitmap1.image.data;
var bytes2 = bitmap2.image.data;
for (i in 0...bytes1.length)
{
for (py in 0...bitmap1.height)
if (bytes1[i] != bytes2[i])
{
if (bitmap1.getPixel32(px, py) != bitmap2.getPixel32(px, py)) return false;
return false;
}
}
@ -199,6 +222,6 @@ class AssetDataHandler
typedef StageEditorObjectData =
{
> StageDataProp,
var xmlData:String;
var animData:String;
var ?bitmap:BitmapData;
}

View file

@ -47,12 +47,14 @@ class StageDataHandler
animations: data.animations,
startingAnimation: data.startingAnimation,
animType: data.animType,
flipX: data.flipX,
flipY: data.flipY,
angle: data.angle,
blend: data.blend,
color: data.assetPath.startsWith("#") ? "#FFFFFF" : data.color
});
if (!xmlMap.exists(data.assetPath) && data.xmlData != "") xmlMap.set(data.assetPath, data.xmlData);
if (!xmlMap.exists(data.assetPath) && data.animData != "") xmlMap.set(data.assetPath, data.animData);
}
// step 1 phase 2: character data
@ -68,6 +70,18 @@ class StageDataHandler
endData.characters.gf.cameraOffsets = state.charCamOffsets[CharacterType.GF].copy();
endData.characters.dad.cameraOffsets = state.charCamOffsets[CharacterType.DAD].copy();
endData.characters.bf.alpha = state.bf.alpha;
endData.characters.gf.alpha = state.gf.alpha;
endData.characters.dad.alpha = state.dad.alpha;
endData.characters.bf.angle = state.bf.angle;
endData.characters.gf.angle = state.gf.angle;
endData.characters.dad.angle = state.dad.angle;
endData.characters.bf.scroll = [state.bf.scrollFactor.x, state.bf.scrollFactor.y];
endData.characters.gf.scroll = [state.gf.scrollFactor.x, state.gf.scrollFactor.y];
endData.characters.dad.scroll = [state.dad.scrollFactor.x, state.dad.scrollFactor.y];
endData.characters.bf.position = [
state.bf.feetPosition.x - state.bf.globalOffsets[0],
state.bf.feetPosition.y - state.bf.globalOffsets[1]
@ -108,15 +122,13 @@ class StageDataHandler
}
// step 2 phase 2: xmls
for (obj in endData.props)
for (path => xml in xmlMap)
{
if (!xmlMap.exists(obj.assetPath)) continue; // damn
var bytes = Bytes.ofString(xmlMap[obj.assetPath]);
var bytes = Bytes.ofString(xml);
var entry:Entry =
{
fileName: obj.assetPath + ".xml",
fileName: path + ".xml",
fileSize: bytes.length,
fileTime: Date.now(),
compressed: false,
@ -208,8 +220,10 @@ class StageDataHandler
scroll: objData.scroll.copy(),
color: objData.color,
blend: objData.blend,
flipX: objData.flipX,
flipY: objData.flipY,
startingAnimation: objData.startingAnimation,
xmlData: xmls[objData.assetPath] ?? ""
animData: xmls[objData.assetPath] ?? ""
});
state.add(spr);
@ -250,6 +264,10 @@ class StageDataHandler
char.cameraFocusPoint.x += charData.cameraOffsets[0];
char.cameraFocusPoint.y += charData.cameraOffsets[1];
char.alpha = charData.alpha;
char.angle = charData.angle;
char.scrollFactor.set(charData.scroll[0], charData.scroll[1]);
state.charCamOffsets[char.characterType] = charData.cameraOffsets.copy();
}
}
@ -292,6 +310,10 @@ class StageDataHandler
var spr = new StageEditorObject();
if (!objData.assetPath.startsWith("#")) state.bitmaps.set(objData.assetPath, Assets.getBitmapData(Paths.image(objData.assetPath)));
var usePacker:Bool = objData.animType == "packer";
var animPath:String = Paths.file("images/" + objData.assetPath + (usePacker ? ".txt" : ".xml"));
var animText:String = Assets.exists(animPath) ? Assets.getText(animPath) : "";
spr.fromData(
{
name: objData.name ?? "Unnamed",
@ -307,15 +329,16 @@ class StageDataHandler
scroll: objData.scroll.copy(),
color: objData.color,
blend: objData.blend,
flipX: objData.flipX,
flipY: objData.flipY,
startingAnimation: objData.startingAnimation,
xmlData: Assets.exists(Paths.file("images/" + objData.assetPath + ".xml")) ? Assets.getText(Paths.file("images/" + objData.assetPath + ".xml")) : ""
animData: animText
});
state.add(spr);
}
state.updateArray();
state.sortAssets();
state.updateMarkerPos();
}

View file

@ -46,7 +46,7 @@ class UndoRedoHandler
state.selectedSprite.x = pos[0];
state.selectedSprite.y = pos[1];
state.updateDialog(StageEditorDialogType.OBJECT);
state.updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
}
case OBJECT_CREATED: // this removes the object
@ -66,7 +66,9 @@ class UndoRedoHandler
obj.destroy();
state.updateArray();
state.updateDialog(StageEditorDialogType.OBJECT);
state.updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
state.updateDialog(StageEditorDialogType.OBJECT_ANIMS);
state.updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
trace("found object");
continue;
@ -86,7 +88,9 @@ class UndoRedoHandler
createAndPushAction(state, OBJECT_CREATED, !redo);
state.add(obj);
state.updateDialog(StageEditorDialogType.OBJECT);
state.updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
state.updateDialog(StageEditorDialogType.OBJECT_ANIMS);
state.updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
state.updateArray();
case OBJECT_ROTATED: // primarily copied from OBJECT_MOVED
@ -102,7 +106,7 @@ class UndoRedoHandler
{
createAndPushAction(state, actionToDo.type, !redo);
state.selectedSprite.angle = angle;
state.updateDialog(StageEditorDialogType.OBJECT);
state.updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
}
default: // do nothing dumbass

View file

@ -3,7 +3,9 @@ package funkin.ui.debug.stageeditor.toolboxes;
import haxe.ui.components.NumberStepper;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.character.CharacterData;
import funkin.util.SortUtil;
import funkin.save.Save;
import haxe.ui.components.Button;
import haxe.ui.components.Slider;
import haxe.ui.containers.menus.Menu;
@ -11,130 +13,117 @@ import haxe.ui.core.Screen;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
import haxe.ui.containers.Grid;
import funkin.play.character.CharacterData;
import haxe.ui.events.UIEvent;
using StringTools;
@:access(funkin.ui.debug.stageeditor.StageEditorState)
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/character-properties.xml"))
class StageEditorCharacterToolbox extends StageEditorDefaultToolbox
{
var characterPosXStepper:NumberStepper;
var characterPosYStepper:NumberStepper;
var characterPosReset:Button;
public var charPosX:NumberStepper;
public var charPosY:NumberStepper;
public var charZIdx:NumberStepper;
public var charScale:NumberStepper;
public var charCamX:NumberStepper;
public var charCamY:NumberStepper;
public var charAlpha:NumberStepper;
public var charAngle:NumberStepper;
public var charScrollX:NumberStepper;
public var charScrollY:NumberStepper;
var characterZIdxStepper:NumberStepper;
var characterZIdxReset:Button;
var characterCamXStepper:NumberStepper;
var characterCamYStepper:NumberStepper;
var characterCamReset:Button;
var characterScaleSlider:Slider;
var characterScaleReset:Button;
var characterTypeButton:Button;
var charType:Button;
var charMenu:StageEditorCharacterMenu;
override public function new(state:StageEditorState)
{
super(state);
// position
characterPosXStepper.onChange = characterPosYStepper.onChange = function(_) {
// Numeric callbacks.
charPosX.onChange = charPosY.onChange = function(_) {
repositionCharacter();
state.saved = false;
}
characterPosReset.onClick = function(_) {
if (!StageEditorState.DEFAULT_POSITIONS.exists(state.selectedChar.characterType)) return;
var oldPositions = StageEditorState.DEFAULT_POSITIONS[state.selectedChar.characterType];
characterPosXStepper.pos = oldPositions[0];
characterPosYStepper.pos = oldPositions[1];
}
// zidx
characterZIdxStepper.max = StageEditorState.MAX_Z_INDEX;
characterZIdxStepper.onChange = function(_) {
state.charGroups[state.selectedChar.characterType].zIndex = Std.int(characterZIdxStepper.pos);
state.saved = false;
charZIdx.max = StageEditorState.MAX_Z_INDEX;
charZIdx.onChange = function(_) {
state.charGroups[state.selectedChar.characterType].zIndex = Std.int(charZIdx.pos);
state.sortAssets();
}
characterZIdxReset.onClick = function(_) {
var thingies = [CharacterType.GF, CharacterType.DAD, CharacterType.BF];
var thingIdxies = thingies.indexOf(state.selectedChar.characterType);
characterZIdxStepper.pos = (thingIdxies * 100);
}
// camera
characterCamXStepper.onChange = characterCamYStepper.onChange = function(_) {
state.charCamOffsets[state.selectedChar.characterType] = [characterCamXStepper.pos, characterCamYStepper.pos];
charCamX.onChange = charCamY.onChange = function(_) {
state.charCamOffsets[state.selectedChar.characterType] = [charCamX.pos, charCamY.pos];
state.updateMarkerPos();
state.saved = false;
}
characterCamReset.onClick = function(_) characterCamXStepper.pos = characterCamYStepper.pos = 0; // lol
// scale
characterScaleSlider.onChange = function(_) {
state.selectedChar.setScale(state.selectedChar.getBaseScale() * characterScaleSlider.pos);
charScale.onChange = function(_) {
state.selectedChar.setScale(state.selectedChar.getBaseScale() * charScale.pos);
repositionCharacter();
state.saved = false;
}
characterScaleReset.onChange = function(_) characterScaleSlider.pos = 1;
charAlpha.onChange = function(_) {
state.selectedChar.alpha = charAlpha.pos;
}
charAngle.onChange = function(_) {
state.selectedChar.angle = charAngle.pos;
}
charScrollX.onChange = charScrollY.onChange = function(_) {
state.selectedChar.scrollFactor.set(charScrollX.pos, charScrollY.pos);
}
// character button
characterTypeButton.onClick = function(_) {
charType.onClick = function(_) {
charMenu = new StageEditorCharacterMenu(state, this);
Screen.instance.addComponent(charMenu);
}
refresh();
this.onDialogClosed = onClose;
}
function onClose(event:UIEvent)
{
stageEditorState.menubarItemWindowCharacter.selected = false;
}
override public function refresh()
{
var name = stageEditorState.selectedChar.characterType;
var curChar = stageEditorState.selectedChar;
characterPosXStepper.step = characterPosYStepper.step = stageEditorState.moveStep;
characterCamXStepper.step = characterCamYStepper.step = stageEditorState.moveStep;
charPosX.step = charPosY.step = stageEditorState.moveStep;
charCamX.step = charCamY.step = stageEditorState.moveStep;
charAngle.step = funkin.save.Save.instance.stageEditorAngleStep;
if (characterPosXStepper.pos != stageEditorState.charPos[name][0]) characterPosXStepper.pos = stageEditorState.charPos[name][0];
if (characterPosYStepper.pos != stageEditorState.charPos[name][1]) characterPosYStepper.pos = stageEditorState.charPos[name][1];
// Always update the displays, since selectedChar is never null.
if (characterZIdxStepper.pos != stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex)
characterZIdxStepper.pos = stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex;
if (charPosX.pos != stageEditorState.charPos[name][0]) charPosX.pos = stageEditorState.charPos[name][0];
if (charPosY.pos != stageEditorState.charPos[name][1]) charPosY.pos = stageEditorState.charPos[name][1];
if (charZIdx.pos != stageEditorState.charGroups[name].zIndex) charZIdx.pos = stageEditorState.charGroups[name].zIndex;
if (charCamX.pos != stageEditorState.charCamOffsets[name][0]) charCamX.pos = stageEditorState.charCamOffsets[name][0];
if (charCamY.pos != stageEditorState.charCamOffsets[name][1]) charCamY.pos = stageEditorState.charCamOffsets[name][1];
if (charScale.pos != curChar.scale.x / curChar.getBaseScale()) charScale.pos = curChar.scale.x / curChar.getBaseScale();
if (charAlpha.pos != curChar.alpha) charAlpha.pos = curChar.alpha;
if (charAngle.pos != curChar.angle) charAngle.pos = curChar.angle;
if (charScrollX.pos != curChar.scrollFactor.x) charScrollX.pos = curChar.scrollFactor.x;
if (charScrollY.pos != curChar.scrollFactor.y) charScrollY.pos = curChar.scrollFactor.y;
if (characterCamXStepper.pos != stageEditorState.charCamOffsets[name][0]) characterCamXStepper.pos = stageEditorState.charCamOffsets[name][0];
if (characterCamYStepper.pos != stageEditorState.charCamOffsets[name][1]) characterCamYStepper.pos = stageEditorState.charCamOffsets[name][1];
var prevText = charType.text;
var charData = CharacterDataParser.fetchCharacterData(curChar.characterId);
charType.icon = (charData == null ? null : CharacterDataParser.getCharPixelIconAsset(curChar.characterId));
charType.text = (charData == null ? "None" : charData.name.length > 6 ? '${charData.name.substr(0, 6)}.' : '${charData.name}');
if (characterScaleSlider.pos != stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale())
characterScaleSlider.pos = stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale();
var prevText = characterTypeButton.text;
var charData = CharacterDataParser.fetchCharacterData(stageEditorState.selectedChar.characterId);
characterTypeButton.icon = (charData == null ? null : CharacterDataParser.getCharPixelIconAsset(stageEditorState.selectedChar.characterId));
characterTypeButton.text = (charData == null ? "None" : charData.name.length > 6 ? '${charData.name.substr(0, 6)}.' : '${charData.name}');
if (prevText != characterTypeButton.text)
{
Screen.instance.removeComponent(charMenu);
}
if (prevText != charType.text) Screen.instance.removeComponent(charMenu);
}
public function repositionCharacter()
{
stageEditorState.selectedChar.x = characterPosXStepper.pos - stageEditorState.selectedChar.characterOrigin.x
+ stageEditorState.selectedChar.globalOffsets[0];
stageEditorState.selectedChar.y = characterPosYStepper.pos - stageEditorState.selectedChar.characterOrigin.y
+ stageEditorState.selectedChar.globalOffsets[1];
stageEditorState.selectedChar.setScale(stageEditorState.selectedChar.getBaseScale() * characterScaleSlider.pos);
stageEditorState.selectedChar.x = charPosX.pos - stageEditorState.selectedChar.characterOrigin.x + stageEditorState.selectedChar.globalOffsets[0];
stageEditorState.selectedChar.y = charPosY.pos - stageEditorState.selectedChar.characterOrigin.y + stageEditorState.selectedChar.globalOffsets[1];
stageEditorState.selectedChar.setScale(stageEditorState.selectedChar.getBaseScale() * charScale.pos);
stageEditorState.updateMarkerPos();
}
}
@ -170,7 +159,7 @@ class StageEditorCharacterMenu extends Menu // copied from chart editor
{
var charData:CharacterData = CharacterDataParser.fetchCharacterData(charId);
var charButton = new haxe.ui.components.Button();
var charButton = new Button();
charButton.width = 70;
charButton.height = 70;
charButton.padding = 8;
@ -207,15 +196,39 @@ class StageEditorCharacterMenu extends Menu // copied from chart editor
// anyways new character!!!!
var newChar = CharacterDataParser.fetchCharacter(charId, true);
if (newChar == null)
{
state.notifyChange("Switch Character", "Couldn't find character " + charId + ". Switching to default.", true);
newChar = CharacterDataParser.fetchCharacter(Constants.DEFAULT_CHARACTER, true);
}
newChar.characterType = type;
newChar.resetCharacter(true);
newChar.flipX = type == CharacterType.BF ? !newChar.getDataFlipX() : newChar.getDataFlipX();
newChar.alpha = parent.charAlpha.pos;
newChar.angle = parent.charAngle.pos;
newChar.scrollFactor.x = parent.charScrollX.pos;
newChar.scrollFactor.y = parent.charScrollY.pos;
state.selectedChar = newChar;
group.add(newChar);
parent.repositionCharacter();
group.zIndex = Std.int(parent.charZIdx.pos ?? 0);
// Save the selection.
switch (type)
{
case BF:
Save.instance.stageBoyfriendChar = charId;
case GF:
Save.instance.stageGirlfriendChar = charId;
case DAD:
Save.instance.stageDadChar = charId;
default:
// Do nothing.
}
};
charButton.onMouseOver = _ -> {

View file

@ -16,7 +16,7 @@ class StageEditorDefaultToolbox extends CollapsibleDialog
this.stageEditorState = stageEditorState;
closable = false;
closable = true;
modal = true;
destroyOnClose = false;
}

View file

@ -0,0 +1,218 @@
package funkin.ui.debug.stageeditor.toolboxes;
import haxe.ui.components.Button;
import haxe.ui.components.CheckBox;
import haxe.ui.components.DropDown;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.TextField;
import haxe.ui.containers.ListView;
import haxe.ui.data.ArrayDataSource;
import flixel.graphics.frames.FlxFrame;
import haxe.ui.events.UIEvent;
using StringTools;
@:access(funkin.ui.debug.stageeditor.StageEditorState)
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-anims.xml"))
class StageEditorObjectAnimsToolbox extends StageEditorDefaultToolbox
{
var linkedObj:StageEditorObject = null;
var objAnims:DropDown;
var objAnimName:TextField;
var objFrameList:ListView;
var objAnimPrefix:TextField;
var objAnimIndices:TextField;
var objAnimLooped:CheckBox;
var objAnimFlipX:CheckBox;
var objAnimFlipY:CheckBox;
var objAnimStart:CheckBox;
var objAnimFramerate:NumberStepper;
var objAnimOffsetX:NumberStepper;
var objAnimOffsetY:NumberStepper;
var objAnimSave:Button;
var objAnimDelete:Button;
override public function new(state:StageEditorState)
{
super(state);
objFrameList.onChange = function(_) {
if (objFrameList.selectedIndex == -1) return;
objAnimPrefix.text = objFrameList.selectedItem.name;
}
objAnims.onChange = function(_) {
var animData = linkedObj?.animDatas[objAnims.selectedItem?.text ?? ""];
if (linkedObj == null || objAnims.selectedIndex == -1 || animData == null)
{
// Reset everything.
objAnimName.text = objAnimPrefix.text = objAnimIndices.text = "";
objAnimLooped.selected = objAnimFlipX.selected = objAnimFlipY.selected = objAnimStart.selected = false;
objAnimFramerate.pos = 24;
objAnimOffsetX.pos = objAnimOffsetY.pos = 0;
return;
}
// Update the displays.
objAnimName.text = objAnims.selectedItem.text;
objAnimPrefix.text = animData.prefix ?? "";
objAnimIndices.text = (animData.frameIndices?.join(", ") ?? "");
objAnimLooped.selected = animData.looped ?? false;
objAnimFlipX.selected = animData.flipX ?? false;
objAnimFlipY.selected = animData.flipY ?? false;
objAnimFramerate.pos = animData.frameRate ?? 24;
objAnimStart.selected = objAnimName.text == linkedObj.startingAnimation;
objAnimOffsetX.pos = (animData.offsets[0] ?? 0);
objAnimOffsetY.pos = (animData.offsets[1] ?? 0);
}
objAnimSave.onClick = function(_) {
if (linkedObj == null) return;
if ((objAnimName.text ?? "") == "")
{
state.notifyChange("Animation Saving Error", "The Animation Name is missing.", true);
return;
}
if ((objAnimPrefix.text ?? "") == "")
{
state.notifyChange("Animation Saving Error", "The Animation Prefix is missing.", true);
return;
}
addAnimation();
}
objAnimDelete.onClick = function(_) {
if (linkedObj == null || linkedObj.animation.getNameList().length <= 0 || objAnims.selectedIndex < 0) return;
linkedObj.animation.pause();
linkedObj.animation.stop();
linkedObj.animation.curAnim = null;
var daAnim:String = linkedObj.animation.getNameList()[objAnims.selectedIndex];
if (linkedObj.startingAnimation == daAnim) linkedObj.startingAnimation = "";
linkedObj.animation.remove(daAnim);
linkedObj.animDatas.remove(daAnim);
linkedObj.offset.set();
state.notifyChange("Animation Deletion Done", "Animation "
+ objAnims.selectedItem.text
+ " has been removed from the Object "
+ linkedObj.name
+ ".");
updateAnimList();
objAnims.selectedIndex = objAnims.dataSource.size - 1;
}
this.onDialogClosed = onClose;
}
function onClose(event:UIEvent)
{
stageEditorState.menubarItemWindowObjectAnims.selected = false;
}
var previousFrames:Array<String> = [];
var previousAnims:Array<String> = [];
override public function refresh()
{
linkedObj = stageEditorState.selectedSprite;
// If the selected object is null, reset the displays.
if (linkedObj == null)
{
updateFrameList();
updateAnimList();
objAnims.selectedIndex = -1;
return;
}
// Otherwise, update them accordingly.
if (previousFrames != [for (f in linkedObj.frames.frames) f.name]) updateFrameList();
if (previousAnims != linkedObj.animation.getNameList().copy()) updateAnimList();
}
function updateFrameList()
{
previousFrames = [];
objFrameList.dataSource = new ArrayDataSource();
if (linkedObj == null) return;
for (fname in linkedObj.frames.frames)
{
if (fname != null) objFrameList.dataSource.add({name: fname.name});
previousFrames.push(fname.name);
}
}
function updateAnimList()
{
objAnims.dataSource.clear();
previousAnims = [];
if (linkedObj == null) return;
for (aname in linkedObj.animation.getNameList())
{
objAnims.dataSource.add({text: aname});
previousAnims.push(aname);
}
if (previousAnims.contains(linkedObj.startingAnimation)) objAnims.selectedIndex = previousAnims.indexOf(linkedObj.startingAnimation);
}
function addAnimation()
{
if (linkedObj.animation.getNameList().contains(objAnimName.text))
{
linkedObj.animation.remove(objAnimName.text);
}
var indices:Array<Null<Int>> = [];
if ((objAnimIndices.text ?? "") != "")
{
var splitter = objAnimIndices.text.replace(" ", "").split(",");
for (num in splitter)
indices.push(Std.parseInt(num));
}
var shouldDoIndices:Bool = (indices.length > 0 && !indices.contains(null));
linkedObj.addAnim(objAnimName.text, objAnimPrefix.text, [objAnimOffsetX.pos, objAnimOffsetY.pos], (shouldDoIndices ? indices : []),
Std.int(objAnimFramerate.pos), objAnimLooped.selected, objAnimFlipX.selected, objAnimFlipY.selected);
if (linkedObj.animation.getByName(objAnimName.text) == null)
{
stageEditorState.notifyChange("Animation Saving Error", "Could not build Animation by the provided Frames.", true);
return;
}
if (objAnimStart.selected) linkedObj.startingAnimation = objAnimName.text;
linkedObj.playAnim(objAnimName.text);
stageEditorState.notifyChange("Animation Saving Done", "Animation " + objAnimName.text + " has been saved to the Object " + linkedObj.name + ".");
updateAnimList();
// Stop the animation after a certain time.
flixel.util.FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() {
if (linkedObj?.animation?.curAnim != null) linkedObj.animation.stop();
});
}
}

View file

@ -0,0 +1,208 @@
package funkin.ui.debug.stageeditor.toolboxes;
import flixel.graphics.frames.FlxAtlasFrames;
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
import haxe.ui.components.Button;
import haxe.ui.components.Image;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.TextArea;
import haxe.ui.containers.dialogs.Dialogs.FileDialogTypes;
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.ToolkitAssets;
import openfl.display.BitmapData;
import haxe.ui.events.UIEvent;
@:access(funkin.ui.debug.stageeditor.StageEditorState)
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-graphic.xml"))
class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox
{
var linkedObj:StageEditorObject = null;
var objImage:Image;
var objLoad:Button;
var objLoadNet:Button;
var objReset:Button;
var objResetFrames:Button;
var objFrameTxt:TextArea;
var objLoadFrames:Button;
var objSetSparrow:Button;
var objSetPacker:Button;
var objImageWidth:NumberStepper;
var objImageHeight:NumberStepper;
var objSplit:Button;
override public function new(state:StageEditorState)
{
super(state);
// Callback for loading the image from the local hard drive.
objLoad.onClick = function(_) {
if (linkedObj == null) return;
Dialogs.openBinaryFile("Open Image File", FileDialogTypes.IMAGES, function(selectedFile) {
if (selectedFile == null) return;
objImage.resource = null;
ToolkitAssets.instance.imageFromBytes(selectedFile.bytes, function(imageInfo) {
if (imageInfo == null) return;
objImage.resource = imageInfo.data;
linkedObj.frame = imageInfo.data;
// This checks if the same image had already been loaded, so that we don't add it twice.
// Kind of hacky but it is what it is.
var bitToLoad:String = state.addBitmap(linkedObj.updateFramePixels());
linkedObj.loadGraphic(state.bitmaps[bitToLoad]);
linkedObj.updateHitbox();
state.removeUnusedBitmaps();
refresh();
objImageWidth.pos = objImageWidth.max;
objImageHeight.pos = objImageHeight.max;
state.notifyChange("Object Graphic Loaded", "The Image File " + selectedFile.name + " has been loaded.");
});
});
}
// Callback for loading the image from the internet.
objLoadNet.onClick = function(_) {
if (linkedObj == null) return;
state.createURLDialog(function(bytes:lime.utils.Bytes) {
var bitToLoad:String = state.addBitmap(BitmapData.fromBytes(bytes));
linkedObj.loadGraphic(state.bitmaps[bitToLoad]);
linkedObj.updateHitbox();
state.removeUnusedBitmaps();
// Update the image preview.
refresh();
stageEditorState.updateDialog(OBJECT_ANIMS);
});
}
// Callback for resetting the image.
objReset.onClick = function(_) {
if (linkedObj == null) return;
linkedObj.loadGraphic(AssetDataHandler.getDefaultGraphic());
linkedObj.updateHitbox();
// remove unused bitmaps
state.removeUnusedBitmaps();
// Update the image preview.
refresh();
stageEditorState.updateDialog(OBJECT_ANIMS);
}
// Callback for resetting frames.
objResetFrames.onClick = function(_) {
if (linkedObj == null) return;
linkedObj.loadGraphic(linkedObj.graphic);
refresh();
stageEditorState.updateDialog(OBJECT_ANIMS);
}
// Callback for loading the text for the Frame Data.
objLoadFrames.onClick = function(_) {
Dialogs.openTextFile("Open Text File", FileDialogTypes.TEXTS, function(selectedFile) {
if (selectedFile.text == null || (!selectedFile.name.endsWith(".xml") && !selectedFile.name.endsWith(".txt"))) return;
objFrameTxt.text = selectedFile.text;
state.notifyChange("Frame Text Loaded", "The Text File " + selectedFile.name + " has been loaded.");
});
}
// Callback for setting the frames as Sparrow.
objSetSparrow.onClick = function(_) setObjFrames(false);
// Callback for setting the frames as Packer.
objSetPacker.onClick = function(_) setObjFrames(true);
// Callback for splitting the graphic into frames.
objSplit.onClick = function(_) {
if (linkedObj == null) return;
linkedObj.loadGraphic(linkedObj.graphic, true, Std.int(objImageWidth.pos), Std.int(objImageHeight.pos));
linkedObj.updateHitbox();
// Set the names of the frames.
for (i in 0...linkedObj.frames.frames.length)
{
linkedObj.frames.framesHash.set('Frame$i', linkedObj.frames.frames[i]);
linkedObj.frames.frames[i].name = 'Frame$i';
}
// Refresh the display.
refresh();
stageEditorState.updateDialog(OBJECT_ANIMS);
}
this.onDialogClosed = onClose;
}
function onClose(event:UIEvent)
{
stageEditorState.menubarItemWindowObjectGraphic.selected = false;
}
override public function refresh()
{
linkedObj = stageEditorState.selectedSprite;
if (linkedObj == null)
{
// If there is no selected object, reset displays.
objImage.resource = null;
return;
}
// Otherwise, update only the changed fields.
if (objImage.resource != linkedObj.frame) objImage.resource = linkedObj.frame;
if (objImageWidth.max != linkedObj.graphic.width) objImageWidth.max = objImageWidth.pos = linkedObj.graphic.width;
if (objImageHeight.max != linkedObj.graphic.height) objImageHeight.max = objImageHeight.pos = linkedObj.graphic.height;
}
/**
* Set the linked object's frames based on its graphic and loaded text.
* @param usePacker
*/
function setObjFrames(usePacker:Bool)
{
if (linkedObj == null || objFrameTxt.text == null || objFrameTxt.text.length == 0) return;
try
{
if (usePacker)
{
linkedObj.frames = FlxAtlasFrames.fromSpriteSheetPacker(linkedObj.graphic, objFrameTxt.text);
}
else
{
linkedObj.frames = FlxAtlasFrames.fromSparrow(linkedObj.graphic, objFrameTxt.text);
}
}
catch (e)
{
stageEditorState.notifyChange("Frame Setup Error", e.toString(), true);
return;
}
linkedObj.animDatas.clear();
linkedObj.animation.destroyAnimations();
linkedObj.updateHitbox();
refresh();
stageEditorState.notifyChange("Frame Setup Done", "Finished the Frame Setup for the Object " + linkedObj.name + ".");
stageEditorState.updateDialog(OBJECT_ANIMS);
}
}

View file

@ -0,0 +1,295 @@
package funkin.ui.debug.stageeditor.toolboxes;
import haxe.ui.containers.VBox;
import haxe.ui.components.CheckBox;
import haxe.ui.components.DropDown;
import haxe.ui.components.NumberStepper;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.util.Color;
import flixel.util.FlxColor;
import haxe.ui.events.UIEvent;
@:access(funkin.ui.debug.stageeditor.StageEditorState)
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-properties.xml"))
class StageEditorObjectPropertiesToolbox extends StageEditorDefaultToolbox
{
var linkedObj:StageEditorObject = null;
var objPosX:NumberStepper;
var objPosY:NumberStepper;
var objZIdx:NumberStepper;
var objAlpha:NumberStepper;
var objAngle:NumberStepper;
var objScaleX:NumberStepper;
var objScaleY:NumberStepper;
var objScrollX:NumberStepper;
var objScrollY:NumberStepper;
var objDance:NumberStepper;
var objPixel:CheckBox;
var objFlipX:CheckBox;
var objFlipY:CheckBox;
var objBlend:DropDown;
var objTint:DropDown;
override public function new(state:StageEditorState)
{
super(state);
// Initialize the custom DropDown view.
DropDownBuilder.HANDLER_MAP.set("objTint", Type.getClassName(ObjectTintHandler));
// Numeric callbacks.
objPosX.onChange = function(_) {
if (linkedObj != null) linkedObj.x = objPosX.pos;
}
objPosY.onChange = function(_) {
if (linkedObj != null) linkedObj.y = objPosY.pos;
}
objZIdx.max = StageEditorState.MAX_Z_INDEX;
objZIdx.onChange = function(_) {
if (linkedObj != null)
{
linkedObj.zIndex = Std.int(objZIdx.pos);
state.updateArray();
}
}
objAlpha.onChange = function(_) {
if (linkedObj != null) linkedObj.alpha = objAlpha.pos;
}
objAngle.onChange = function(_) {
if (linkedObj != null) linkedObj.angle = objAngle.pos;
}
objScaleX.onChange = function(_) {
if (linkedObj != null)
{
linkedObj.scale.x = objScaleX.pos;
linkedObj.updateHitbox();
}
}
objScaleY.onChange = function(_) {
if (linkedObj != null)
{
linkedObj.scale.y = objScaleY.pos;
linkedObj.updateHitbox();
}
}
objScrollX.onChange = function(_) {
if (linkedObj != null) linkedObj.scrollFactor.x = objScrollX.pos;
}
objScrollY.onChange = function(_) {
if (linkedObj != null) linkedObj.scrollFactor.y = objScrollY.pos;
}
objDance.onChange = function(_) {
if (linkedObj != null) linkedObj.danceEvery = Std.int(objDance.pos);
}
// Boolean callbacks.
objPixel.onChange = function(_) {
if (linkedObj != null) linkedObj.antialiasing = objPixel.selected; // Kind of misleading, but objPixel has the 'Antialiasing' label attached to it!
}
objFlipX.onChange = function(_) {
if (linkedObj != null) linkedObj.flipX = objFlipX.selected;
}
objFlipY.onChange = function(_) {
if (linkedObj != null) linkedObj.flipY = objFlipY.selected;
}
objBlend.onChange = function(_) {
if (linkedObj != null)
{
linkedObj.blend = (objBlend.selectedItem?.text ?? "NONE") == "NONE" ? null : AssetDataHandler.blendFromString(objBlend.selectedItem.text);
}
}
objTint.onChange = function(_) {
if (linkedObj != null)
{
linkedObj.color = FlxColor.fromString(_.value) ?? 0xFFFFFFFF;
}
}
this.onDialogClosed = onClose;
}
function onClose(event:UIEvent)
{
stageEditorState.menubarItemWindowObjectProps.selected = false;
}
override public function refresh()
{
linkedObj = stageEditorState.selectedSprite;
objPosX.step = stageEditorState.moveStep;
objPosY.step = stageEditorState.moveStep;
objAngle.step = funkin.save.Save.instance.stageEditorAngleStep;
if (linkedObj == null)
{
// If there is no selected object, reset displays.
objPosX.pos = 0;
objPosY.pos = 0;
objZIdx.pos = 0;
objAlpha.pos = 1;
objAngle.pos = 0;
objScaleX.pos = 1;
objScaleY.pos = 1;
objScrollX.pos = 1;
objScrollY.pos = 1;
objDance.pos = 0;
objPixel.selected = true;
objFlipX.selected = false;
objFlipY.selected = false;
objBlend.selectedIndex = 0;
objTint.selectedItem = Color.fromString("white");
return;
}
// Otherwise, only update components whose linked object values have been changed.
if (objPosX.pos != linkedObj.x) objPosX.pos = linkedObj.x;
if (objPosY.pos != linkedObj.y) objPosY.pos = linkedObj.y;
if (objZIdx.pos != linkedObj.zIndex) objZIdx.pos = linkedObj.zIndex;
if (objAlpha.pos != linkedObj.alpha) objAlpha.pos = linkedObj.alpha;
if (objAngle.pos != linkedObj.angle) objAngle.pos = linkedObj.angle;
if (objScaleX.pos != linkedObj.scale.x) objScaleX.pos = linkedObj.scale.x;
if (objScaleY.pos != linkedObj.scale.y) objScaleY.pos = linkedObj.scale.y;
if (objScrollX.pos != linkedObj.scrollFactor.x) objScrollX.pos = linkedObj.scrollFactor.x;
if (objScrollY.pos != linkedObj.scrollFactor.y) objScrollY.pos = linkedObj.scrollFactor.y;
if (objDance.pos != linkedObj.danceEvery) objDance.pos = linkedObj.danceEvery;
if (objPixel.selected != linkedObj.antialiasing) objPixel.selected = linkedObj.antialiasing;
if (objFlipX.selected != linkedObj.flipX) objFlipX.selected = linkedObj.flipX;
if (objFlipY.selected != linkedObj.flipY) objFlipY.selected = linkedObj.flipY;
var blendMode:String = Std.string(linkedObj.blend) ?? "NONE";
if (objBlend.selectedItem != blendMode.toUpperCase()) objBlend.selectedItem = blendMode.toUpperCase();
var objColor:Color = Color.fromComponents(linkedObj.color.red, linkedObj.color.green, linkedObj.color.blue, linkedObj.color.alpha);
if (objTint.selectedItem != objColor) objTint.selectedItem = objColor;
}
}
// The following two classes are mostly carbon copies of ColorPickerPopup's handlers, just with an actually functioning onChange callback.
// The code looks like a bit of a mess though.
@:access(haxe.ui.core.Component)
private class ObjectTintHandler extends DropDownHandler
{
private var _view:ObjectTintView = null;
private override function get_component()
{
if (_view == null)
{
_view = new ObjectTintView();
_view.dropdown = _dropdown;
_view.currentColor = _cachedSelectedColor;
_view.onChange = onColorChange;
}
return _view;
}
public override function prepare(_)
{
super.prepare(_);
if (_view != null) _view.currentColor = _cachedSelectedColor;
}
private var _cachedSelectedColor:Null<Color> = null;
private override function get_selectedItem():Dynamic
{
if (_view != null) _cachedSelectedColor = _view.currentColor;
return _cachedSelectedColor;
}
private override function set_selectedItem(value:Dynamic):Dynamic
{
if ((value is String)) _cachedSelectedColor = Color.fromString(value);
else
_cachedSelectedColor = value;
if (_view != null) _view.currentColor = _cachedSelectedColor;
_dropdown.text = _cachedSelectedColor.toHex();
return value;
}
private function onColorChange(e:UIEvent)
{
if (_view != null)
{
_cachedSelectedColor = _view.currentColor;
}
_dropdown.text = _cachedSelectedColor.toHex();
var event = new UIEvent(UIEvent.CHANGE);
event.value = _cachedSelectedColor.toHex();
_dropdown.dispatch(event);
}
}
@:xml('
<vbox style="spacing:0;padding:5px;">
<color-picker id="picker" />
<box id="cancelApplyButtons" style="padding-top: 5px;" width="100%">
<hbox horizontalAlign="right">
<button id="cancelButton" text="Cancel" styleName="text-small" style="padding: 4px 8px;" />
<button id="applyButton" text="Apply" styleName="text-small" style="padding: 4px 8px;" />
</hbox>
</box>
</vbox>
')
private class ObjectTintView extends VBox
{
public var dropdown:DropDown = null;
public var currentColor(get, set):Null<Color>;
private function get_currentColor():Null<Color>
{
return picker.currentColor;
}
private function set_currentColor(value:Null<Color>):Null<Color>
{
picker.currentColor = value;
return value;
}
@:bind(cancelButton, MouseEvent.CLICK)
private function onCancel(_)
{
dropdown.hideDropDown();
}
@:bind(applyButton, MouseEvent.CLICK)
private function onApply(_)
{
dropdown.text = currentColor.toHex();
var event = new UIEvent(UIEvent.CHANGE);
event.value = currentColor.toHex();
dropdown.dispatch(event);
}
}

View file

@ -1,577 +0,0 @@
package funkin.ui.debug.stageeditor.toolboxes;
import haxe.ui.components.HorizontalSlider;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.TextField;
import haxe.ui.components.TextArea;
import haxe.ui.components.Button;
import haxe.ui.components.Image;
import haxe.ui.containers.dialogs.Dialogs.FileDialogTypes;
import haxe.ui.ToolkitAssets;
import haxe.ui.containers.dialogs.Dialogs;
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
import flixel.graphics.frames.FlxAtlasFrames;
import haxe.ui.components.DropDown;
import haxe.ui.containers.ListView;
import haxe.ui.components.CheckBox;
import flixel.util.FlxTimer;
import haxe.ui.data.ArrayDataSource;
import haxe.ui.components.ColorPicker;
import flixel.util.FlxColor;
import haxe.ui.util.Color;
import flixel.graphics.frames.FlxFrame;
import funkin.util.FileUtil;
import openfl.display.BitmapData;
using StringTools;
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-properties.xml"))
class StageEditorObjectToolbox extends StageEditorDefaultToolbox
{
var linkedObject:StageEditorObject = null;
var objectImagePreview:Image;
var objectLoadImageButton:Button;
var objectLoadInternetButton:Button;
var objectDownloadImageButton:Button;
var objectResetImageButton:Button;
var objectZIdxStepper:NumberStepper;
var objectZIdxReset:Button;
var objectPosXStepper:NumberStepper;
var objectPosYStepper:NumberStepper;
var objectPosResetButton:Button;
var objectAlphaSlider:HorizontalSlider;
var objectAlphaResetButton:Button;
var objectAngleSlider:HorizontalSlider;
var objectAngleResetButton:Button;
var objectScaleXStepper:NumberStepper;
var objectScaleYStepper:NumberStepper;
var objectScaleResetButton:Button;
var objectSizeXStepper:NumberStepper;
var objectSizeYStepper:NumberStepper;
var objectSizeResetButton:Button;
var objectScrollXSlider:HorizontalSlider;
var objectScrollYSlider:HorizontalSlider;
var objectScrollResetButton:Button;
var objectFrameText:TextArea;
var objectFrameTextLoad:Button;
var objectFrameTextSparrow:Button;
var objectFrameTextPacker:Button;
var objectFrameImageWidth:NumberStepper;
var objectFrameImageHeight:NumberStepper;
var objectFrameImageSetter:Button;
var objectFrameReset:Button;
var objectAnimDropdown:DropDown;
var objectAnimName:TextField;
var objectAnimFrameList:ListView;
var objectAnimPrefix:TextField;
var objectAnimFrames:TextField;
var objectAnimLooped:CheckBox;
var objectAnimFlipX:CheckBox;
var objectAnimFlipY:CheckBox;
var objectAnimFramerate:NumberStepper;
var objectAnimOffsetX:NumberStepper;
var objectAnimOffsetY:NumberStepper;
var objectAnimDanceBeat:NumberStepper;
var objectAnimDanceBeatReset:Button;
var objectAnimStart:TextField;
var objectAnimStartReset:Button;
var objectMiscAntialias:CheckBox;
var objectMiscAntialiasReset:Button;
var objectMiscFlipReset:Button;
var objectMiscBlendDrop:DropDown;
var objectMiscBlendReset:Button;
var objectMiscColor:ColorPicker;
var objectMiscColorReset:Button;
override public function new(state:StageEditorState)
{
super(state);
// basic callbacks
objectLoadImageButton.onClick = function(_) {
if (linkedObject == null) return;
Dialogs.openBinaryFile("Open Image File", FileDialogTypes.IMAGES, function(selectedFile) {
if (selectedFile == null) return;
objectImagePreview.resource = null;
ToolkitAssets.instance.imageFromBytes(selectedFile.bytes, function(imageInfo) {
if (imageInfo == null) return;
objectImagePreview.resource = imageInfo.data;
linkedObject.frame = imageInfo.data;
var bit = linkedObject.updateFramePixels();
var bitToLoad = state.addBitmap(bit);
linkedObject.loadGraphic(state.bitmaps[bitToLoad]);
linkedObject.updateHitbox();
// update size stuff
objectSizeXStepper.pos = linkedObject.width;
objectSizeYStepper.pos = linkedObject.height;
// remove unused bitmaps
state.removeUnusedBitmaps();
});
});
}
objectLoadInternetButton.onClick = function(_) {
if (linkedObject == null) return;
state.createURLDialog(function(bytes:lime.utils.Bytes) {
linkedObject.loadGraphic(BitmapData.fromBytes(bytes));
linkedObject.updateHitbox();
refresh();
});
}
objectDownloadImageButton.onClick = function(_) {
if (linkedObject == null) return;
FileUtil.saveFile(linkedObject.pixels.image.encode(PNG), [FileUtil.FILE_FILTER_PNG], null, null,
linkedObject.name + "-graphic.png"); // i'on need any callbacks
}
objectZIdxStepper.max = StageEditorState.MAX_Z_INDEX;
objectZIdxStepper.onChange = function(_) {
if (linkedObject != null)
{
linkedObject.zIndex = Std.int(objectZIdxStepper.pos);
state.sortAssets();
}
}
// numeric callbacks
objectPosXStepper.onChange = function(_) {
if (linkedObject != null) linkedObject.x = objectPosXStepper.pos;
};
objectPosYStepper.onChange = function(_) {
if (linkedObject != null) linkedObject.y = objectPosYStepper.pos;
};
objectAlphaSlider.onChange = function(_) {
if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos;
};
objectAngleSlider.onChange = function(_) {
if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos;
};
objectScaleXStepper.onChange = objectScaleYStepper.onChange = function(_) {
if (linkedObject != null)
{
linkedObject.scale.set(objectScaleXStepper.pos, objectScaleYStepper.pos);
linkedObject.updateHitbox();
objectSizeXStepper.pos = linkedObject.width;
objectSizeYStepper.pos = linkedObject.height;
linkedObject.playAnim(linkedObject.animation.name); // load offsets
}
};
objectSizeXStepper.onChange = objectSizeYStepper.onChange = function(_) {
if (linkedObject != null)
{
linkedObject.setGraphicSize(Std.int(objectSizeXStepper.pos), Std.int(objectSizeYStepper.pos));
linkedObject.updateHitbox();
objectScaleXStepper.pos = linkedObject.scale.x;
objectScaleYStepper.pos = linkedObject.scale.y;
linkedObject.playAnim(linkedObject.animation.name); // load offsets
}
};
objectScrollXSlider.onChange = objectScrollYSlider.onChange = function(_) {
if (linkedObject != null) linkedObject.scrollFactor.set(objectScrollXSlider.pos, objectScrollYSlider.pos);
};
// frame callbacks
objectFrameTextLoad.onClick = function(_) {
Dialogs.openTextFile("Open Text File", FileDialogTypes.TEXTS, function(selectedFile) {
if (selectedFile.text == null || (!selectedFile.name.endsWith(".xml") && !selectedFile.name.endsWith(".txt"))) return;
objectFrameText.text = selectedFile.text;
state.notifyChange("Frame Text Loaded", "The Text File " + selectedFile.name + " has been loaded.");
});
}
objectFrameTextSparrow.onClick = function(_) {
if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return;
try
{
linkedObject.frames = FlxAtlasFrames.fromSparrow(linkedObject.graphic, objectFrameText.text);
}
catch (e)
{
state.notifyChange("Frame Setup Error", e.toString(), true);
return;
}
// might as well clear animations because frames SUCK
linkedObject.animDatas.clear();
linkedObject.animation.destroyAnimations();
linkedObject.updateHitbox();
refresh();
state.notifyChange("Frame Setup Done", "Finished the Sparrow Frame Setup for the Object " + linkedObject.name + ".");
}
objectFrameTextPacker.onClick = function(_) {
if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return;
try // crash prevention
{
linkedObject.frames = FlxAtlasFrames.fromSpriteSheetPacker(linkedObject.graphic, objectFrameText.text);
}
catch (e)
{
state.notifyChange("Frame Setup Error", e.toString(), true);
return;
}
// might as well clear animations because frames SUCK
linkedObject.animDatas.clear();
linkedObject.animation.destroyAnimations();
linkedObject.updateHitbox();
refresh();
state.notifyChange("Frame Setup Done", "Finished the Packer Frame Setup for the Object " + linkedObject.name + ".");
}
objectFrameImageSetter.onClick = function(_) {
if (linkedObject == null) return;
linkedObject.loadGraphic(linkedObject.graphic, true, Std.int(objectFrameImageWidth.pos), Std.int(objectFrameImageHeight.pos));
linkedObject.updateHitbox();
// set da names
for (i in 0...linkedObject.frames.frames.length)
{
linkedObject.frames.framesHash.set("Frame" + i, linkedObject.frames.frames[i]);
linkedObject.frames.frames[i].name = "Frame" + i;
}
// might as well clear animations because frames SUCK
linkedObject.animDatas.clear();
linkedObject.animation.destroyAnimations();
refresh();
state.notifyChange("Frame Setup Done", "Finished the Image Frame Setup for the Object " + linkedObject.name + ".");
}
// animation
objectAnimDropdown.onChange = function(_) {
if (linkedObject == null) return;
if (objectAnimDropdown.selectedIndex == -1) // RESET EVERYTHING INSTANTENEOUSLY
{
objectAnimName.text = "";
objectAnimLooped.selected = objectAnimFlipX.selected = objectAnimFlipY.selected = false;
objectAnimFramerate.pos = 24;
objectAnimOffsetX.pos = objectAnimOffsetY.pos = 0;
objectAnimFrames.text = "";
return;
}
var animData = linkedObject.animDatas[objectAnimDropdown.selectedItem.text];
if (animData == null) return;
objectAnimName.text = objectAnimDropdown.selectedItem.text;
objectAnimPrefix.text = animData.prefix ?? "";
objectAnimFrames.text = (animData.frameIndices != null && animData.frameIndices.length > 0 ? animData.frameIndices.join(", ") : "");
objectAnimLooped.selected = animData.looped ?? false;
objectAnimFlipX.selected = animData.flipX ?? false;
objectAnimFlipY.selected = animData.flipY ?? false;
objectAnimFramerate.pos = animData.frameRate ?? 24;
objectAnimOffsetX.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[0] : 0);
objectAnimOffsetY.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[1] : 0);
}
objectAnimSave.onClick = function(_) {
if (linkedObject == null) return;
if (objectAnimName.text == null || objectAnimName.text == "")
{
state.notifyChange("Animation Saving Error", "Invalid Animation Name!", true);
return;
}
if (objectAnimPrefix.text == null || objectAnimPrefix.text == "")
{
state.notifyChange("Animation Saving Error", "Missing Animation Prefix!", true);
return;
}
if (linkedObject.animation.getNameList().contains(objectAnimName.text)) linkedObject.animation.remove(objectAnimName.text);
var indices = [];
if (objectAnimFrames.text != null && objectAnimFrames.text != "")
{
var splitter = objectAnimFrames.text.replace(" ", "").split(",");
for (num in splitter)
{
indices.push(Std.parseInt(num));
}
}
var shouldDoIndices:Bool = (indices.length > 0 && !indices.contains(null));
linkedObject.addAnim(objectAnimName.text, objectAnimPrefix.text, [objectAnimOffsetX.pos, objectAnimOffsetY.pos], (shouldDoIndices ? indices : []),
Std.int(objectAnimFramerate.pos), objectAnimLooped.selected, objectAnimFlipX.selected, objectAnimFlipY.selected);
if (linkedObject.animation.getByName(objectAnimName.text) == null)
{
state.notifyChange("Animation Saving Error", "Invalid Frames!", true);
return;
}
linkedObject.playAnim(objectAnimName.text);
state.notifyChange("Animation Saving Done", "Animation " + objectAnimName.text + " has been saved to the Object " + linkedObject.name + ".");
updateAnimList();
// stops the animation preview if animation is looped for too long
FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() {
if (linkedObject != null && linkedObject.animation.curAnim != null)
linkedObject.animation.stop(); // null check cuz if we stop an anim for a null object the game crashes :[
});
}
objectAnimDelete.onClick = function(_) {
if (linkedObject == null || linkedObject.animation.getNameList().length <= 0 || objectAnimDropdown.selectedIndex < 0) return;
linkedObject.animation.pause();
linkedObject.animation.stop();
linkedObject.animation.curAnim = null;
var daAnim = linkedObject.animation.getNameList()[objectAnimDropdown.selectedIndex];
linkedObject.animation.remove(daAnim);
linkedObject.animDatas.remove(daAnim);
linkedObject.offset.set();
state.notifyChange("Animation Deletion Done",
"Animation "
+ objectAnimDropdown.selectedItem.text
+ " has been removed from the Object "
+ linkedObject.name
+ ".");
updateAnimList();
objectAnimDropdown.selectedIndex = objectAnimDropdown.dataSource.size - 1;
}
objectAnimDanceBeat.onChange = function(_) {
if (linkedObject != null) linkedObject.danceEvery = Std.int(objectAnimDanceBeat.pos);
}
objectAnimStart.onChange = function(_) {
if (linkedObject != null)
{
if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white";
else
objectAnimStart.styleString = "color: indianred";
linkedObject.startingAnimation = objectAnimStart.text;
}
}
// misc
objectMiscAntialias.onClick = function(_) {
if (linkedObject != null) linkedObject.antialiasing = objectMiscAntialias.selected;
}
objectMiscBlendDrop.onChange = function(_) {
if (linkedObject != null)
linkedObject.blend = objectMiscBlendDrop.selectedItem.text == "NONE" ? null : AssetDataHandler.blendFromString(objectMiscBlendDrop.selectedItem.text);
}
objectMiscColor.onChange = function(_) {
if (linkedObject != null) linkedObject.color = FlxColor.fromRGB(objectMiscColor.currentColor.r, objectMiscColor.currentColor.g,
objectMiscColor.currentColor.b);
}
// reset button callbacks
objectResetImageButton.onClick = function(_) {
if (linkedObject != null)
{
linkedObject.loadGraphic(AssetDataHandler.getDefaultGraphic());
linkedObject.updateHitbox();
refresh();
// remove unused bitmaps
state.removeUnusedBitmaps();
}
}
objectZIdxReset.onClick = function(_) {
if (linkedObject != null) objectZIdxStepper.pos = 0; // corner cutting because onChange will activate with this
}
objectPosResetButton.onClick = function(_) {
if (linkedObject != null)
{
linkedObject.screenCenter();
objectPosXStepper.pos = linkedObject.x;
objectPosYStepper.pos = linkedObject.y;
}
}
objectAlphaResetButton.onClick = function(_) {
if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos = 1;
}
objectAngleResetButton.onClick = function(_) {
if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos = 0;
}
objectScaleResetButton.onClick = objectSizeResetButton.onClick = function(_) // the corner cutting goes crazy
{
if (linkedObject != null)
{
linkedObject.scale.set(1, 1);
refresh(); // refreshes like multiple shit
}
}
objectScrollResetButton.onClick = function(_) {
if (linkedObject != null) linkedObject.scrollFactor.x = linkedObject.scrollFactor.y = objectScrollXSlider.pos = objectScrollYSlider.pos = 1;
}
objectFrameReset.onClick = function(_) {
if (linkedObject == null) return;
linkedObject.loadGraphic(linkedObject.pixels);
linkedObject.animDatas.clear();
linkedObject.animation.destroyAnimations();
refresh();
}
objectMiscAntialiasReset.onClick = function(_) {
if (linkedObject != null) objectMiscAntialias.selected = true;
}
objectMiscBlendReset.onClick = function(_) {
if (linkedObject != null) objectMiscBlendDrop.selectedItem = "NORMAL";
}
objectMiscColorReset.onClick = function(_) {
if (linkedObject != null) objectMiscColor.currentColor = Color.fromString("white");
}
objectAnimDanceBeatReset.onClick = function(_) {
if (linkedObject != null) objectAnimDanceBeat.pos = 0;
}
objectAnimStartReset.onClick = function(_) {
if (linkedObject != null) objectAnimStart.text = "";
}
refresh();
}
var prevFrames:Array<FlxFrame> = [];
var prevAnims:Array<String> = [];
override public function refresh()
{
linkedObject = stageEditorState.selectedSprite;
objectPosXStepper.step = stageEditorState.moveStep;
objectPosYStepper.step = stageEditorState.moveStep;
objectAngleSlider.step = funkin.save.Save.instance.stageEditorAngleStep;
if (linkedObject == null)
{
updateFrameList();
updateAnimList();
return;
}
// saving fps
if (objectImagePreview.resource != linkedObject.frame) objectImagePreview.resource = linkedObject.frame;
if (objectZIdxStepper.pos != linkedObject.zIndex) objectZIdxStepper.pos = linkedObject.zIndex;
if (objectPosXStepper.pos != linkedObject.x) objectPosXStepper.pos = linkedObject.x;
if (objectPosYStepper.pos != linkedObject.y) objectPosYStepper.pos = linkedObject.y;
if (objectAlphaSlider.pos != linkedObject.alpha) objectAlphaSlider.pos = linkedObject.alpha;
if (objectAngleSlider.pos != linkedObject.angle) objectAngleSlider.pos = linkedObject.angle;
if (objectScaleXStepper.pos != linkedObject.scale.x) objectScaleXStepper.pos = linkedObject.scale.x;
if (objectScaleYStepper.pos != linkedObject.scale.y) objectScaleYStepper.pos = linkedObject.scale.y;
if (objectSizeXStepper.pos != linkedObject.width) objectSizeXStepper.pos = linkedObject.width;
if (objectSizeYStepper.pos != linkedObject.height) objectSizeYStepper.pos = linkedObject.height;
if (objectScrollXSlider.pos != linkedObject.scrollFactor.x) objectScrollXSlider.pos = linkedObject.scrollFactor.x;
if (objectScrollYSlider.pos != linkedObject.scrollFactor.y) objectScrollYSlider.pos = linkedObject.scrollFactor.y;
if (objectMiscAntialias.selected != linkedObject.antialiasing) objectMiscAntialias.selected = linkedObject.antialiasing;
if (objectMiscColor.currentColor != Color.fromString(linkedObject.color.toHexString() ?? "white"))
objectMiscColor.currentColor = Color.fromString(linkedObject.color.toHexString());
if (objectAnimDanceBeat.pos != linkedObject.danceEvery) objectAnimDanceBeat.pos = linkedObject.danceEvery;
if (objectAnimStart.text != linkedObject.startingAnimation) objectAnimStart.text = linkedObject.startingAnimation;
var objBlend = Std.string(linkedObject.blend) ?? "NONE";
if (objectMiscBlendDrop.selectedItem != objBlend.toUpperCase()) objectMiscBlendDrop.selectedItem = objBlend.toUpperCase();
// ough the max
if (objectFrameImageWidth.max != linkedObject.pixels.width) objectFrameImageWidth.max = linkedObject.graphic.width;
if (objectFrameImageHeight.max != linkedObject.pixels.height) objectFrameImageHeight.max = linkedObject.graphic.height;
// update some anim shit
if (prevFrames != linkedObject.frames.frames.copy()) updateFrameList();
if (prevAnims != linkedObject.animation.getNameList().copy()) updateAnimList();
}
function updateFrameList()
{
prevFrames = [];
objectAnimFrameList.dataSource = new ArrayDataSource();
if (linkedObject == null) return;
for (fname in linkedObject.frames.frames)
{
if (fname != null) objectAnimFrameList.dataSource.add({name: fname.name, tooltip: fname.name});
prevFrames.push(fname);
}
}
function updateAnimList()
{
objectAnimDropdown.dataSource.clear();
prevAnims = [];
if (linkedObject == null) return;
for (aname in linkedObject.animation.getNameList())
{
objectAnimDropdown.dataSource.add({text: aname});
prevAnims.push(aname);
}
if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white";
else
objectAnimStart.styleString = "color: indianred";
linkedObject.startingAnimation = objectAnimStart.text;
}
}

View file

@ -4,7 +4,9 @@ import haxe.ui.components.NumberStepper;
import haxe.ui.components.TextField;
import haxe.ui.components.DropDown;
import funkin.util.SortUtil;
import haxe.ui.events.UIEvent;
@:access(funkin.ui.debug.stageeditor.StageEditorState)
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/stage-settings.xml"))
class StageEditorStageToolbox extends StageEditorDefaultToolbox
{
@ -27,7 +29,7 @@ class StageEditorStageToolbox extends StageEditorDefaultToolbox
state.saved = false;
}
final EXCLUDE_LIBS = ["art", "default", "vlc", "videos", "songs"];
final EXCLUDE_LIBS = ["art", "default", "vlc", "videos", "songs", "libvlc"];
var allLibs = [];
@:privateAccess
@ -49,6 +51,13 @@ class StageEditorStageToolbox extends StageEditorDefaultToolbox
}
refresh();
this.onDialogClosed = onClose;
}
function onClose(event:UIEvent)
{
stageEditorState.menubarItemWindowStage.selected = false;
}
override public function refresh()

View file

@ -45,6 +45,7 @@ import funkin.util.MathUtil;
import funkin.util.SortUtil;
import openfl.display.BlendMode;
import funkin.data.freeplay.style.FreeplayStyleRegistry;
import funkin.ui.debug.charting.ChartEditorState;
#if FEATURE_DISCORD_RPC
import funkin.api.discord.DiscordClient;
#end
@ -191,21 +192,26 @@ class FreeplayState extends MusicBeatSubState
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
{
currentCharacterId = params?.character ?? rememberedCharacterId;
styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacterId);
var fetchPlayableCharacter = function():PlayableCharacter {
var targetCharId = params?.character ?? rememberedCharacterId;
var result = PlayerRegistry.instance.fetchEntry(targetCharId);
if (result == null) throw 'No valid playable character with id ${targetCharId}';
if (result == null)
{
trace('No valid playable character with id ${targetCharId}');
result = PlayerRegistry.instance.fetchEntry(Constants.DEFAULT_CHARACTER);
if (result == null) throw 'WTH your default character is null?????';
}
return result;
};
currentCharacter = fetchPlayableCharacter();
currentCharacterId = currentCharacter.getFreeplayStyleID();
currentVariation = rememberedVariation;
currentDifficulty = rememberedDifficulty;
styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacter.getFreeplayStyleID());
styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacterId);
rememberedCharacterId = currentCharacter?.id ?? Constants.DEFAULT_CHARACTER;
fromCharSelect = params?.fromCharSelect ?? false;
fromResultsParams = params?.fromResults;
prepForNewRank = fromResultsParams?.playRankAnim ?? false;
@ -403,6 +409,7 @@ class FreeplayState extends MusicBeatSubState
}
albumRoll.albumId = null;
albumRoll.applyExitMovers(exitMovers, exitMoversCharSel);
add(albumRoll);
var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 164, FlxColor.BLACK);
@ -416,7 +423,6 @@ class FreeplayState extends MusicBeatSubState
}
else
{
albumRoll.applyExitMovers(exitMovers, exitMoversCharSel);
FlxTween.tween(overhangStuff, {y: -100}, 0.3, {ease: FlxEase.quartOut});
FlxTween.tween(blackOverlayBullshitLOLXD, {x: 387.76}, 0.7, {ease: FlxEase.quintOut});
}
@ -433,8 +439,11 @@ class FreeplayState extends MusicBeatSubState
charSelectHint.font = "5by7";
charSelectHint.color = 0xFF5F5F5F;
charSelectHint.text = 'Press [ ${controls.getDialogueNameFromControl(FREEPLAY_CHAR_SELECT, true)} ] to change characters';
charSelectHint.y -= 100;
FlxTween.tween(charSelectHint, {y: charSelectHint.y + 100}, 0.8, {ease: FlxEase.quartOut});
if (!fromCharSelect)
{
charSelectHint.y -= 100;
FlxTween.tween(charSelectHint, {y: charSelectHint.y + 100}, 0.8, {ease: FlxEase.quartOut});
}
exitMovers.set([overhangStuff, fnfFreeplay, ostName, charSelectHint],
{
@ -497,6 +506,7 @@ class FreeplayState extends MusicBeatSubState
// Reminder, this is a callback function being set, rather than these being called here in create()
letterSort.changeSelectionCallback = (str) -> {
var curSong:Null<FreeplaySongData> = grpCapsules.members[curSelected]?.freeplayData;
grpCapsules.members[curSelected].selected = false;
switch (str)
{
@ -550,7 +560,7 @@ class FreeplayState extends MusicBeatSubState
add(fnfFreeplay);
add(ostName);
if (PlayerRegistry.instance.hasNewCharacter())
if (PlayerRegistry.instance.countUnlockedCharacters() > 1)
{
add(charSelectHint);
}
@ -726,10 +736,6 @@ class FreeplayState extends MusicBeatSubState
currentFilteredSongs = tempSongs;
curSelected = 0;
// If curSelected is 0, the result will be null and fall back to the rememberedSongId.
// We set this so if we change the filter, we'd remain on the same song if it's still in the list.
rememberedSongId = grpCapsules.members[curSelected]?.freeplayData?.data.id ?? rememberedSongId;
grpCapsules.killMembers();
// Initialize the random capsule, with empty/blank info (which we display once bf/pico does his hand)
@ -829,13 +835,12 @@ class FreeplayState extends MusicBeatSubState
case ALL:
// no filter!
case FAVORITE:
// sort favorites by week, not alphabetically
songsToFilter = songsToFilter.filter(filteredSong -> {
if (filteredSong == null) return true; // Random
return filteredSong.isFav;
});
songsToFilter.sort(filterAlphabetically);
default:
// return all on default
}
@ -855,6 +860,9 @@ class FreeplayState extends MusicBeatSubState
rememberedSongId = fromResults.songId;
rememberedDifficulty = fromResults.difficultyId;
capsuleToRank.fakeRanking.visible = true;
capsuleToRank.fakeRanking.alpha = 0; // If this isn't done, you'd see a tiny E being replaced for the first rank
changeSelection();
changeDiff();
@ -895,6 +903,7 @@ class FreeplayState extends MusicBeatSubState
sparksADD.cameras = [rankCamera];
sparksADD.color = fromResults.oldRank.getRankingFreeplayColor();
// sparksADD.color = sparks.color;
capsuleToRank.fakeRanking.alpha = 1.0;
}
capsuleToRank.doLerp = false;
@ -907,7 +916,6 @@ class FreeplayState extends MusicBeatSubState
trace(originalPos);
capsuleToRank.ranking.visible = false;
capsuleToRank.fakeRanking.visible = false;
rankCamera.zoom = 1.85;
FlxTween.tween(rankCamera, {"zoom": 1.8}, 0.6, {ease: FlxEase.sineIn});
@ -1179,7 +1187,8 @@ class FreeplayState extends MusicBeatSubState
fadeShader.fade(1.0, 0.0, 0.8, {ease: FlxEase.quadIn});
FlxG.sound.music?.fadeOut(0.9, 0);
new FlxTimer().start(0.9, _ -> {
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState({character: currentCharacterId} // Passing the currrent Freeplay character to the CharSelect so we can start it with that character selected
));
});
for (grpSpr in exitMoversCharSel.keys())
{
@ -1259,7 +1268,6 @@ class FreeplayState extends MusicBeatSubState
capsule.doLerp = true;
fromCharSelect = false;
busy = false;
albumRoll.applyExitMovers(exitMovers, exitMoversCharSel);
}
}
});
@ -1343,7 +1351,7 @@ class FreeplayState extends MusicBeatSubState
grpCapsules.members[realShit].favIconBlurred.animation.play('fav');
FunkinSound.playOnce(Paths.sound('fav'), 1);
grpCapsules.members[realShit].checkClip();
grpCapsules.members[realShit].selected = grpCapsules.members[realShit].selected; // set selected again, so it can run it's getter function to initialize movement
grpCapsules.members[realShit].selected = true; // set selected again, so it can run it's getter function to initialize movement
busy = true;
grpCapsules.members[realShit].doLerp = false;
@ -1368,6 +1376,7 @@ class FreeplayState extends MusicBeatSubState
grpCapsules.members[realShit].favIcon.visible = false;
grpCapsules.members[realShit].favIconBlurred.visible = false;
grpCapsules.members[realShit].checkClip();
grpCapsules.members[realShit].selected = true; // set selected again, so it can run it's getter function to initialize movement
});
busy = true;
@ -1660,6 +1669,45 @@ class FreeplayState extends MusicBeatSubState
{
grpCapsules.members[curSelected].onConfirm();
}
if (controls.DEBUG_CHART && !busy)
{
busy = true;
var targetSongID = grpCapsules.members[curSelected]?.freeplayData?.data.id ?? 'unknown';
if (targetSongID == 'unknown')
{
trace('CHART RANDOM SONG');
letterSort.inputEnabled = false;
var availableSongCapsules:Array<SongMenuItem> = grpCapsules.members.filter(function(cap:SongMenuItem) {
// Dead capsules are ones which were removed from the list when changing filters.
return cap.alive && cap.freeplayData != null;
});
trace('Available songs: ${availableSongCapsules.map(function(cap) {
return cap?.freeplayData?.data.songName;
})}');
if (availableSongCapsules.length == 0)
{
trace('No songs available!');
busy = false;
letterSort.inputEnabled = true;
FunkinSound.playOnce(Paths.sound('cancelMenu'));
return;
}
var targetSong:SongMenuItem = FlxG.random.getObject(availableSongCapsules);
// Seeing if I can do an animation...
curSelected = grpCapsules.members.indexOf(targetSong);
changeSelection(0);
targetSongID = grpCapsules.members[curSelected]?.freeplayData?.data.id ?? 'unknown';
}
FlxG.switchState(() -> new ChartEditorState(
{
targetSongId: targetSongID,
}));
}
}
override function beatHit():Bool
@ -1718,6 +1766,7 @@ class FreeplayState extends MusicBeatSubState
touchTimer = 0;
var previousVariation:String = currentVariation;
var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected].freeplayData;
grpCapsules.members[curSelected].selected = false;
// Available variations for current character. We get this since bf is usually `default` variation, and `pico` is `pico`
// but sometimes pico can be the default variation (weekend 1 songs), and bf can be `bf` variation (darnell)
@ -1765,8 +1814,8 @@ class FreeplayState extends MusicBeatSubState
var songScore:Null<SaveScoreData> = Save.instance.getSongScore(daSong.data.id, currentDifficulty, currentVariation);
intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore == null ? 0.0 : Math.max(0, ((songScore.tallies.sick +
songScore.tallies.good - songScore.tallies.missed) / songScore.tallies.totalNotes));
intendedCompletion = songScore == null ? 0.0 : Math.max(0,
((songScore.tallies.sick + songScore.tallies.good - songScore.tallies.missed) / songScore.tallies.totalNotes));
rememberedDifficulty = currentDifficulty;
grpCapsules.members[curSelected].refreshDisplay((prepForNewRank == true) ? false : true);
}
@ -1832,6 +1881,8 @@ class FreeplayState extends MusicBeatSubState
// Set difficulty star count.
albumRoll.setDifficultyStars(daSong?.data.getDifficulty(currentDifficulty, currentVariation)?.difficultyRating ?? 0);
grpCapsules.members[curSelected].selected = true; // set selected again, so it can run it's getter function to initialize movement
}
function capsuleOnConfirmRandom(randomCapsule:SongMenuItem):Void
@ -2098,7 +2149,7 @@ class FreeplayState extends MusicBeatSubState
if (index < curSelected) capsule.targetPos.y -= 100; // another 100 for good measure
}
if (grpCapsules.countLiving() > 0 && !prepForNewRank)
if (grpCapsules.countLiving() > 0 && !prepForNewRank && !busy)
{
playCurSongPreview(daSongCapsule);
grpCapsules.members[curSelected].selected = true;

View file

@ -286,7 +286,7 @@ class SongMenuItem extends FlxSpriteGroup
var clipSize:Int = 290;
var clipType:Int = 0;
if (ranking.visible)
if (ranking.visible || fakeRanking.visible)
{
favIconBlurred.x = this.x + 370;
favIcon.x = favIconBlurred.x;
@ -492,8 +492,6 @@ class SongMenuItem extends FlxSpriteGroup
spr.visible = value;
}
textAppear();
updateSelected();
}
@ -660,7 +658,11 @@ class SongMenuItem extends FlxSpriteGroup
*/
public function confirm():Void
{
if (songText != null) songText.flickerText();
if (songText != null)
{
textAppear();
songText.flickerText();
}
if (pixelIcon != null && pixelIcon.visible)
{
pixelIcon.animation.play('confirm');

View file

@ -104,11 +104,12 @@ class MainMenuState extends MusicBeatState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
var rememberedFreeplayCharacter = FreeplayState.rememberedCharacterId;
#if FEATURE_DEBUG_FUNCTIONS
// Debug function: Hold SHIFT when selecting Freeplay to swap character without the char select menu
var targetCharacter:Null<String> = (FlxG.keys.pressed.SHIFT) ? (FreeplayState.rememberedCharacterId == "pico" ? "bf" : "pico") : null;
var targetCharacter:Null<String> = (FlxG.keys.pressed.SHIFT) ? (FreeplayState.rememberedCharacterId == "pico" ? "bf" : "pico") : rememberedFreeplayCharacter;
#else
var targetCharacter:Null<String> = null;
var targetCharacter:Null<String> = rememberedFreeplayCharacter;
#end
openSubState(new FreeplayState(
@ -302,6 +303,9 @@ class MainMenuState extends MusicBeatState
{
persistentUpdate = false;
// Cancel the currently flickering menu item because it's about to call a state switch
if (menuItems.busy) menuItems.cancelAccept();
FlxG.state.openSubState(new DebugMenuSubState());
}

View file

@ -3,7 +3,9 @@ package funkin.ui.options;
import funkin.ui.Page.PageName;
import funkin.ui.transition.LoadingState;
import funkin.ui.debug.latency.LatencyState;
import flixel.math.FlxPoint;
import flixel.FlxSprite;
import flixel.FlxObject;
import flixel.FlxSubState;
import flixel.group.FlxGroup;
import flixel.util.FlxSignal;
@ -25,6 +27,8 @@ class OptionsState extends MusicBeatState
{
var optionsCodex:Codex<OptionsMenuPageName>;
public static var rememberedSelectedIndex:Int = 0;
override function create():Void
{
persistentUpdate = true;
@ -85,14 +89,23 @@ class OptionsMenu extends Page<OptionsMenuPageName>
{
var items:TextMenuList;
/**
* Camera focus point
*/
var camFocusPoint:FlxObject;
final CAMERA_MARGIN:Int = 150;
public function new()
{
super();
add(items = new TextMenuList());
createItem("PREFERENCES", function() codex.switchPage(Preferences));
createItem("CONTROLS", function() codex.switchPage(Controls));
createItem("INPUT OFFSETS", function() {
OptionsState.rememberedSelectedIndex = items.selectedIndex;
#if web
LoadingState.transitionToState(() -> new LatencyState());
#else
@ -135,6 +148,27 @@ class OptionsMenu extends Page<OptionsMenuPageName>
});
createItem("EXIT", exit);
// Create an object for the camera to track.
camFocusPoint = new FlxObject(0, 0, 140, 70);
add(camFocusPoint);
// Follow the camera focus as we scroll.
FlxG.camera.follow(camFocusPoint, null, 0.085);
FlxG.camera.deadzone.set(0, CAMERA_MARGIN / 2, FlxG.camera.width, FlxG.camera.height - CAMERA_MARGIN + 40);
FlxG.camera.minScrollY = -CAMERA_MARGIN / 2;
// Move the camera when the menu is scrolled.
items.onChange.add(onMenuChange);
onMenuChange(items.members[0]);
items.selectItem(OptionsState.rememberedSelectedIndex);
}
function onMenuChange(selected:TextMenuList.TextMenuItem)
{
camFocusPoint.y = selected.y;
}
function createItem(name:String, callback:Void->Void, fireInstantly = false)

View file

@ -116,10 +116,15 @@ class PreferencesMenu extends Page<OptionsState.OptionsMenuPageName>
createPrefItemCheckbox('Pause on Unfocus', 'If enabled, game automatically pauses when it loses focus.', function(value:Bool):Void {
Preferences.autoPause = value;
}, Preferences.autoPause);
createPrefItemCheckbox('Launch in Fullscreen', 'Automatically launch the game in fullscreen on startup', function(value:Bool):Void {
createPrefItemCheckbox('Launch in Fullscreen', 'Automatically launch the game in fullscreen on startup.', function(value:Bool):Void {
Preferences.autoFullscreen = value;
}, Preferences.autoFullscreen);
#if web
createPrefItemCheckbox('Unlocked Framerate', 'If enabled, the framerate will be unlocked.', function(value:Bool):Void {
Preferences.unlockedFramerate = value;
}, Preferences.unlockedFramerate);
#else
// disabled on macos due to "error: Late swap tearing currently unsupported"
#if !mac
createPrefItemEnum('VSync', 'If enabled, game will attempt to match framerate with your monitor.', [
@ -136,14 +141,9 @@ class PreferencesMenu extends Page<OptionsState.OptionsMenuPageName>
case WindowVSyncMode.ADAPTIVE: "Adaptive";
});
#end
#if web
createPrefItemCheckbox('Unlocked Framerate', 'If enabled, the framerate will be unlocked.', function(value:Bool):Void {
Preferences.unlockedFramerate = value;
}, Preferences.unlockedFramerate);
#else
createPrefItemNumber('FPS', 'The maximum framerate that the game targets.', function(value:Float) {
Preferences.framerate = Std.int(value);
}, null, Preferences.framerate, 30, 300, 5, 0);
}, null, Preferences.framerate, 30, 500, 5, 0);
#end
createPrefItemCheckbox('Hide Mouse', 'If enabled, the mouse will be hidden when taking a screenshot.', function(value:Bool):Void {
@ -155,13 +155,6 @@ class PreferencesMenu extends Page<OptionsState.OptionsMenuPageName>
createPrefItemCheckbox('Preview on save', 'If enabled, the preview will be shown only after a screenshot is saved.', function(value:Bool):Void {
Preferences.previewOnSave = value;
}, Preferences.previewOnSave);
// TODO: having oValue is weird, probably change this later? was done to accomodate VSync changes.
createPrefItemEnum('Save Format', 'Save screenshots to this format.', ['PNG' => 'PNG', 'JPEG' => 'JPEG'], function(value:String, oValue:String):Void {
Preferences.saveFormat = value;
}, Preferences.saveFormat);
createPrefItemNumber('JPEG Quality', 'The quality of JPEG screenshots.', function(value:Float) {
Preferences.jpegQuality = Std.int(value);
}, null, Preferences.jpegQuality, 0, 100, 5, 0);
}
override function update(elapsed:Float):Void

View file

@ -231,6 +231,8 @@ class TitleState extends MusicBeatState
Conductor.instance.update();
funkin.input.Cursor.hide();
/* if (FlxG.onMobile)
{
if (gfDance != null)
@ -240,12 +242,20 @@ class TitleState extends MusicBeatState
}
}
*/
if (FlxG.keys.justPressed.I)
if (outlineShaderShit != null)
{
FlxTween.tween(outlineShaderShit, {funnyX: 50, funnyY: 50}, 0.6, {ease: FlxEase.quartOut});
if (FlxG.keys.justPressed.I)
{
FlxTween.tween(outlineShaderShit, {funnyX: 50, funnyY: 50}, 0.6, {ease: FlxEase.quartOut});
}
if (FlxG.keys.pressed.D)
{
outlineShaderShit.funnyX += 1;
}
// outlineShaderShit.xPos.value[0] += 1;
}
if (FlxG.keys.pressed.D) outlineShaderShit.funnyX += 1;
// outlineShaderShit.xPos.value[0] += 1;
if (FlxG.keys.justPressed.Y)
{

View file

@ -72,6 +72,17 @@ class CLIUtil
result.chart.shouldLoadChart = true;
result.chart.chartPath = args.shift();
}
case "--stage":
if (args.length == 0)
{
trace('No stage path provided.');
printUsage();
}
else
{
result.stage.shouldLoadStage = true;
result.stage.stagePath = args.shift();
}
}
}
else
@ -83,6 +94,11 @@ class CLIUtil
result.chart.shouldLoadChart = true;
result.chart.chartPath = arg;
}
else if (arg.endsWith(Constants.EXT_STAGE))
{
result.stage.shouldLoadStage = true;
result.stage.stagePath = arg;
}
else
{
trace('Unrecognized argument: ${arg}');
@ -96,7 +112,7 @@ class CLIUtil
static function printUsage():Void
{
trace('Usage: Funkin.exe [--chart <chart>] [--help] [--version]');
trace('Usage: Funkin.exe [--chart <chart>] [--stage <stage>] [--help] [--version]');
}
static function buildDefaultParams():CLIParams
@ -108,6 +124,11 @@ class CLIUtil
{
shouldLoadChart: false,
chartPath: null
},
stage:
{
shouldLoadStage: false,
stagePath: null
}
};
}
@ -138,6 +159,7 @@ typedef CLIParams =
var args:Array<String>;
var chart:CLIChartParams;
var stage:CLIStageParams;
}
typedef CLIChartParams =
@ -145,3 +167,9 @@ typedef CLIChartParams =
var shouldLoadChart:Bool;
var chartPath:Null<String>;
};
typedef CLIStageParams =
{
var shouldLoadStage:Bool;
var stagePath:Null<String>;
};

View file

@ -537,6 +537,11 @@ class Constants
*/
public static final EXT_CHART = "fnfc";
/**
* The file extension used when exporting stage files.
*/
public static final EXT_STAGE = "fnfs";
/**
* The file extension used when loading audio files.
*/

View file

@ -527,20 +527,23 @@ class FileUtil
/**
* Prompts the user to save a file to their computer.
*/
public static function writeFileReference(path:String, data:String):Void
public static function writeFileReference(path:String, data:String, callback:String->Void)
{
var file = new FileReference();
file.addEventListener(Event.COMPLETE, function(e:Event) {
trace('Successfully wrote file: "$path"');
callback("success");
});
file.addEventListener(Event.CANCEL, function(e:Event) {
trace('Cancelled writing file: "$path"');
callback("info");
});
file.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent) {
trace('IO error writing file: "$path"');
callback("error");
});
file.save(data, path);
@ -1143,12 +1146,17 @@ class FileUtil
* Runs platform-specific code to open a path in the file explorer.
*
* @param pathFolder The path of the folder to open.
* @param createIfNotExists If `true`, creates the folder if missing; otherwise, throws an error.
*/
public static function openFolder(pathFolder:String):Void
public static function openFolder(pathFolder:String, createIfNotExists:Bool = true):Void
{
#if sys
pathFolder = pathFolder.trim();
if (!directoryExists(pathFolder))
if (createIfNotExists)
{
createDirIfNotExists(pathFolder);
}
else if (!directoryExists(pathFolder))
{
throw 'Path is not a directory: "$pathFolder"';
}
@ -1393,9 +1401,9 @@ class FileUtilSandboxed
FileUtil.browseFileReference(callback);
}
public static function writeFileReference(path:String, data:String):Void
public static function writeFileReference(path:String, data:String, callback:String->Void):Void
{
FileUtil.writeFileReference(path, data);
FileUtil.writeFileReference(path, data, callback);
}
public static function readJSONFromPath(path:String):Dynamic
@ -1518,9 +1526,9 @@ class FileUtilSandboxed
return FileUtil.makeZIPEntryFromBytes(name, data);
}
public static function openFolder(pathFolder:String):Void
public static function openFolder(pathFolder:String, createIfNotExists:Bool = true):Void
{
FileUtil.openFolder(sanitizePath(pathFolder));
FileUtil.openFolder(sanitizePath(pathFolder), createIfNotExists);
}
public static function openSelectFile(path:String):Void

View file

@ -85,12 +85,6 @@ class WindowUtil
*/
public static function initTracy():Void
{
// Apply a marker to indicate frame end for the Tracy profiler.
// Do this only if Tracy is configured to prevent lag.
openfl.Lib.current.stage.addEventListener(openfl.events.Event.EXIT_FRAME, (e:openfl.events.Event) -> {
cpp.vm.tracy.TracyProfiler.frameMark();
});
var appInfoMessage = funkin.util.logging.CrashHandler.buildSystemInfo();
trace("Friday Night Funkin': Connection to Tracy profiler successful.");

View file

@ -469,7 +469,7 @@ class ScreenshotPlugin extends FlxBasic
}
/**
* Convert a Bitmap to a PNG or JPEG ByteArray to save to a file.
* Convert a Bitmap to a PNG ByteArray to save to a file.
*/
function encode(bitmap:Bitmap):ByteArray
{
@ -494,7 +494,7 @@ class ScreenshotPlugin extends FlxBasic
if (previousScreenshotName != targetPath && previousScreenshotName != (targetPath + ' (${previousScreenshotCopyNum})'))
{
previousScreenshotName = targetPath;
targetPath = getScreenshotPath() + targetPath + '.' + Std.string(Preferences.saveFormat).toLowerCase();
targetPath = getScreenshotPath() + targetPath + '.png';
previousScreenshotCopyNum = 2;
}
else
@ -506,7 +506,7 @@ class ScreenshotPlugin extends FlxBasic
newTargetPath = targetPath + ' (${previousScreenshotCopyNum})';
}
previousScreenshotName = newTargetPath;
targetPath = getScreenshotPath() + newTargetPath + '.' + Std.string(Preferences.saveFormat).toLowerCase();
targetPath = getScreenshotPath() + newTargetPath + '.png';
}
// TODO: Make screenshot saving work on browser.
@ -520,7 +520,7 @@ class ScreenshotPlugin extends FlxBasic
if (pngData == null)
{
trace('[WARN] Failed to encode ${Preferences.saveFormat} data');
trace('[WARN] Failed to encode PNG data');
previousScreenshotName = null;
// Just in case
unsavedScreenshotBuffer.shift();
@ -544,7 +544,7 @@ class ScreenshotPlugin extends FlxBasic
if (pngData == null)
{
trace('[WARN] Failed to encode ${Preferences.saveFormat} data');
trace('[WARN] Failed to encode PNG data');
previousScreenshotName = null;
return;
}

View file

@ -96,4 +96,18 @@ class ArrayTools
// Switch it around.
return isSuperset(superset, subset);
}
/**
* Like `join` but adds a word before the last element.
* @param array The array to join.
* @param separator The separator to use between elements.
* @param andWord The word to use before the last element.
* @return The joined string.
*/
public static function joinPlural(array:Array<String>, separator:String = ', ', andWord:String = 'and'):String
{
if (array.length == 0) return '';
if (array.length == 1) return array[0];
return '${array.slice(0, array.length - 1).join(separator)} $andWord ${array[array.length - 1]}';
}
}

View file

@ -55,8 +55,25 @@ class SongNoteDataArrayTools
}
else
{
// We may be close, so constrain the range (but only a little) and try again.
highIndex -= 1;
// Notes might have same time but not same data, do scans towards both sides
// Scan left from midIndex
var i = midIndex;
while (i >= 0 && input[i].time == note.time)
{
if (input[i] == note) return i;
i--;
}
// Scan right from midIndex + 1
i = midIndex + 1;
while (i < input.length && input[i].time == note.time)
{
if (input[i] == note) return i;
i++;
}
// No matching note found, despite time match
break;
}
}
return -1;