From 8dc2a5bced5be75708fbd347be937333c1eed7e8 Mon Sep 17 00:00:00 2001 From: dan63047 Date: Sat, 6 Jan 2024 02:11:45 +0300 Subject: [PATCH] Finnaly readind replays In plans: advanced stats, saving those stats into DB in order not to use szy api shitton of times Mb even new graphs idk idk --- lib/data_objects/tetrio.dart | 6 ++ .../tetrio_multiplayer_replay.dart | 12 +++- lib/gen/strings.g.dart | 16 ++++- lib/services/tetrio_crud.dart | 36 +++++++++-- lib/views/main_view.dart | 4 +- lib/views/settings_view.dart | 1 + lib/views/tl_match_view.dart | 64 +++++++++++++++++-- res/i18n/strings.i18n.json | 3 + res/i18n/strings_ru.i18n.json | 3 + 9 files changed, 130 insertions(+), 15 deletions(-) diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index a4119e7..62d2beb 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -856,6 +856,12 @@ class EndContextMulti { playstyleTracking = [for (int i = 0; i < secondaryTracking.length; i++) Playstyle(secondaryTracking[i], tertiaryTracking[i], nerdStatsTracking[i].app, nerdStatsTracking[i].vsapm, nerdStatsTracking[i].dsp, nerdStatsTracking[i].gbe, estTrTracking[i].srarea, estTrTracking[i].statrank)]; } + @override + bool operator == (covariant EndContextMulti other){ + if (userId != other.userId) return false; + return true; + } + Map toJson() { final Map data = {}; data['user'] = {'_id': userId, 'username': username}; diff --git a/lib/data_objects/tetrio_multiplayer_replay.dart b/lib/data_objects/tetrio_multiplayer_replay.dart index 79633f6..b4ea465 100644 --- a/lib/data_objects/tetrio_multiplayer_replay.dart +++ b/lib/data_objects/tetrio_multiplayer_replay.dart @@ -49,6 +49,8 @@ class ReplayStats{ late Finesse finesse; late int kills; + double get finessePercentage => finesse.perfectPieces / piecesPlaced; + ReplayStats({ required this.seed, required this.linesCleared, @@ -116,6 +118,7 @@ class ReplayStats{ class ReplayData{ late String id; + late Map rawJson; late List endcontext; late List> stats; late List totalStats; @@ -126,10 +129,15 @@ class ReplayData{ required this.id, required this.endcontext, required this.stats, - required this.roundLengths - }); + required this.totalStats, + required this.roundLengths, + required this.totalLength + }){ + rawJson = {}; + } ReplayData.fromJson(Map json){ + rawJson = json; id = json["_id"]; endcontext = [EndContextMulti.fromJson(json["endcontext"][0]), EndContextMulti.fromJson(json["endcontext"][1])]; roundLengths = []; diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index f6e2244..752c53a 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 988 (494 per locale) +/// Strings: 994 (497 per locale) /// -/// Built on 2024-01-01 at 16:00 UTC +/// Built on 2024-01-05 at 16:51 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -178,6 +178,8 @@ class Translations implements BaseTranslations { String get tlLeaderboard => 'Tetra League leaderboard'; String get noRecords => 'No records'; String get noRecord => 'No record'; + String get botRecord => 'Bots are not allowed to set records'; + String get anonRecord => 'Guests are not allowed to set records'; String get notEnoughData => 'Not enough data'; String get noHistorySaved => 'No history saved'; String obtainDate({required Object date}) => 'Obtained ${date}'; @@ -204,6 +206,7 @@ class Translations implements BaseTranslations { String get exactValue => 'Exact value'; String get neverPlayedTL => 'That user never played Tetra League'; String get botTL => 'Bots are not allowed to play Tetra League'; + String get anonTL => 'Guests are not allowed to play Tetra League'; String get exportDB => 'Export local database'; String get exportDBDescription => 'It contains states and Tetra League records of the tracked players and list of tracked players.'; String get desktopExportAlertTitle => 'Desktop export'; @@ -756,6 +759,8 @@ class _StringsRu implements Translations { @override String get tlLeaderboard => 'Рейтинговая таблица'; @override String get noRecords => 'Нет записей'; @override String get noRecord => 'Нет рекорда'; + @override String get botRecord => 'Ботам нельзя ставить рекорды'; + @override String get anonRecord => 'Гостям нельзя ставить рекорды'; @override String get notEnoughData => 'Недостаточно данных'; @override String get noHistorySaved => 'Нет сохранённой истории'; @override String obtainDate({required Object date}) => 'Получено ${date}'; @@ -782,6 +787,7 @@ class _StringsRu implements Translations { @override String get exactValue => 'Точное значение'; @override String get neverPlayedTL => 'Этот игрок никогда не играл в Тетра Лигу'; @override String get botTL => 'Ботам нельзя играть в Тетра Лигу'; + @override String get anonTL => 'Гостям нельзя играть в Тетра Лигу'; @override String get exportDB => 'Экспортировать локальную базу данных'; @override String get exportDBDescription => 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.'; @override String get desktopExportAlertTitle => 'Экспорт на десктопе'; @@ -1326,6 +1332,8 @@ extension on Translations { case 'tlLeaderboard': return 'Tetra League leaderboard'; case 'noRecords': return 'No records'; case 'noRecord': return 'No record'; + case 'botRecord': return 'Bots are not allowed to set records'; + case 'anonRecord': return 'Guests are not allowed to set records'; case 'notEnoughData': return 'Not enough data'; case 'noHistorySaved': return 'No history saved'; case 'obtainDate': return ({required Object date}) => 'Obtained ${date}'; @@ -1352,6 +1360,7 @@ extension on Translations { case 'exactValue': return 'Exact value'; case 'neverPlayedTL': return 'That user never played Tetra League'; case 'botTL': return 'Bots are not allowed to play Tetra League'; + case 'anonTL': return 'Guests are not allowed to play Tetra League'; case 'exportDB': return 'Export local database'; case 'exportDBDescription': return 'It contains states and Tetra League records of the tracked players and list of tracked players.'; case 'desktopExportAlertTitle': return 'Desktop export'; @@ -1830,6 +1839,8 @@ extension on _StringsRu { case 'tlLeaderboard': return 'Рейтинговая таблица'; case 'noRecords': return 'Нет записей'; case 'noRecord': return 'Нет рекорда'; + case 'botRecord': return 'Ботам нельзя ставить рекорды'; + case 'anonRecord': return 'Гостям нельзя ставить рекорды'; case 'notEnoughData': return 'Недостаточно данных'; case 'noHistorySaved': return 'Нет сохранённой истории'; case 'obtainDate': return ({required Object date}) => 'Получено ${date}'; @@ -1856,6 +1867,7 @@ extension on _StringsRu { case 'exactValue': return 'Точное значение'; case 'neverPlayedTL': return 'Этот игрок никогда не играл в Тетра Лигу'; case 'botTL': return 'Ботам нельзя играть в Тетра Лигу'; + case 'anonTL': return 'Гостям нельзя играть в Тетра Лигу'; case 'exportDB': return 'Экспортировать локальную базу данных'; case 'exportDBDescription': return 'Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.'; case 'desktopExportAlertTitle': return 'Экспорт на десктопе'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 0c5bd0c..e8a53db 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; import 'package:path_provider/path_provider.dart'; +import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/main.dart' show packageInfo; import 'package:flutter/foundation.dart'; import 'package:tetra_stats/services/custom_http_client.dart'; @@ -51,6 +52,15 @@ const String createTetrioTLRecordsTable = ''' ) '''; +const String createTetrioTLReplayStats = ''' + CREATE TABLE "tetrioTLReplayStats" ( + "id" TEXT NOT NULL, + "player1" TEXT NOT NULL, + "player2" TEXT NOT NULL, + PRIMARY KEY("id") + ) +'''; + class TetrioService extends DB { Map> _players = {}; final Map _playersCache = {}; @@ -110,19 +120,19 @@ class TetrioService extends DB { } } - Future szyDownloadAndSaveReplay(String replayID) async { + Future> szyGetReplay(String replayID) async { Uri url = Uri.https('inoue.szy.lol', '/api/replay/$replayID'); var downloadPath = await getDownloadsDirectory(); downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); var replayFile = File("${downloadPath.path}/$replayID.ttrm"); - if (replayFile.existsSync()) throw TetrioReplayAlreadyExist(); + if (replayFile.existsSync()) return [replayFile.readAsStringSync(), replayFile.readAsBytesSync()]; try{ final response = await client.get(url); switch (response.statusCode) { case 200: - await replayFile.writeAsBytes(response.bodyBytes); - return replayFile.path; + developer.log("szyDownload: Replay downloaded", name: "services/tetrio_crud", error: response.statusCode); + return [response.body, response.bodyBytes]; case 404: throw SzyNotFound(); case 403: @@ -137,7 +147,7 @@ class TetrioService extends DB { case 504: throw SzyInternalProblem(); default: - developer.log("szyDownloadAndSaveReplay: Failed to download a replay", name: "services/tetrio_crud", error: response.statusCode); + developer.log("szyDownload: Failed to download a replay", name: "services/tetrio_crud", error: response.statusCode); throw ConnectionIssue(response.statusCode, response.reasonPhrase??"No reason"); } } on http.ClientException catch (e, s) { @@ -146,6 +156,22 @@ class TetrioService extends DB { } } + Future SaveReplay(String replayID) async { + Uri url = Uri.https('inoue.szy.lol', '/api/replay/$replayID'); + var downloadPath = await getDownloadsDirectory(); + downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); + var replayFile = File("${downloadPath.path}/$replayID.ttrm"); + if (replayFile.existsSync()) throw TetrioReplayAlreadyExist(); + var replay = await szyGetReplay(replayID); + await replayFile.writeAsBytes(replay[1]); + return replayFile.path; + } + + Future analyzeReplay(String replayID) async{ + Map toAnalyze = jsonDecode((await szyGetReplay(replayID))[0]); + return ReplayData.fromJson(toAnalyze); + } + Future fetchTopTR(String id) async { try{ var cached = _topTRcache.entries.firstWhere((element) => element.value.keys.first == id); diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 3300254..24e4592 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -35,7 +35,7 @@ var chartsData = >>[]; List _historyShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR"]; int _chartsIndex = 0; final NumberFormat _timeInSec = NumberFormat("#,###.###s."); -final NumberFormat _secs = NumberFormat("00.###"); +final NumberFormat secs = NumberFormat("00.###"); final NumberFormat _f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2); final NumberFormat _f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4); final DateFormat _dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); @@ -55,7 +55,7 @@ Future copyToClipboard(String text) async { } String get40lTime(int microseconds){ - return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(_secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000); + return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000); } class _MainState extends State with SingleTickerProviderStateMixin { diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 01bfe68..29def7a 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -237,6 +237,7 @@ class SettingsState extends State { ), ), ListTile(title: Text("Customization"), + subtitle: Text("I don't want to implement this"), trailing: Icon(Icons.arrow_right), onTap: () { Navigator.pushNamed(context, "/customization"); diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index 909944d..d7e0036 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -1,10 +1,11 @@ // ignore_for_file: use_build_context_synchronously import 'dart:io'; +import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/views/compare_view.dart' show CompareThingy, CompareBoolThingy; import 'package:tetra_stats/widgets/vs_graphs.dart'; -import 'main_view.dart' show teto; +import 'main_view.dart' show teto, secs; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -21,6 +22,10 @@ int roundSelector = -1; // -1 = match averages, otherwise round number-1 List rounds = []; // index zero will be match stats late String oldWindowTitle; +Duration framesToTime(int frames){ + return Duration(microseconds: frames~/6e-5); +} + class TlMatchResultView extends StatefulWidget { final TetraLeagueAlphaRecord record; final String initPlayerId; @@ -33,12 +38,14 @@ class TlMatchResultView extends StatefulWidget { class TlMatchResultState extends State { late ScrollController _scrollController; + late Future replayData; @override void initState(){ _scrollController = ScrollController(); rounds = [DropdownMenuItem(value: -1, child: Text(t.match))]; rounds.addAll([for (int i = 0; i < widget.record.endContext.first.secondaryTracking.length; i++) DropdownMenuItem(value: i, child: Text(t.roundNumber(n: i+1)))]); + replayData = teto.analyzeReplay(widget.record.replayId); if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){ windowManager.getTitle().then((value) => oldWindowTitle = value); windowManager.setTitle("Tetra Stats: ${widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).username.toUpperCase()} ${t.vs} ${widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).username.toUpperCase()} ${t.inTLmatch} ${dateFormat.format(widget.record.timestamp)}"); @@ -86,7 +93,7 @@ class TlMatchResultState extends State { //anchor.remove(); } else{ try{ - String path = await teto.szyDownloadAndSaveReplay(widget.record.replayId); + String path = await teto.SaveReplay(widget.record.replayId); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.replaySaved(path: path)))); } on TetrioReplayAlreadyExist{ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.errors.replayAlreadySaved))); @@ -189,6 +196,22 @@ class TlMatchResultState extends State { ), ), ), + SliverToBoxAdapter(child: FutureBuilder(future: replayData, builder: (context, snapshot) { + switch(snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return CircularProgressIndicator(); + case ConnectionState.done: + if (!snapshot.hasError){ + var time = framesToTime(snapshot.data!.totalLength); + return Center(child: Text("Match Length: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}")); + }else{ + return Text("skill issue"); + } + + } + },),), const SliverToBoxAdapter( child: Divider(), ) @@ -219,6 +242,39 @@ class TlMatchResultState extends State { fractionDigits: 2, higherIsBetter: true, ), + FutureBuilder(future: replayData, builder: (BuildContext context, AsyncSnapshot snapshot){ + switch(snapshot.connectionState){ + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return LinearProgressIndicator(); + case ConnectionState.done: + if (!snapshot.hasError){ + var greenSidePlayer = snapshot.data!.endcontext.indexWhere(((element) => element.userId == widget.initPlayerId)); + var redSidePlayer = snapshot.data!.endcontext.indexWhere(((element) => element.userId != widget.initPlayerId)); + return Column(children: [ + CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].piecesPlaced, + redSide: snapshot.data!.totalStats[redSidePlayer].piecesPlaced, + label: "Pieces Placed", higherIsBetter: true), + CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].linesCleared, + redSide: snapshot.data!.totalStats[redSidePlayer].linesCleared, + label: "Lines Cleared", higherIsBetter: true), + CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100, + redSide: snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100, + label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true), + CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].topCombo, + redSide: snapshot.data!.totalStats[redSidePlayer].topCombo, + label: "Best Combo", higherIsBetter: true), + CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].topBtB, + redSide: snapshot.data!.totalStats[redSidePlayer].topBtB, + label: "Best BtB", higherIsBetter: true), + ],); + }else{ + return Text("skill issue"); + } + + } + }) ], ), const Divider(), @@ -356,12 +412,12 @@ class TlMatchResultState extends State { CompareThingy( greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das, redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.das, - label: "DAS", + label: "DAS", fractionDigits: 1, higherIsBetter: false), CompareThingy( greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr, redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.arr, - label: "ARR", + label: "ARR", fractionDigits: 1, higherIsBetter: false), CompareThingy( greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf, diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 31046fa..24e3fc2 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -43,6 +43,8 @@ "tlLeaderboard": "Tetra League leaderboard", "noRecords": "No records", "noRecord": "No record", + "botRecord": "Bots are not allowed to set records", + "anonRecord": "Guests are not allowed to set records", "notEnoughData": "Not enough data", "noHistorySaved": "No history saved", "obtainDate": "Obtained ${date}", @@ -69,6 +71,7 @@ "exactValue": "Exact value", "neverPlayedTL": "That user never played Tetra League", "botTL": "Bots are not allowed to play Tetra League", + "anonTL": "Guests are not allowed to play Tetra League", "exportDB": "Export local database", "exportDBDescription": "It contains states and Tetra League records of the tracked players and list of tracked players.", "desktopExportAlertTitle": "Desktop export", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index 9bfbfc2..447e95c 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -43,6 +43,8 @@ "tlLeaderboard": "Рейтинговая таблица", "noRecords": "Нет записей", "noRecord": "Нет рекорда", + "botRecord": "Ботам нельзя ставить рекорды", + "anonRecord": "Гостям нельзя ставить рекорды", "notEnoughData": "Недостаточно данных", "noHistorySaved": "Нет сохранённой истории", "obtainDate": "Получено ${date}", @@ -69,6 +71,7 @@ "exactValue": "Точное значение", "neverPlayedTL": "Этот игрок никогда не играл в Тетра Лигу", "botTL": "Ботам нельзя играть в Тетра Лигу", + "anonTL": "Гостям нельзя играть в Тетра Лигу", "exportDB": "Экспортировать локальную базу данных", "exportDBDescription": "Она содержит состояния аккаунтов и их матчей в Тетра Лиге для отслеживаемых игроков и список таких игроков.", "desktopExportAlertTitle": "Экспорт на десктопе",