diff --git a/lib/models/input_direction.dart b/lib/models/input_direction.dart index 08096c7..b15b880 100644 --- a/lib/models/input_direction.dart +++ b/lib/models/input_direction.dart @@ -3,7 +3,8 @@ class InputDirection { bool down = false; bool left = false; bool right = false; - + + /// reset all directions to false void reset() { up = false; down = false; diff --git a/lib/screens/level.dart b/lib/screens/level.dart index 9c5823b..a22c237 100644 --- a/lib/screens/level.dart +++ b/lib/screens/level.dart @@ -87,6 +87,7 @@ class _LevelState extends State with SingleTickerProviderStateMixin { } }); + // go to GameOverStats when level finishes player.onPlayerComplete.listen((void _) { Route route = MaterialPageRoute( builder: (context) => GameOverStats( @@ -105,6 +106,7 @@ class _LevelState extends State with SingleTickerProviderStateMixin { }); } + // convert beats to notes widget.simfile.chartSimplest?.beats.forEach((time, noteData) { int arrowIndex = noteData.indexOf('1'); if (arrowIndex < 0 || arrowIndex > 3) { @@ -126,6 +128,7 @@ class _LevelState extends State with SingleTickerProviderStateMixin { super.dispose(); } + /// toggle between pause and resume void _pauseResume() { if (_isPlaying) { player.pause(); @@ -140,18 +143,24 @@ class _LevelState extends State with SingleTickerProviderStateMixin { } } + /// checks if the [note] is hit on [time] with the correct InputDirection void _noteHitCheck(Note note, Duration time) { note.position = note.time - time.inMilliseconds / 60000.0; if (note.wasHit != null) { return; } + + // you have +- half a second to hit if (note.position.abs() < 0.5 * 1.0 / 60.0) { + // combine keyboard and esense input InputDirection esenseDirection = ESenseInput.instance.getInputDirection(note.direction); inputDirection.up |= esenseDirection.up; inputDirection.down |= esenseDirection.down; inputDirection.left |= esenseDirection.left; inputDirection.right |= esenseDirection.right; + + // check if input matches arrow direction bool keypressCorrect = false; switch (note.direction) { case ArrowDirection.up: @@ -189,6 +198,7 @@ class _LevelState extends State with SingleTickerProviderStateMixin { } } + /// sets the InputDirection based on the arrow keys void _keyboardHandler(event) { bool isDown = false; if (event is KeyDownEvent) { diff --git a/lib/screens/level_selection.dart b/lib/screens/level_selection.dart index 7241ad7..5984e65 100644 --- a/lib/screens/level_selection.dart +++ b/lib/screens/level_selection.dart @@ -28,6 +28,7 @@ class _LevelSelectionState extends State { loadFolderPath(); } + /// gets folder path from persistent storage and updates state with loaded simfiles Future loadFolderPath() async { SharedPreferences prefs = await SharedPreferences.getInstance(); final String? stepmaniaCoursesPathSetting = @@ -44,6 +45,7 @@ class _LevelSelectionState extends State { }); } + /// open folder selection dialog and save selected folder in persistent storage Future selectFolder() async { await Permission.manageExternalStorage.request(); String? selectedFolder = await FilePicker.platform.getDirectoryPath(); @@ -57,6 +59,7 @@ class _LevelSelectionState extends State { } } + /// load all simfiles from a [directoryPath] Future> listFilesAndFolders(String directoryPath) async { final directory = Directory(directoryPath); try { @@ -86,6 +89,7 @@ class _LevelSelectionState extends State { } } + /// filter stepmaniaCoursesFolders based on [input] void filterLevels(String input) { setState(() { stepmaniaCoursesFoldersFiltered = stepmaniaCoursesFolders diff --git a/lib/utils/esense_input.dart b/lib/utils/esense_input.dart index b738664..14cf7de 100644 --- a/lib/utils/esense_input.dart +++ b/lib/utils/esense_input.dart @@ -8,9 +8,12 @@ import 'package:sense_the_rhythm/models/arrow_direction.dart'; import 'package:sense_the_rhythm/models/input_direction.dart'; class ESenseInput { + // create singleton that is available on all widgets so it does not have to be + // carried down in the widget tree static final instance = ESenseInput._(); ESenseManager eSenseManager = ESenseManager('unknown'); + // valuenotifier allows widgets to rerender when the value changes ValueNotifier deviceStatus = ValueNotifier('Disconnected'); StreamSubscription? subscription; @@ -29,8 +32,11 @@ class ESenseInput { _listenToESense(); } + /// ask and check if permissions are enabled and granted Future _askForPermissions() async { + // is desktop if (!Platform.isAndroid && !Platform.isIOS) return false; + // is bluetooth even enabled? if (!await Permission.bluetooth.serviceStatus.isEnabled) { deviceStatus.value = "Bluetooth is disabled!"; return false; @@ -55,6 +61,7 @@ class ESenseInput { return true; } + /// listen to connectionEvents and set deviceStatus void _listenToESense() { // if you want to get the connection events when connecting, // set up the listener BEFORE connecting... @@ -89,12 +96,14 @@ class ESenseInput { }); } + /// get eSenseEvent stream only containung button events Stream buttonEvents() { return eSenseManager.eSenseEvents .where((event) => event.runtimeType == ButtonEventChanged) .cast(); } + /// sets sampling rate and listens to sensorEvents void _startListenToSensorEvents() async { // // any changes to the sampling frequency must be done BEFORE listening to sensor events print('setting sampling frequency...'); @@ -115,11 +124,14 @@ class ESenseInput { sampling = true; } + /// cancels the sensorEvents listening void _pauseListenToSensorEvents() async { subscription?.cancel(); sampling = false; } + /// add up all new gyro [data] in the form of deg/s multiplied by scaling factor + /// to get real angles void _parseGyroData(List data) { // Float value in deg/s = Gyro value / Gyro scale factor // The default configuration is +- 500deg/s for the gyroscope. @@ -131,6 +143,7 @@ class ESenseInput { // print('${z.toDouble() / 500.0 * (1.0 / 10.0)}'); } + /// nulls all angles and reset inputDirection void resetAngles() { inputDirection.reset(); x = 0; @@ -138,26 +151,29 @@ class ESenseInput { z = 0; } + /// get InputDirection by checking if angels are in defined ranges and + /// calibrating based on the [expect]ed direction from ArrowDirection InputDirection getInputDirection(ArrowDirection expect) { + // check if angle is in range inputDirection.up = z > 180 && z < 340; inputDirection.down = z > 20 && z < 180; inputDirection.left = y > 0 && y < 180; inputDirection.right = y > 180 && y < 360; + // calibrate based on expected directoin from ArrowDirection if (expect == ArrowDirection.up && inputDirection.up || expect == ArrowDirection.down && inputDirection.down) { y = 0; - print("ehit"); } if (expect == ArrowDirection.left && inputDirection.left || expect == ArrowDirection.right && inputDirection.right) { z = 0; - print("ehit"); } return inputDirection; } + /// connect to ESense with [deviceName] by first asking for permissions Future connectToESense(String deviceName) async { if (!connected) { bool permissionSuccessfull = await _askForPermissions(); diff --git a/lib/utils/simfile.dart b/lib/utils/simfile.dart index c528084..71613a9 100644 --- a/lib/utils/simfile.dart +++ b/lib/utils/simfile.dart @@ -55,6 +55,7 @@ class Simfile { Simfile(this.simfilePath); + /// parses a chart tag with metadata [keys] and note data [value] void _parseChart({required List keys, required String value}) { Chart chart = Chart(); chart.chartType = keys[1]; @@ -63,6 +64,7 @@ class Simfile { chart.numericalMeter = int.parse(keys[4]); chart.radarValues = keys[5]; + // find simplest chart if (chartSimplest == null || (chart.difficulty!.index <= chartSimplest!.difficulty!.index && chart.numericalMeter! <= chartSimplest!.numericalMeter!)) { @@ -78,8 +80,10 @@ class Simfile { measures.add(measure); } + // for now only use the first bpm value double bpm = bpms.entries.first.value; + // calculate timing for all notes based on offset, bpm and measure for (final (measureIndex, measure) in measures.indexed) { for (final (noteIndex, noteData) in measure.indexed) { double beat = measureIndex * 4.0 + @@ -95,10 +99,13 @@ class Simfile { } } + /// parse a tag based on a regex match [fieldData] and parsing the value based + /// on the key void _parseTag(RegExpMatch fieldData) { List keys = fieldData[1]!.split(':').map((key) => key.trim()).toList(); String value = fieldData[2]!; + if (keys[0] == "BPMS") { for (final pairRaw in value.split(',')) { List pair = pairRaw.split('='); @@ -122,14 +129,18 @@ class Simfile { _parseChart(keys: keys, value: value); } + /// load the simfile Future load() async { directoryPath = File(simfilePath).parent.path; lines = File(simfilePath).readAsStringSync(); + // remove comments RegExp commentsRegExp = RegExp(r'//.*$'); lines = lines?.replaceAll(commentsRegExp, ''); + // find all tags RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);'); + // parse all tags for (final fieldData in fieldDataRegExp.allMatches(lines!)) { try { _parseTag(fieldData); @@ -138,11 +149,8 @@ class Simfile { } } - String? musicFileName = tags["MUSIC"]; - if (musicFileName == null) return false; - String? bannerFileName = tags["BANNER"]; - if (bannerFileName == null) return false; - + // searching for audio and banned in the directory is more robust than using + // values from metadata as they are wrong more often for (FileSystemEntity entity in Directory(directoryPath!).listSync()) { if (entity.path.endsWith('.ogg')) { audioPath = entity.path; @@ -152,6 +160,11 @@ class Simfile { } } + // dont use this simfile of files are missing + if (audioPath == null) return false; + if (bannerPath == null) return false; + + // get duration from audio AudioPlayer audioplayer = AudioPlayer(); await audioplayer.setSource(DeviceFileSource(audioPath!)); duration = await audioplayer.getDuration(); diff --git a/lib/widgets/esense_connect_dialog.dart b/lib/widgets/esense_connect_dialog.dart index 8320dd4..fba37b0 100644 --- a/lib/widgets/esense_connect_dialog.dart +++ b/lib/widgets/esense_connect_dialog.dart @@ -21,6 +21,7 @@ class _ESenseConnectDialogState extends State { @override Widget build(BuildContext context) { + // rerender whenever the deviceStatus changes return ValueListenableBuilder( valueListenable: widget.deviceStatus, builder: (BuildContext context, String deviceStatus, Widget? child) { diff --git a/lib/widgets/level_list_entry.dart b/lib/widgets/level_list_entry.dart index abb4784..2ee8c42 100644 --- a/lib/widgets/level_list_entry.dart +++ b/lib/widgets/level_list_entry.dart @@ -16,11 +16,13 @@ class LevelListEntry extends StatelessWidget { final Simfile simfile; + /// navigates to level screen void navigateToLevel(BuildContext context) { Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => Level(simfile))); } + /// opens ESenseConnectDialog void openESenseConnectDialog(context) { showDialog( context: context, @@ -38,6 +40,7 @@ class LevelListEntry extends StatelessWidget { ); } + /// when clocked on the level, warn if not connected to ESense void tapHandler(BuildContext context) { if (ESenseInput.instance.connected) { navigateToLevel(context);