Merge branch 'rewrite/master' into ansi-trace

This commit is contained in:
Eric 2024-01-18 10:01:18 -05:00 committed by GitHub
commit ae907beab9
41 changed files with 1467 additions and 1053 deletions

View file

@ -3,7 +3,7 @@ description: "sets up haxe shit, using HMM!"
runs:
using: "composite"
steps:
- uses: krdlab/setup-haxe@v1.5.1
- uses: funkincrew/ci-haxe@v2
with:
haxe-version: 4.3.1
- name: Config haxelib

View file

@ -13,11 +13,18 @@ jobs:
steps:
- name: ensure git cli is installed
run: apt update && apt install sudo git -y
- uses: actions/checkout@v4
- name: get token from gh app
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: checkout repo
uses: funkincrew/ci-checkout@v5
with:
submodules: 'recursive'
fetch-depth: 0
token: ${{ secrets.GH_RO_PAT }}
token: ${{ steps.app_token.outputs.token }}
- name: check whether submodules exist
run: |
git config --global --add safe.directory $GITHUB_WORKSPACE
@ -48,15 +55,24 @@ jobs:
apt install sudo git curl unzip -y
echo $GITHUB_WORKSPACE
git config --global --add safe.directory $GITHUB_WORKSPACE
- uses: actions/checkout@v4
- name: get token from gh app
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: checkout repo
uses: funkincrew/ci-checkout@v5
with:
submodules: 'recursive'
fetch-depth: 0
token: ${{ secrets.GH_RO_PAT }}
token: ${{ steps.app_token.outputs.token }}
- uses: ./.github/actions/setup-haxeshit
- name: Build game
- name: game build dependencies
run: |
sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev
- name: build game
run: |
haxelib run lime build html5 -release --times
ls
- uses: ./.github/actions/upload-itch
@ -69,14 +85,21 @@ jobs:
if: ${{ needs.check_date.outputs.should_run != 'false'}}
runs-on: windows-latest
permissions:
contents: write
actions: write
contents: write
actions: write
steps:
- uses: actions/checkout@v4
- name: get token from gh app
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: checkout repo
uses: funkincrew/ci-checkout@v5
with:
submodules: 'recursive'
fetch-depth: 0
token: ${{ secrets.GH_RO_PAT }}
token: ${{ steps.app_token.outputs.token }}
- uses: ./.github/actions/setup-haxeshit
- name: Make HXCPP cache dir
run: |
@ -101,6 +124,50 @@ jobs:
butler-key: ${{ secrets.BUTLER_API_KEY }}
build-dir: export/release/windows/bin
target: win
create-nightly-mac:
needs: check_date
if: ${{ needs.check_date.outputs.should_run != 'false'}}
runs-on: [self-hosted, macos]
steps:
- name: prepare container
run: |
git config --global --add safe.directory $GITHUB_WORKSPACE
- name: get token from gh app
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- name: checkout repo
uses: funkincrew/ci-checkout@v5
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
- uses: ./.github/actions/setup-haxeshit
- name: Make HXCPP cache dir
run: |
mkdir -p ${{ runner.temp }}/hxcpp_cache
- name: Restore build cache
id: cache-build-win
uses: actions/cache@v3
with:
path: |
.haxelib
export
${{ runner.temp }}/hxcpp_cache
key: ${{ runner.os }}-build-mac-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: Build game
run: |
haxelib run lime build macos -release --times
ls
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}/hxcpp_cache"
- uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
build-dir: export/release/macos/bin
target: macos
# test-unit-win:
# needs: create-nightly-win
# runs-on: windows-latest
@ -108,7 +175,7 @@ jobs:
# contents: write
# actions: write
# steps:
# - uses: actions/checkout@v4
# - uses: funkincrew/ci-checkout@v5
# with:
# submodules: 'recursive'
# fetch-depth: 0

2
assets

@ -1 +1 @@
Subproject commit d094640f727a670a348b3579d11af5ff6a2ada3a
Subproject commit 7e19c4cfa7db57178f03ed4a58a9fd4d2b93dea7

View file

@ -11,7 +11,7 @@
"name": "flixel",
"type": "git",
"dir": null,
"ref": "a83738673e7edbf8acba3a1426af284dfe6719fe",
"ref": "07c6018008801972d12275690fc144fcc22e3de6",
"url": "https://github.com/FunkinCrew/flixel"
},
{
@ -37,7 +37,7 @@
"name": "flxanimate",
"type": "git",
"dir": null,
"ref": "d7c5621be742e2c98d523dfe5af7528835eaff1e",
"ref": "9bacdd6ea39f5e3a33b0f5dfb7bc583fe76060d4",
"url": "https://github.com/FunkinCrew/flxanimate"
},
{

View file

@ -20,11 +20,11 @@ import openfl.display.BitmapData;
import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.stage.StageRegistry;
import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
import funkin.play.cutscene.dialogue.SpeakerDataParser;
import funkin.data.song.SongRegistry;
import funkin.play.stage.StageData.StageDataParser;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.modding.module.ModuleHandler;
import funkin.ui.title.TitleState;
@ -217,8 +217,9 @@ class InitState extends FlxState
ConversationDataParser.loadConversationCache();
DialogueBoxDataParser.loadDialogueBoxCache();
SpeakerDataParser.loadSpeakerCache();
StageDataParser.loadStageCache();
StageRegistry.instance.loadEntries();
CharacterDataParser.loadCharacterCache();
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();

View file

View file

View file

View file

@ -15,7 +15,7 @@ abstract SongEventSchema(SongEventSchemaRaw)
}
@:arrayAccess
public inline function getByName(name:String):SongEventSchemaField
public function getByName(name:String):SongEventSchemaField
{
for (field in this)
{
@ -41,6 +41,32 @@ abstract SongEventSchema(SongEventSchemaRaw)
{
return this[k] = v;
}
public function stringifyFieldValue(name:String, value:Dynamic):String
{
var field:SongEventSchemaField = getByName(name);
if (field == null) return 'Unknown';
switch (field.type)
{
case SongEventFieldType.STRING:
return Std.string(value);
case SongEventFieldType.INTEGER:
return Std.string(value);
case SongEventFieldType.FLOAT:
return Std.string(value);
case SongEventFieldType.BOOL:
return Std.string(value);
case SongEventFieldType.ENUM:
for (key in field.keys.keys())
{
if (field.keys.get(key) == value) return key;
}
return Std.string(value);
default:
return 'Unknown';
}
}
}
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;

View file

@ -7,9 +7,9 @@ import funkin.ui.story.ScriptedLevel;
class LevelRegistry extends BaseRegistry<Level, LevelData>
{
/**
* The current version string for the stage data format.
* The current version string for the level data format.
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
* and adding migration to the `migrateLevelData()` function.
*/
public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.0";

View file

@ -1,6 +1,7 @@
package funkin.data.song;
import funkin.data.event.SongEventRegistry;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.song.SongRegistry;
import thx.semver.Version;
@ -702,6 +703,11 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
}
}
public inline function getHandler():Null<SongEvent>
{
return SongEventRegistry.getEvent(this.event);
}
public inline function getSchema():Null<SongEventSchema>
{
return SongEventRegistry.getEventSchema(this.event);
@ -752,6 +758,39 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public function buildTooltip():String
{
var eventHandler = getHandler();
var eventSchema = getSchema();
if (eventSchema == null) return 'Unknown Event: ${this.event}';
var result = '${eventHandler.getTitle()}';
var defaultKey = eventSchema.getFirstField()?.name;
var valueStruct:haxe.DynamicAccess<Dynamic> = valueAsStruct(defaultKey);
for (pair in valueStruct.keyValueIterator())
{
var key = pair.key;
var value = pair.value;
var title = eventSchema.getByName(key)?.title ?? 'UnknownField';
if (eventSchema.stringifyFieldValue(key, value) != null) trace(eventSchema.stringifyFieldValue(key, value));
var valueStr = eventSchema.stringifyFieldValue(key, value) ?? 'UnknownValue';
result += '\n- ${title}: ${valueStr}';
}
return result;
}
public function clone():SongEventData
{
return new SongEventData(this.time, this.event, this.value);
}
@:op(A == B)
public function op_equals(other:SongEventData):Bool
{

View file

@ -127,7 +127,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
switch (loadEntryMetadataFile(id, variation))
{
@ -150,7 +150,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
@ -210,7 +210,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
switch (loadEntryMetadataFile(id, variation))
{
@ -232,7 +232,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
switch (loadEntryMetadataFile(id, variation))
{
@ -252,7 +252,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
{
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
@ -266,7 +266,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
{
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
@ -347,7 +347,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongChartData>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
switch (loadEntryChartFile(id, variation))
{
@ -370,7 +370,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongChartData>();
parser.ignoreUnknownVariables = false;
parser.ignoreUnknownVariables = true;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)

View file

View file

@ -0,0 +1,199 @@
package funkin.data.stage;
import funkin.data.animation.AnimationData;
@:nullSafety
class StageData
{
/**
* The sematic version number of the stage data JSON format.
* Supports fancy comparisons like NPM does it's neat.
*/
@:default(funkin.data.stage.StageRegistry.STAGE_DATA_VERSION)
public var version:String;
public var name:String = 'Unknown';
public var props:Array<StageDataProp> = [];
public var characters:StageDataCharacters;
@:default(1.0)
@:optional
public var cameraZoom:Null<Float>;
public function new()
{
this.version = StageRegistry.STAGE_DATA_VERSION;
this.characters = makeDefaultCharacters();
}
function makeDefaultCharacters():StageDataCharacters
{
return {
bf:
{
zIndex: 0,
position: [0, 0],
cameraOffsets: [-100, -100]
},
dad:
{
zIndex: 0,
position: [0, 0],
cameraOffsets: [100, -100]
},
gf:
{
zIndex: 0,
position: [0, 0],
cameraOffsets: [0, 0]
}
};
}
/**
* Convert this StageData into a JSON string.
*/
public function serialize(pretty:Bool = true):String
{
var writer = new json2object.JsonWriter<StageData>();
return writer.write(this, pretty ? ' ' : null);
}
}
typedef StageDataCharacters =
{
var bf:StageDataCharacter;
var dad:StageDataCharacter;
var gf:StageDataCharacter;
};
typedef StageDataProp =
{
/**
* The name of the prop for later lookup by scripts.
* Optional; if unspecified, the prop can't be referenced by scripts.
*/
@:optional
var name:String;
/**
* The asset used to display the prop.
* NOTE: As of Stage data v1.0.1, you can also use a color here to create a rectangle, like "#ff0000".
* In this case, the `scale` property will be used to determine the size of the prop.
*/
var assetPath:String;
/**
* The position of the prop as an [x, y] array of two floats.
*/
var position:Array<Float>;
/**
* A number determining the stack order of the prop, relative to other props and the characters in the stage.
* Props with lower numbers render below those with higher numbers.
* This is just like CSS, it isn't hard.
* @default 0
*/
@:optional
@:default(0)
var zIndex:Int;
/**
* If set to true, anti-aliasing will be forcibly disabled on the sprite.
* This prevents blurry images on pixel-art levels.
* @default false
*/
@:optional
@:default(false)
var isPixel:Bool;
/**
* Either the scale of the prop as a float, or the [w, h] scale as an array of two floats.
* Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory.
*/
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
@:optional
var scale:haxe.ds.Either<Float, Array<Float>>;
/**
* The alpha of the prop, as a float.
* @default 1.0
*/
@:optional
@:default(1.0)
var alpha:Float;
/**
* If not zero, this prop will play an animation every X beats of the song.
* This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
* they will alternated between, otherwise the `idle` animation will be used.
*
* @default 0
*/
@:default(0)
@:optional
var danceEvery:Int;
/**
* How much the prop scrolls relative to the camera. Used to create a parallax effect.
* Represented as an [x, y] array of two floats.
* [1, 1] means the prop moves 1:1 with the camera.
* [0.5, 0.5] means the prop half as much as the camera.
* [0, 0] means the prop is not moved.
* @default [0, 0]
*/
@:optional
@:default([0, 0])
var scroll:Array<Float>;
/**
* An optional array of animations which the prop can play.
* @default Prop has no animations.
*/
@:optional
@:default([])
var animations:Array<AnimationData>;
/**
* If animations are used, this is the name of the animation to play first.
* @default Don't play an animation.
*/
@:optional
var startingAnimation:Null<String>;
/**
* The animation type to use.
* Options: "sparrow", "packer"
* @default "sparrow"
*/
@:default("sparrow")
@:optional
var animType:String;
};
typedef StageDataCharacter =
{
/**
* A number determining the stack order of the character, relative to props and other characters in the stage.
* Again, just like CSS.
* @default 0
*/
@:optional
@:default(0)
var zIndex:Int;
/**
* The position to render the character at.
*/
@:optional
@:default([0, 0])
var position:Array<Float>;
/**
* The camera offsets to apply when focusing on the character on this stage.
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
*/
@:optional
var cameraOffsets:Array<Float>;
};

View file

@ -0,0 +1,103 @@
package funkin.data.stage;
import funkin.data.stage.StageData;
import funkin.play.stage.Stage;
import funkin.play.stage.ScriptedStage;
class StageRegistry extends BaseRegistry<Stage, StageData>
{
/**
* The current version string for the stage data format.
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.1";
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final instance:StageRegistry = new StageRegistry();
public function new()
{
super('STAGE', 'stages', STAGE_DATA_VERSION_RULE);
}
/**
* Read, parse, and validate the JSON data and produce the corresponding data object.
*/
public function parseEntryData(id:String):Null<StageData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<StageData>();
parser.ignoreUnknownVariables = false;
switch (loadEntryFile(id))
{
case {fileName: fileName, contents: contents}:
parser.fromJson(contents, fileName);
default:
return null;
}
if (parser.errors.length > 0)
{
printErrors(parser.errors, id);
return null;
}
return parser.value;
}
/**
* Parse and validate the JSON data and produce the corresponding data object.
*
* NOTE: Must be implemented on the implementation class.
* @param contents The JSON as a string.
* @param fileName An optional file name for error reporting.
*/
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<StageData>
{
var parser = new json2object.JsonParser<StageData>();
parser.ignoreUnknownVariables = false;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
return parser.value;
}
function createScriptedEntry(clsName:String):Stage
{
return ScriptedStage.init(clsName, "unknown");
}
function getScriptedClassNames():Array<String>
{
return ScriptedStage.listScriptClasses();
}
/**
* A list of all the stages from the base game, in order.
* TODO: Should this be hardcoded?
*/
public function listBaseGameStageIds():Array<String>
{
return [
"mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets",
"phillyBlazin",
];
}
/**
* A list of all installed story weeks that are not from the base game.
*/
public function listModdedStageIds():Array<String>
{
return listEntryIds().filter(function(id:String):Bool {
return listBaseGameStageIds().indexOf(id) == -1;
});
}
}

View file

@ -0,0 +1,53 @@
package funkin.graphics;
import flixel.FlxSprite;
import flixel.util.FlxColor;
import flixel.graphics.FlxGraphic;
/**
* An FlxSprite with additional functionality.
*/
class FunkinSprite extends FlxSprite
{
/**
* @param x Starting X position
* @param y Starting Y position
*/
public function new(?x:Float = 0, ?y:Float = 0)
{
super(x, y);
}
/**
* Acts similarly to `makeGraphic`, but with improved memory usage,
* at the expense of not being able to paint onto the sprite.
*
* @param width The target width of the sprite.
* @param height The target height of the sprite.
* @param color The color to fill the sprite with.
*/
public function makeSolidColor(width:Int, height:Int, color:FlxColor = FlxColor.WHITE):FunkinSprite
{
var graphic:FlxGraphic = FlxG.bitmap.create(2, 2, color, false, 'solid#${color.toHexString(true, false)}');
frames = graphic.imageFrame;
scale.set(width / 2, height / 2);
updateHitbox();
return this;
}
/**
* Ensure scale is applied when cloning a sprite.
* The default `clone()` method acts kinda weird TBH.
* @return A clone of this sprite.
*/
public override function clone():FunkinSprite
{
var result = new FunkinSprite(this.x, this.y);
result.frames = this.frames;
result.scale.set(this.scale.x, this.scale.y);
result.updateHitbox();
return result;
}
}

View file

@ -4,11 +4,12 @@ import funkin.util.macro.ClassMacro;
import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.data.song.SongData;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import polymod.Polymod;
import polymod.backends.PolymodAssets.PolymodAssetType;
import polymod.format.ParseRules.TextFileFormat;
import funkin.data.event.SongEventRegistry;
import funkin.data.stage.StageRegistry;
import funkin.util.FileUtil;
import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
@ -275,7 +276,7 @@ class PolymodHandler
ConversationDataParser.loadConversationCache();
DialogueBoxDataParser.loadDialogueBoxCache();
SpeakerDataParser.loadSpeakerCache();
StageDataParser.loadStageCache();
StageRegistry.instance.loadEntries();
CharacterDataParser.loadCharacterCache();
ModuleHandler.loadModuleCache();
}

View file

@ -7,6 +7,7 @@ import flixel.sound.FlxSound;
import funkin.ui.story.StoryMenuState;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatSubState;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
@ -22,6 +23,12 @@ import funkin.play.character.BaseCharacter;
*/
class GameOverSubState extends MusicBeatSubState
{
/**
* The currently active GameOverSubState.
* There should be only one GameOverSubState in existance at a time, we can use a singleton.
*/
public static var instance:GameOverSubState = null;
/**
* Which alternate animation on the character to use.
* You can set this via script.
@ -87,6 +94,13 @@ class GameOverSubState extends MusicBeatSubState
override public function create()
{
if (instance != null)
{
// TODO: Do something in this case? IDK.
trace('WARNING: GameOverSubState instance already exists. This should not happen.');
}
instance = this;
super.create();
//
@ -94,7 +108,7 @@ class GameOverSubState extends MusicBeatSubState
//
// Add a black background to the screen.
var bg = new FlxSprite().makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
// We make this transparent so that we can see the stage underneath during debugging,
// but it's normally opaque.
bg.alpha = transparent ? 0.25 : 1.0;
@ -282,10 +296,10 @@ class GameOverSubState extends MusicBeatSubState
*/
function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void
{
var musicPath = Paths.music('gameOver' + musicSuffix);
var musicPath = Paths.music('gameplay/gameover/gameOver' + musicSuffix);
if (isEnding)
{
musicPath = Paths.music('gameOverEnd' + musicSuffix);
musicPath = Paths.music('gameplay/gameover/gameOverEnd' + musicSuffix);
}
if (!gameOverMusic.playing || force)
{
@ -305,7 +319,7 @@ class GameOverSubState extends MusicBeatSubState
public static function playBlueBalledSFX()
{
blueballed = true;
FlxG.sound.play(Paths.sound('fnf_loss_sfx' + blueBallSuffix));
FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
}
var playingJeffQuote:Bool = false;
@ -328,6 +342,11 @@ class GameOverSubState extends MusicBeatSubState
}
});
}
public override function toString():String
{
return "GameOverSubState";
}
}
typedef GameOverParams =

View file

@ -50,11 +50,11 @@ import funkin.play.notes.SustainTrail;
import funkin.play.scoring.Scoring;
import funkin.play.song.Song;
import funkin.data.song.SongRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.play.stage.Stage;
import funkin.play.stage.StageData.StageDataParser;
import funkin.ui.transition.LoadingState;
import funkin.play.components.PopUpStuff;
import funkin.ui.options.PreferencesMenu;
@ -1353,7 +1353,8 @@ class PlayState extends MusicBeatSubState
*/
function loadStage(id:String):Void
{
currentStage = StageDataParser.fetchStage(id);
currentStage = StageRegistry.instance.fetchEntry(id);
currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory.
if (currentStage != null)
{

View file

@ -9,6 +9,7 @@ import flixel.math.FlxMath;
import flixel.math.FlxPoint.FlxCallbackPoint;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import funkin.graphics.FunkinSprite;
import flixel.system.FlxAssets.FlxGraphicAsset;
import flixel.util.FlxColor;
import flixel.util.FlxDestroyUtil;
@ -621,7 +622,7 @@ class AnimateAtlasCharacter extends BaseCharacter
* This functionality isn't supported in SpriteGroup
* @return this sprite group
*/
public override function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite
public override function loadGraphicFromSprite(Sprite:FlxSprite):FunkinSprite
{
#if FLX_DEBUG
throw "This function is not supported in FlxSpriteGroup";

View file

@ -5,13 +5,16 @@ import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint;
import flixel.system.FlxAssets.FlxShader;
import flixel.util.FlxSort;
import flixel.util.FlxColor;
import funkin.modding.IScriptedClass;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventType;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter;
import funkin.play.stage.StageData.StageDataCharacter;
import funkin.play.stage.StageData.StageDataParser;
import funkin.data.IRegistryEntry;
import funkin.data.stage.StageData;
import funkin.data.stage.StageData.StageDataCharacter;
import funkin.data.stage.StageRegistry;
import funkin.play.stage.StageProp;
import funkin.util.SortUtil;
import funkin.util.assets.FlxAnimationUtil;
@ -23,14 +26,25 @@ typedef StagePropGroup = FlxTypedSpriteGroup<StageProp>;
*
* A Stage is comprised of one or more props, each of which is a FlxSprite.
*/
class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements IRegistryEntry<StageData>
{
public final stageId:String;
public final stageName:String;
public final id:String;
final _data:StageData;
public final _data:StageData;
public var camZoom:Float = 1.0;
public var stageName(get, never):String;
function get_stageName():String
{
return _data?.name ?? 'Unknown';
}
public var camZoom(get, never):Float;
function get_camZoom():Float
{
return _data?.cameraZoom ?? 1.0;
}
var namedProps:Map<String, StageProp> = new Map<String, StageProp>();
var characters:Map<String, BaseCharacter> = new Map<String, BaseCharacter>();
@ -41,21 +55,18 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
* They're used to cache the data needed to build the stage,
* then accessed and fleshed out when the stage needs to be built.
*
* @param stageId
* @param id
*/
public function new(stageId:String)
public function new(id:String)
{
super();
this.stageId = stageId;
_data = StageDataParser.parseStageData(this.stageId);
this.id = id;
_data = _fetchData(id);
if (_data == null)
{
throw 'Could not find stage data for stageId: $stageId';
}
else
{
this.stageName = _data.name;
throw 'Could not find stage data for stage id: $id';
}
}
@ -129,9 +140,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
*/
function buildStage():Void
{
trace('Building stage for display: ${this.stageId}');
this.camZoom = _data.cameraZoom;
trace('Building stage for display: ${this.id}');
this.debugIconGroup = new FlxSpriteGroup();
@ -139,6 +148,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
{
trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})');
var isSolidColor = dataProp.assetPath.startsWith('#');
var isAnimated = dataProp.animations.length > 0;
var propSprite:StageProp;
@ -162,6 +172,22 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath);
}
}
else if (isSolidColor)
{
var width:Int = 1;
var height:Int = 1;
switch (dataProp.scale)
{
case Left(value):
width = Std.int(value);
height = Std.int(value);
case Right(values):
width = Std.int(values[0]);
height = Std.int(values[1]);
}
propSprite.makeSolidColor(width, height, FlxColor.fromString(dataProp.assetPath));
}
else
{
// Initalize static sprite.
@ -177,13 +203,16 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
continue;
}
switch (dataProp.scale)
if (!isSolidColor)
{
case Left(value):
propSprite.scale.set(value);
switch (dataProp.scale)
{
case Left(value):
propSprite.scale.set(value);
case Right(values):
propSprite.scale.set(values[0], values[1]);
case Right(values):
propSprite.scale.set(values[0], values[1]);
}
}
propSprite.updateHitbox();
@ -195,15 +224,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
// If pixel, disable antialiasing.
propSprite.antialiasing = !dataProp.isPixel;
switch (dataProp.scroll)
{
case Left(value):
propSprite.scrollFactor.x = value;
propSprite.scrollFactor.y = value;
case Right(values):
propSprite.scrollFactor.x = values[0];
propSprite.scrollFactor.y = values[1];
}
propSprite.scrollFactor.x = dataProp.scroll[0];
propSprite.scrollFactor.y = dataProp.scroll[1];
propSprite.zIndex = dataProp.zIndex;
@ -731,6 +753,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
return Sprite;
}
static function _fetchData(id:String):Null<StageData>
{
return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id));
}
public function onScriptEvent(event:ScriptEvent) {}
public function onPause(event:PauseScriptEvent) {}

View file

@ -1,548 +0,0 @@
package funkin.play.stage;
import funkin.data.animation.AnimationData;
import funkin.play.stage.ScriptedStage;
import funkin.play.stage.Stage;
import funkin.util.VersionUtil;
import funkin.util.assets.DataAssets;
import haxe.Json;
import openfl.Assets;
/**
* Contains utilities for loading and parsing stage data.
*/
class StageDataParser
{
/**
* The current version string for the stage data format.
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
public static final STAGE_DATA_VERSION:String = "1.0.0";
/**
* The current version rule check for the stage data format.
*/
public static final STAGE_DATA_VERSION_RULE:String = "1.0.x";
static final stageCache:Map<String, Stage> = new Map<String, Stage>();
static final DEFAULT_STAGE_ID = 'UNKNOWN';
/**
* Parses and preloads the game's stage data and scripts when the game starts.
*
* If you want to force stages to be reloaded, you can just call this function again.
*/
public static function loadStageCache():Void
{
// Clear any stages that are cached if there were any.
clearStageCache();
trace("Loading stage cache...");
//
// SCRIPTED STAGES
//
var scriptedStageClassNames:Array<String> = ScriptedStage.listScriptClasses();
trace(' Instantiating ${scriptedStageClassNames.length} scripted stages...');
for (stageCls in scriptedStageClassNames)
{
var stage:Stage = ScriptedStage.init(stageCls, DEFAULT_STAGE_ID);
if (stage != null)
{
trace(' Loaded scripted stage: ${stage.stageName}');
// Disable the rendering logic for stage until it's loaded.
// Note that kill() =/= destroy()
stage.kill();
// Then store it.
stageCache.set(stage.stageId, stage);
}
else
{
trace(' Failed to instantiate scripted stage class: ${stageCls}');
}
}
//
// UNSCRIPTED STAGES
//
var stageIdList:Array<String> = DataAssets.listDataFilesInPath('stages/');
var unscriptedStageIds:Array<String> = stageIdList.filter(function(stageId:String):Bool {
return !stageCache.exists(stageId);
});
trace(' Instantiating ${unscriptedStageIds.length} non-scripted stages...');
for (stageId in unscriptedStageIds)
{
var stage:Stage;
try
{
stage = new Stage(stageId);
if (stage != null)
{
trace(' Loaded stage data: ${stage.stageName}');
stageCache.set(stageId, stage);
}
}
catch (e)
{
trace(' An error occurred while loading stage data: ${stageId}');
// Assume error was already logged.
continue;
}
}
trace(' Successfully loaded ${Lambda.count(stageCache)} stages.');
}
public static function fetchStage(stageId:String):Null<Stage>
{
if (stageCache.exists(stageId))
{
trace('Successfully fetch stage: ${stageId}');
var stage:Stage = stageCache.get(stageId);
stage.revive();
return stage;
}
else
{
trace('Failed to fetch stage, not found in cache: ${stageId}');
return null;
}
}
static function clearStageCache():Void
{
if (stageCache != null)
{
for (stage in stageCache)
{
stage.destroy();
}
stageCache.clear();
}
}
/**
* Load a stage's JSON file, parse its data, and return it.
*
* @param stageId The stage to load.
* @return The stage data, or null if validation failed.
*/
public static function parseStageData(stageId:String):Null<StageData>
{
var rawJson:String = loadStageFile(stageId);
var stageData:StageData = migrateStageData(rawJson, stageId);
return validateStageData(stageId, stageData);
}
public static function listStageIds():Array<String>
{
return stageCache.keys().array();
}
static function loadStageFile(stagePath:String):String
{
var stageFilePath:String = Paths.json('stages/${stagePath}');
var rawJson = Assets.getText(stageFilePath).trim();
while (!rawJson.endsWith("}"))
{
rawJson = rawJson.substr(0, rawJson.length - 1);
}
return rawJson;
}
static function migrateStageData(rawJson:String, stageId:String):Null<StageData>
{
// If you update the stage data format in a breaking way,
// handle migration here by checking the `version` value.
try
{
var parser = new json2object.JsonParser<StageData>();
parser.ignoreUnknownVariables = false;
parser.fromJson(rawJson, '$stageId.json');
if (parser.errors.length > 0)
{
trace('[STAGE] Failed to parse stage data');
for (error in parser.errors)
funkin.data.DataError.printError(error);
return null;
}
return parser.value;
}
catch (e)
{
trace(' Error parsing data for stage: ${stageId}');
trace(' ${e}');
return null;
}
}
static final DEFAULT_ANIMTYPE:String = "sparrow";
static final DEFAULT_CAMERAZOOM:Float = 1.0;
static final DEFAULT_DANCEEVERY:Int = 0;
static final DEFAULT_ISPIXEL:Bool = false;
static final DEFAULT_NAME:String = "Untitled Stage";
static final DEFAULT_OFFSETS:Array<Float> = [0, 0];
static final DEFAULT_CAMERA_OFFSETS_BF:Array<Float> = [-100, -100];
static final DEFAULT_CAMERA_OFFSETS_DAD:Array<Float> = [150, -100];
static final DEFAULT_POSITION:Array<Float> = [0, 0];
static final DEFAULT_SCALE:Float = 1.0;
static final DEFAULT_ALPHA:Float = 1.0;
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
static final DEFAULT_ZINDEX:Int = 0;
static final DEFAULT_CHARACTER_DATA:StageDataCharacter =
{
zIndex: DEFAULT_ZINDEX,
position: DEFAULT_POSITION,
cameraOffsets: DEFAULT_OFFSETS,
}
/**
* Set unspecified parameters to their defaults.
* If the parameter is mandatory, print an error message.
* @param id
* @param input
* @return The validated stage data
*/
static function validateStageData(id:String, input:StageData):Null<StageData>
{
if (input == null)
{
trace('ERROR: Could not parse stage data for "${id}".');
return null;
}
if (input.version == null)
{
trace('ERROR: Could not load stage data for "$id": missing version');
return null;
}
if (!VersionUtil.validateVersionStr(input.version, STAGE_DATA_VERSION_RULE))
{
trace('ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})');
return null;
}
if (input.name == null)
{
trace('WARN: Stage data for "$id" missing name');
input.name = DEFAULT_NAME;
}
if (input.cameraZoom == null)
{
input.cameraZoom = DEFAULT_CAMERAZOOM;
}
if (input.props == null)
{
input.props = [];
}
for (inputProp in input.props)
{
// It's fine for inputProp.name to be null
if (inputProp.assetPath == null)
{
trace('ERROR: Could not load stage data for "$id": missing assetPath for prop "${inputProp.name}"');
return null;
}
if (inputProp.position == null)
{
inputProp.position = DEFAULT_POSITION;
}
if (inputProp.zIndex == null)
{
inputProp.zIndex = DEFAULT_ZINDEX;
}
if (inputProp.isPixel == null)
{
inputProp.isPixel = DEFAULT_ISPIXEL;
}
if (inputProp.danceEvery == null)
{
inputProp.danceEvery = DEFAULT_DANCEEVERY;
}
if (inputProp.animType == null)
{
inputProp.animType = DEFAULT_ANIMTYPE;
}
switch (inputProp.scale)
{
case null:
inputProp.scale = Right([DEFAULT_SCALE, DEFAULT_SCALE]);
case Left(value):
inputProp.scale = Right([value, value]);
case Right(_):
// Do nothing
}
switch (inputProp.scroll)
{
case null:
inputProp.scroll = Right(DEFAULT_SCROLL);
case Left(value):
inputProp.scroll = Right([value, value]);
case Right(_):
// Do nothing
}
if (inputProp.alpha == null)
{
inputProp.alpha = DEFAULT_ALPHA;
}
if (inputProp.animations == null)
{
inputProp.animations = [];
}
if (inputProp.animations.length == 0 && inputProp.startingAnimation != null)
{
trace('ERROR: Could not load stage data for "$id": missing animations for prop "${inputProp.name}"');
return null;
}
for (inputAnimation in inputProp.animations)
{
if (inputAnimation.name == null)
{
trace('ERROR: Could not load stage data for "$id": missing animation name for prop "${inputProp.name}"');
return null;
}
if (inputAnimation.frameRate == null)
{
inputAnimation.frameRate = 24;
}
if (inputAnimation.offsets == null)
{
inputAnimation.offsets = DEFAULT_OFFSETS;
}
if (inputAnimation.looped == null)
{
inputAnimation.looped = true;
}
if (inputAnimation.flipX == null)
{
inputAnimation.flipX = false;
}
if (inputAnimation.flipY == null)
{
inputAnimation.flipY = false;
}
}
}
if (input.characters == null)
{
trace('ERROR: Could not load stage data for "$id": missing characters');
return null;
}
if (input.characters.bf == null)
{
input.characters.bf = DEFAULT_CHARACTER_DATA;
}
if (input.characters.dad == null)
{
input.characters.dad = DEFAULT_CHARACTER_DATA;
}
if (input.characters.gf == null)
{
input.characters.gf = DEFAULT_CHARACTER_DATA;
}
for (inputCharacter in [input.characters.bf, input.characters.dad, input.characters.gf])
{
if (inputCharacter.position == null || inputCharacter.position.length != 2)
{
inputCharacter.position = [0, 0];
}
}
// All good!
return input;
}
}
class StageData
{
/**
* The sematic version number of the stage data JSON format.
* Supports fancy comparisons like NPM does it's neat.
*/
public var version:String;
public var name:String;
public var cameraZoom:Null<Float>;
public var props:Array<StageDataProp>;
public var characters:StageDataCharacters;
public function new()
{
this.version = StageDataParser.STAGE_DATA_VERSION;
}
/**
* Convert this StageData into a JSON string.
*/
public function serialize(pretty:Bool = true):String
{
var writer = new json2object.JsonWriter<StageData>();
return writer.write(this, pretty ? ' ' : null);
}
}
typedef StageDataCharacters =
{
var bf:StageDataCharacter;
var dad:StageDataCharacter;
var gf:StageDataCharacter;
};
typedef StageDataProp =
{
/**
* The name of the prop for later lookup by scripts.
* Optional; if unspecified, the prop can't be referenced by scripts.
*/
@:optional
var name:String;
/**
* The asset used to display the prop.
*/
var assetPath:String;
/**
* The position of the prop as an [x, y] array of two floats.
*/
var position:Array<Float>;
/**
* A number determining the stack order of the prop, relative to other props and the characters in the stage.
* Props with lower numbers render below those with higher numbers.
* This is just like CSS, it isn't hard.
* @default 0
*/
@:optional
@:default(0)
var zIndex:Int;
/**
* If set to true, anti-aliasing will be forcibly disabled on the sprite.
* This prevents blurry images on pixel-art levels.
* @default false
*/
@:optional
@:default(false)
var isPixel:Bool;
/**
* Either the scale of the prop as a float, or the [w, h] scale as an array of two floats.
* Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory.
*/
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
@:optional
var scale:haxe.ds.Either<Float, Array<Float>>;
/**
* The alpha of the prop, as a float.
* @default 1.0
*/
@:optional
@:default(1.0)
var alpha:Float;
/**
* If not zero, this prop will play an animation every X beats of the song.
* This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
* they will alternated between, otherwise the `idle` animation will be used.
*
* @default 0
*/
@:default(0)
@:optional
var danceEvery:Int;
/**
* How much the prop scrolls relative to the camera. Used to create a parallax effect.
* Represented as a float or as an [x, y] array of two floats.
* [1, 1] means the prop moves 1:1 with the camera.
* [0.5, 0.5] means the prop half as much as the camera.
* [0, 0] means the prop is not moved.
* @default [0, 0]
*/
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
@:optional
var scroll:haxe.ds.Either<Float, Array<Float>>;
/**
* An optional array of animations which the prop can play.
* @default Prop has no animations.
*/
@:optional
var animations:Array<AnimationData>;
/**
* If animations are used, this is the name of the animation to play first.
* @default Don't play an animation.
*/
@:optional
var startingAnimation:Null<String>;
/**
* The animation type to use.
* Options: "sparrow", "packer"
* @default "sparrow"
*/
@:default("sparrow")
@:optional
var animType:String;
};
typedef StageDataCharacter =
{
/**
* A number determining the stack order of the character, relative to props and other characters in the stage.
* Again, just like CSS.
* @default 0
*/
var zIndex:Int;
/**
* The position to render the character at.
*/
var position:Array<Float>;
/**
* The camera offsets to apply when focusing on the character on this stage.
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
*/
var cameraOffsets:Array<Float>;
};

View file

@ -1,10 +1,10 @@
package funkin.play.stage;
import funkin.modding.events.ScriptEvent;
import flixel.FlxSprite;
import funkin.graphics.FunkinSprite;
import funkin.modding.IScriptedClass.IStateStageProp;
class StageProp extends FlxSprite implements IStateStageProp
class StageProp extends FunkinSprite implements IStateStageProp
{
/**
* An internal name for this prop.

View file

@ -12,6 +12,7 @@ import flixel.FlxCamera;
import flixel.FlxSprite;
import flixel.FlxSubState;
import flixel.group.FlxSpriteGroup;
import funkin.graphics.FunkinSprite;
import flixel.input.keyboard.FlxKey;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
@ -34,6 +35,7 @@ import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongMetadata;
import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongOffsets;
import funkin.data.song.SongDataUtils;
@ -56,7 +58,7 @@ import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongDataUtils;
import funkin.ui.debug.charting.commands.ChartEditorCommand;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import funkin.save.Save;
import funkin.ui.debug.charting.commands.AddEventsCommand;
import funkin.ui.debug.charting.commands.AddNotesCommand;
@ -104,6 +106,7 @@ import haxe.ui.components.Label;
import haxe.ui.components.Button;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
import haxe.ui.components.VerticalSlider;
import haxe.ui.components.TextField;
import haxe.ui.containers.dialogs.CollapsibleDialog;
import haxe.ui.containers.Frame;
@ -720,6 +723,34 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return hitsoundsEnabledPlayer || hitsoundsEnabledOpponent;
}
/**
* Sound multiplier for vocals and hitsounds on the player's side.
*/
var soundMultiplierPlayer(default, set):Float = 1.0;
function set_soundMultiplierPlayer(value:Float):Float
{
soundMultiplierPlayer = value;
var vocalTargetVolume:Float = (menubarItemVolumeVocals.value ?? 100.0) / 100.0;
if (audioVocalTrackGroup != null) audioVocalTrackGroup.playerVolume = vocalTargetVolume * soundMultiplierPlayer;
return soundMultiplierPlayer;
}
/**
* Sound multiplier for vocals and hitsounds on the opponent's side.
*/
var soundMultiplierOpponent(default, set):Float = 1.0;
function set_soundMultiplierOpponent(value:Float):Float
{
soundMultiplierOpponent = value;
var vocalTargetVolume:Float = (menubarItemVolumeVocals.value ?? 100.0) / 100.0;
if (audioVocalTrackGroup != null) audioVocalTrackGroup.opponentVolume = vocalTargetVolume * soundMultiplierOpponent;
return soundMultiplierOpponent;
}
// Auto-save
/**
@ -1749,6 +1780,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var buttonSelectEvent:Button;
/**
* The slider above the grid that sets the volume of the player's sounds.
* Constructed manually and added to the layout so we can control its position.
*/
var sliderVolumePlayer:Slider;
/**
* The slider above the grid that sets the volume of the opponent's sounds.
* Constructed manually and added to the layout so we can control its position.
*/
var sliderVolumeOpponent:Slider;
/**
* RENDER OBJECTS
*/
@ -1958,7 +2001,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
buildGrid();
buildMeasureTicks();
buildNotePreview();
buildSelectionBox();
buildAdditionalUI();
populateOpenRecentMenu();
@ -2214,7 +2256,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
add(gridGhostHoldNote);
gridGhostHoldNote.zIndex = 11;
gridGhostEvent = new ChartEditorEventSprite(this);
gridGhostEvent = new ChartEditorEventSprite(this, true);
gridGhostEvent.alpha = 0.6;
gridGhostEvent.eventData = new SongEventData(-1, '', {});
gridGhostEvent.visible = false;
@ -2230,7 +2272,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2);
var playheadBaseYPos:Float = GRID_INITIAL_Y_POS;
gridPlayhead.setPosition(GRID_X_POS, playheadBaseYPos);
var playheadSprite:FlxSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR);
var playheadSprite:FunkinSprite = new FunkinSprite().makeSolidColor(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR);
playheadSprite.x = -PLAYHEAD_SCROLL_AREA_WIDTH;
playheadSprite.y = 0;
gridPlayhead.add(playheadSprite);
@ -2287,17 +2329,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
}
function buildSelectionBox():Void
{
if (selectionBoxSprite == null) throw 'ERROR: Tried to build selection box, but selectionBoxSprite is null! Check ChartEditorThemeHandler.updateTheme().';
selectionBoxSprite.scrollFactor.set(0, 0);
add(selectionBoxSprite);
selectionBoxSprite.zIndex = 30;
setSelectionBoxBounds();
}
function setSelectionBoxBounds(bounds:FlxRect = null):Void
{
if (selectionBoxSprite == null)
@ -2319,6 +2350,19 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
}
/**
* Automatically goes through and calls render on everything you added.
*/
override public function draw():Void
{
if (selectionBoxStartPos != null)
{
trace('selectionBoxSprite: ${selectionBoxSprite.visible} ${selectionBoxSprite.exists} ${this.members.contains(selectionBoxSprite)}');
}
super.draw();
}
function calculateNotePreviewViewportBounds():FlxRect
{
var bounds:FlxRect = new FlxRect();
@ -2557,6 +2601,37 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
performCommand(new SetItemSelectionCommand([], currentSongChartEventData));
}
}
function setupSideSlider(x, y):VerticalSlider
{
var slider = new VerticalSlider();
slider.allowFocus = false;
slider.x = x;
slider.y = y;
slider.width = NOTE_SELECT_BUTTON_HEIGHT;
slider.height = GRID_SIZE * 4;
slider.pos = slider.max;
slider.tooltip = "Slide to set the volume of sounds on this side.";
slider.zIndex = 110;
slider.styleNames = "sideSlider";
add(slider);
return slider;
}
var sliderY = GRID_INITIAL_Y_POS + 34;
sliderVolumeOpponent = setupSideSlider(GRID_X_POS - 64, sliderY);
sliderVolumePlayer = setupSideSlider(buttonSelectEvent.x + buttonSelectEvent.width, sliderY);
sliderVolumePlayer.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0;
soundMultiplierPlayer = volume;
}
sliderVolumeOpponent.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0;
soundMultiplierOpponent = volume;
}
}
/**
@ -2797,7 +2872,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemVolumeVocals.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0;
if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
if (audioVocalTrackGroup != null)
{
audioVocalTrackGroup.playerVolume = volume * soundMultiplierPlayer;
audioVocalTrackGroup.opponentVolume = volume * soundMultiplierOpponent;
}
menubarLabelVolumeVocals.text = 'Voices - ${Std.int(event.value)}%';
}
@ -3366,6 +3445,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Setting event data resets position relative to the grid so we fix that.
eventSprite.x += renderedEvents.x;
eventSprite.y += renderedEvents.y;
eventSprite.updateTooltipPosition();
}
// Add hold notes that have been made visible (but not their parents)
@ -4653,48 +4733,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
difficultySelectDirty = false;
// Manage the Select Difficulty tree view.
var difficultyToolbox:Null<CollapsibleDialog> = this.getToolbox_OLD(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
var difficultyToolbox:ChartEditorDifficultyToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
if (difficultyToolbox == null) return;
var treeView:Null<TreeView> = difficultyToolbox.findComponent('difficultyToolboxTree');
if (treeView == null) return;
// Clear the tree view so we can rebuild it.
treeView.clearNodes();
// , icon: 'haxeui-core/styles/default/haxeui_tiny.png'
var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName'});
treeSong.expanded = true;
for (curVariation in availableVariations)
{
trace('DIFFICULTY TOOLBOX: Variation ${curVariation}');
var variationMetadata:Null<SongMetadata> = songMetadata.get(curVariation);
if (variationMetadata == null) continue;
var treeVariation:TreeViewNode = treeSong.addNode(
{
id: 'stv_variation_$curVariation',
text: 'V: ${curVariation.toTitleCase()}'
});
treeVariation.expanded = true;
var difficultyList:Array<String> = variationMetadata.playData.difficulties;
for (difficulty in difficultyList)
{
trace('DIFFICULTY TOOLBOX: Difficulty ${curVariation}_$difficulty');
var _treeDifficulty:TreeViewNode = treeVariation.addNode(
{
id: 'stv_difficulty_${curVariation}_$difficulty',
text: 'D: ${difficulty.toTitleCase()}'
});
}
}
treeView.onChange = onChangeTreeDifficulty;
refreshDifficultyTreeSelection(treeView);
difficultyToolbox.updateTree();
}
}
@ -5196,6 +5238,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
autoSave(true);
stopWelcomeMusic();
stopAudioPlayback();
var startTimestamp:Float = 0;
if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs;
@ -5439,7 +5482,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return event != null && currentEventSelection.indexOf(event) != -1;
}
function createDifficulty(variation:String, difficulty:String, scrollSpeed:Float = 1.0)
function createDifficulty(variation:String, difficulty:String, scrollSpeed:Float = 1.0):Void
{
var variationMetadata:Null<SongMetadata> = songMetadata.get(variation);
if (variationMetadata == null) return;
@ -5461,6 +5504,42 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
difficultySelectDirty = true; // Force the Difficulty toolbox to update.
}
function removeDifficulty(variation:String, difficulty:String):Void
{
var variationMetadata:Null<SongMetadata> = songMetadata.get(variation);
if (variationMetadata == null) return;
variationMetadata.playData.difficulties.remove(difficulty);
var resultChartData = songChartData.get(variation);
if (resultChartData != null)
{
resultChartData.scrollSpeed.remove(difficulty);
resultChartData.notes.remove(difficulty);
}
if (songMetadata.size() > 1)
{
if (variationMetadata.playData.difficulties.length == 0)
{
songMetadata.remove(variation);
songChartData.remove(variation);
}
if (variation == selectedVariation)
{
var firstVariation = songMetadata.keyValues()[0];
if (firstVariation != null) selectedVariation = firstVariation;
variationMetadata = songMetadata.get(selectedVariation);
}
}
if (selectedDifficulty == difficulty
|| !variationMetadata.playData.difficulties.contains(selectedDifficulty)) selectedDifficulty = variationMetadata.playData.difficulties[0];
difficultySelectDirty = true; // Force the Difficulty toolbox to update.
}
function incrementDifficulty(change:Int):Void
{
var currentDifficultyIndex:Int = availableDifficulties.indexOf(selectedDifficulty);
@ -5509,8 +5588,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
Conductor.instance.mapTimeChanges(this.currentSongMetadata.timeChanges);
updateTimeSignature();
refreshDifficultyTreeSelection();
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
}
else
{
@ -5518,8 +5597,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var prevDifficulty = availableDifficulties[currentDifficultyIndex - 1];
selectedDifficulty = prevDifficulty;
refreshDifficultyTreeSelection();
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
}
}
else
@ -5537,8 +5616,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var nextDifficulty = availableDifficulties[0];
selectedDifficulty = nextDifficulty;
refreshDifficultyTreeSelection();
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
}
else
{
@ -5546,7 +5625,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var nextDifficulty = availableDifficulties[currentDifficultyIndex + 1];
selectedDifficulty = nextDifficulty;
refreshDifficultyTreeSelection();
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
}
}
@ -5662,7 +5741,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
audioInstTrack.volume = instTargetVolume;
audioInstTrack.onComplete = null;
}
if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = vocalTargetVolume;
if (audioVocalTrackGroup != null)
{
audioVocalTrackGroup.playerVolume = vocalTargetVolume * soundMultiplierPlayer;
audioVocalTrackGroup.opponentVolume = vocalTargetVolume * soundMultiplierOpponent;
}
}
function updateTimeSignature():Void
@ -5678,92 +5761,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
// ==================
/**
* Set the currently selected item in the Difficulty tree view to the node representing the current difficulty.
* @param treeView The tree view to update. If `null`, the tree view will be found.
*/
function refreshDifficultyTreeSelection(?treeView:TreeView):Void
{
if (treeView == null)
{
// Manage the Select Difficulty tree view.
var difficultyToolbox:Null<CollapsibleDialog> = this.getToolbox_OLD(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
if (difficultyToolbox == null) return;
treeView = difficultyToolbox.findComponent('difficultyToolboxTree');
if (treeView == null) return;
}
var currentTreeDifficultyNode = getCurrentTreeDifficultyNode(treeView);
if (currentTreeDifficultyNode != null) treeView.selectedNode = currentTreeDifficultyNode;
}
/**
* Retrieve the node representing the current difficulty in the Difficulty tree view.
* @param treeView The tree view to search. If `null`, the tree view will be found.
* @return The node representing the current difficulty, or `null` if not found.
*/
function getCurrentTreeDifficultyNode(?treeView:TreeView = null):Null<TreeViewNode>
{
if (treeView == null)
{
var difficultyToolbox:Null<CollapsibleDialog> = this.getToolbox_OLD(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
if (difficultyToolbox == null) return null;
treeView = difficultyToolbox.findComponent('difficultyToolboxTree');
if (treeView == null) return null;
}
var result:TreeViewNode = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty',
'id');
if (result == null) return null;
return result;
}
/**
* Called when selecting a tree element in the Difficulty toolbox.
* @param event The click event.
*/
function onChangeTreeDifficulty(event:UIEvent):Void
{
// Get the newly selected node.
var treeView:TreeView = cast event.target;
var targetNode:TreeViewNode = treeView.selectedNode;
if (targetNode == null)
{
trace('No target node!');
// Reset the user's selection.
var currentTreeDifficultyNode = getCurrentTreeDifficultyNode(treeView);
if (currentTreeDifficultyNode != null) treeView.selectedNode = currentTreeDifficultyNode;
return;
}
switch (targetNode.data.id.split('_')[1])
{
case 'difficulty':
var variation:String = targetNode.data.id.split('_')[2];
var difficulty:String = targetNode.data.id.split('_')[3];
if (variation != null && difficulty != null)
{
trace('Changing difficulty to "$variation:$difficulty"');
selectedVariation = variation;
selectedDifficulty = difficulty;
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
}
// case 'song':
// case 'variation':
default:
// Reset the user's selection.
trace('Selected wrong node type, resetting selection.');
var currentTreeDifficultyNode = getCurrentTreeDifficultyNode(treeView);
if (currentTreeDifficultyNode != null) treeView.selectedNode = currentTreeDifficultyNode;
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
}
}
/**
* STATIC FUNCTIONS
*/
@ -5864,9 +5861,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
switch (noteData.getStrumlineIndex())
{
case 0: // Player
if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolume);
if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolume * soundMultiplierPlayer);
case 1: // Opponent
if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolume);
if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolume * soundMultiplierOpponent);
}
}
}

View file

@ -11,6 +11,9 @@ import flixel.graphics.frames.FlxFramesCollection;
import flixel.graphics.frames.FlxTileFrames;
import flixel.math.FlxPoint;
import funkin.data.song.SongData.SongEventData;
import haxe.ui.tooltips.ToolTipRegionOptions;
import funkin.util.HaxeUIUtil;
import haxe.ui.tooltips.ToolTipManager;
/**
* A sprite that can be used to display a song event in a chart.
@ -36,6 +39,13 @@ class ChartEditorEventSprite extends FlxSprite
public var overrideStepTime(default, set):Null<Float> = null;
public var tooltip:ToolTipRegionOptions;
/**
* Whether this sprite is a "ghost" sprite used when hovering to place a new event.
*/
public var isGhost:Bool = false;
function set_overrideStepTime(value:Null<Float>):Null<Float>
{
if (overrideStepTime == value) return overrideStepTime;
@ -45,12 +55,14 @@ class ChartEditorEventSprite extends FlxSprite
return overrideStepTime;
}
public function new(parent:ChartEditorState)
public function new(parent:ChartEditorState, isGhost:Bool = false)
{
super();
this.parentState = parent;
this.isGhost = isGhost;
this.tooltip = HaxeUIUtil.buildTooltip('N/A');
this.frames = buildFrames();
buildAnimations();
@ -142,6 +154,7 @@ class ChartEditorEventSprite extends FlxSprite
// Disown parent. MAKE SURE TO REVIVE BEFORE REUSING
this.kill();
this.visible = false;
updateTooltipPosition();
return null;
}
else
@ -151,6 +164,8 @@ class ChartEditorEventSprite extends FlxSprite
this.eventData = value;
// Update the position to match the note data.
updateEventPosition();
// Update the tooltip text.
this.tooltip.tipData = {text: this.eventData.buildTooltip()};
return this.eventData;
}
}
@ -169,6 +184,31 @@ class ChartEditorEventSprite extends FlxSprite
this.x += origin.x;
this.y += origin.y;
}
this.updateTooltipPosition();
}
public function updateTooltipPosition():Void
{
// No tooltip for ghost sprites.
if (this.isGhost) return;
if (this.eventData == null)
{
// Disable the tooltip.
ToolTipManager.instance.unregisterTooltipRegion(this.tooltip);
}
else
{
// Update the position.
this.tooltip.left = this.x;
this.tooltip.top = this.y;
this.tooltip.width = this.width;
this.tooltip.height = this.height;
// Enable the tooltip.
ToolTipManager.instance.registerTooltipRegion(this.tooltip);
}
}
/**

View file

@ -13,6 +13,7 @@ import haxe.ui.notifications.NotificationType;
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-chart.xml"))
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorUploadChartDialog extends ChartEditorBaseDialog
{
var dropHandlers:Array<DialogDropTarget> = [];

View file

@ -0,0 +1,311 @@
package funkin.ui.debug.charting.dialogs;
import funkin.input.Cursor;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogParams;
import funkin.util.FileUtil;
import funkin.play.character.CharacterData;
import haxe.io.Path;
import haxe.ui.components.Button;
import haxe.ui.components.Label;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
import haxe.ui.containers.Box;
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.core.Component;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals.xml"))
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorUploadVocalsDialog extends ChartEditorBaseDialog
{
var dropHandlers:Array<DialogDropTarget> = [];
var vocalContainer:Component;
var dialogCancel:Button;
var dialogNoVocals:Button;
var dialogContinue:Button;
var charIds:Array<String>;
var instId:String;
var hasClearedVocals:Bool = false;
public function new(state2:ChartEditorState, charIds:Array<String>, params2:DialogParams)
{
super(state2, params2);
this.charIds = charIds;
this.instId = chartEditorState.currentInstrumentalId;
dialogCancel.onClick = function(_) {
hideDialog(DialogButton.CANCEL);
}
dialogNoVocals.onClick = function(_) {
// Dismiss
chartEditorState.wipeVocalData();
hideDialog(DialogButton.APPLY);
};
dialogContinue.onClick = function(_) {
// Dismiss
hideDialog(DialogButton.APPLY);
};
buildDropHandlers();
}
function buildDropHandlers():Void
{
for (charKey in charIds)
{
trace('Adding vocal upload for character ${charKey}');
var charMetadata:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charKey);
var charName:String = charMetadata?.name ?? charKey;
var vocalsEntry = new ChartEditorUploadVocalsEntry(charName);
var dropHandler:DialogDropTarget = {component: vocalsEntry, handler: null};
var onDropFile:String->Void = function(pathStr:String) {
trace('Selected file: $pathStr');
var path:Path = new Path(pathStr);
if (chartEditorState.loadVocalsFromPath(path, charKey, this.instId, !this.hasClearedVocals))
{
this.hasClearedVocals = true;
// Tell the user the load was successful.
chartEditorState.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${chartEditorState.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
#else
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}';
#end
dialogNoVocals.hidden = true;
chartEditorState.removeDropHandler(dropHandler);
}
else
{
trace('Failed to load vocal track (${path.file}.${path.ext})');
chartEditorState.error('Failed to Load Vocals',
'Failed to load vocal track (${path.file}.${path.ext}) for variation (${chartEditorState.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
}
};
vocalsEntry.onClick = function(_event) {
Dialogs.openBinaryFile('Open $charName Vocals', [
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) {
if (selectedFile != null && selectedFile.bytes != null)
{
trace('Selected file: ' + selectedFile.name);
if (chartEditorState.loadVocalsFromBytes(selectedFile.bytes, charKey, this.instId, !this.hasClearedVocals))
{
hasClearedVocals = true;
// Tell the user the load was successful.
chartEditorState.success('Loaded Vocals',
'Loaded vocals for $charName (${selectedFile.name}), variation ${chartEditorState.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}';
#end
dialogNoVocals.hidden = true;
}
else
{
trace('Failed to load vocal track (${selectedFile.fullPath})');
chartEditorState.error('Failed to Load Vocals',
'Failed to load vocal track (${selectedFile.name}) for variation (${chartEditorState.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
}
}
});
}
dropHandler.handler = onDropFile;
// onDropFile
#if FILE_DROP_SUPPORTED
dropHandlers.push(dropHandler);
#end
vocalContainer.addComponent(vocalsEntry);
}
}
public static function build(state:ChartEditorState, charIds:Array<String>, ?closable:Bool, ?modal:Bool):ChartEditorUploadVocalsDialog
{
var dialog = new ChartEditorUploadVocalsDialog(state, charIds,
{
closable: closable ?? false,
modal: modal ?? true
});
for (dropTarget in dialog.dropHandlers)
{
state.addDropHandler(dropTarget);
}
dialog.showDialog(modal ?? true);
return dialog;
}
public override function onClose(event:DialogEvent):Void
{
super.onClose(event);
if (event.button != DialogButton.APPLY && !this.closable)
{
// User cancelled the wizard! Back to the welcome dialog.
chartEditorState.openWelcomeDialog(this.closable);
}
for (dropTarget in dropHandlers)
{
chartEditorState.removeDropHandler(dropTarget);
}
}
public override function lock():Void
{
super.lock();
this.dialogCancel.disabled = true;
}
public override function unlock():Void
{
super.unlock();
this.dialogCancel.disabled = false;
}
/**
* Called when clicking the Upload Chart box.
*/
public function onClickChartBox():Void
{
if (this.locked) return;
this.lock();
// TODO / BUG: File filtering not working on mac finder dialog, so we don't use it for now
#if !mac
FileUtil.browseForBinaryFile('Open Chart', [FileUtil.FILE_EXTENSION_INFO_FNFC], onSelectFile, onCancelBrowse);
#else
FileUtil.browseForBinaryFile('Open Chart', null, onSelectFile, onCancelBrowse);
#end
}
/**
* Called when a file is selected by dropping a file onto the Upload Chart box.
*/
function onDropFileChartBox(pathStr:String):Void
{
var path:Path = new Path(pathStr);
trace('Dropped file (${path})');
try
{
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(chartEditorState, path.toString());
if (result != null)
{
chartEditorState.success('Loaded Chart',
result.length == 0 ? 'Loaded chart (${path.toString()})' : 'Loaded chart (${path.toString()})\n${result.join("\n")}');
this.hideDialog(DialogButton.APPLY);
}
else
{
chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()})');
}
}
catch (err)
{
chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()}): ${err}');
}
}
/**
* Called when a file is selected by the dialog displayed when clicking the Upload Chart box.
*/
function onSelectFile(selectedFile:SelectedFileInfo):Void
{
this.unlock();
if (selectedFile != null && selectedFile.bytes != null)
{
try
{
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFC(chartEditorState, selectedFile.bytes);
if (result != null)
{
chartEditorState.success('Loaded Chart',
result.length == 0 ? 'Loaded chart (${selectedFile.name})' : 'Loaded chart (${selectedFile.name})\n${result.join("\n")}');
if (selectedFile.fullPath != null) chartEditorState.currentWorkingFilePath = selectedFile.fullPath;
this.hideDialog(DialogButton.APPLY);
}
}
catch (err)
{
chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${selectedFile.name}): ${err}');
}
}
}
function onCancelBrowse():Void
{
this.unlock();
}
}
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals-entry.xml"))
class ChartEditorUploadVocalsEntry extends Box
{
public var vocalsEntryLabel:Label;
var charName:String;
public function new(charName:String)
{
super();
this.charName = charName;
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
this.onMouseOver = function(_event) {
// if (this.locked) return;
this.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
}
this.onMouseOut = function(_event) {
this.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default;
}
}
}

View file

@ -13,12 +13,13 @@ import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.Song;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import funkin.ui.debug.charting.dialogs.ChartEditorAboutDialog;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget;
import funkin.ui.debug.charting.dialogs.ChartEditorCharacterIconSelectorMenu;
import funkin.ui.debug.charting.dialogs.ChartEditorUploadChartDialog;
import funkin.ui.debug.charting.dialogs.ChartEditorWelcomeDialog;
import funkin.ui.debug.charting.dialogs.ChartEditorUploadVocalsDialog;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.util.Constants;
import funkin.util.DateUtil;
@ -59,11 +60,8 @@ using Lambda;
class ChartEditorDialogHandler
{
// Paths to HaxeUI layout files for each dialog.
static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart');
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst');
static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry');
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts');
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry');
static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart');
@ -105,6 +103,56 @@ class ChartEditorDialogHandler
return dialog;
}
/**
* Builds and opens a dialog letting the user browse for a chart file to open.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog>
{
var dialog = ChartEditorUploadChartDialog.build(state, closable);
dialog.zIndex = 1000;
state.isHaxeUIDialogOpen = true;
return dialog;
}
/**
* Builds and opens a dialog where the user uploads vocals for the current song.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
var hasClearedVocals:Bool = false;
var charIdsForVocals:Array<String> = [charData.player, charData.opponent];
var dialog = ChartEditorUploadVocalsDialog.build(state, charIdsForVocals, closable);
dialog.zIndex = 1000;
state.isHaxeUIDialogOpen = true;
return dialog;
}
/**
* Builds and opens the dialog for selecting a character.
*/
public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null<Menu>
{
var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition);
menu.zIndex = 1000;
return menu;
}
/**
* Builds and opens a dialog letting the user know a backup is available, and prompting them to load it.
*/
@ -186,22 +234,6 @@ class ChartEditorDialogHandler
return dialog;
}
/**
* Builds and opens a dialog letting the user browse for a chart file to open.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog>
{
var dialog = ChartEditorUploadChartDialog.build(state, closable);
dialog.zIndex = 1000;
state.isHaxeUIDialogOpen = true;
return dialog;
}
/**
* Open the wizard for opening an existing chart from individual files.
* @param state
@ -288,15 +320,6 @@ class ChartEditorDialogHandler
};
}
public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null<Menu>
{
var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition);
menu.zIndex = 1000;
return menu;
}
public static function openCreateSongWizardBasicOnly(state:ChartEditorState, closable:Bool):Void
{
// Step 1. Song Metadata
@ -699,150 +722,6 @@ class ChartEditorDialogHandler
return dialog;
}
/**
* Builds and opens a dialog where the user uploads vocals for the current song.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
var instId:String = state.currentInstrumentalId;
var charIdsForVocals:Array<String> = [];
var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
var hasClearedVocals:Bool = false;
charIdsForVocals.push(charData.player);
charIdsForVocals.push(charData.opponent);
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable);
if (dialog == null) throw 'Could not locate Upload Vocals dialog';
var dialogContainer:Null<Component> = dialog.findComponent('vocalContainer');
if (dialogContainer == null) throw 'Could not locate vocalContainer in Upload Vocals dialog';
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Vocals dialog';
buttonCancel.onClick = function(_) {
dialog.hideDialog(DialogButton.CANCEL);
}
var dialogNoVocals:Null<Button> = dialog.findComponent('dialogNoVocals', Button);
if (dialogNoVocals == null) throw 'Could not locate dialogNoVocals button in Upload Vocals dialog';
dialogNoVocals.onClick = function(_) {
// Dismiss
state.wipeVocalData();
dialog.hideDialog(DialogButton.APPLY);
};
for (charKey in charIdsForVocals)
{
trace('Adding vocal upload for character ${charKey}');
var charMetadata:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charKey);
var charName:String = charMetadata != null ? charMetadata.name : charKey;
var vocalsEntry:Component = RuntimeComponentBuilder.fromAsset(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT);
var vocalsEntryLabel:Null<Label> = vocalsEntry.findComponent('vocalsEntryLabel', Label);
if (vocalsEntryLabel == null) throw 'Could not locate vocalsEntryLabel in Upload Vocals dialog';
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
var dropHandler:DialogDropTarget = {component: vocalsEntry, handler: null};
var onDropFile:String->Void = function(pathStr:String) {
trace('Selected file: $pathStr');
var path:Path = new Path(pathStr);
if (state.loadVocalsFromPath(path, charKey, instId, !hasClearedVocals))
{
hasClearedVocals = true;
// Tell the user the load was successful.
state.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
#else
vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}';
#end
dialogNoVocals.hidden = true;
state.removeDropHandler(dropHandler);
}
else
{
trace('Failed to load vocal track (${path.file}.${path.ext})');
state.error('Failed to Load Vocals', 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
}
};
dropHandler.handler = onDropFile;
vocalsEntry.onClick = function(_event) {
Dialogs.openBinaryFile('Open $charName Vocals', [
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) {
if (selectedFile != null && selectedFile.bytes != null)
{
trace('Selected file: ' + selectedFile.name);
if (state.loadVocalsFromBytes(selectedFile.bytes, charKey, instId, !hasClearedVocals))
{
hasClearedVocals = true;
// Tell the user the load was successful.
state.success('Loaded Vocals', 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}';
#end
dialogNoVocals.hidden = true;
}
else
{
trace('Failed to load vocal track (${selectedFile.fullPath})');
state.error('Failed to Load Vocals', 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
}
}
});
}
// onDropFile
#if FILE_DROP_SUPPORTED
addDropHandler(dropHandler);
#end
dialogContainer.addComponent(vocalsEntry);
}
var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
if (dialogContinue == null) throw 'Could not locate dialogContinue button in Upload Vocals dialog';
dialogContinue.onClick = function(_) {
// Dismiss
dialog.hideDialog(DialogButton.APPLY);
};
return dialog;
}
/**
* Builds and opens a dialog where the user upload the JSON files for a song.
* @param state The current chart editor state.

View file

@ -317,6 +317,12 @@ class ChartEditorThemeHandler
ChartEditorState.GRID_SIZE
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 8)),
32, 32);
state.selectionBoxSprite.scrollFactor.set(0, 0);
state.selectionBoxSprite.zIndex = 30;
state.add(state.selectionBoxSprite);
state.setSelectionBoxBounds();
}
static function updateNotePreview(state:ChartEditorState):Void

View file

@ -1,7 +1,6 @@
package funkin.ui.debug.charting.handlers;
import funkin.play.stage.StageData.StageDataParser;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import haxe.ui.components.HorizontalSlider;
@ -16,8 +15,7 @@ import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.event.SongEvent;
import funkin.play.song.SongSerializer;
import funkin.play.stage.StageData;
import funkin.play.stage.StageData.StageDataParser;
import funkin.data.stage.StageData;
import haxe.ui.RuntimeComponentBuilder;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.ui.haxeui.components.CharacterPlayer;
@ -38,6 +36,7 @@ import haxe.ui.containers.dialogs.Dialog.DialogEvent;
import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox;
import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox;
import haxe.ui.containers.Frame;
import haxe.ui.containers.Grid;
import haxe.ui.containers.TreeView;
@ -86,7 +85,7 @@ class ChartEditorToolboxHandler
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
onShowToolboxPlaytestProperties(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
onShowToolboxDifficulty(state, toolbox);
cast(toolbox, ChartEditorBaseToolbox).refresh();
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
// TODO: Fix this.
cast(toolbox, ChartEditorBaseToolbox).refresh();
@ -125,8 +124,6 @@ class ChartEditorToolboxHandler
onHideToolboxEventData(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
onHideToolboxPlaytestProperties(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
onHideToolboxDifficulty(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
onHideToolboxMetadata(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
@ -311,8 +308,6 @@ class ChartEditorToolboxHandler
static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
@ -360,91 +355,15 @@ class ChartEditorToolboxHandler
return toolbox;
}
static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<CollapsibleDialog>
static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
{
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
var toolbox:ChartEditorBaseToolbox = ChartEditorDifficultyToolbox.build(state);
if (toolbox == null) return null;
// Starting position.
toolbox.x = 125;
toolbox.y = 200;
toolbox.onDialogClosed = function(event:UIEvent) {
state.menubarItemToggleToolboxDifficulty.selected = false;
}
var difficultyToolboxAddVariation:Null<Button> = toolbox.findComponent('difficultyToolboxAddVariation', Button);
if (difficultyToolboxAddVariation == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddVariation component.';
var difficultyToolboxAddDifficulty:Null<Button> = toolbox.findComponent('difficultyToolboxAddDifficulty', Button);
if (difficultyToolboxAddDifficulty == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddDifficulty component.';
var difficultyToolboxSaveMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxSaveMetadata', Button);
if (difficultyToolboxSaveMetadata == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveMetadata component.';
var difficultyToolboxSaveChart:Null<Button> = toolbox.findComponent('difficultyToolboxSaveChart', Button);
if (difficultyToolboxSaveChart == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveChart component.';
// var difficultyToolboxSaveAll:Null<Button> = toolbox.findComponent('difficultyToolboxSaveAll', Button);
// if (difficultyToolboxSaveAll == null) throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveAll component.';
var difficultyToolboxLoadMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxLoadMetadata', Button);
if (difficultyToolboxLoadMetadata == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadMetadata component.';
var difficultyToolboxLoadChart:Null<Button> = toolbox.findComponent('difficultyToolboxLoadChart', Button);
if (difficultyToolboxLoadChart == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadChart component.';
difficultyToolboxAddVariation.onClick = function(_:UIEvent) {
state.openAddVariationDialog(true);
};
difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) {
state.openAddDifficultyDialog(true);
};
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
FileUtil.writeFileReference('${state.currentSongId}$vari-metadata.json', state.currentSongMetadata.serialize());
};
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
FileUtil.writeFileReference('${state.currentSongId}$vari-chart.json', state.currentSongChartData.serialize());
};
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
// Replace metadata for current variation.
SongSerializer.importSongMetadataAsync(function(songMetadata) {
state.currentSongMetadata = songMetadata;
});
};
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
// Replace chart data for current variation.
SongSerializer.importSongChartDataAsync(function(songChartData) {
state.currentSongChartData = songChartData;
state.noteDisplayDirty = true;
});
};
state.difficultySelectDirty = true;
return toolbox;
}
static function onShowToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void
{
// Update the selected difficulty when reopening the toolbox.
var treeView:Null<TreeView> = toolbox.findComponent('difficultyToolboxTree');
if (treeView == null) return;
var current = state.getCurrentTreeDifficultyNode(treeView);
if (current == null) return;
treeView.selectedNode = current;
trace('selected node: ${treeView.selectedNode}');
}
static function buildToolboxMetadataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
{
var toolbox:ChartEditorBaseToolbox = ChartEditorMetadataToolbox.build(state);

View file

@ -0,0 +1,239 @@
package funkin.ui.debug.charting.toolboxes;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData;
import funkin.data.stage.StageData;
import funkin.data.stage.StageRegistry;
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import haxe.ui.components.Button;
import haxe.ui.components.CheckBox;
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.FileUtil;
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.containers.Frame;
import haxe.ui.events.UIEvent;
/**
* The toolbox which allows viewing the list of difficulties, switching to a specific one,
* and adding/removing variations and difficulties.
*/
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/difficulty.xml"))
class ChartEditorDifficultyToolbox extends ChartEditorBaseToolbox
{
var difficultyToolboxTree:TreeView;
var difficultyToolboxAddVariation:Button;
var difficultyToolboxAddDifficulty:Button;
var difficultyToolboxRemoveDifficulty:Button;
var difficultyToolboxSaveMetadata:Button;
var difficultyToolboxSaveChart:Button;
var difficultyToolboxLoadMetadata:Button;
var difficultyToolboxLoadChart:Button;
public function new(chartEditorState2:ChartEditorState)
{
super(chartEditorState2);
initialize();
this.onDialogClosed = onClose;
}
function onClose(event:UIEvent)
{
chartEditorState.menubarItemToggleToolboxDifficulty.selected = false;
}
function initialize():Void
{
// Starting position.
// TODO: Save and load this.
this.x = 150;
this.y = 250;
difficultyToolboxAddVariation.onClick = function(_:UIEvent) {
chartEditorState.openAddVariationDialog(true);
};
difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) {
chartEditorState.openAddDifficultyDialog(true);
};
difficultyToolboxRemoveDifficulty.onClick = function(_:UIEvent) {
var currentVariation:String = chartEditorState.selectedVariation;
var currentDifficulty:String = chartEditorState.selectedDifficulty;
trace('Removing difficulty "$currentVariation:$currentDifficulty"');
var callback = (button) -> {
switch (button)
{
case DialogButton.YES:
// Remove the difficulty.
chartEditorState.removeDifficulty(currentVariation, currentDifficulty);
refresh();
case DialogButton.NO: // Do nothing.
default: // Do nothing.
}
}
Dialogs.messageBox("Are you sure? This cannot be undone.", "Remove Difficulty", MessageBoxType.TYPE_YESNO, callback);
};
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-metadata.json', chartEditorState.currentSongMetadata.serialize());
};
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-chart.json', chartEditorState.currentSongChartData.serialize());
};
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
// Replace metadata for current variation.
SongSerializer.importSongMetadataAsync(function(songMetadata) {
chartEditorState.currentSongMetadata = songMetadata;
});
};
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
// Replace chart data for current variation.
SongSerializer.importSongChartDataAsync(function(songChartData) {
chartEditorState.currentSongChartData = songChartData;
chartEditorState.noteDisplayDirty = true;
});
};
refresh();
}
/**
* Clear the tree view and rebuild it with the current song metadata (variation and difficulty list).
*/
public function updateTree():Void
{
// Clear the tree view so we can rebuild it.
difficultyToolboxTree.clearNodes();
// , icon: 'haxeui-core/styles/default/haxeui_tiny.png'
var treeSong:TreeViewNode = difficultyToolboxTree.addNode({id: 'stv_song', text: 'S: ${chartEditorState.currentSongName}'});
treeSong.expanded = true;
for (curVariation in chartEditorState.availableVariations)
{
var variationMetadata:Null<SongMetadata> = chartEditorState.songMetadata.get(curVariation);
if (variationMetadata == null) continue;
var treeVariation:TreeViewNode = treeSong.addNode(
{
id: 'stv_variation_$curVariation',
text: 'V: ${curVariation.toTitleCase()}'
});
treeVariation.expanded = true;
var difficultyList:Array<String> = variationMetadata.playData.difficulties;
for (difficulty in difficultyList)
{
var _treeDifficulty:TreeViewNode = treeVariation.addNode(
{
id: 'stv_difficulty_${curVariation}_$difficulty',
text: 'D: ${difficulty.toTitleCase()}'
});
}
}
difficultyToolboxTree.onChange = onTreeChange;
refreshTreeSelection();
}
/**
* Set the selected item in the tree to the current variation/difficulty.
*
* @param targetNode The node to select. If null, the current variation/difficulty will be used.
*/
public function refreshTreeSelection():Void
{
var targetNode = getCurrentTreeNode();
if (targetNode != null) difficultyToolboxTree.selectedNode = targetNode;
}
/**
* Get the node in the tree representing the current variation/difficulty.
*/
function getCurrentTreeNode():TreeViewNode
{
return
difficultyToolboxTree.findNodeByPath('stv_song/stv_variation_$chartEditorState.selectedVariation/stv_difficulty_${chartEditorState.selectedVariation}_$chartEditorState.selectedDifficulty',
'id');
}
/**
* Called when an item in the tree is selected. Updates the current variation/difficulty.
*/
function onTreeChange(event:UIEvent):Void
{
// Get the newly selected node.
var treeView:TreeView = cast event.target;
var targetNode:TreeViewNode = difficultyToolboxTree.selectedNode;
if (targetNode == null)
{
trace('No target node!');
// Reset the user's selection.
refreshTreeSelection();
return;
}
switch (targetNode.data.id.split('_')[1])
{
case 'difficulty':
var variation:String = targetNode.data.id.split('_')[2];
var difficulty:String = targetNode.data.id.split('_')[3];
if (variation != null && difficulty != null)
{
trace('Changing difficulty to "$variation:$difficulty"');
chartEditorState.selectedVariation = variation;
chartEditorState.selectedDifficulty = difficulty;
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
refreshTreeSelection();
}
// case 'song':
// case 'variation':
default:
// Reset the user's selection.
trace('Selected wrong node type, resetting selection.');
refreshTreeSelection();
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
}
}
public override function refresh():Void
{
super.refresh();
refreshTreeSelection();
}
public static function build(chartEditorState:ChartEditorState):ChartEditorDifficultyToolbox
{
return new ChartEditorDifficultyToolbox(chartEditorState);
}
}

View file

@ -2,7 +2,7 @@ package funkin.ui.debug.charting.toolboxes;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;

View file

@ -2,7 +2,8 @@ package funkin.ui.debug.charting.toolboxes;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import funkin.data.stage.StageRegistry;
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import haxe.ui.components.Button;
@ -13,6 +14,7 @@ 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.Frame;
import haxe.ui.events.UIEvent;
@ -199,11 +201,11 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature};
var stageId:String = chartEditorState.currentSongMetadata.playData.stage;
var stageData:Null<StageData> = StageDataParser.parseStageData(stageId);
var stage:Null<Stage> = StageRegistry.instance.fetchEntry(stageId);
if (inputStage != null)
{
inputStage.value = (stageData != null) ?
{id: stageId, text: stageData.name} :
inputStage.value = (stage != null) ?
{id: stage.id, text: stage.stageName} :
{id: "mainStage", text: "Main Stage"};
}

View file

@ -2,10 +2,11 @@ package funkin.ui.debug.charting.util;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.stage.StageData;
import funkin.play.stage.StageData.StageDataParser;
import funkin.data.stage.StageData;
import funkin.data.stage.StageRegistry;
import funkin.play.character.CharacterData;
import haxe.ui.components.DropDown;
import funkin.play.stage.Stage;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData.CharacterDataParser;
@ -60,16 +61,16 @@ class ChartEditorDropdowns
{
dropDown.dataSource.clear();
var stageIds:Array<String> = StageDataParser.listStageIds();
var stageIds:Array<String> = StageRegistry.instance.listEntryIds();
var returnValue:DropDownEntry = {id: "mainStage", text: "Main Stage"};
for (stageId in stageIds)
{
var stage:Null<StageData> = StageDataParser.parseStageData(stageId);
var stage:Null<Stage> = StageRegistry.instance.fetchEntry(stageId);
if (stage == null) continue;
var value = {id: stageId, text: stage.name};
var value = {id: stage.id, text: stage.stageName};
if (startingStageId == stageId) returnValue = value;
dropDown.dataSource.add(value);

View file

@ -5,15 +5,17 @@ import flixel.input.mouse.FlxMouseEvent;
import flixel.math.FlxPoint;
import funkin.play.character.BaseCharacter;
import funkin.play.PlayState;
import funkin.play.stage.StageData;
import funkin.data.stage.StageData;
import funkin.play.stage.StageProp;
import funkin.graphics.shaders.StrokeShader;
import funkin.ui.haxeui.HaxeUISubState;
import funkin.ui.debug.stage.StageEditorCommand;
import funkin.util.SerializerUtil;
import funkin.data.stage.StageRegistry;
import funkin.util.MouseUtil;
import haxe.ui.containers.ListView;
import haxe.ui.core.Component;
import funkin.graphics.FunkinSprite;
import haxe.ui.events.UIEvent;
import haxe.ui.RuntimeComponentBuilder;
import openfl.events.Event;
@ -354,7 +356,13 @@ class StageOffsetSubState extends HaxeUISubState
function prepStageStuff():String
{
var stageLol:StageData = StageDataParser.parseStageData(PlayState.instance.currentStageId);
var stageLol:StageData = StageRegistry.instance.fetchEntry(PlayState.instance.currentStageId)?._data;
if (stageLol == null)
{
FlxG.log.error("Stage not found in registry!");
return "";
}
for (prop in stageLol.props)
{
@ -378,6 +386,6 @@ class StageOffsetSubState extends HaxeUISubState
stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x);
stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y);
return SerializerUtil.toJSON(stageLol);
return stageLol.serialize();
}
}

View file

@ -8,6 +8,7 @@ import flixel.group.FlxGroup;
import flixel.input.actions.FlxActionInput;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey;
import funkin.graphics.FunkinSprite;
import funkin.input.Controls;
import funkin.ui.AtlasText;
import funkin.ui.MenuList;
@ -61,8 +62,8 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page
if (FlxG.gamepads.numActiveGamepads > 0)
{
var devicesBg:FlxSprite = new FlxSprite();
devicesBg.makeGraphic(FlxG.width, 100, 0xFFFAFD6D);
var devicesBg:FunkinSprite = new FunkinSprite();
devicesBg.makeSolidColor(FlxG.width, 100, 0xFFFAFD6D);
add(devicesBg);
deviceList = new TextMenuList(Horizontal, None);
add(deviceList);

View file

@ -10,6 +10,7 @@ import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.text.FlxText;
import flixel.addons.transition.FlxTransitionableState;
import flixel.tweens.FlxEase;
import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
@ -153,7 +154,7 @@ class StoryMenuState extends MusicBeatState
updateBackground();
var black:FlxSprite = new FlxSprite(levelBackground.x, 0).makeGraphic(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK);
var black:FunkinSprite = new FunkinSprite(levelBackground.x, 0).makeSolidColor(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK);
black.zIndex = levelBackground.zIndex - 1;
add(black);

View file

@ -15,7 +15,7 @@ class OutdatedSubState extends MusicBeatState
override function create()
{
super.create();
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK);
add(bg);
var ver = "v" + Application.current.meta.get('version');
var txt:FlxText = new FlxText(0, 0, FlxG.width,

View file

@ -13,6 +13,7 @@ import funkin.audio.visualize.SpectogramSprite;
import funkin.graphics.shaders.ColorSwap;
import funkin.graphics.shaders.LeftMaskShader;
import funkin.data.song.SongRegistry;
import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState;
import funkin.data.song.SongData.SongMusicData;
import funkin.graphics.shaders.TitleOutline;
@ -118,7 +119,8 @@ class TitleState extends MusicBeatState
persistentUpdate = true;
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK);
bg.screenCenter();
add(bg);
logoBl = new FlxSprite(-150, -100);

View file

@ -13,6 +13,7 @@ import funkin.play.song.Song.SongDifficulty;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.MusicBeatState;
import haxe.io.Path;
import funkin.graphics.FunkinSprite;
import lime.app.Future;
import lime.app.Promise;
import lime.utils.AssetLibrary;
@ -42,7 +43,7 @@ class LoadingState extends MusicBeatState
override function create():Void
{
var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, 0xFFcaff4d);
var bg:FlxSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, 0xFFcaff4d);
add(bg);
funkay = new FlxSprite();
@ -53,7 +54,7 @@ class LoadingState extends MusicBeatState
funkay.scrollFactor.set();
funkay.screenCenter();
loadBar = new FlxSprite(0, FlxG.height - 20).makeGraphic(FlxG.width, 10, 0xFFff16d2);
loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(FlxG.width, 10, 0xFFff16d2);
loadBar.screenCenter(X);
add(loadBar);

View file

@ -0,0 +1,17 @@
package funkin.util;
import haxe.ui.tooltips.ToolTipRegionOptions;
class HaxeUIUtil
{
public static function buildTooltip(text:String, ?left:Float, ?top:Float, ?width:Float, ?height:Float):ToolTipRegionOptions
{
return {
tipData: {text: text},
left: left ?? 0.0,
top: top ?? 0.0,
width: width ?? 0.0,
height: height ?? 0.0
}
}
}