Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions project.hxp
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,9 @@ class Project extends HXProject
{
// This macro allows addition of new functionality to existing Flixel. -->
addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')");

// This macro will go over every song in the assets folder and store them in an array to check for cheated scores.
addHaxeMacro("funkin.util.macro.SongDataValidator.loadSongData()");
}

function configureOutputDir()
Expand Down
7 changes: 7 additions & 0 deletions source/funkin/data/song/SongRegistry.hx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import funkin.play.song.ScriptedSong;
import funkin.play.song.Song;
import funkin.util.assets.DataAssets;
import funkin.util.VersionUtil;
import funkin.util.macro.SongDataValidator;
import funkin.util.tools.ISingleton;
import funkin.data.DefaultRegistryImpl;

Expand Down Expand Up @@ -51,6 +52,7 @@ using funkin.data.song.migrator.SongDataMigrator;
public override function loadEntries():Void
{
clearEntries();
SongDataValidator.clearLists();

//
// SCRIPTED ENTRIES
Expand Down Expand Up @@ -476,11 +478,16 @@ using funkin.data.song.migrator.SongDataMigrator;
function loadEntryChartFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;

var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
if (!openfl.Assets.exists(entryFilePath)) return null;

var rawJson:String = openfl.Assets.getText(entryFilePath);
if (rawJson == null) return null;

rawJson = rawJson.trim();
SongDataValidator.checkChartValidity(rawJson, id, variation);

return {fileName: entryFilePath, contents: rawJson};
}

Expand Down
3 changes: 2 additions & 1 deletion source/funkin/play/PlayState.hx
Original file line number Diff line number Diff line change
Expand Up @@ -3316,8 +3316,9 @@ class PlayState extends MusicBeatSubState

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

if (currentSong != null && currentSong.validScore)
if (currentSong != null && currentSong.validScore && isChartValid)
{
// crackhead double thingie, sets whether was new highscore, AND saves the song!
var data =
Expand Down
3 changes: 2 additions & 1 deletion source/funkin/play/ResultState.hx
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,8 @@ class ResultState extends MusicBeatSubState
clearPercentCounter.curNumber = clearPercentTarget;

#if FEATURE_NEWGROUNDS
var isScoreValid = !(params?.isPracticeMode ?? false) && !(params?.isBotPlayMode ?? false);
var isChartValid:Bool = funkin.util.macro.SongDataValidator.isChartValid(params?.songId ?? "", params?.variationId ?? Constants.DEFAULT_VARIATION);
var isScoreValid = !(params?.isPracticeMode ?? false) && !(params?.isBotPlayMode ?? false) && isChartValid;
// This is the easiest spot to do the medal calculation lol.
if (isScoreValid && clearPercentTarget == 69) Medals.award(Nice);
#end
Expand Down
149 changes: 149 additions & 0 deletions source/funkin/util/macro/SongDataValidator.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package funkin.util.macro;

import haxe.crypto.Sha1;
import haxe.rtti.Meta;
#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.io.Path;
import sys.FileSystem;
#end

class SongDataValidator
{
static var _allCharts:Map<String, String> = null;
static var _checkedCharts:Array<String> = [];
static var _invalidCharts:Array<String> = [];

/**
* See if the chart for the variation is valid, i.e. if the chart content differs from the compilation-time one.
* If it isn't, add it to an array of invalid charts.
* @param chartContent The content of the chart file.
*/
public static function checkChartValidity(chartContent:String, songId:String, variation:String = "default"):Void
{
var songFormat:String = '${songId}::${variation}';

// If the chart is already checked, do nothing.
if (_checkedCharts.contains(songFormat)) return;

// If the all charts list is null, fetch it from the class' type.
if (_allCharts == null)
{
var metaData:Dynamic = Meta.getType(SongDataValidator);

if (metaData.charts != null)
{
_allCharts = [];

for (element in (metaData.charts ?? []))
{
if (element.length != 2) throw 'Malformed element in chart datas: ' + element;

var song:String = element[0];
var data:String = element[1];

_allCharts.set(song, data);
}
}
else
{
throw 'No chart datas found in SongDataValidator';
}
}

var isValid:Bool = false;

// If there is no chart found for the song and variation, it's a custom song and it should always be valid.
if (!_allCharts.exists(songFormat))
{
isValid = true;
}
else
{
// Check if the content matches.
var chartClean:String = Sha1.encode(chartContent);
if (chartClean == _allCharts.get(songFormat)) isValid = true;
}

// Add to an array if the chart is invalid.
if (!isValid)
{
trace(' [WARN] The chart file for the song $songId and variation $variation has been tampered with.');
_invalidCharts.push(songFormat);
}

// Add the song to the checked charts so that we don't have to run checks again.
_checkedCharts.push(songFormat);
}

/**
* Returns true if the chart isn't in the invalid charts list.
*/
public static function isChartValid(songId:String, variation:String = "default"):Bool
{
return !_invalidCharts.contains('${songId}::${variation}');
}

/**
* Clear the lists so we can check for songs again.
*/
public static function clearLists():Void
{
_checkedCharts = [];
_invalidCharts = [];
}

#if macro
public static inline final BASE_PATH:String = "assets/preload/data/songs";

static var calledBefore:Bool = false;
#end

public static macro function loadSongData():Void
{
Context.onAfterTyping(function(_) {
if (calledBefore) return;
calledBefore = true;

var allCharts:Array<Expr> = [];

// Load songs from the assets folder.
var songs:Array<String> = FileSystem.readDirectory(BASE_PATH);
for (song in songs)
{
var songFiles:Array<String> = FileSystem.readDirectory(Path.join([BASE_PATH, song]));
for (file in songFiles)
{
if (!StringTools.endsWith(file, ".json")) continue; // Exclude non-json files.

var splitter:Array<String> = StringTools.replace(file, ".json", "").split("-");

if (splitter[1] != "chart") continue; // Exclude non-chart files.

var variation:String = splitter[2] ?? "default";
var chart:String = sys.io.File.getContent(Path.join([BASE_PATH, song, file]));

chart = Sha1.encode(StringTools.trim(chart));

var entry = [macro $v{'${song}::${variation}'}, macro $v{chart}];

allCharts.push(macro $a{entry});
}
}

// Add the chart data to the class.
var dataClass = Context.getType('funkin.util.macro.SongDataValidator');

switch (dataClass)
{
case TInst(t, _):
var dataClassType = t.get();
dataClassType.meta.remove('charts');
dataClassType.meta.add('charts', allCharts, Context.currentPos());
default:
throw 'Could not find SongDataValidator type';
}
});
}
}