Merge pull request #289 from FunkinCrew/feature/stage-solids

Stage solids
This commit is contained in:
Cameron Taylor 2024-01-17 21:41:50 -05:00 committed by GitHub
commit 33982b140a
30 changed files with 482 additions and 629 deletions

2
assets

@ -1 +1 @@
Subproject commit d094640f727a670a348b3579d11af5ff6a2ada3a
Subproject commit 9e385784b1d2f4332de0d696b1df655cfa269da0

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

@ -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

@ -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;
@ -94,7 +95,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;

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,7 @@ class PlayState extends MusicBeatSubState
*/
function loadStage(id:String):Void
{
currentStage = StageDataParser.fetchStage(id);
currentStage = StageRegistry.instance.fetchEntry(id);
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;
@ -56,7 +57,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;
@ -2230,7 +2231,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);

View file

@ -13,7 +13,7 @@ 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;

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;

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);