Enabled by default, the idea is that the less tech savy players won't bother changing the option to false and end up reporting that they lost screenshots after changing states
1664 lines
41 KiB
Haxe
1664 lines
41 KiB
Haxe
package funkin.save;
|
|
|
|
import flixel.util.FlxSave;
|
|
import funkin.input.Controls.Device;
|
|
import funkin.play.scoring.Scoring;
|
|
import funkin.play.scoring.Scoring.ScoringRank;
|
|
import funkin.save.migrator.RawSaveData_v1_0_0;
|
|
import funkin.save.migrator.SaveDataMigrator;
|
|
import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle;
|
|
import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
|
|
import funkin.ui.debug.stageeditor.StageEditorState.StageEditorTheme;
|
|
import funkin.util.FileUtil;
|
|
import funkin.util.SerializerUtil;
|
|
import thx.semver.Version;
|
|
#if FEATURE_NEWGROUNDS
|
|
import funkin.api.newgrounds.Medals;
|
|
import funkin.api.newgrounds.Leaderboards;
|
|
#end
|
|
|
|
@:nullSafety
|
|
class Save
|
|
{
|
|
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.1.0";
|
|
public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=2.1.0 <2.2.0";
|
|
|
|
// We load this version's saves from a new save path, to maintain SOME level of backwards compatibility.
|
|
static final SAVE_PATH:String = 'FunkinCrew';
|
|
static final SAVE_NAME:String = 'Funkin';
|
|
|
|
static final SAVE_PATH_LEGACY:String = 'ninjamuffin99';
|
|
static final SAVE_NAME_LEGACY:String = 'funkin';
|
|
|
|
public static var instance(get, never):Save;
|
|
static var _instance:Null<Save> = null;
|
|
|
|
static function get_instance():Save
|
|
{
|
|
if (_instance == null)
|
|
{
|
|
return _instance = load();
|
|
}
|
|
return _instance;
|
|
}
|
|
|
|
var data:RawSaveData;
|
|
|
|
public static function load():Save
|
|
{
|
|
trace("[SAVE] Loading save...");
|
|
|
|
// Bind save data.
|
|
return loadFromSlot(1);
|
|
}
|
|
|
|
/**
|
|
* Constructing a new Save will load the default values.
|
|
*/
|
|
public function new(?data:RawSaveData)
|
|
{
|
|
if (data == null) this.data = Save.getDefault();
|
|
else
|
|
this.data = data;
|
|
|
|
// Make sure the verison number is up to date before we flush.
|
|
updateVersionToLatest();
|
|
}
|
|
|
|
public static function getDefault():RawSaveData
|
|
{
|
|
return {
|
|
// Version number is an abstract(Array) internally.
|
|
// This means it copies by reference, so merging save data overides the version number lol.
|
|
version: thx.Dynamics.clone(Save.SAVE_DATA_VERSION),
|
|
|
|
volume: 1.0,
|
|
mute: false,
|
|
|
|
api:
|
|
{
|
|
newgrounds:
|
|
{
|
|
sessionId: null,
|
|
}
|
|
},
|
|
scores:
|
|
{
|
|
// No saved scores.
|
|
levels: [],
|
|
songs: [],
|
|
},
|
|
|
|
favoriteSongs: [],
|
|
|
|
options:
|
|
{
|
|
// Reasonable defaults.
|
|
framerate: 60,
|
|
naughtyness: true,
|
|
downscroll: false,
|
|
flashingLights: true,
|
|
zoomCamera: true,
|
|
debugDisplay: false,
|
|
autoPause: true,
|
|
strumlineBackgroundOpacity: 0,
|
|
autoFullscreen: false,
|
|
inputOffset: 0,
|
|
audioVisualOffset: 0,
|
|
unlockedFramerate: false,
|
|
|
|
screenshot:
|
|
{
|
|
shouldHideMouse: true,
|
|
fancyPreview: true,
|
|
previewOnSave: true,
|
|
saveFormat: 'PNG',
|
|
jpegQuality: 80,
|
|
},
|
|
|
|
controls:
|
|
{
|
|
// Leave controls blank so defaults are loaded.
|
|
p1:
|
|
{
|
|
keyboard: {},
|
|
gamepad: {},
|
|
},
|
|
p2:
|
|
{
|
|
keyboard: {},
|
|
gamepad: {},
|
|
},
|
|
},
|
|
},
|
|
|
|
mods:
|
|
{
|
|
// No mods enabled.
|
|
enabledMods: [],
|
|
modOptions: [],
|
|
},
|
|
|
|
unlocks:
|
|
{
|
|
// Default to having seen the default character.
|
|
charactersSeen: ["bf"],
|
|
oldChar: false
|
|
},
|
|
|
|
optionsChartEditor:
|
|
{
|
|
// Reasonable defaults.
|
|
previousFiles: [],
|
|
noteQuant: 3,
|
|
chartEditorLiveInputStyle: ChartEditorLiveInputStyle.None,
|
|
theme: ChartEditorTheme.Light,
|
|
playtestStartTime: false,
|
|
downscroll: false,
|
|
metronomeVolume: 1.0,
|
|
hitsoundVolumePlayer: 1.0,
|
|
hitsoundVolumeOpponent: 1.0,
|
|
themeMusic: true
|
|
},
|
|
|
|
optionsStageEditor:
|
|
{
|
|
previousFiles: [],
|
|
moveStep: "1px",
|
|
angleStep: 5,
|
|
theme: StageEditorTheme.Light
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* NOTE: Modifications will not be saved without calling `Save.flush()`!
|
|
*/
|
|
public var options(get, never):SaveDataOptions;
|
|
|
|
function get_options():SaveDataOptions
|
|
{
|
|
return data.options;
|
|
}
|
|
|
|
/**
|
|
* NOTE: Modifications will not be saved without calling `Save.flush()`!
|
|
*/
|
|
public var modOptions(get, never):Map<String, Dynamic>;
|
|
|
|
function get_modOptions():Map<String, Dynamic>
|
|
{
|
|
return data.mods.modOptions;
|
|
}
|
|
|
|
/**
|
|
* The current session ID for the logged-in Newgrounds user, or null if the user is cringe.
|
|
*/
|
|
public var ngSessionId(get, set):Null<String>;
|
|
|
|
function get_ngSessionId():Null<String>
|
|
{
|
|
return data.api.newgrounds.sessionId;
|
|
}
|
|
|
|
function set_ngSessionId(value:Null<String>):Null<String>
|
|
{
|
|
data.api.newgrounds.sessionId = value;
|
|
flush();
|
|
return data.api.newgrounds.sessionId;
|
|
}
|
|
|
|
public var enabledModIds(get, set):Array<String>;
|
|
|
|
function get_enabledModIds():Array<String>
|
|
{
|
|
return data.mods.enabledMods;
|
|
}
|
|
|
|
function set_enabledModIds(value:Array<String>):Array<String>
|
|
{
|
|
data.mods.enabledMods = value;
|
|
flush();
|
|
return data.mods.enabledMods;
|
|
}
|
|
|
|
public var chartEditorPreviousFiles(get, set):Array<String>;
|
|
|
|
function get_chartEditorPreviousFiles():Array<String>
|
|
{
|
|
if (data.optionsChartEditor.previousFiles == null) data.optionsChartEditor.previousFiles = [];
|
|
|
|
return data.optionsChartEditor.previousFiles;
|
|
}
|
|
|
|
function set_chartEditorPreviousFiles(value:Array<String>):Array<String>
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.previousFiles = value;
|
|
flush();
|
|
return data.optionsChartEditor.previousFiles;
|
|
}
|
|
|
|
public var chartEditorHasBackup(get, set):Bool;
|
|
|
|
function get_chartEditorHasBackup():Bool
|
|
{
|
|
if (data.optionsChartEditor.hasBackup == null) data.optionsChartEditor.hasBackup = false;
|
|
|
|
return data.optionsChartEditor.hasBackup;
|
|
}
|
|
|
|
function set_chartEditorHasBackup(value:Bool):Bool
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.hasBackup = value;
|
|
flush();
|
|
return data.optionsChartEditor.hasBackup;
|
|
}
|
|
|
|
public var chartEditorNoteQuant(get, set):Int;
|
|
|
|
function get_chartEditorNoteQuant():Int
|
|
{
|
|
if (data.optionsChartEditor.noteQuant == null) data.optionsChartEditor.noteQuant = 3;
|
|
|
|
return data.optionsChartEditor.noteQuant;
|
|
}
|
|
|
|
function set_chartEditorNoteQuant(value:Int):Int
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.noteQuant = value;
|
|
flush();
|
|
return data.optionsChartEditor.noteQuant;
|
|
}
|
|
|
|
public var chartEditorLiveInputStyle(get, set):ChartEditorLiveInputStyle;
|
|
|
|
function get_chartEditorLiveInputStyle():ChartEditorLiveInputStyle
|
|
{
|
|
if (data.optionsChartEditor.chartEditorLiveInputStyle == null) data.optionsChartEditor.chartEditorLiveInputStyle = ChartEditorLiveInputStyle.None;
|
|
|
|
return data.optionsChartEditor.chartEditorLiveInputStyle;
|
|
}
|
|
|
|
function set_chartEditorLiveInputStyle(value:ChartEditorLiveInputStyle):ChartEditorLiveInputStyle
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.chartEditorLiveInputStyle = value;
|
|
flush();
|
|
return data.optionsChartEditor.chartEditorLiveInputStyle;
|
|
}
|
|
|
|
public var chartEditorDownscroll(get, set):Bool;
|
|
|
|
function get_chartEditorDownscroll():Bool
|
|
{
|
|
if (data.optionsChartEditor.downscroll == null) data.optionsChartEditor.downscroll = false;
|
|
|
|
return data.optionsChartEditor.downscroll;
|
|
}
|
|
|
|
function set_chartEditorDownscroll(value:Bool):Bool
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.downscroll = value;
|
|
flush();
|
|
return data.optionsChartEditor.downscroll;
|
|
}
|
|
|
|
public var chartEditorPlaytestStartTime(get, set):Bool;
|
|
|
|
function get_chartEditorPlaytestStartTime():Bool
|
|
{
|
|
if (data.optionsChartEditor.playtestStartTime == null) data.optionsChartEditor.playtestStartTime = false;
|
|
|
|
return data.optionsChartEditor.playtestStartTime;
|
|
}
|
|
|
|
function set_chartEditorPlaytestStartTime(value:Bool):Bool
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.playtestStartTime = value;
|
|
flush();
|
|
return data.optionsChartEditor.playtestStartTime;
|
|
}
|
|
|
|
public var chartEditorTheme(get, set):ChartEditorTheme;
|
|
|
|
function get_chartEditorTheme():ChartEditorTheme
|
|
{
|
|
if (data.optionsChartEditor.theme == null) data.optionsChartEditor.theme = ChartEditorTheme.Light;
|
|
|
|
return data.optionsChartEditor.theme;
|
|
}
|
|
|
|
function set_chartEditorTheme(value:ChartEditorTheme):ChartEditorTheme
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.theme = value;
|
|
flush();
|
|
return data.optionsChartEditor.theme;
|
|
}
|
|
|
|
public var chartEditorMetronomeVolume(get, set):Float;
|
|
|
|
function get_chartEditorMetronomeVolume():Float
|
|
{
|
|
if (data.optionsChartEditor.metronomeVolume == null) data.optionsChartEditor.metronomeVolume = 1.0;
|
|
|
|
return data.optionsChartEditor.metronomeVolume;
|
|
}
|
|
|
|
function set_chartEditorMetronomeVolume(value:Float):Float
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.metronomeVolume = value;
|
|
flush();
|
|
return data.optionsChartEditor.metronomeVolume;
|
|
}
|
|
|
|
public var chartEditorHitsoundVolumePlayer(get, set):Float;
|
|
|
|
function get_chartEditorHitsoundVolumePlayer():Float
|
|
{
|
|
if (data.optionsChartEditor.hitsoundVolumePlayer == null) data.optionsChartEditor.hitsoundVolumePlayer = 1.0;
|
|
|
|
return data.optionsChartEditor.hitsoundVolumePlayer;
|
|
}
|
|
|
|
function set_chartEditorHitsoundVolumePlayer(value:Float):Float
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.hitsoundVolumePlayer = value;
|
|
flush();
|
|
return data.optionsChartEditor.hitsoundVolumePlayer;
|
|
}
|
|
|
|
public var chartEditorHitsoundVolumeOpponent(get, set):Float;
|
|
|
|
function get_chartEditorHitsoundVolumeOpponent():Float
|
|
{
|
|
if (data.optionsChartEditor.hitsoundVolumeOpponent == null) data.optionsChartEditor.hitsoundVolumeOpponent = 1.0;
|
|
|
|
return data.optionsChartEditor.hitsoundVolumeOpponent;
|
|
}
|
|
|
|
function set_chartEditorHitsoundVolumeOpponent(value:Float):Float
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.hitsoundVolumeOpponent = value;
|
|
flush();
|
|
return data.optionsChartEditor.hitsoundVolumeOpponent;
|
|
}
|
|
|
|
public var chartEditorThemeMusic(get, set):Bool;
|
|
|
|
function get_chartEditorThemeMusic():Bool
|
|
{
|
|
if (data.optionsChartEditor.themeMusic == null) data.optionsChartEditor.themeMusic = true;
|
|
|
|
return data.optionsChartEditor.themeMusic;
|
|
}
|
|
|
|
function set_chartEditorThemeMusic(value:Bool):Bool
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.themeMusic = value;
|
|
flush();
|
|
return data.optionsChartEditor.themeMusic;
|
|
}
|
|
|
|
public var chartEditorPlaybackSpeed(get, set):Float;
|
|
|
|
function get_chartEditorPlaybackSpeed():Float
|
|
{
|
|
if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 1.0;
|
|
|
|
return data.optionsChartEditor.playbackSpeed;
|
|
}
|
|
|
|
function set_chartEditorPlaybackSpeed(value:Float):Float
|
|
{
|
|
// Set and apply.
|
|
data.optionsChartEditor.playbackSpeed = value;
|
|
flush();
|
|
return data.optionsChartEditor.playbackSpeed;
|
|
}
|
|
|
|
public var charactersSeen(get, never):Array<String>;
|
|
|
|
function get_charactersSeen():Array<String>
|
|
{
|
|
return data.unlocks.charactersSeen;
|
|
}
|
|
|
|
/**
|
|
* Marks whether the player has seen the spotlight animation, which should only display once per save file ever.
|
|
*/
|
|
public var oldChar(get, set):Bool;
|
|
|
|
function get_oldChar():Bool
|
|
{
|
|
return data.unlocks.oldChar;
|
|
}
|
|
|
|
function set_oldChar(value:Bool):Bool
|
|
{
|
|
data.unlocks.oldChar = value;
|
|
flush();
|
|
return data.unlocks.oldChar;
|
|
}
|
|
|
|
public var stageEditorPreviousFiles(get, set):Array<String>;
|
|
|
|
function get_stageEditorPreviousFiles():Array<String>
|
|
{
|
|
if (data.optionsStageEditor.previousFiles == null) data.optionsStageEditor.previousFiles = [];
|
|
|
|
return data.optionsStageEditor.previousFiles;
|
|
}
|
|
|
|
function set_stageEditorPreviousFiles(value:Array<String>):Array<String>
|
|
{
|
|
// Set and apply.
|
|
data.optionsStageEditor.previousFiles = value;
|
|
flush();
|
|
return data.optionsStageEditor.previousFiles;
|
|
}
|
|
|
|
public var stageEditorHasBackup(get, set):Bool;
|
|
|
|
function get_stageEditorHasBackup():Bool
|
|
{
|
|
if (data.optionsStageEditor.hasBackup == null) data.optionsStageEditor.hasBackup = false;
|
|
|
|
return data.optionsStageEditor.hasBackup;
|
|
}
|
|
|
|
function set_stageEditorHasBackup(value:Bool):Bool
|
|
{
|
|
// Set and apply.
|
|
data.optionsStageEditor.hasBackup = value;
|
|
flush();
|
|
return data.optionsStageEditor.hasBackup;
|
|
}
|
|
|
|
public var stageEditorMoveStep(get, set):String;
|
|
|
|
function get_stageEditorMoveStep():String
|
|
{
|
|
if (data.optionsStageEditor.moveStep == null) data.optionsStageEditor.moveStep = "1px";
|
|
|
|
return data.optionsStageEditor.moveStep;
|
|
}
|
|
|
|
function set_stageEditorMoveStep(value:String):String
|
|
{
|
|
// Set and apply.
|
|
data.optionsStageEditor.moveStep = value;
|
|
flush();
|
|
return data.optionsStageEditor.moveStep;
|
|
}
|
|
|
|
public var stageEditorAngleStep(get, set):Float;
|
|
|
|
function get_stageEditorAngleStep():Float
|
|
{
|
|
if (data.optionsStageEditor.angleStep == null) data.optionsStageEditor.angleStep = 5;
|
|
|
|
return data.optionsStageEditor.angleStep;
|
|
}
|
|
|
|
function set_stageEditorAngleStep(value:Float):Float
|
|
{
|
|
// Set and apply.
|
|
data.optionsStageEditor.angleStep = value;
|
|
flush();
|
|
return data.optionsStageEditor.angleStep;
|
|
}
|
|
|
|
public var stageEditorTheme(get, set):StageEditorTheme;
|
|
|
|
function get_stageEditorTheme():StageEditorTheme
|
|
{
|
|
if (data.optionsStageEditor.theme == null) data.optionsStageEditor.theme = StageEditorTheme.Light;
|
|
|
|
return data.optionsStageEditor.theme;
|
|
}
|
|
|
|
function set_stageEditorTheme(value:StageEditorTheme):StageEditorTheme
|
|
{
|
|
// Set and apply.
|
|
data.optionsStageEditor.theme = value;
|
|
flush();
|
|
return data.optionsStageEditor.theme;
|
|
}
|
|
|
|
/**
|
|
* When we've seen a character unlock, add it to the list of characters seen.
|
|
* @param character
|
|
*/
|
|
public function addCharacterSeen(character:String):Void
|
|
{
|
|
if (!data.unlocks.charactersSeen.contains(character))
|
|
{
|
|
trace('Character seen: ' + character);
|
|
data.unlocks.charactersSeen.push(character);
|
|
trace('New characters seen list: ' + data.unlocks.charactersSeen);
|
|
flush();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the score the user achieved for a given level on a given difficulty.
|
|
*
|
|
* @param levelId The ID of the level/week.
|
|
* @param difficultyId The difficulty to check.
|
|
* @return A data structure containing score, judgement counts, and accuracy. Returns `null` if no score is saved.
|
|
*/
|
|
public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null<SaveScoreData>
|
|
{
|
|
if (data.scores?.levels == null)
|
|
{
|
|
if (data.scores == null)
|
|
{
|
|
data.scores =
|
|
{
|
|
songs: [],
|
|
levels: []
|
|
};
|
|
}
|
|
else
|
|
{
|
|
data.scores.levels = [];
|
|
}
|
|
}
|
|
|
|
var level = data.scores.levels.get(levelId);
|
|
if (level == null)
|
|
{
|
|
level = [];
|
|
data.scores.levels.set(levelId, level);
|
|
}
|
|
|
|
return level.get(difficultyId);
|
|
}
|
|
|
|
/**
|
|
* Apply the score the user achieved for a given level on a given difficulty.
|
|
*/
|
|
public function setLevelScore(levelId:String, difficultyId:String, score:SaveScoreData):Void
|
|
{
|
|
var level = data.scores.levels.get(levelId);
|
|
if (level == null)
|
|
{
|
|
level = [];
|
|
data.scores.levels.set(levelId, level);
|
|
}
|
|
level.set(difficultyId, score);
|
|
|
|
flush();
|
|
}
|
|
|
|
public function isLevelHighScore(levelId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
|
|
{
|
|
var level = data.scores.levels.get(levelId);
|
|
if (level == null)
|
|
{
|
|
level = [];
|
|
data.scores.levels.set(levelId, level);
|
|
}
|
|
|
|
var currentScore = level.get(difficultyId);
|
|
if (currentScore == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return score.score > currentScore.score;
|
|
}
|
|
|
|
public function hasBeatenLevel(levelId:String, ?difficultyList:Array<String>):Bool
|
|
{
|
|
if (difficultyList == null)
|
|
{
|
|
difficultyList = ['easy', 'normal', 'hard'];
|
|
}
|
|
for (difficulty in difficultyList)
|
|
{
|
|
var score:Null<SaveScoreData> = getLevelScore(levelId, difficulty);
|
|
if (score != null)
|
|
{
|
|
if (score.score > 0)
|
|
{
|
|
// Level has score data, which means we cleared it!
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Level has score data, but the score is 0.
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return the score the user achieved for a given song on a given difficulty.
|
|
*
|
|
* @param songId The ID of the song.
|
|
* @param difficultyId The difficulty to check.
|
|
* @param variation The variation to check. Defaults to empty string. Appended to difficulty with `-`, e.g. `easy-pico`.
|
|
* @return A data structure containing score, judgement counts, and accuracy. Returns `null` if no score is saved.
|
|
*/
|
|
public function getSongScore(songId:String, difficultyId:String = 'normal', ?variation:String):Null<SaveScoreData>
|
|
{
|
|
var song = data.scores.songs.get(songId);
|
|
if (song == null)
|
|
{
|
|
trace('Could not find song data for $songId $difficultyId $variation');
|
|
song = [];
|
|
data.scores.songs.set(songId, song);
|
|
}
|
|
|
|
// 'default' variations are left with no suffix ('easy', 'normal', 'hard'),
|
|
// along with 'erect' variations ('erect', 'nightmare')
|
|
// otherwise, we want to add a suffix of our current variation to get the save data.
|
|
if (variation != null && variation != '' && variation != 'default' && variation != 'erect')
|
|
{
|
|
difficultyId = '${difficultyId}-${variation}';
|
|
}
|
|
|
|
return song.get(difficultyId);
|
|
}
|
|
|
|
public function getSongRank(songId:String, difficultyId:String = 'normal', ?variation:String):Null<ScoringRank>
|
|
{
|
|
return Scoring.calculateRank(getSongScore(songId, difficultyId, variation));
|
|
}
|
|
|
|
/**
|
|
* Directly set the score the user achieved for a given song on a given difficulty.
|
|
*/
|
|
public function setSongScore(songId:String, difficultyId:String, score:SaveScoreData):Void
|
|
{
|
|
var song = data.scores.songs.get(songId);
|
|
if (song == null)
|
|
{
|
|
song = [];
|
|
data.scores.songs.set(songId, song);
|
|
}
|
|
song.set(difficultyId, score);
|
|
|
|
flush();
|
|
}
|
|
|
|
/**
|
|
* Only replace the ranking data for the song, because the old score is still better.
|
|
*/
|
|
public function applySongRank(songId:String, difficultyId:String, newScoreData:SaveScoreData):Void
|
|
{
|
|
var newRank = Scoring.calculateRank(newScoreData);
|
|
if (newScoreData == null || newRank == null) return;
|
|
|
|
var song = data.scores.songs.get(songId);
|
|
if (song == null)
|
|
{
|
|
song = [];
|
|
data.scores.songs.set(songId, song);
|
|
}
|
|
|
|
var previousScoreData = song.get(difficultyId);
|
|
|
|
var previousRank = Scoring.calculateRank(previousScoreData);
|
|
|
|
if (previousScoreData == null || previousRank == null)
|
|
{
|
|
// Directly set the highscore.
|
|
setSongScore(songId, difficultyId, newScoreData);
|
|
|
|
#if FEATURE_NEWGROUNDS
|
|
Leaderboards.submitSongScore(songId, difficultyId, newScoreData.score);
|
|
#end
|
|
|
|
return;
|
|
}
|
|
|
|
var newCompletion = (newScoreData.tallies.sick + newScoreData.tallies.good) / newScoreData.tallies.totalNotes;
|
|
var previousCompletion = (previousScoreData.tallies.sick + previousScoreData.tallies.good) / previousScoreData.tallies.totalNotes;
|
|
|
|
// Set the high score and the high rank separately.
|
|
var newScore:SaveScoreData =
|
|
{
|
|
score: (previousScoreData.score > newScoreData.score) ? previousScoreData.score : newScoreData.score,
|
|
tallies: (previousRank > newRank || previousCompletion > newCompletion) ? previousScoreData.tallies : newScoreData.tallies
|
|
};
|
|
|
|
song.set(difficultyId, newScore);
|
|
|
|
flush();
|
|
}
|
|
|
|
/**
|
|
* Is the provided score data better than the current high score for the given song?
|
|
* @param songId The song ID to check.
|
|
* @param difficultyId The difficulty to check.
|
|
* @param score The score to check.
|
|
* @return Whether the score is better than the current high score.
|
|
*/
|
|
public function isSongHighScore(songId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
|
|
{
|
|
var song = data.scores.songs.get(songId);
|
|
if (song == null)
|
|
{
|
|
song = [];
|
|
data.scores.songs.set(songId, song);
|
|
}
|
|
|
|
var currentScore = song.get(difficultyId);
|
|
if (currentScore == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return score.score > currentScore.score;
|
|
}
|
|
|
|
/**
|
|
* Is the provided score data better than the current rank for the given song?
|
|
* @param songId The song ID to check.
|
|
* @param difficultyId The difficulty to check.
|
|
* @param score The score to check the rank for.
|
|
* @return Whether the score's rank is better than the current rank.
|
|
*/
|
|
public function isSongHighRank(songId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
|
|
{
|
|
var newScoreRank = Scoring.calculateRank(score);
|
|
if (newScoreRank == null)
|
|
{
|
|
// The provided score is invalid.
|
|
return false;
|
|
}
|
|
|
|
var song = data.scores.songs.get(songId);
|
|
if (song == null)
|
|
{
|
|
song = [];
|
|
data.scores.songs.set(songId, song);
|
|
}
|
|
var currentScore = song.get(difficultyId);
|
|
var currentScoreRank = Scoring.calculateRank(currentScore);
|
|
if (currentScoreRank == null)
|
|
{
|
|
// There is no primary highscore for this song.
|
|
return true;
|
|
}
|
|
|
|
return newScoreRank > currentScoreRank;
|
|
}
|
|
|
|
/**
|
|
* Has the provided song been beaten on one of the listed difficulties?
|
|
* Note: This function can still take in the 'difficulty-variation' format for the difficultyList parameter
|
|
* as it is used in the old save data format. However inputting a variation will append it to the difficulty
|
|
* so you can do `hasBeatenSong('dadbattle', ['easy-pico'])` to check if you've beaten the Pico mix on easy.
|
|
* or you can do `hasBeatenSong('dadbattle', ['easy'], 'pico')` to check if you've beaten the Pico mix on easy.
|
|
* however you should not mix the two as it will append '-pico' to the 'easy-pico' if it's inputted into the array.
|
|
* @param songId The song ID to check.
|
|
* @param difficultyList The difficulties to check. Defaults to `easy`, `normal`, and `hard`.
|
|
* @param variation The variation to check. Defaults to empty string. Appended to difficulty list with `-`, e.g. `easy-pico`.
|
|
* This is our old format for getting difficulty/variation information, however we don't want to mess around with
|
|
* save migration just yet.
|
|
* @return Whether the song has been beaten on any of the listed difficulties.
|
|
*/
|
|
public function hasBeatenSong(songId:String, ?difficultyList:Array<String>, ?variation:String):Bool
|
|
{
|
|
if (difficultyList == null)
|
|
{
|
|
difficultyList = ['easy', 'normal', 'hard'];
|
|
}
|
|
|
|
if (variation == null) variation = '';
|
|
|
|
for (difficulty in difficultyList)
|
|
{
|
|
if (variation != '') difficulty = '${difficulty}-${variation}';
|
|
|
|
var score:Null<SaveScoreData> = getSongScore(songId, difficulty);
|
|
if (score != null)
|
|
{
|
|
if (score.score > 0)
|
|
{
|
|
// Level has score data, which means we cleared it!
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// Level has score data, but the score is 0.
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public function isSongFavorited(id:String):Bool
|
|
{
|
|
if (data.favoriteSongs == null)
|
|
{
|
|
data.favoriteSongs = [];
|
|
flush();
|
|
};
|
|
|
|
return data.favoriteSongs.contains(id);
|
|
}
|
|
|
|
public function favoriteSong(id:String):Void
|
|
{
|
|
if (!isSongFavorited(id))
|
|
{
|
|
data.favoriteSongs.push(id);
|
|
flush();
|
|
}
|
|
}
|
|
|
|
public function unfavoriteSong(id:String):Void
|
|
{
|
|
if (isSongFavorited(id))
|
|
{
|
|
data.favoriteSongs.remove(id);
|
|
flush();
|
|
}
|
|
}
|
|
|
|
public function getControls(playerId:Int, inputType:Device):Null<SaveControlsData>
|
|
{
|
|
switch (inputType)
|
|
{
|
|
case Keys:
|
|
return (playerId == 0) ? data?.options?.controls?.p1.keyboard : data?.options?.controls?.p2.keyboard;
|
|
case Gamepad(_):
|
|
return (playerId == 0) ? data?.options?.controls?.p1.gamepad : data?.options?.controls?.p2.gamepad;
|
|
}
|
|
}
|
|
|
|
public function hasControls(playerId:Int, inputType:Device):Bool
|
|
{
|
|
var controls = getControls(playerId, inputType);
|
|
if (controls == null) return false;
|
|
|
|
var controlsFields = Reflect.fields(controls);
|
|
return controlsFields.length > 0;
|
|
}
|
|
|
|
public function setControls(playerId:Int, inputType:Device, controls:SaveControlsData):Void
|
|
{
|
|
switch (inputType)
|
|
{
|
|
case Keys:
|
|
if (playerId == 0)
|
|
{
|
|
data.options.controls.p1.keyboard = controls;
|
|
}
|
|
else
|
|
{
|
|
data.options.controls.p2.keyboard = controls;
|
|
}
|
|
case Gamepad(_):
|
|
if (playerId == 0)
|
|
{
|
|
data.options.controls.p1.gamepad = controls;
|
|
}
|
|
else
|
|
{
|
|
data.options.controls.p2.gamepad = controls;
|
|
}
|
|
}
|
|
|
|
flush();
|
|
}
|
|
|
|
public function isCharacterUnlocked(characterId:String):Bool
|
|
{
|
|
switch (characterId)
|
|
{
|
|
case 'bf':
|
|
return true;
|
|
case 'pico':
|
|
return hasBeatenLevel('weekend1');
|
|
default:
|
|
trace('Unknown character ID: ' + characterId);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The user's current volume setting.
|
|
*/
|
|
public var volume(get, set):Float;
|
|
|
|
function get_volume():Float
|
|
{
|
|
return data.volume;
|
|
}
|
|
|
|
function set_volume(value:Float):Float
|
|
{
|
|
return data.volume = value;
|
|
}
|
|
|
|
/**
|
|
* Whether the user's volume is currently muted.
|
|
*/
|
|
public var mute(get, set):Bool;
|
|
|
|
function get_mute():Bool
|
|
{
|
|
return data.mute;
|
|
}
|
|
|
|
function set_mute(value:Bool):Bool
|
|
{
|
|
return data.mute = value;
|
|
}
|
|
|
|
/**
|
|
* Call this to make sure the save data is written to disk.
|
|
*/
|
|
public function flush():Void
|
|
{
|
|
FlxG.save.flush();
|
|
}
|
|
|
|
/**
|
|
* If you set slot to `2`, it will load an independe
|
|
* @param slot
|
|
*/
|
|
static function loadFromSlot(slot:Int):Save
|
|
{
|
|
trace('[SAVE] Loading save from slot $slot...');
|
|
|
|
FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH);
|
|
|
|
switch (FlxG.save.status)
|
|
{
|
|
case EMPTY:
|
|
trace('[SAVE] Save data in slot ${slot} is empty, checking for legacy save data...');
|
|
var legacySaveData = fetchLegacySaveData();
|
|
if (legacySaveData != null)
|
|
{
|
|
trace('[SAVE] Found legacy save data, converting...');
|
|
var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData);
|
|
FlxG.save.mergeData(gameSave.data, true);
|
|
return gameSave;
|
|
}
|
|
else
|
|
{
|
|
trace('[SAVE] No legacy save data found.');
|
|
var gameSave = new Save();
|
|
FlxG.save.mergeData(gameSave.data, true);
|
|
return gameSave;
|
|
}
|
|
case ERROR(_): // DEPRECATED: Unused
|
|
return handleSaveDataError(slot);
|
|
case SAVE_ERROR(_):
|
|
return handleSaveDataError(slot);
|
|
case LOAD_ERROR(_):
|
|
return handleSaveDataError(slot);
|
|
case BOUND(_, _):
|
|
trace('[SAVE] Loaded existing save data in slot ${slot}.');
|
|
var gameSave = SaveDataMigrator.migrate(FlxG.save.data);
|
|
FlxG.save.mergeData(gameSave.data, true);
|
|
|
|
return gameSave;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call this when there is an error loading the save data in slot X.
|
|
*/
|
|
static function handleSaveDataError(slot:Int):Save
|
|
{
|
|
var msg = 'There was an error loading your save data in slot ${slot}.';
|
|
msg += '\nPlease report this issue to the developers.';
|
|
lime.app.Application.current.window.alert(msg, "Save Data Failure");
|
|
|
|
// Don't touch that slot anymore.
|
|
// Instead, load the next available slot.
|
|
|
|
var nextSlot = slot + 1;
|
|
|
|
if (nextSlot < 1000)
|
|
{
|
|
return loadFromSlot(nextSlot);
|
|
}
|
|
else
|
|
{
|
|
throw "End of save data slots. Can't load any more.";
|
|
}
|
|
}
|
|
|
|
public static function archiveBadSaveData(data:Dynamic):Int
|
|
{
|
|
// We want to save this somewhere so we can try to recover it for the user in the future!
|
|
|
|
final RECOVERY_SLOT_START = 1000;
|
|
|
|
return writeToAvailableSlot(RECOVERY_SLOT_START, data);
|
|
}
|
|
|
|
public static function debug_queryBadSaveData():Void
|
|
{
|
|
final RECOVERY_SLOT_START = 1000;
|
|
final RECOVERY_SLOT_END = 1100;
|
|
var firstBadSaveData = querySlotRange(RECOVERY_SLOT_START, RECOVERY_SLOT_END);
|
|
if (firstBadSaveData > 0)
|
|
{
|
|
trace('[SAVE] Found bad save data in slot ${firstBadSaveData}!');
|
|
trace('We should look into recovery...');
|
|
|
|
trace(haxe.Json.stringify(fetchFromSlotRaw(firstBadSaveData)));
|
|
}
|
|
}
|
|
|
|
static function fetchFromSlotRaw(slot:Int):Null<Dynamic>
|
|
{
|
|
var targetSaveData = new FlxSave();
|
|
targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
|
|
if (targetSaveData.isEmpty()) return null;
|
|
return targetSaveData.data;
|
|
}
|
|
|
|
static function writeToAvailableSlot(slot:Int, data:Dynamic):Int
|
|
{
|
|
trace('[SAVE] Finding slot to write data to (starting with ${slot})...');
|
|
|
|
var targetSaveData = new FlxSave();
|
|
targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
|
|
while (!targetSaveData.isEmpty())
|
|
{
|
|
// Keep trying to bind to slots until we find an empty slot.
|
|
trace('[SAVE] Slot ${slot} is taken, continuing...');
|
|
slot++;
|
|
targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
|
|
}
|
|
|
|
trace('[SAVE] Writing data to slot ${slot}...');
|
|
targetSaveData.mergeData(data, true);
|
|
|
|
trace('[SAVE] Data written to slot ${slot}!');
|
|
return slot;
|
|
}
|
|
|
|
/**
|
|
* Return true if the given save slot is not empty.
|
|
* @param slot The slot number to check.
|
|
* @return Whether the slot is not empty.
|
|
*/
|
|
static function querySlot(slot:Int):Bool
|
|
{
|
|
var targetSaveData = new FlxSave();
|
|
targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
|
|
switch (targetSaveData.status)
|
|
{
|
|
case EMPTY:
|
|
return false;
|
|
case ERROR(_): // DEPRECATED: Unused
|
|
return false;
|
|
case LOAD_ERROR(_):
|
|
return false;
|
|
case SAVE_ERROR(_):
|
|
return false;
|
|
case BOUND(_, _):
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if any of the slots in the given range is not empty.
|
|
* @param start The starting slot number to check.
|
|
* @param end The ending slot number to check.
|
|
* @return The first slot in the range that is not empty, or `-1` if none are.
|
|
*/
|
|
static function querySlotRange(start:Int, end:Int):Int
|
|
{
|
|
for (i in start...end)
|
|
{
|
|
if (querySlot(i))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static function fetchLegacySaveData():Null<RawSaveData_v1_0_0>
|
|
{
|
|
trace("[SAVE] Checking for legacy save data...");
|
|
var legacySave:FlxSave = new FlxSave();
|
|
legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY);
|
|
if (legacySave.isEmpty())
|
|
{
|
|
trace("[SAVE] No legacy save data found.");
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
trace("[SAVE] Legacy save data found.");
|
|
trace(legacySave.data);
|
|
return cast legacySave.data;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serialize this Save into a JSON string.
|
|
* @param pretty Whether the JSON should be big ol string (false),
|
|
* or formatted with tabs (true)
|
|
* @return The JSON string.
|
|
*/
|
|
public function serialize(pretty:Bool = true):String
|
|
{
|
|
var ignoreNullOptionals = true;
|
|
var writer = new json2object.JsonWriter<RawSaveData>(ignoreNullOptionals);
|
|
return writer.write(data, pretty ? ' ' : null);
|
|
}
|
|
|
|
public function updateVersionToLatest():Void
|
|
{
|
|
this.data.version = Save.SAVE_DATA_VERSION;
|
|
}
|
|
|
|
public function debug_dumpSave():Void
|
|
{
|
|
FileUtil.saveFile(haxe.io.Bytes.ofString(this.serialize()), [FileUtil.FILE_FILTER_JSON], null, null, './save.json', 'Write save data as JSON...');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An anonymous structure containingg all the user's save data.
|
|
* Isn't stored with JSON, stored with some sort of Haxe built-in serialization?
|
|
*/
|
|
typedef RawSaveData =
|
|
{
|
|
// Flixel save data.
|
|
var volume:Float;
|
|
var mute:Bool;
|
|
|
|
/**
|
|
* A semantic versioning string for the save data format.
|
|
*/
|
|
var version:Version;
|
|
|
|
var api:SaveApiData;
|
|
|
|
/**
|
|
* The user's saved scores.
|
|
*/
|
|
var scores:SaveHighScoresData;
|
|
|
|
/**
|
|
* The user's preferences.
|
|
*/
|
|
var options:SaveDataOptions;
|
|
|
|
var unlocks:SaveDataUnlocks;
|
|
|
|
/**
|
|
* The user's favorited songs in the Freeplay menu,
|
|
* as a list of song IDs.
|
|
*/
|
|
var favoriteSongs:Array<String>;
|
|
|
|
var mods:SaveDataMods;
|
|
|
|
/**
|
|
* The user's preferences specific to the Chart Editor.
|
|
*/
|
|
var optionsChartEditor:SaveDataChartEditorOptions;
|
|
|
|
/**
|
|
* The user's preferences specific to the Stage Editor.
|
|
*/
|
|
var optionsStageEditor:SaveDataStageEditorOptions;
|
|
};
|
|
|
|
typedef SaveApiData =
|
|
{
|
|
var newgrounds:SaveApiNewgroundsData;
|
|
}
|
|
|
|
typedef SaveApiNewgroundsData =
|
|
{
|
|
var sessionId:Null<String>;
|
|
}
|
|
|
|
typedef SaveDataUnlocks =
|
|
{
|
|
/**
|
|
* Every time we see the unlock animation for a character,
|
|
* add it to this list so that we don't show it again.
|
|
*/
|
|
var charactersSeen:Array<String>;
|
|
|
|
/**
|
|
* This is a conditional when the player enters the character state
|
|
* For the first time ever
|
|
*/
|
|
var oldChar:Bool;
|
|
}
|
|
|
|
/**
|
|
* An anoymous structure containing options about the user's high scores.
|
|
*/
|
|
typedef SaveHighScoresData =
|
|
{
|
|
/**
|
|
* Scores for each level (or week).
|
|
*/
|
|
var levels:SaveScoreLevelsData;
|
|
|
|
/**
|
|
* Scores for individual songs.
|
|
*/
|
|
var songs:SaveScoreSongsData;
|
|
};
|
|
|
|
typedef SaveDataMods =
|
|
{
|
|
var enabledMods:Array<String>;
|
|
|
|
// TODO: Make this not trip up the serializer when debugging.
|
|
@:jignored
|
|
var modOptions:Map<String, Dynamic>;
|
|
}
|
|
|
|
/**
|
|
* Key is the level ID, value is the SaveScoreLevelData.
|
|
*/
|
|
typedef SaveScoreLevelsData = Map<String, SaveScoreDifficultiesData>;
|
|
|
|
/**
|
|
* Key is the song ID, value is the data for each difficulty.
|
|
*/
|
|
typedef SaveScoreSongsData = Map<String, SaveScoreDifficultiesData>;
|
|
|
|
/**
|
|
* Key is the difficulty ID, value is the score.
|
|
*/
|
|
typedef SaveScoreDifficultiesData = Map<String, SaveScoreData>;
|
|
|
|
/**
|
|
* An individual score. Contains the score, accuracy, and count of each judgement hit.
|
|
*/
|
|
typedef SaveScoreData =
|
|
{
|
|
/**
|
|
* The score achieved.
|
|
*/
|
|
var score:Int;
|
|
|
|
/**
|
|
* The count of each judgement hit.
|
|
*/
|
|
var tallies:SaveScoreTallyData;
|
|
}
|
|
|
|
typedef SaveScoreTallyData =
|
|
{
|
|
var sick:Int;
|
|
var good:Int;
|
|
var bad:Int;
|
|
var shit:Int;
|
|
var missed:Int;
|
|
var combo:Int;
|
|
var maxCombo:Int;
|
|
var totalNotesHit:Int;
|
|
var totalNotes:Int;
|
|
}
|
|
|
|
/**
|
|
* An anonymous structure containing all the user's options and preferences for the main game.
|
|
* Every time you add a new option, it needs to be added here.
|
|
*/
|
|
typedef SaveDataOptions =
|
|
{
|
|
/**
|
|
* FPS
|
|
* @default `60`
|
|
*/
|
|
var framerate:Int;
|
|
|
|
/**
|
|
* Whether some particularly foul language is displayed.
|
|
* @default `true`
|
|
*/
|
|
var naughtyness:Bool;
|
|
|
|
/**
|
|
* If enabled, the strumline is at the bottom of the screen rather than the top.
|
|
* @default `false`
|
|
*/
|
|
var downscroll:Bool;
|
|
|
|
/**
|
|
* If disabled, flashing lights in the main menu and other areas will be less intense.
|
|
* @default `true`
|
|
*/
|
|
var flashingLights:Bool;
|
|
|
|
/**
|
|
* If disabled, the camera bump synchronized to the beat.
|
|
* @default `false`
|
|
*/
|
|
var zoomCamera:Bool;
|
|
|
|
/**
|
|
* If enabled, an FPS and memory counter will be displayed even if this is not a debug build.
|
|
* @default `false`
|
|
*/
|
|
var debugDisplay:Bool;
|
|
|
|
/**
|
|
* If enabled, the game will automatically pause when tabbing out.
|
|
* @default `true`
|
|
*/
|
|
var autoPause:Bool;
|
|
|
|
/**
|
|
* If >0, the game will display a semi-opaque background under the notes.
|
|
* `0` for no background, `100` for solid black if you're freaky like that
|
|
* @default `0`
|
|
*/
|
|
var strumlineBackgroundOpacity:Int;
|
|
|
|
/**
|
|
* If enabled, the game will automatically launch in fullscreen on startup.
|
|
* @default `true`
|
|
*/
|
|
var autoFullscreen:Bool;
|
|
|
|
/**
|
|
* Offset the user's inputs by this many ms.
|
|
* @default `0`
|
|
*/
|
|
var inputOffset:Int;
|
|
|
|
/**
|
|
* Affects the delay between the audio and the visuals during gameplay.
|
|
* @default `0`
|
|
*/
|
|
var audioVisualOffset:Int;
|
|
|
|
/**
|
|
* If we want the framerate to be unlocked on HTML5.
|
|
* @default `false`
|
|
*/
|
|
var unlockedFramerate:Bool;
|
|
|
|
/**
|
|
* Screenshot options
|
|
* @param shouldHideMouse Should the mouse be hidden when taking a screenshot? Default: `true`
|
|
* @param fancyPreview Show a fancy preview? Default: `true`
|
|
* @param previewOnSave Only show the fancy preview after a screenshot is saved? Default: `true`
|
|
* @param saveFormat The save format of the screenshot, PNG or JPEG. Default: `PNG`
|
|
* @param jpegQuality The JPEG Quality, if we're saving to the format. Default: `80`
|
|
*/
|
|
var screenshot:
|
|
{
|
|
var shouldHideMouse:Bool;
|
|
var fancyPreview:Bool;
|
|
var previewOnSave:Bool;
|
|
var saveFormat:String;
|
|
var jpegQuality:Int;
|
|
};
|
|
|
|
var controls:
|
|
{
|
|
var p1:
|
|
{
|
|
var keyboard:SaveControlsData;
|
|
var gamepad:SaveControlsData;
|
|
};
|
|
var p2:
|
|
{
|
|
var keyboard:SaveControlsData;
|
|
var gamepad:SaveControlsData;
|
|
};
|
|
};
|
|
};
|
|
|
|
/**
|
|
* An anonymous structure containing a specific player's bound keys.
|
|
* Each key is an action name and each value is an array of keycodes.
|
|
*
|
|
* If a keybind is `null`, it needs to be reinitialized to the default.
|
|
* If a keybind is `[]`, it is UNBOUND by the user and should not be rebound.
|
|
*/
|
|
typedef SaveControlsData =
|
|
{
|
|
/**
|
|
* Keybind for navigating in the menu.
|
|
* @default `Up Arrow`
|
|
*/
|
|
var ?UI_UP:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for navigating in the menu.
|
|
* @default `Left Arrow`
|
|
*/
|
|
var ?UI_LEFT:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for navigating in the menu.
|
|
* @default `Right Arrow`
|
|
*/
|
|
var ?UI_RIGHT:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for navigating in the menu.
|
|
* @default `Down Arrow`
|
|
*/
|
|
var ?UI_DOWN:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for hitting notes.
|
|
* @default `A` and `Left Arrow`
|
|
*/
|
|
var ?NOTE_LEFT:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for hitting notes.
|
|
* @default `W` and `Up Arrow`
|
|
*/
|
|
var ?NOTE_UP:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for hitting notes.
|
|
* @default `S` and `Down Arrow`
|
|
*/
|
|
var ?NOTE_DOWN:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for hitting notes.
|
|
* @default `D` and `Right Arrow`
|
|
*/
|
|
var ?NOTE_RIGHT:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for continue/OK in menus.
|
|
* @default `Enter` and `Space`
|
|
*/
|
|
var ?ACCEPT:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for back/cancel in menus.
|
|
* @default `Escape`
|
|
*/
|
|
var ?BACK:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for pausing the game.
|
|
* @default `Escape`
|
|
*/
|
|
var ?PAUSE:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for advancing cutscenes.
|
|
* @default `Z` and `Space` and `Enter`
|
|
*/
|
|
var ?CUTSCENE_ADVANCE:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for increasing volume.
|
|
* @default `Plus`
|
|
*/
|
|
var ?VOLUME_UP:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for decreasing volume.
|
|
* @default `Minus`
|
|
*/
|
|
var ?VOLUME_DOWN:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for muting/unmuting volume.
|
|
* @default `Zero`
|
|
*/
|
|
var ?VOLUME_MUTE:Array<Int>;
|
|
|
|
/**
|
|
* Keybind for restarting a song.
|
|
* @default `R`
|
|
*/
|
|
var ?RESET:Array<Int>;
|
|
}
|
|
|
|
/**
|
|
* An anonymous structure containing all the user's options and preferences, specific to the Chart Editor.
|
|
*/
|
|
typedef SaveDataChartEditorOptions =
|
|
{
|
|
/**
|
|
* Whether the Chart Editor created a backup the last time it closed.
|
|
* Prompt the user to load it, then set this back to `false`.
|
|
* @default `false`
|
|
*/
|
|
var ?hasBackup:Bool;
|
|
|
|
/**
|
|
* Previous files opened in the Chart Editor.
|
|
* @default `[]`
|
|
*/
|
|
var ?previousFiles:Array<String>;
|
|
|
|
/**
|
|
* Note snapping level in the Chart Editor.
|
|
* @default `3`
|
|
*/
|
|
var ?noteQuant:Int;
|
|
|
|
/**
|
|
* Live input style in the Chart Editor.
|
|
* @default `ChartEditorLiveInputStyle.None`
|
|
*/
|
|
var ?chartEditorLiveInputStyle:ChartEditorLiveInputStyle;
|
|
|
|
/**
|
|
* Theme in the Chart Editor.
|
|
* @default `ChartEditorTheme.Light`
|
|
*/
|
|
var ?theme:ChartEditorTheme;
|
|
|
|
/**
|
|
* Downscroll in the Chart Editor.
|
|
* @default `false`
|
|
*/
|
|
var ?downscroll:Bool;
|
|
|
|
/**
|
|
* Metronome volume in the Chart Editor.
|
|
* @default `1.0`
|
|
*/
|
|
var ?metronomeVolume:Float;
|
|
|
|
/**
|
|
* Hitsound volume (player) in the Chart Editor.
|
|
* @default `1.0`
|
|
*/
|
|
var ?hitsoundVolumePlayer:Float;
|
|
|
|
/**
|
|
* Hitsound volume (opponent) in the Chart Editor.
|
|
* @default `1.0`
|
|
*/
|
|
var ?hitsoundVolumeOpponent:Float;
|
|
|
|
/**
|
|
* If true, playtest songs from the current position in the Chart Editor.
|
|
* @default `false`
|
|
*/
|
|
var ?playtestStartTime:Bool;
|
|
|
|
/**
|
|
* Theme music in the Chart Editor.
|
|
* @default `true`
|
|
*/
|
|
var ?themeMusic:Bool;
|
|
|
|
/**
|
|
* Instrumental volume in the Chart Editor.
|
|
* @default `1.0`
|
|
*/
|
|
var ?instVolume:Float;
|
|
|
|
/**
|
|
* Voices volume in the Chart Editor.
|
|
* @default `1.0`
|
|
*/
|
|
var ?voicesVolume:Float;
|
|
|
|
/**
|
|
* Playback speed in the Chart Editor.
|
|
* @default `1.0`
|
|
*/
|
|
var ?playbackSpeed:Float;
|
|
};
|
|
|
|
typedef SaveDataStageEditorOptions =
|
|
{
|
|
// a lot of these things were copied from savedatacharteditoroptions
|
|
|
|
/**
|
|
* Whether the Stage Editor created a backup the last time it closed.
|
|
* Prompt the user to load it, then set this back to `false`.
|
|
* @default `false`
|
|
*/
|
|
var ?hasBackup:Bool;
|
|
|
|
/**
|
|
* Previous files opened in the Stage Editor.
|
|
* @default `[]`
|
|
*/
|
|
var ?previousFiles:Array<String>;
|
|
|
|
/**
|
|
* The Step at which an Object or Character is moved.
|
|
* @default `1px`
|
|
*/
|
|
var ?moveStep:String;
|
|
|
|
/**
|
|
* The Step at which an Object is rotated.
|
|
* @default `5`
|
|
*/
|
|
var ?angleStep:Float;
|
|
|
|
/**
|
|
* Theme in the Stage Editor.
|
|
* @default `StageEditorTheme.Light`
|
|
*/
|
|
var ?theme:StageEditorTheme;
|
|
};
|