diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/models/input_direction.dart | 3 | ||||
| -rw-r--r-- | lib/screens/level.dart | 10 | ||||
| -rw-r--r-- | lib/screens/level_selection.dart | 4 | ||||
| -rw-r--r-- | lib/utils/esense_input.dart | 20 | ||||
| -rw-r--r-- | lib/utils/simfile.dart | 23 | ||||
| -rw-r--r-- | lib/widgets/esense_connect_dialog.dart | 1 | ||||
| -rw-r--r-- | lib/widgets/level_list_entry.dart | 3 | 
7 files changed, 56 insertions, 8 deletions
| 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<Level> 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<Level> 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<Level> with SingleTickerProviderStateMixin {      super.dispose();    } +  /// toggle between pause and resume    void _pauseResume() {      if (_isPlaying) {        player.pause(); @@ -140,18 +143,24 @@ class _LevelState extends State<Level> 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<Level> 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<LevelSelection> {      loadFolderPath();    } +  /// gets folder path from persistent storage and updates state with loaded simfiles    Future<void> loadFolderPath() async {      SharedPreferences prefs = await SharedPreferences.getInstance();      final String? stepmaniaCoursesPathSetting = @@ -44,6 +45,7 @@ class _LevelSelectionState extends State<LevelSelection> {      });    } +  /// open folder selection dialog and save selected folder in persistent storage    Future<void> selectFolder() async {      await Permission.manageExternalStorage.request();      String? selectedFolder = await FilePicker.platform.getDirectoryPath(); @@ -57,6 +59,7 @@ class _LevelSelectionState extends State<LevelSelection> {      }    } +  /// load all simfiles from a [directoryPath]    Future<List<Simfile>> listFilesAndFolders(String directoryPath) async {      final directory = Directory(directoryPath);      try { @@ -86,6 +89,7 @@ class _LevelSelectionState extends State<LevelSelection> {      }    } +  /// 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<String> deviceStatus = ValueNotifier('Disconnected');    StreamSubscription? subscription; @@ -29,8 +32,11 @@ class ESenseInput {      _listenToESense();    } +  /// ask and check if permissions are enabled and granted    Future<bool> _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<ButtonEventChanged> 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<int> 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<void> 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<String> 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<String> keys =          fieldData[1]!.split(':').map((key) => key.trim()).toList();      String value = fieldData[2]!; +      if (keys[0] == "BPMS") {        for (final pairRaw in value.split(',')) {          List<String> pair = pairRaw.split('='); @@ -122,14 +129,18 @@ class Simfile {      _parseChart(keys: keys, value: value);    } +  /// load the simfile    Future<bool> 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<ESenseConnectDialog> {    @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); | 
