Skip to content

Commit e0f0c81

Browse files
committed
use macro to detect if the chart has been tampered with on runtime
1 parent 5b5d76a commit e0f0c81

File tree

5 files changed

+162
-2
lines changed

5 files changed

+162
-2
lines changed

project.hxp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,9 @@ class Project extends HXProject
11051105
{
11061106
// This macro allows addition of new functionality to existing Flixel. -->
11071107
addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')");
1108+
1109+
// This macro will go over every song in the assets folder and store them in an array to check for cheated scores.
1110+
addHaxeMacro("funkin.util.macro.SongDataValidator.loadSongData()");
11081111
}
11091112

11101113
function configureOutputDir()

source/funkin/data/song/SongRegistry.hx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import funkin.play.song.ScriptedSong;
1010
import funkin.play.song.Song;
1111
import funkin.util.assets.DataAssets;
1212
import funkin.util.VersionUtil;
13+
import funkin.util.macro.SongDataValidator;
1314
import funkin.util.tools.ISingleton;
1415
import funkin.data.DefaultRegistryImpl;
1516

@@ -51,6 +52,7 @@ using funkin.data.song.migrator.SongDataMigrator;
5152
public override function loadEntries():Void
5253
{
5354
clearEntries();
55+
SongDataValidator.clearLists();
5456

5557
//
5658
// SCRIPTED ENTRIES
@@ -476,11 +478,16 @@ using funkin.data.song.migrator.SongDataMigrator;
476478
function loadEntryChartFile(id:String, ?variation:String):Null<JsonFile>
477479
{
478480
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
481+
479482
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
480483
if (!openfl.Assets.exists(entryFilePath)) return null;
484+
481485
var rawJson:String = openfl.Assets.getText(entryFilePath);
482486
if (rawJson == null) return null;
487+
483488
rawJson = rawJson.trim();
489+
SongDataValidator.checkChartValidity(rawJson, id, variation);
490+
484491
return {fileName: entryFilePath, contents: rawJson};
485492
}
486493

source/funkin/play/PlayState.hx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3316,8 +3316,9 @@ class PlayState extends MusicBeatSubState
33163316

33173317
var isNewHighscore = false;
33183318
var prevScoreData:Null<SaveScoreData> = Save.instance.getSongScore(currentSong.id, suffixedDifficulty);
3319+
var isChartValid:Bool = funkin.util.macro.SongDataValidator.isChartValid(currentSong.id, currentVariation);
33193320

3320-
if (currentSong != null && currentSong.validScore)
3321+
if (currentSong != null && currentSong.validScore && isChartValid)
33213322
{
33223323
// crackhead double thingie, sets whether was new highscore, AND saves the song!
33233324
var data =

source/funkin/play/ResultState.hx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,8 @@ class ResultState extends MusicBeatSubState
565565
clearPercentCounter.curNumber = clearPercentTarget;
566566

567567
#if FEATURE_NEWGROUNDS
568-
var isScoreValid = !(params?.isPracticeMode ?? false) && !(params?.isBotPlayMode ?? false);
568+
var isChartValid:Bool = funkin.util.macro.SongDataValidator.isChartValid(params?.songId ?? "", params?.variationId ?? Constants.DEFAULT_VARIATION);
569+
var isScoreValid = !(params?.isPracticeMode ?? false) && !(params?.isBotPlayMode ?? false) && isChartValid;
569570
// This is the easiest spot to do the medal calculation lol.
570571
if (isScoreValid && clearPercentTarget == 69) Medals.award(Nice);
571572
#end
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package funkin.util.macro;
2+
3+
import haxe.rtti.Meta;
4+
#if macro
5+
import haxe.macro.Context;
6+
import haxe.macro.Expr;
7+
import haxe.io.Path;
8+
import sys.FileSystem;
9+
#end
10+
11+
class SongDataValidator
12+
{
13+
static var _allCharts:Map<String, String> = null;
14+
static var _checkedCharts:Array<String> = [];
15+
static var _invalidCharts:Array<String> = [];
16+
17+
/**
18+
* See if the chart for the variation is valid, i.e. if the chart content differs from the compilation-time one.
19+
* If it isn't, add it to an array of invalid charts.
20+
* @param chartContent The content of the chart file.
21+
*/
22+
public static function checkChartValidity(chartContent:String, songId:String, variation:String = "default"):Void
23+
{
24+
var songFormat:String = '${songId}::${variation}';
25+
26+
// If the chart is already checked, do nothing.
27+
if (_checkedCharts.contains(songFormat)) return;
28+
29+
// If the all charts list is null, fetch it from the class' type.
30+
if (_allCharts == null)
31+
{
32+
var metaData:Dynamic = Meta.getType(SongDataValidator);
33+
34+
if (metaData.charts != null)
35+
{
36+
_allCharts = [];
37+
38+
for (element in (metaData.charts ?? []))
39+
{
40+
if (element.length != 2) throw 'Malformed element in chart datas: ' + element;
41+
42+
var song:String = element[0];
43+
var data:String = element[1];
44+
45+
_allCharts.set(song, data);
46+
}
47+
}
48+
else
49+
{
50+
throw 'No chart datas found in SongDataValidator';
51+
}
52+
}
53+
54+
var isValid:Bool = false;
55+
56+
// If there is no chart found for the song and variation, it's a custom song and it should always be valid.
57+
if (!_allCharts.exists(songFormat))
58+
{
59+
isValid = true;
60+
}
61+
else
62+
{
63+
// Check if the content matches.
64+
var chartClean:String = StringTools.trim(StringTools.replace(chartContent, "\n", ""));
65+
if (chartClean == _allCharts.get(songFormat)) isValid = true;
66+
}
67+
68+
// Add to an array if the chart is invalid.
69+
if (!isValid)
70+
{
71+
trace(' [WARN] The chart file for the song $songId and variation $variation has been tampered with.');
72+
_invalidCharts.push(songFormat);
73+
}
74+
75+
// Add the song to the checked charts so that we don't have to run checks again.
76+
_checkedCharts.push(songFormat);
77+
}
78+
79+
/**
80+
* Returns true if the chart isn't in the invalid charts list.
81+
*/
82+
public static function isChartValid(songId:String, variation:String = "default"):Bool
83+
{
84+
return !_invalidCharts.contains('${songId}::${variation}');
85+
}
86+
87+
/**
88+
* Clear the lists so we can check for songs again.
89+
*/
90+
public static function clearLists():Void
91+
{
92+
_checkedCharts = [];
93+
_invalidCharts = [];
94+
}
95+
96+
#if macro
97+
public static inline final BASE_PATH:String = "assets/preload/data/songs";
98+
99+
static var calledBefore:Bool = false;
100+
#end
101+
102+
public static macro function loadSongData():Void
103+
{
104+
Context.onAfterTyping(function(_) {
105+
if (calledBefore) return;
106+
calledBefore = true;
107+
108+
var allCharts:Array<Expr> = [];
109+
110+
// Load songs from the assets folder.
111+
var songs:Array<String> = FileSystem.readDirectory(BASE_PATH);
112+
for (song in songs)
113+
{
114+
var songFiles:Array<String> = FileSystem.readDirectory(Path.join([BASE_PATH, song]));
115+
for (file in songFiles)
116+
{
117+
if (!StringTools.endsWith(file, ".json")) continue; // Exclude non-json files.
118+
119+
var splitter:Array<String> = StringTools.replace(file, ".json", "").split("-");
120+
121+
if (splitter[1] != "chart") continue; // Exclude non-chart files.
122+
123+
var variation:String = splitter[2] ?? "default";
124+
var chart:String = sys.io.File.getContent(Path.join([BASE_PATH, song, file]));
125+
126+
chart = StringTools.trim(StringTools.replace(chart, "\n", ""));
127+
128+
var entry = [macro $v{'${song}::${variation}'}, macro $v{chart}];
129+
130+
allCharts.push(macro $a{entry});
131+
}
132+
}
133+
134+
// Add the chart data to the class.
135+
var dataClass = Context.getType('funkin.util.macro.SongDataValidator');
136+
137+
switch (dataClass)
138+
{
139+
case TInst(t, _):
140+
var dataClassType = t.get();
141+
dataClassType.meta.remove('charts');
142+
dataClassType.meta.add('charts', allCharts, Context.currentPos());
143+
default:
144+
throw 'Could not find SongDataValidator type';
145+
}
146+
});
147+
}
148+
}

0 commit comments

Comments
 (0)