Skip to content

Commit 7ff0720

Browse files
author
Lenin
committed
Enhance PathRenderer for adaptive divisions, improve dialogue handling, and refine UI interactions
- Added adaptive divisions calculation in PathRenderer for better path rendering quality. - Updated DialogueBox to handle null or empty dialogue lists gracefully. - Improved Limo and School stages to ensure proper layering and dialogue initialization. - Enhanced PauseSubState with dynamic difficulty loading based on available charts. - Refined ResetScoreSubState dialog messages for clarity. - Adjusted LoadingState to handle null current file gracefully. - Improved MenuCharacter to reload character only when necessary. - Updated MaterialDialog button focus behavior for better UX. - Enhanced MaterialNumericStepper with pointer detection for mobile. - Added touch-zone hitboxes in FreeplayState for improved gesture handling. - Updated NoteOffsetState to support touch input alongside mouse and gamepad. - Enhanced StoryMenuState to filter story difficulties based on week data. - Refined WindowMode to manage window sizing and positioning more effectively.
1 parent 3d8bdfc commit 7ff0720

19 files changed

Lines changed: 515 additions & 72 deletions

File tree

source/funkin/modding/modchart/backend/graphics/CtxRenderer.hx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import openfl.display.BlendMode;
1818
using funkin.modding.modchart.backend.util.SortUtil;
1919

2020
class CtxRenderer {
21+
/**
22+
* Global quality multiplier consumed by PathRenderer.
23+
* 1.0 = full quality, lower values reduce path divisions to protect FPS.
24+
*/
25+
public static var pathQualityScale:Float = 1.0;
26+
2127
var ctx:Context;
2228

2329
public function new() {}
@@ -89,11 +95,15 @@ class CtxRenderer {
8995
if (avgFps < 45 && cur > 2) {
9096
__targetSubdivisions = cur; // save before lowering
9197
Adapter.instance.setHoldSubdivisions(2);
98+
pathQualityScale = 0.65;
99+
} else if (avgFps < 50) {
100+
pathQualityScale = 0.8;
92101
} else if (avgFps >= 55) {
93102
if (cur == 2 && __targetSubdivisions > 2)
94103
Adapter.instance.setHoldSubdivisions(__targetSubdivisions);
95104
else
96105
__targetSubdivisions = cur; // keep in sync with Lua overrides
106+
pathQualityScale = 1.0;
97107
}
98108
}
99109

@@ -161,6 +171,8 @@ class CtxRenderer {
161171
if (pathCount > 0) {
162172
// iterate through receptors, yes
163173
for (receptor in curItems[0]) {
174+
if (!getVisibility(receptor))
175+
continue;
164176
var _ = emitPathCmd(receptor);
165177
if (_ != null)
166178
this.append(_);

source/funkin/modding/modchart/backend/graphics/renderers/ArrowRenderer.hx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package funkin.modding.modchart.backend.graphics.renderers;
22

3+
import flixel.math.FlxMath;
4+
35
using flixel.util.FlxColorTransformUtil;
46

57
final matrix:Matrix = new Matrix();
@@ -119,11 +121,18 @@ final class ArrowRenderer extends BaseRenderer<FlxSprite> {
119121
final projectionZ = _projZ;
120122

121123
var vertPointer = 0;
124+
final distAbs = Math.abs(arrowData.distance);
125+
// Very soft far-distance compensation to keep angles close to target value
126+
// while still avoiding extreme over-tilt on very distant notes.
127+
final farRatio = FlxMath.bound((distAbs - 600.0) / 2600.0, 0, 1);
128+
final perspectiveDamp = 1.0 - (farRatio * 0.15);
129+
final visualAngleX = output.visuals.angleX * perspectiveDamp;
130+
final visualAngleY = output.visuals.angleY * perspectiveDamp;
122131
@:privateAccess do {
123132
rotationVector.setTo(planeVertices[vertPointer], planeVertices[vertPointer + 1], 0);
124133

125134
// The result of the vert rotation
126-
var rotation = ModchartUtil.rotate3DVector(rotationVector, output.visuals.angleX, output.visuals.angleY,
135+
var rotation = ModchartUtil.rotate3DVector(rotationVector, visualAngleX, visualAngleY,
127136
ModchartUtil.getFrameAngle(arrow) + output.visuals.angleZ + arrow.angle);
128137

129138
// apply skewness

source/funkin/modding/modchart/backend/graphics/renderers/HoldRenderer.hx

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package funkin.modding.modchart.backend.graphics.renderers;
22

3+
import haxe.ds.ObjectMap;
4+
35
using flixel.util.FlxColorTransformUtil;
46

57
typedef HoldSegmentOutput = {
@@ -41,6 +43,35 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
4143
var _uvtCacheVals:Array<openfl.Vector<Float>> = [];
4244
var _uvtCacheSubs:Int = -1;
4345

46+
// Per-hold pools to avoid per-frame allocations of geometry/color buffers.
47+
var _holdVerticesPool:ObjectMap<FlxSprite, openfl.Vector<Float>> = new ObjectMap<FlxSprite, openfl.Vector<Float>>();
48+
var _holdColorsPool:ObjectMap<FlxSprite, NativeVector<ColorTransform>> = new ObjectMap<FlxSprite, NativeVector<ColorTransform>>();
49+
var _holdPoolSubs:ObjectMap<FlxSprite, Int> = new ObjectMap<FlxSprite, Int>();
50+
51+
inline private function _getPooledVertices(item:FlxSprite, subs:Int):openfl.Vector<Float> {
52+
final oldSubs = _holdPoolSubs.get(item);
53+
var verts = _holdVerticesPool.get(item);
54+
if (verts == null || oldSubs == null || oldSubs != subs || verts.length != subs * 8) {
55+
verts = new openfl.Vector<Float>(subs * 8, true);
56+
_holdVerticesPool.set(item, verts);
57+
_holdPoolSubs.set(item, subs);
58+
}
59+
return verts;
60+
}
61+
62+
inline private function _getPooledColors(item:FlxSprite, subs:Int):NativeVector<ColorTransform> {
63+
final oldSubs = _holdPoolSubs.get(item);
64+
var cols = _holdColorsPool.get(item);
65+
if (cols == null || oldSubs == null || oldSubs != subs || cols.length != subs) {
66+
cols = new NativeVector<ColorTransform>(subs);
67+
for (i in 0...subs)
68+
cols[i] = new ColorTransform();
69+
_holdColorsPool.set(item, cols);
70+
_holdPoolSubs.set(item, subs);
71+
}
72+
return cols;
73+
}
74+
4475
inline private function _getCachedUVT(item:FlxSprite, subs:Int):openfl.Vector<Float> {
4576
if (subs != _uvtCacheSubs) {
4677
// Subdivision count changed (settings) — invalidate entire cache
@@ -115,7 +146,7 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
115146

116147
final size = hold.frame.frame.width * hold.scale.x * .5;
117148

118-
var origin:ModifierOutput = parent.modifiers.getPath(basePos.clone(), params);
149+
var origin:ModifierOutput = parent.modifiers.getPath(copyVec3(basePos, _pathInputA), params);
119150
var curPoint = new Vector3(origin.pos.x, origin.pos.y, 0);
120151
final depth = (origin.pos.z - 1) * 1000;
121152
final worldX = origin.rawX;
@@ -128,7 +159,7 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
128159
if (Config.OPTIMIZE_HOLDS) {
129160
unit = __holdUnitUp; // reuse static up-vector, no allocation
130161
} else {
131-
var next = parent.modifiers.getPath(basePos.clone(), params, 1, false, true);
162+
var next = parent.modifiers.getPath(copyVec3(basePos, _pathInputB), params, 1, false, true);
132163
next.pos.z = 0;
133164

134165
// normalized points difference (from 0-1)
@@ -224,6 +255,24 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
224255
final _holdArrowBuf:ArrowData = {hitTime: 0, distance: 0, sourceTime: 0, lane: 0, player: 0, hitten: false, isTapArrow: false, straightHolds: false};
225256
/** Pre-allocated ArrowData buffer for parentData (rotate path) to avoid per-hold heap allocation. */
226257
final _parentDataBuf:ArrowData = {hitTime: 0, distance: 0, sourceTime: 0, lane: 0, player: 0, hitten: false, isTapArrow: false, straightHolds: false};
258+
/** Reused path input vectors to avoid allocating basePos.clone() for every getPath call. */
259+
final _pathInputA:Vector3 = new Vector3();
260+
final _pathInputB:Vector3 = new Vector3();
261+
262+
// Cached hold metadata used by getArrowParams() in the subdivision loop.
263+
var __cachedHoldPlayer:Int = 0;
264+
var __cachedHoldLane:Int = 0;
265+
var __cachedHoldHitTime:Float = 0;
266+
var __cachedHoldParentTime:Float = 0;
267+
var __cachedHoldHitten:Bool = false;
268+
var __cachedSongPos:Float = 0;
269+
270+
inline private function copyVec3(from:Vector3, into:Vector3):Vector3 {
271+
into.x = from.x;
272+
into.y = from.y;
273+
into.z = from.z;
274+
return into;
275+
}
227276

228277
override public function prepare(item:FlxSprite):Null<DrawCommand> {
229278
if (item == null || item.graphic == null || item.frame == null) {
@@ -245,14 +294,25 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
245294

246295
final player = Adapter.instance.getPlayerFromArrow(item);
247296
final lane = Adapter.instance.getLaneFromArrow(item);
297+
final hitten = Adapter.instance.arrowHit(item);
298+
final holdHitTime = Adapter.instance.getTimeFromArrow(item);
299+
final holdParentTime = Adapter.instance.getHoldParentTime(item);
300+
final songPosNow = Adapter.instance.getSongPosition();
301+
302+
__cachedHoldPlayer = player;
303+
__cachedHoldLane = lane;
304+
__cachedHoldHitTime = holdHitTime;
305+
__cachedHoldParentTime = holdParentTime;
306+
__cachedHoldHitten = hitten;
307+
__cachedSongPos = songPosNow;
248308

249309
basePos = ModchartUtil.getHalfPos();
250310
basePos.x += Adapter.instance.getDefaultReceptorX(lane, player);
251311
basePos.y += Adapter.instance.getDefaultReceptorY(lane, player);
252312

253-
// build directly as openfl.Vector to avoid conversion at render time
254-
var vertices = new openfl.Vector<Float>(8 * HOLD_SUBDIVISIONS, true);
255-
var transfTotal = new NativeVector<ColorTransform>(HOLD_SUBDIVISIONS);
313+
// Reuse per-hold buffers to avoid per-frame heap churn.
314+
var vertices = _getPooledVertices(item, HOLD_SUBDIVISIONS);
315+
var transfTotal = _getPooledColors(item, HOLD_SUBDIVISIONS);
256316
var tID = 0;
257317

258318
var lastData:ArrowData = null;
@@ -272,19 +332,19 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
272332
__rotateY = canUseLast ? __lastRY : (__lastRY = parent.getPercent('holdRotateY', player));
273333
__rotateZ = canUseLast ? __lastRZ : (__lastRZ = parent.getPercent('holdRotateZ', player));
274334

275-
var parentTime = Adapter.instance.getHoldParentTime(item);
335+
var parentTime = holdParentTime;
276336
_parentDataBuf.hitTime = parentTime;
277337
// this fixed the clipping gaps
278-
_parentDataBuf.distance = Math.max(0, parentTime - Adapter.instance.getSongPosition());
338+
_parentDataBuf.distance = Math.max(0, parentTime - songPosNow);
279339
_parentDataBuf.sourceTime = parentTime;
280340
_parentDataBuf.lane = lane;
281341
_parentDataBuf.player = player;
282-
_parentDataBuf.hitten = Adapter.instance.arrowHit(item);
342+
_parentDataBuf.hitten = hitten;
283343
_parentDataBuf.isTapArrow = true;
284344
_parentDataBuf.straightHolds = __straightHolds > 0;
285345
final parentData = _parentDataBuf;
286346
if (__rotateX != 0 || __rotateY != 0 || __rotateZ != 0) {
287-
__parentOutput = parent.modifiers.getPath(basePos.clone(), parentData);
347+
__parentOutput = parent.modifiers.getPath(copyVec3(basePos, _pathInputA), parentData);
288348
}
289349

290350
var vertPointer = 0;
@@ -345,10 +405,19 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
345405
final negGlow = 1 - out1.visuals.glow;
346406
final absGlow = out1.visuals.glow * 255;
347407

348-
var ctr:ColorTransform;
349-
350-
transfTotal[tID++] = ctr = new ColorTransform(negGlow, negGlow, negGlow, out1.visuals.alpha * item.alpha,
351-
Math.round(out1.visuals.glowR * absGlow), Math.round(out1.visuals.glowG * absGlow), Math.round(out1.visuals.glowB * absGlow));
408+
var ctr = transfTotal[tID++];
409+
if (ctr == null) {
410+
ctr = new ColorTransform();
411+
transfTotal[tID - 1] = ctr;
412+
}
413+
ctr.redMultiplier = negGlow;
414+
ctr.greenMultiplier = negGlow;
415+
ctr.blueMultiplier = negGlow;
416+
ctr.alphaMultiplier = out1.visuals.alpha * item.alpha;
417+
ctr.redOffset = Math.round(out1.visuals.glowR * absGlow);
418+
ctr.greenOffset = Math.round(out1.visuals.glowG * absGlow);
419+
ctr.blueOffset = Math.round(out1.visuals.glowB * absGlow);
420+
ctr.alphaOffset = 0;
352421

353422
if (ctr.hasRGBMultipliers() || ctr.alphaMultiplier != 1)
354423
hasC = true;
@@ -383,24 +452,29 @@ final class HoldRenderer extends BaseRenderer<FlxSprite> {
383452
}
384453

385454
inline private function getArrowParams(arrow:FlxSprite, posOff:Float = 0):ArrowData {
386-
final player = Adapter.instance.getPlayerFromArrow(arrow);
387-
final lane = Adapter.instance.getLaneFromArrow(arrow);
388-
389455
final timeC2 = flixel.FlxG.height * 0.25 * __centered2;
390-
final hitTime = Adapter.instance.getTimeFromArrow(arrow);
456+
final hitTime = __cachedHoldHitTime;
391457

392-
var pos = (hitTime - Adapter.instance.getSongPosition()) + posOff;
458+
var pos = (hitTime - __cachedSongPos) + posOff;
393459
pos += timeC2;
394460

395461
// Reuse _holdArrowBuf to avoid a heap allocation per segment.
396462
_holdArrowBuf.hitTime = hitTime + posOff + timeC2;
397463
_holdArrowBuf.distance = pos;
398-
_holdArrowBuf.sourceTime = Adapter.instance.getHoldParentTime(arrow);
399-
_holdArrowBuf.lane = lane;
400-
_holdArrowBuf.player = player;
401-
_holdArrowBuf.hitten = Adapter.instance.arrowHit(arrow);
464+
_holdArrowBuf.sourceTime = __cachedHoldParentTime;
465+
_holdArrowBuf.lane = __cachedHoldLane;
466+
_holdArrowBuf.player = __cachedHoldPlayer;
467+
_holdArrowBuf.hitten = __cachedHoldHitten;
402468
_holdArrowBuf.isTapArrow = true;
403469
_holdArrowBuf.straightHolds = __straightHolds > 0;
404470
return _holdArrowBuf;
405471
}
472+
473+
override function dispose() {
474+
_uvtCacheKeys = [];
475+
_uvtCacheVals = [];
476+
_holdVerticesPool = new ObjectMap<FlxSprite, openfl.Vector<Float>>();
477+
_holdColorsPool = new ObjectMap<FlxSprite, NativeVector<ColorTransform>>();
478+
_holdPoolSubs = new ObjectMap<FlxSprite, Int>();
479+
}
406480
}

source/funkin/modding/modchart/backend/graphics/renderers/PathRenderer.hx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package funkin.modding.modchart.backend.graphics.renderers;
22

33
import flixel.FlxG;
44
import flixel.graphics.FlxGraphic;
5+
import flixel.math.FlxMath;
56
import flixel.util.FlxDestroyUtil;
67
import openfl.geom.ColorTransform;
78
import funkin.modding.modchart.engine.modifiers.list.Reverse;
@@ -17,6 +18,8 @@ var pathVector = new Vector3();
1718
#end
1819
final class PathRenderer extends BaseRenderer<FlxSprite> {
1920
static inline final DEFAULT_PATH_BOUND:Float = 300;
21+
static inline final BASE_PATH_LIMIT:Float = 1800 + DEFAULT_PATH_BOUND;
22+
static inline final MAX_ADAPTIVE_DIVISIONS:Int = 512;
2023

2124
var __lineGraphic:FlxGraphic;
2225
var __lastDivisions:Int = -1;
@@ -181,6 +184,15 @@ final class PathRenderer extends BaseRenderer<FlxSprite> {
181184
return DEFAULT_PATH_BOUND;
182185
}
183186

187+
private inline function getAdaptiveDivisions(limit:Float):Int {
188+
final baseDivisions = Std.int(Config.ARROW_PATHS_CONFIG.BASE_DIVISIONS * Config.ARROW_PATHS_CONFIG.RESOLUTION);
189+
final safeBase = Std.int(Math.max(2, baseDivisions));
190+
final ratio = BASE_PATH_LIMIT > 0 ? (limit / BASE_PATH_LIMIT) : 1.0;
191+
final qualityScale = FlxMath.bound(funkin.modding.modchart.backend.graphics.CtxRenderer.pathQualityScale, 0.5, 1.0);
192+
final scaledDivisions = Std.int(Math.ceil(safeBase * Math.max(1.0, ratio) * qualityScale));
193+
return Std.int(FlxMath.bound(scaledDivisions, 2, MAX_ADAPTIVE_DIVISIONS));
194+
}
195+
184196
// The entry sprite should be A RECEPTOR / STRUM.
185197
override public function prepare(item:FlxSprite):Null<DrawCommand> {
186198
final lane = Adapter.instance.getLaneFromArrow(item);
@@ -201,8 +213,8 @@ final class PathRenderer extends BaseRenderer<FlxSprite> {
201213
__lastPlayer = fn;
202214
__lastLane = lane;
203215

204-
final divisions = Std.int(Config.ARROW_PATHS_CONFIG.BASE_DIVISIONS * Config.ARROW_PATHS_CONFIG.RESOLUTION);
205216
final limit = 1800 + pathBound;
217+
final divisions = getAdaptiveDivisions(limit);
206218
final interval = limit / (divisions - 1); // max point distance = limit (receptor → furthest sample)
207219
final songPos = Adapter.instance.getSongPosition();
208220
final segs = divisions - 1;

source/funkin/play/PlayState.hx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package funkin.play;
22

3-
import sys.thread.Thread;
3+
impor sys.thread.Thread;
44
import funkin.save.Highscore;
55
import funkin.data.stage.StageData;
66
import funkin.data.story.level.WeekData;

source/funkin/play/cutscene/dialogue/DialogueBox.hx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class DialogueBox extends FlxSpriteGroup
6767
hasDialog = false;
6868
}
6969

70-
this.dialogueList = dialogueList;
70+
this.dialogueList = dialogueList != null ? dialogueList : [];
7171

7272
if (!hasDialog)
7373
return;
@@ -128,6 +128,17 @@ class DialogueBox extends FlxSpriteGroup
128128
// HARD CODING CUZ IM STUPDI
129129
super.update(elapsed);
130130

131+
if (dialogueList == null || dialogueList.length == 0)
132+
{
133+
if (!isEnding)
134+
{
135+
if (finishThing != null)
136+
finishThing();
137+
kill();
138+
}
139+
return;
140+
}
141+
131142
switch(songName)
132143
{
133144
case 'roses':
@@ -216,7 +227,18 @@ class DialogueBox extends FlxSpriteGroup
216227

217228
function startDialogue():Void
218229
{
230+
if (dialogueList == null || dialogueList.length == 0)
231+
{
232+
dialogueCompleted();
233+
return;
234+
}
235+
219236
cleanDialog();
237+
if (dialogueList == null || dialogueList.length == 0 || dialogueList[0] == null)
238+
{
239+
dialogueCompleted();
240+
return;
241+
}
220242
// var theDialog:Alphabet = new Alphabet(0, 70, dialogueList[0], false, true);
221243
// dialogue = theDialog;
222244
// add(theDialog);
@@ -254,8 +276,19 @@ class DialogueBox extends FlxSpriteGroup
254276

255277
function cleanDialog():Void
256278
{
279+
if (dialogueList == null || dialogueList.length == 0 || dialogueList[0] == null)
280+
return;
281+
257282
var splitName:Array<String> = dialogueList[0].split(':');
258-
curCharacter = splitName[1];
259-
dialogueList[0] = dialogueList[0].substr(splitName[1].length + 2).trim();
283+
if (splitName.length > 1 && splitName[1] != null)
284+
{
285+
curCharacter = splitName[1];
286+
dialogueList[0] = dialogueList[0].substr(splitName[1].length + 2).trim();
287+
}
288+
else
289+
{
290+
curCharacter = 'dad';
291+
dialogueList[0] = dialogueList[0].trim();
292+
}
260293
}
261294
}

source/funkin/play/stage/Limo.hx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ class Limo extends BaseStage
8181
addBehindGF(fastCar);
8282

8383
var limo:BGSprite = new BGSprite('limo/limoDrive', -120, 550, 1, 1, ['Limo stage'], true);
84-
addBehindGF(limo); //Shitty layering but whatev it works LOL
84+
// Keep GF between the back limo and this front limo layer.
85+
addBehindDad(limo);
8586
}
8687

8788
var limoSpeed:Float = 0;

0 commit comments

Comments
 (0)