Input System Rewrite (#810)

* the freaking rewrite bro!

* backwards compatibility

* listened to some feedback

* legacyJudge now determines on the API number

* listened to feedback (again)
This commit is contained in:
syrup
2025-11-17 15:49:53 +07:00
committed by GitHub
parent 43d599acfd
commit 5b2a86aace
7 changed files with 313 additions and 37 deletions
@@ -118,6 +118,10 @@ final class NoteHitEvent extends CancellableEvent {
* The attached healthIcon used distinction for icons amongst others
*/
public var healthIcon:HealthIcon;
/**
* Whether note hits are judged in the old way or not.
*/
public var legacyJudge(get, set):Bool;
/**
* Prevents the default sing animation from being played.
@@ -196,4 +200,15 @@ final class NoteHitEvent extends CancellableEvent {
characters = [char];
return char;
}
private var _explicitLegacyJudge:Null<Bool> = null;
private inline function get_legacyJudge():Bool {
if (_explicitLegacyJudge != null)
return _explicitLegacyJudge;
return Flags.CURRENT_API_VERSION == 1;
}
private function set_legacyJudge(value:Bool):Bool {
_explicitLegacyJudge = value;
return value;
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ class Flags {
@:lazy public static var SAVE_PATH:String = haxe.macro.Compiler.getDefine("SAVE_PATH");
@:lazy public static var SAVE_NAME:String = haxe.macro.Compiler.getDefine("SAVE_NAME");
public static var CURRENT_API_VERSION:Int = 1;
public static var CURRENT_API_VERSION:Int = 2;
public static var COMMIT_NUMBER:Int = GitCommitMacro.commitNumber;
public static var COMMIT_HASH:String = GitCommitMacro.commitHash;
public static var COMMIT_MESSAGE:String = 'Commit $COMMIT_NUMBER ($COMMIT_HASH)';
+11
View File
@@ -60,6 +60,15 @@ class Note extends FlxSprite
*/
public var sustainParent:Null<Note>;
/**
* Number of active sustain pieces attached to this note
*
* Increases by 1 every time a hold piece is initialized.
*
* Decreases by 1 every time a hold piece gets destroyed.
*/
public var tailCount:Int = 0;
/**
* Name of the splash.
*/
@@ -104,6 +113,8 @@ class Note extends FlxSprite
public var animSuffix:String = null;
public var tripTimer:Float = 0; // ranges from 0 to 1
private static function customTypePathExists(path:String) {
if (__customNoteTypeExists.exists(path))
+34 -24
View File
@@ -29,6 +29,8 @@ import funkin.editors.charter.Charter;
import funkin.editors.charter.CharterSelection;
import funkin.game.SplashHandler;
import funkin.game.cutscenes.*;
import funkin.game.scoring.*;
import funkin.game.scoring.RatingManager.Rating;
import funkin.menus.*;
import funkin.backend.week.WeekData;
import funkin.savedata.FunkinSave;
@@ -529,6 +531,10 @@ class PlayState extends MusicBeatState
* Group containing all of the combo sprites.
*/
public var comboGroup:RotatingSpriteGroup;
/**
* Manager that helps judge note hits to return ratings.
*/
public var ratingManager:RatingManager = new RatingManager();
/**
* Whenever the Rating sprites should be shown or not.
*
@@ -1882,40 +1888,44 @@ class PlayState extends MusicBeatState
* CALCULATES RATING
*/
var noteDiff = Math.abs(Conductor.songPosition - note.strumTime);
var daRating:String = "sick";
var score:Int = 300;
var accuracy:Float = 1;
if (noteDiff > hitWindow * 0.9)
{
daRating = 'shit';
score = 50;
accuracy = 0.25;
}
else if (noteDiff > hitWindow * 0.75)
{
daRating = 'bad';
score = 100;
accuracy = 0.45;
}
else if (noteDiff > hitWindow * 0.2)
{
daRating = 'good';
score = 200;
accuracy = 0.75;
}
var daRating:Rating = ratingManager.judgeNote(noteDiff);
var event:NoteHitEvent;
if (strumLine != null && !strumLine.cpu)
event = EventManager.get(NoteHitEvent).recycle(false, !note.isSustainNote, !note.isSustainNote, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, true, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, score, note.isSustainNote ? null : accuracy, 0.023, daRating, Options.splashesEnabled && !note.isSustainNote && daRating == "sick", 0.5, true, 0.7, true, true, iconP1);
event = EventManager.get(NoteHitEvent).recycle(false, !note.isSustainNote, !note.isSustainNote, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, true, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, daRating.score, note.isSustainNote ? null : daRating.accuracy, 0.023, daRating.name, Options.splashesEnabled && !note.isSustainNote && daRating.splash, 0.5, true, 0.7, true, true, iconP1);
else
event = EventManager.get(NoteHitEvent).recycle(false, false, false, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, false, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, 0, null, 0, daRating, false, 0.5, true, 0.7, true, true, iconP2);
event = EventManager.get(NoteHitEvent).recycle(false, false, false, null, defaultDisplayRating, defaultDisplayCombo, note, strumLine.characters, false, note.noteType, note.animSuffix.getDefault(note.strumID < strumLine.members.length ? strumLine.members[note.strumID].animSuffix : strumLine.animSuffix), "game/score/", "", note.strumID, 0, null, 0, daRating.name, false, 0.5, true, 0.7, true, true, iconP2);
event.deleteNote = !note.isSustainNote; // work around, to allow sustain notes to be deleted
event = scripts.event(strumLine != null && !strumLine.cpu ? "onPlayerHit" : "onDadHit", event);
strumLine.onHit.dispatch(event);
gameAndCharsEvent("onNoteHit", event);
if (!event.cancelled) {
if (event.legacyJudge) {
event.rating = 'sick';
event.score = 300;
event.accuracy = 1;
if (noteDiff > hitWindow * 0.9)
{
event.rating = 'shit';
event.score = 50;
event.accuracy = 0.25;
}
else if (noteDiff > hitWindow * 0.75)
{
event.rating = 'bad';
event.score = 100;
event.accuracy = 0.45;
}
else if (noteDiff > hitWindow * 0.2)
{
event.rating = 'good';
event.score = 200;
event.accuracy = 0.75;
}
}
if (!note.isSustainNote) {
if (event.countScore) songScore += event.score;
if (event.accuracy != null) {
+41 -12
View File
@@ -161,6 +161,9 @@ class StrumLine extends FlxTypedGroup<Strum> {
curLen = Math.min(len, Conductor.stepCrochet);
notes.members[total-(il++)-1] = prev = new Note(this, note, true, curLen, note.sLen - len, prev);
len -= curLen;
if (prev != null && prev.sustainParent != null)
prev.sustainParent.tailCount++;
}
}
}
@@ -236,10 +239,23 @@ class StrumLine extends FlxTypedGroup<Strum> {
if (__updateNote_event.strum == null) return;
if (__updateNote_event.__reposNote) __updateNote_event.strum.updateNotePosition(daNote);
if (daNote.isSustainNote)
{
daNote.updateSustain(__updateNote_event.strum);
if (daNote.tripTimer > 0 && daNote.tailCount > 3)
{
daNote.tripTimer -= 0.05 / daNote.sustainLength;
if (daNote.tripTimer <= 0)
{
daNote.tripTimer = 0;
daNote.canBeHit = false;
}
}
}
}
var __funcsToExec:Array<Note->Void> = [];
@@ -249,16 +265,27 @@ class StrumLine extends FlxTypedGroup<Strum> {
var __notePerStrum:Array<Note> = [];
function __inputProcessPressed(note:Note) {
if (__pressed[note.strumID] && note.isSustainNote && note.strumTime < __updateNote_songPos && !note.wasGoodHit) {
if (__pressed[note.strumID] && note.isSustainNote && note.sustainParent != null && note.sustainParent.wasGoodHit && note.strumTime < __updateNote_songPos && !note.wasGoodHit) {
note.tripTimer = 1;
PlayState.instance.goodNoteHit(this, note);
note.updateSustainClip();
}
}
function __inputProcessJustPressed(note:Note) {
if (__justPressed[note.strumID] && !note.isSustainNote && !note.wasGoodHit && note.canBeHit) {
if (__notePerStrum[note.strumID] == null) __notePerStrum[note.strumID] = note;
else if (Math.abs(__notePerStrum[note.strumID].strumTime - note.strumTime) <= 2) deleteNote(note);
else if (note.strumTime < __notePerStrum[note.strumID].strumTime) __notePerStrum[note.strumID] = note;
var cur = __notePerStrum[note.strumID];
var songPos = __updateNote_songPos;
var noteDist = Math.abs(note.strumTime - songPos);
var curDist = cur != null ? Math.abs(cur.strumTime - songPos) : 999999;
var notePenalty = note.avoid ? 1 : 0;
var curPenalty = (cur != null && cur.avoid) ? 1 : 0;
if (cur == null
|| notePenalty < curPenalty
|| (notePenalty == curPenalty && noteDist < curDist))
__notePerStrum[note.strumID] = note;
}
}
@@ -272,14 +299,14 @@ class StrumLine extends FlxTypedGroup<Strum> {
if (cpu) return;
__funcsToExec.clear();
__pressed.clear();
__justPressed.clear();
__justReleased.clear();
__pressed.resize(members.length);
__justPressed.resize(members.length);
__justReleased.resize(members.length);
for(s in members) {
__pressed.push(s.__getPressed(this));
__justPressed.push(s.__getJustPressed(this));
__justReleased.push(s.__getJustReleased(this));
for(i in 0...members.length) {
__pressed[i] = members[i].__getPressed(this);
__justPressed[i] = members[i].__getJustPressed(this);
__justReleased[i] = members[i].__getJustReleased(this);
}
var event = EventManager.get(InputSystemEvent).recycle(__pressed, __justPressed, __justReleased, this, id);
@@ -411,6 +438,8 @@ class StrumLine extends FlxTypedGroup<Strum> {
var event:SimpleNoteEvent = EventManager.get(SimpleNoteEvent).recycle(note);
onNoteDelete.dispatch(event);
if (!event.cancelled) {
if (note.isSustainNote && note.sustainParent != null && note.sustainParent.tailCount > 0)
note.sustainParent.tailCount--;
note.kill();
notes.remove(note, true);
note.destroy();
@@ -0,0 +1,76 @@
package funkin.game.scoring;
import haxe.ds.StringMap;
class HitWindowData
{
public static function getWindows(preset:WindowPreset):StringMap<Float>
{
var map = new StringMap<Float>();
switch (preset) {
// Old Codename, really forgiving inputs (hard to get bad ratings)
case CNE_CLASSIC:
map.set("sick", 50.0);
map.set("good", 187.5);
map.set("bad", 225.0);
map.set("shit", 250.0);
// Week 7
case FNF_CLASSIC:
map.set("sick", 33.334);
map.set("good", 125.0025);
map.set("bad", 150.003);
map.set("shit", 166.67);
// V-Slice
case FNF_VSLICE:
map.set("sick", 45.0);
map.set("good", 90.0);
map.set("bad", 135.4);
map.set("shit", 180.0);
// Default, taken from Etterna
case _:
map.set("sick", 37.8);
map.set("good", 75.6);
map.set("bad", 113.4);
map.set("shit", 180.0);
}
return map;
}
public static var JUDGE_SCALES:Array<Float> = [1.5, 1.33, 1.16, 1.0, 0.84, 0.66, 0.5, 0.33, 0.2];
public static function scaleWindows(windows:StringMap<Float>, scale:Float):StringMap<Float>
{
var scaled = new StringMap<Float>();
for (k in windows.keys())
scaled.set(k, windows.get(k) * scale);
return scaled;
}
public static function offsetWindows(windows:StringMap<Float>, offset:Float):StringMap<Float>
{
var adjusted = new StringMap<Float>();
for (k in windows.keys())
adjusted.set(k, windows.get(k) + offset);
return adjusted;
}
}
enum abstract WindowPreset(Int) from Int to Int
{
var DEFAULT = 0;
var CNE_CLASSIC = 1;
var FNF_CLASSIC = 2;
var FNF_VSLICE = 3;
public function toString():String
{
return switch (cast this : WindowPreset)
{
case CNE_CLASSIC: "Codename (Classic)";
case FNF_CLASSIC: "Funkin' (Week 7)";
case FNF_VSLICE: "Funkin' (V-Slice)";
case _: "Default";
}
}
}
+135
View File
@@ -0,0 +1,135 @@
package funkin.game.scoring;
import funkin.game.scoring.*;
import funkin.game.scoring.HitWindowData.WindowPreset;
import haxe.ds.StringMap;
/**
* Judges note hits and returns a rating.
*/
class RatingManager
{
public var hitWindows:StringMap<Float>;
public var ratingData:Array<Rating> = [];
public function new(?preset:WindowPreset):Void
{
var usedPreset = preset != null ? preset : WindowPreset.DEFAULT;
hitWindows = HitWindowData.getWindows(usedPreset);
initDefaultData(hitWindows);
}
/**
* Returns a rating based on a wimdow of time.
* @param time The timing window to judge.
*/
public function judgeNote(time:Float):Rating
{
for (i => rating in ratingData)
{
if (rating.hittable && rating.window > -1 && time <= rating.window)
{
return rating;
}
}
return ratingData.last();
}
/**
* Initializes the default rating data containing the four judgements.
*
* "Sick", "Good", "Bad", "Shit"
*/
public function initDefaultData(windows:StringMap<Float>)
{
inline function getWindow(name:String):Float
{
return windows.exists(name) ? windows.get(name) : -1;
}
addRating({name: "sick", window: getWindow("sick"), accuracy: 1, score: 300, splash: true});
addRating({name: "good", window: getWindow("good"), accuracy: 0.75, score: 200});
addRating({name: "bad", window: getWindow("bad"), accuracy: 0.45, score: 100});
addRating({name: "shit", window: getWindow("shit"), accuracy: 0.25, score: 50});
}
public function addRating(data:Dynamic)
{
if (data == null || data.name == null) return;
var name = data.name.toLowerCase();
var window = data.window != null
? data.window
: (hitWindows.exists(name) ? hitWindows.get(name) : -1);
var newRating:Rating = {
name: name,
window: window,
accuracy: data.accuracy != null ? data.accuracy : 1,
score: data.score != null ? data.score : 0,
splash: data.splash == true,
hittable: data.hittable != null ? data.hittable : true
};
var existingIndex = -1;
for (i in 0...ratingData.length)
if (ratingData[i].name == name)
existingIndex = i;
if (existingIndex >= 0)
ratingData[existingIndex] = newRating;
else
ratingData.push(newRating);
ratingData.sort((a, b) -> Reflect.compare(a.window, b.window));
}
public function removeRating(name:String):Void
{
if (name == null) return;
name = name.toLowerCase();
ratingData = ratingData.filter(r -> r.name != name);
}
public function getHitWindow(name:String):Float
{
return hitWindows.exists(name) ? hitWindows.get(name) : -1;
}
}
@:structInit
final class Rating
{
/**
* Name of rating.
*
* Also used for the image file name of the rating.
*/
public var name:String = "unknown";
/**
* Amount of accuracy given when earning this rating.
*/
public var accuracy:Float = 0.0;
/**
* MS Timing Window to hit the rating.
*/
public var window:Float = -1;
/**
* Amount of score given when earning this rating.
*/
public var score:Int = 0;
/**
* If this rating was hit, a note splash will appear.
*/
@:optional public var splash:Bool = false;
/**
* Whether the rating is hittable or not.
*/
@:optional public var hittable:Bool = true;
}