feat: add comments

This commit is contained in:
Orangerot 2025-01-14 17:35:24 +01:00
parent 28d0fe7d8c
commit e030b60c25
7 changed files with 56 additions and 8 deletions

View file

@ -3,7 +3,8 @@ class InputDirection {
bool down = false; bool down = false;
bool left = false; bool left = false;
bool right = false; bool right = false;
/// reset all directions to false
void reset() { void reset() {
up = false; up = false;
down = false; down = false;

View file

@ -87,6 +87,7 @@ class _LevelState extends State<Level> with SingleTickerProviderStateMixin {
} }
}); });
// go to GameOverStats when level finishes
player.onPlayerComplete.listen((void _) { player.onPlayerComplete.listen((void _) {
Route route = MaterialPageRoute( Route route = MaterialPageRoute(
builder: (context) => GameOverStats( 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) { widget.simfile.chartSimplest?.beats.forEach((time, noteData) {
int arrowIndex = noteData.indexOf('1'); int arrowIndex = noteData.indexOf('1');
if (arrowIndex < 0 || arrowIndex > 3) { if (arrowIndex < 0 || arrowIndex > 3) {
@ -126,6 +128,7 @@ class _LevelState extends State<Level> with SingleTickerProviderStateMixin {
super.dispose(); super.dispose();
} }
/// toggle between pause and resume
void _pauseResume() { void _pauseResume() {
if (_isPlaying) { if (_isPlaying) {
player.pause(); 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) { void _noteHitCheck(Note note, Duration time) {
note.position = note.time - time.inMilliseconds / 60000.0; note.position = note.time - time.inMilliseconds / 60000.0;
if (note.wasHit != null) { if (note.wasHit != null) {
return; return;
} }
// you have +- half a second to hit
if (note.position.abs() < 0.5 * 1.0 / 60.0) { if (note.position.abs() < 0.5 * 1.0 / 60.0) {
// combine keyboard and esense input
InputDirection esenseDirection = InputDirection esenseDirection =
ESenseInput.instance.getInputDirection(note.direction); ESenseInput.instance.getInputDirection(note.direction);
inputDirection.up |= esenseDirection.up; inputDirection.up |= esenseDirection.up;
inputDirection.down |= esenseDirection.down; inputDirection.down |= esenseDirection.down;
inputDirection.left |= esenseDirection.left; inputDirection.left |= esenseDirection.left;
inputDirection.right |= esenseDirection.right; inputDirection.right |= esenseDirection.right;
// check if input matches arrow direction
bool keypressCorrect = false; bool keypressCorrect = false;
switch (note.direction) { switch (note.direction) {
case ArrowDirection.up: 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) { void _keyboardHandler(event) {
bool isDown = false; bool isDown = false;
if (event is KeyDownEvent) { if (event is KeyDownEvent) {

View file

@ -28,6 +28,7 @@ class _LevelSelectionState extends State<LevelSelection> {
loadFolderPath(); loadFolderPath();
} }
/// gets folder path from persistent storage and updates state with loaded simfiles
Future<void> loadFolderPath() async { Future<void> loadFolderPath() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
final String? stepmaniaCoursesPathSetting = 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 { Future<void> selectFolder() async {
await Permission.manageExternalStorage.request(); await Permission.manageExternalStorage.request();
String? selectedFolder = await FilePicker.platform.getDirectoryPath(); 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 { Future<List<Simfile>> listFilesAndFolders(String directoryPath) async {
final directory = Directory(directoryPath); final directory = Directory(directoryPath);
try { try {
@ -86,6 +89,7 @@ class _LevelSelectionState extends State<LevelSelection> {
} }
} }
/// filter stepmaniaCoursesFolders based on [input]
void filterLevels(String input) { void filterLevels(String input) {
setState(() { setState(() {
stepmaniaCoursesFoldersFiltered = stepmaniaCoursesFolders stepmaniaCoursesFoldersFiltered = stepmaniaCoursesFolders

View file

@ -8,9 +8,12 @@ import 'package:sense_the_rhythm/models/arrow_direction.dart';
import 'package:sense_the_rhythm/models/input_direction.dart'; import 'package:sense_the_rhythm/models/input_direction.dart';
class ESenseInput { 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._(); static final instance = ESenseInput._();
ESenseManager eSenseManager = ESenseManager('unknown'); ESenseManager eSenseManager = ESenseManager('unknown');
// valuenotifier allows widgets to rerender when the value changes
ValueNotifier<String> deviceStatus = ValueNotifier('Disconnected'); ValueNotifier<String> deviceStatus = ValueNotifier('Disconnected');
StreamSubscription? subscription; StreamSubscription? subscription;
@ -29,8 +32,11 @@ class ESenseInput {
_listenToESense(); _listenToESense();
} }
/// ask and check if permissions are enabled and granted
Future<bool> _askForPermissions() async { Future<bool> _askForPermissions() async {
// is desktop
if (!Platform.isAndroid && !Platform.isIOS) return false; if (!Platform.isAndroid && !Platform.isIOS) return false;
// is bluetooth even enabled?
if (!await Permission.bluetooth.serviceStatus.isEnabled) { if (!await Permission.bluetooth.serviceStatus.isEnabled) {
deviceStatus.value = "Bluetooth is disabled!"; deviceStatus.value = "Bluetooth is disabled!";
return false; return false;
@ -55,6 +61,7 @@ class ESenseInput {
return true; return true;
} }
/// listen to connectionEvents and set deviceStatus
void _listenToESense() { void _listenToESense() {
// if you want to get the connection events when connecting, // if you want to get the connection events when connecting,
// set up the listener BEFORE connecting... // set up the listener BEFORE connecting...
@ -89,12 +96,14 @@ class ESenseInput {
}); });
} }
/// get eSenseEvent stream only containung button events
Stream<ButtonEventChanged> buttonEvents() { Stream<ButtonEventChanged> buttonEvents() {
return eSenseManager.eSenseEvents return eSenseManager.eSenseEvents
.where((event) => event.runtimeType == ButtonEventChanged) .where((event) => event.runtimeType == ButtonEventChanged)
.cast(); .cast();
} }
/// sets sampling rate and listens to sensorEvents
void _startListenToSensorEvents() async { void _startListenToSensorEvents() async {
// // any changes to the sampling frequency must be done BEFORE listening to sensor events // // any changes to the sampling frequency must be done BEFORE listening to sensor events
print('setting sampling frequency...'); print('setting sampling frequency...');
@ -115,11 +124,14 @@ class ESenseInput {
sampling = true; sampling = true;
} }
/// cancels the sensorEvents listening
void _pauseListenToSensorEvents() async { void _pauseListenToSensorEvents() async {
subscription?.cancel(); subscription?.cancel();
sampling = false; 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) { void _parseGyroData(List<int> data) {
// Float value in deg/s = Gyro value / Gyro scale factor // Float value in deg/s = Gyro value / Gyro scale factor
// The default configuration is +- 500deg/s for the gyroscope. // The default configuration is +- 500deg/s for the gyroscope.
@ -131,6 +143,7 @@ class ESenseInput {
// print('${z.toDouble() / 500.0 * (1.0 / 10.0)}'); // print('${z.toDouble() / 500.0 * (1.0 / 10.0)}');
} }
/// nulls all angles and reset inputDirection
void resetAngles() { void resetAngles() {
inputDirection.reset(); inputDirection.reset();
x = 0; x = 0;
@ -138,26 +151,29 @@ class ESenseInput {
z = 0; 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) { InputDirection getInputDirection(ArrowDirection expect) {
// check if angle is in range
inputDirection.up = z > 180 && z < 340; inputDirection.up = z > 180 && z < 340;
inputDirection.down = z > 20 && z < 180; inputDirection.down = z > 20 && z < 180;
inputDirection.left = y > 0 && y < 180; inputDirection.left = y > 0 && y < 180;
inputDirection.right = y > 180 && y < 360; inputDirection.right = y > 180 && y < 360;
// calibrate based on expected directoin from ArrowDirection
if (expect == ArrowDirection.up && inputDirection.up || if (expect == ArrowDirection.up && inputDirection.up ||
expect == ArrowDirection.down && inputDirection.down) { expect == ArrowDirection.down && inputDirection.down) {
y = 0; y = 0;
print("ehit");
} }
if (expect == ArrowDirection.left && inputDirection.left || if (expect == ArrowDirection.left && inputDirection.left ||
expect == ArrowDirection.right && inputDirection.right) { expect == ArrowDirection.right && inputDirection.right) {
z = 0; z = 0;
print("ehit");
} }
return inputDirection; return inputDirection;
} }
/// connect to ESense with [deviceName] by first asking for permissions
Future<void> connectToESense(String deviceName) async { Future<void> connectToESense(String deviceName) async {
if (!connected) { if (!connected) {
bool permissionSuccessfull = await _askForPermissions(); bool permissionSuccessfull = await _askForPermissions();

View file

@ -55,6 +55,7 @@ class Simfile {
Simfile(this.simfilePath); Simfile(this.simfilePath);
/// parses a chart tag with metadata [keys] and note data [value]
void _parseChart({required List<String> keys, required String value}) { void _parseChart({required List<String> keys, required String value}) {
Chart chart = Chart(); Chart chart = Chart();
chart.chartType = keys[1]; chart.chartType = keys[1];
@ -63,6 +64,7 @@ class Simfile {
chart.numericalMeter = int.parse(keys[4]); chart.numericalMeter = int.parse(keys[4]);
chart.radarValues = keys[5]; chart.radarValues = keys[5];
// find simplest chart
if (chartSimplest == null || if (chartSimplest == null ||
(chart.difficulty!.index <= chartSimplest!.difficulty!.index && (chart.difficulty!.index <= chartSimplest!.difficulty!.index &&
chart.numericalMeter! <= chartSimplest!.numericalMeter!)) { chart.numericalMeter! <= chartSimplest!.numericalMeter!)) {
@ -78,8 +80,10 @@ class Simfile {
measures.add(measure); measures.add(measure);
} }
// for now only use the first bpm value
double bpm = bpms.entries.first.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 (measureIndex, measure) in measures.indexed) {
for (final (noteIndex, noteData) in measure.indexed) { for (final (noteIndex, noteData) in measure.indexed) {
double beat = measureIndex * 4.0 + 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) { void _parseTag(RegExpMatch fieldData) {
List<String> keys = List<String> keys =
fieldData[1]!.split(':').map((key) => key.trim()).toList(); fieldData[1]!.split(':').map((key) => key.trim()).toList();
String value = fieldData[2]!; String value = fieldData[2]!;
if (keys[0] == "BPMS") { if (keys[0] == "BPMS") {
for (final pairRaw in value.split(',')) { for (final pairRaw in value.split(',')) {
List<String> pair = pairRaw.split('='); List<String> pair = pairRaw.split('=');
@ -122,14 +129,18 @@ class Simfile {
_parseChart(keys: keys, value: value); _parseChart(keys: keys, value: value);
} }
/// load the simfile
Future<bool> load() async { Future<bool> load() async {
directoryPath = File(simfilePath).parent.path; directoryPath = File(simfilePath).parent.path;
lines = File(simfilePath).readAsStringSync(); lines = File(simfilePath).readAsStringSync();
// remove comments
RegExp commentsRegExp = RegExp(r'//.*$'); RegExp commentsRegExp = RegExp(r'//.*$');
lines = lines?.replaceAll(commentsRegExp, ''); lines = lines?.replaceAll(commentsRegExp, '');
// find all tags
RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);'); RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);');
// parse all tags
for (final fieldData in fieldDataRegExp.allMatches(lines!)) { for (final fieldData in fieldDataRegExp.allMatches(lines!)) {
try { try {
_parseTag(fieldData); _parseTag(fieldData);
@ -138,11 +149,8 @@ class Simfile {
} }
} }
String? musicFileName = tags["MUSIC"]; // searching for audio and banned in the directory is more robust than using
if (musicFileName == null) return false; // values from metadata as they are wrong more often
String? bannerFileName = tags["BANNER"];
if (bannerFileName == null) return false;
for (FileSystemEntity entity in Directory(directoryPath!).listSync()) { for (FileSystemEntity entity in Directory(directoryPath!).listSync()) {
if (entity.path.endsWith('.ogg')) { if (entity.path.endsWith('.ogg')) {
audioPath = entity.path; 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(); AudioPlayer audioplayer = AudioPlayer();
await audioplayer.setSource(DeviceFileSource(audioPath!)); await audioplayer.setSource(DeviceFileSource(audioPath!));
duration = await audioplayer.getDuration(); duration = await audioplayer.getDuration();

View file

@ -21,6 +21,7 @@ class _ESenseConnectDialogState extends State<ESenseConnectDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// rerender whenever the deviceStatus changes
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: widget.deviceStatus, valueListenable: widget.deviceStatus,
builder: (BuildContext context, String deviceStatus, Widget? child) { builder: (BuildContext context, String deviceStatus, Widget? child) {

View file

@ -16,11 +16,13 @@ class LevelListEntry extends StatelessWidget {
final Simfile simfile; final Simfile simfile;
/// navigates to level screen
void navigateToLevel(BuildContext context) { void navigateToLevel(BuildContext context) {
Navigator.push(context, Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) => Level(simfile))); MaterialPageRoute(builder: (BuildContext context) => Level(simfile)));
} }
/// opens ESenseConnectDialog
void openESenseConnectDialog(context) { void openESenseConnectDialog(context) {
showDialog( showDialog(
context: context, 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) { void tapHandler(BuildContext context) {
if (ESenseInput.instance.connected) { if (ESenseInput.instance.connected) {
navigateToLevel(context); navigateToLevel(context);