Skip to content

Commit

Permalink
squashed commits
Browse files Browse the repository at this point in the history
  • Loading branch information
lemz1 committed Jan 22, 2025
1 parent dfe02ec commit 3b4dbae
Show file tree
Hide file tree
Showing 10 changed files with 444 additions and 248 deletions.
2 changes: 1 addition & 1 deletion assets
151 changes: 151 additions & 0 deletions source/funkin/data/notes/SongNoteSchema.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package funkin.data.notes;

@:forward(name, title, type, min, max, step, precision, keys, defaultValue, iterator)
abstract SongNoteSchema(SongNoteSchemaRaw)
{
public function new(?fields:Array<SongNoteSchemaField>)
{
this = fields;
}

@:arrayAccess
public function getByName(name:String):SongNoteSchemaField
{
for (field in this)
{
if (field.name == name) return field;
}

return null;
}

public function getFirstField():SongNoteSchemaField
{
return this[0];
}

@:arrayAccess
public inline function get(index:Int):SongNoteSchemaField
{
return this[index];
}

@:arrayAccess
public inline function set(index:Int, value:SongNoteSchemaField):SongNoteSchemaField
{
return this[index] = value;
}

public function stringifyFieldValue(name:String, value:Dynamic):String
{
var field:SongNoteSchemaField = getByName(name);
if (field == null) return 'Unknown';

switch (field.type)
{
case SongNoteFieldType.STRING:
return Std.string(value);
case SongNoteFieldType.INTEGER | SongNoteFieldType.FLOAT | SongNoteFieldType.BOOL:
var returnValue:String = Std.string(value);
return returnValue;
case SongNoteFieldType.ENUM:
var valueString:String = Std.string(value);
for (key in field.keys.keys())
{
// Comparing these values as strings because comparing Dynamic variables is jank.
if (Std.string(field.keys.get(key)) == valueString) return key;
}
return valueString;
default:
return 'Unknown';
}
}
}

typedef SongNoteSchemaRaw = Array<SongNoteSchemaField>;

typedef SongNoteSchemaField =
{
/**
* The name of the property as it should be saved in the event data.
*/
name:String,

/**
* The title of the field to display in the UI.
*/
title:String,

/**
* The type of the field.
*/
type:SongNoteFieldType,

/**
* Used for INTEGER and FLOAT values.
* The minimum value that can be entered.
* @default No minimum
*/
?min:Float,

/**
* Used for INTEGER and FLOAT values.
* The maximum value that can be entered.
* @default No maximum
*/
?max:Float,

/**
* Used for INTEGER and FLOAT values.
* The step value that will be used when incrementing/decrementing the value.
* @default `0.1`
*/
?step:Float,

/**
* Used for INTEGER and FLOAT values.
* The amount of decimal places.
* @default `0`
*/
?precision:Int,

/**
* Used only for ENUM values.
* The key is the display name and the value is the actual value.
*/
?keys:Map<String, Dynamic>,

/**
* An optional default value for the field.
*/
?defaultValue:Dynamic,
}

enum abstract SongNoteFieldType(String) from String to String
{
/**
* The STRING type will display as a text field.
*/
var STRING = "string";

/**
* The INTEGER type will display as a text field that only accepts numbers.
*/
var INTEGER = "integer";

/**
* The FLOAT type will display as a text field that only accepts numbers.
*/
var FLOAT = "float";

/**
* The BOOL type will display as a checkbox.
*/
var BOOL = "bool";

/**
* The ENUM type will display as a dropdown.
* Make sure to specify the `keys` field in the schema.
*/
var ENUM = "enum";
}
119 changes: 103 additions & 16 deletions source/funkin/data/song/SongData.hx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package funkin.data.song;
import funkin.data.event.SongEventRegistry;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventSchema;
import funkin.data.notes.SongNoteSchema;
import funkin.data.song.SongRegistry;
import funkin.play.notes.notekind.NoteKindManager;
import thx.semver.Version;
import funkin.util.tools.ICloneable;

Expand Down Expand Up @@ -984,17 +986,18 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
}

@:alias("p")
@:default([])
@:optional
public var params:Array<NoteParamData>;
@:jcustomparse(funkin.data.DataParse.dynamicValue)
@:jcustomwrite(funkin.data.DataWrite.dynamicValue)
public var params:Dynamic = null;

public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<NoteParamData>)
public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Dynamic)
{
this.time = time;
this.data = data;
this.length = length;
this.kind = kind;
this.params = params ?? [];
this.params = params;
}

/**
Expand All @@ -1003,7 +1006,7 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
*
* 0 = left, 1 = down, 2 = up, 3 = right
*/
public inline function getDirection(strumlineSize:Int = 4):Int
public function getDirection(strumlineSize:Int = 4):Int
{
return this.data % strumlineSize;
}
Expand Down Expand Up @@ -1089,25 +1092,109 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
_stepLength = null;
}

public function cloneParams():Array<NoteParamData>
public function clone():SongNoteDataRaw
{
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, this.params);
}

public function toString():String
{
var params:Array<NoteParamData> = [];
for (param in this.params)
return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}'
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
}

public function paramsAsStruct(?defaultKey:String = "key"):Dynamic
{
if (this.params == null) return {};

if (Reflect.isObject(this.params))
{
params.push(param.clone());
// We enter this case if the params are a struct.
return cast this.params;
}
else
{
var result:haxe.DynamicAccess<Dynamic> = {};
result.set(defaultKey, this.params);
return cast result;
}
return params;
}

public function clone():SongNoteDataRaw
public function getSchema():Null<SongNoteSchema>
{
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, cloneParams());
return NoteKindManager.getSchema(this.kind);
}

public function toString():String
public function getDynamic(key:String):Null<Dynamic>
{
return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}'
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
return this.params == null ? null : Reflect.field(this.params, key);
}

public function getBool(key:String):Null<Bool>
{
return this.params == null ? null : cast Reflect.field(this.params, key);
}

public function getInt(key:String):Null<Int>
{
if (this.params == null) return null;
var result = Reflect.field(this.params, key);
if (result == null) return null;
if (Std.isOfType(result, Int)) return result;
if (Std.isOfType(result, String)) return Std.parseInt(cast result);
return cast result;
}

public function getFloat(key:String):Null<Float>
{
if (this.params == null) return null;
var result = Reflect.field(this.params, key);
if (result == null) return null;
if (Std.isOfType(result, Float)) return result;
if (Std.isOfType(result, String)) return Std.parseFloat(cast result);
return cast result;
}

public function getString(key:String):String
{
return this.params == null ? null : cast Reflect.field(this.params, key);
}

public function getArray(key:String):Array<Dynamic>
{
return this.params == null ? null : cast Reflect.field(this.params, key);
}

public function getBoolArray(key:String):Array<Bool>
{
return this.params == null ? null : cast Reflect.field(this.params, key);
}

public function buildTooltip():Null<String>
{
var noteSchema = getSchema();

if (noteSchema == null) return null;

var result = '${this.kind}';

var defaultKey = noteSchema.getFirstField()?.name;
var paramsStruct:haxe.DynamicAccess<Dynamic> = paramsAsStruct(defaultKey);

for (pair in paramsStruct.keyValueIterator())
{
var key = pair.key;
var value = pair.value;

var title = noteSchema.getByName(key)?.title ?? 'UnknownField';

// if (noteSchema.stringifyFieldValue(key, value) != null) trace(noteSchema.stringifyFieldValue(key, value));
var valueStr = noteSchema.stringifyFieldValue(key, value) ?? 'UnknownValue';

result += '\n- ${title}: ${valueStr}';
}

return result;
}
}

Expand All @@ -1117,7 +1204,7 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
@:forward
abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
{
public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<NoteParamData>)
public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Dynamic)
{
this = new SongNoteDataRaw(time, data, length, kind, params);
}
Expand Down
17 changes: 5 additions & 12 deletions source/funkin/play/notes/NoteSprite.hx
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@ class NoteSprite extends FunkinSprite
}

/**
* An array of custom parameters for this note
* Custom parameters for this note
*/
public var params(get, set):Array<NoteParamData>;
public var params(get, set):Dynamic;

function get_params():Array<NoteParamData>
function get_params():Dynamic
{
return this.noteData?.params ?? [];
}

function set_params(value:Array<NoteParamData>):Array<NoteParamData>
function set_params(value:Dynamic):Dynamic
{
if (this.noteData == null) return value;
return this.noteData.params = value;
Expand Down Expand Up @@ -175,14 +175,7 @@ class NoteSprite extends FunkinSprite
*/
public function getParam(name:String):Null<Dynamic>
{
for (param in params)
{
if (param.name == name)
{
return param.value;
}
}
return null;
return this.noteData?.getDynamic(name);
}

#if FLX_DEBUG
Expand Down
Loading

0 comments on commit 3b4dbae

Please sign in to comment.