Spike counter + caching for szy api + more badges

ToDo: save advanced stats to the DB, polish UI, add interactivity to graphs
This commit is contained in:
dan63047 2024-01-07 01:54:00 +03:00
parent 8dc2a5bced
commit 39569ffe0c
11 changed files with 102 additions and 50 deletions

View File

@ -5,6 +5,29 @@ import 'tetrio.dart';
// I want to implement those fancy TWC stats // I want to implement those fancy TWC stats
// So, i'm going to read replay for things // So, i'm going to read replay for things
int biggestSpikeFromReplay(events){
int previousIGEeventFrame = -1;
int spikeCounter = 0;
int biggestSpike = 0;
for (var event in events){
if (event["type"] == "ige" && event["data"]["data"]["type"] == "interaction"){
if (previousIGEeventFrame.isNegative){
spikeCounter = event['data']['data']['data']['amt'];
biggestSpike = spikeCounter;
}else{
if (event['data']['data']['frame'] - previousIGEeventFrame < 60){
spikeCounter = spikeCounter + event['data']['data']['data']['amt'] as int;
}else{
spikeCounter = event['data']['data']['data']['amt'];
}
biggestSpike = max(biggestSpike, spikeCounter);
}
previousIGEeventFrame = event['data']['data']['frame'];
}
}
return biggestSpike;
}
class Garbage{ // charsys where??? class Garbage{ // charsys where???
late int sent; late int sent;
late int recived; late int recived;
@ -43,11 +66,11 @@ class ReplayStats{
late int score; late int score;
late int topCombo; late int topCombo;
late int topBtB; late int topBtB;
late int topSpike;
late int tspins; late int tspins;
late Clears clears; late Clears clears;
late Garbage garbage; late Garbage garbage;
late Finesse finesse; late Finesse finesse;
late int kills;
double get finessePercentage => finesse.perfectPieces / piecesPlaced; double get finessePercentage => finesse.perfectPieces / piecesPlaced;
@ -60,14 +83,14 @@ class ReplayStats{
required this.score, required this.score,
required this.topCombo, required this.topCombo,
required this.topBtB, required this.topBtB,
required this.topSpike,
required this.tspins, required this.tspins,
required this.clears, required this.clears,
required this.garbage, required this.garbage,
required this.finesse, required this.finesse,
required this.kills,
}); });
ReplayStats.fromJson(Map<String, dynamic> json){ ReplayStats.fromJson(Map<String, dynamic> json, int inputTopSpike){
seed = json['seed']; seed = json['seed'];
linesCleared = json['lines']; linesCleared = json['lines'];
piecesPlaced = json['piecesplaced']; piecesPlaced = json['piecesplaced'];
@ -76,11 +99,11 @@ class ReplayStats{
score = json['score']; score = json['score'];
topCombo = json['topcombo']; topCombo = json['topcombo'];
topBtB = json['topbtb']; topBtB = json['topbtb'];
topSpike = inputTopSpike;
tspins = json['tspins']; tspins = json['tspins'];
clears = Clears.fromJson(json['clears']); clears = Clears.fromJson(json['clears']);
garbage = Garbage.fromJson(json['garbage']); garbage = Garbage.fromJson(json['garbage']);
finesse = Finesse.fromJson(json['finesse']); finesse = Finesse.fromJson(json['finesse']);
kills = json['kills'];
} }
ReplayStats.createEmpty(){ ReplayStats.createEmpty(){
@ -92,11 +115,11 @@ class ReplayStats{
score = 0; score = 0;
topCombo = 0; topCombo = 0;
topBtB = 0; topBtB = 0;
topSpike = 0;
tspins = 0; tspins = 0;
clears = Clears(singles: 0, doubles: 0, triples: 0, quads: 0, pentas: 0, allClears: 0, tSpinZeros: 0, tSpinSingles: 0, tSpinDoubles: 0, tSpinTriples: 0, tSpinPentas: 0, tSpinQuads: 0, tSpinMiniZeros: 0, tSpinMiniSingles: 0, tSpinMiniDoubles: 0); clears = Clears(singles: 0, doubles: 0, triples: 0, quads: 0, pentas: 0, allClears: 0, tSpinZeros: 0, tSpinSingles: 0, tSpinDoubles: 0, tSpinTriples: 0, tSpinPentas: 0, tSpinQuads: 0, tSpinMiniZeros: 0, tSpinMiniSingles: 0, tSpinMiniDoubles: 0);
garbage = Garbage(sent: 0, recived: 0, attack: 0, cleared: 0); garbage = Garbage(sent: 0, recived: 0, attack: 0, cleared: 0);
finesse = Finesse(combo: 0, faults: 0, perfectPieces: 0); finesse = Finesse(combo: 0, faults: 0, perfectPieces: 0);
kills = 0;
} }
ReplayStats operator + (ReplayStats other){ ReplayStats operator + (ReplayStats other){
@ -108,11 +131,12 @@ class ReplayStats{
score: score + other.score, score: score + other.score,
topCombo: max(topCombo, other.topCombo), topCombo: max(topCombo, other.topCombo),
topBtB: max(topBtB, other.topBtB), topBtB: max(topBtB, other.topBtB),
topSpike: max(topSpike, other.topSpike),
tspins: tspins + other.tspins, tspins: tspins + other.tspins,
clears: clears + other.clears, clears: clears + other.clears,
garbage: garbage + other.garbage, garbage: garbage + other.garbage,
finesse: finesse + other.finesse, finesse: finesse + other.finesse
kills: kills + other.kills); );
} }
} }
@ -122,6 +146,7 @@ class ReplayData{
late List<EndContextMulti> endcontext; late List<EndContextMulti> endcontext;
late List<List<ReplayStats>> stats; late List<List<ReplayStats>> stats;
late List<ReplayStats> totalStats; late List<ReplayStats> totalStats;
late List<List<String>> roundWinners;
late List<int> roundLengths; // in frames late List<int> roundLengths; // in frames
late int totalLength; // in frames late int totalLength; // in frames
@ -130,6 +155,7 @@ class ReplayData{
required this.endcontext, required this.endcontext,
required this.stats, required this.stats,
required this.totalStats, required this.totalStats,
required this.roundWinners,
required this.roundLengths, required this.roundLengths,
required this.totalLength required this.totalLength
}){ }){
@ -143,24 +169,20 @@ class ReplayData{
roundLengths = []; roundLengths = [];
totalLength = 0; totalLength = 0;
stats = []; stats = [];
roundWinners = [];
totalStats = [ReplayStats.createEmpty(), ReplayStats.createEmpty()]; totalStats = [ReplayStats.createEmpty(), ReplayStats.createEmpty()];
int firstInEndContext = json["data"][0]["board"].indexWhere((element) => element["id"] == endcontext[0].userId);
int secondInEndContext = json["data"][0]["board"].indexWhere((element) => element["id"] == endcontext[1].userId);
for(var round in json['data']) { for(var round in json['data']) {
roundLengths.add(max(round['replays'][0]['frames'], round['replays'][1]['frames'])); roundLengths.add(max(round['replays'][0]['frames'], round['replays'][1]['frames']));
totalLength = totalLength + max(round['replays'][0]['frames'], round['replays'][1]['frames']); totalLength = totalLength + max(round['replays'][0]['frames'], round['replays'][1]['frames']);
ReplayStats playerOne = ReplayStats.fromJson(round['replays'][0]['events'].last['data']['export']['stats']); int winner = round['board'].indexWhere((element) => element["success"] == true);
ReplayStats playerTwo = ReplayStats.fromJson(round['replays'][1]['events'].last['data']['export']['stats']); roundWinners.add([round['board'][winner]["id"], round['board'][winner]["username"]]);
ReplayStats playerOne = ReplayStats.fromJson(round['replays'][firstInEndContext]['events'].last['data']['export']['stats'], biggestSpikeFromReplay(round['replays'][secondInEndContext]['events'])); // (events contain recived attacks)
ReplayStats playerTwo = ReplayStats.fromJson(round['replays'][secondInEndContext]['events'].last['data']['export']['stats'], biggestSpikeFromReplay(round['replays'][firstInEndContext]['events']));
stats.add([playerOne, playerTwo]); stats.add([playerOne, playerTwo]);
totalStats[0] = totalStats[0] + playerOne; totalStats[0] = totalStats[0] + playerOne;
totalStats[1] = totalStats[1] + playerTwo; totalStats[1] = totalStats[1] + playerTwo;
// print(round['replays'][0]['events'].last['data']['export']['stats']);
// for (var event in round['replays'][0]['events']){
// if (event["type"] == "ige" && event["data"]["data"]["type"] == "interaction_confirm"){
// print(event['data']["data"]["data"]); // lol
// }
// }
} }
// Сами по себе эти ивенты ничего не дают,
// Хотя можно попробовать вычислить biggest spike
} }
} }

View File

@ -65,6 +65,7 @@ class TetrioService extends DB {
Map<String, List<TetrioPlayer>> _players = {}; Map<String, List<TetrioPlayer>> _players = {};
final Map<String, TetrioPlayer> _playersCache = {}; final Map<String, TetrioPlayer> _playersCache = {};
final Map<String, Map<String, dynamic>> _recordsCache = {}; final Map<String, Map<String, dynamic>> _recordsCache = {};
final Map<String, dynamic> _replaysCache = {};
final Map<String, TetrioPlayersLeaderboard> _leaderboardsCache = {}; final Map<String, TetrioPlayersLeaderboard> _leaderboardsCache = {};
final Map<String, List<News>> _newsCache = {}; final Map<String, List<News>> _newsCache = {};
final Map<String, Map<String, double?>> _topTRcache = {}; final Map<String, Map<String, double?>> _topTRcache = {};
@ -121,6 +122,12 @@ class TetrioService extends DB {
} }
Future<List<dynamic>> szyGetReplay(String replayID) async { Future<List<dynamic>> szyGetReplay(String replayID) async {
try{
var cached = _replaysCache.entries.firstWhere((element) => element.key == replayID);
return cached.value;
}catch (e){
// actually going to obtain
}
Uri url = Uri.https('inoue.szy.lol', '/api/replay/$replayID'); Uri url = Uri.https('inoue.szy.lol', '/api/replay/$replayID');
var downloadPath = await getDownloadsDirectory(); var downloadPath = await getDownloadsDirectory();
downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory();
@ -132,6 +139,7 @@ class TetrioService extends DB {
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
developer.log("szyDownload: Replay downloaded", name: "services/tetrio_crud", error: response.statusCode); developer.log("szyDownload: Replay downloaded", name: "services/tetrio_crud", error: response.statusCode);
_replaysCache[replayID] = [response.body, response.bodyBytes];
return [response.body, response.bodyBytes]; return [response.body, response.bodyBytes];
case 404: case 404:
throw SzyNotFound(); throw SzyNotFound();
@ -157,7 +165,6 @@ class TetrioService extends DB {
} }
Future<String> SaveReplay(String replayID) async { Future<String> SaveReplay(String replayID) async {
Uri url = Uri.https('inoue.szy.lol', '/api/replay/$replayID');
var downloadPath = await getDownloadsDirectory(); var downloadPath = await getDownloadsDirectory();
downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory();
var replayFile = File("${downloadPath.path}/$replayID.ttrm"); var replayFile = File("${downloadPath.path}/$replayID.ttrm");

View File

@ -801,6 +801,7 @@ class CompareThingy extends StatelessWidget {
final bool higherIsBetter; final bool higherIsBetter;
final int? fractionDigits; final int? fractionDigits;
final String? postfix; final String? postfix;
final String? prefix;
const CompareThingy( const CompareThingy(
{super.key, {super.key,
required this.greenSide, required this.greenSide,
@ -808,6 +809,7 @@ class CompareThingy extends StatelessWidget {
required this.label, required this.label,
required this.higherIsBetter, required this.higherIsBetter,
this.fractionDigits, this.fractionDigits,
this.prefix,
this.postfix}); this.postfix});
String verdict(num greenSide, num redSide, int fraction) { String verdict(num greenSide, num redSide, int fraction) {
@ -845,7 +847,7 @@ class CompareThingy extends StatelessWidget {
], ],
)), )),
child: Text( child: Text(
f.format(greenSide) + (postfix ?? ""), (prefix ?? "") + f.format(greenSide) + (postfix ?? ""),
style: const TextStyle( style: const TextStyle(
fontSize: 22, fontSize: 22,
shadows: <Shadow>[ shadows: <Shadow>[
@ -899,7 +901,7 @@ class CompareThingy extends StatelessWidget {
], ],
)), )),
child: Text( child: Text(
f.format(redSide) + (postfix ?? ""), (prefix ?? "") + f.format(redSide) + (postfix ?? ""),
style: const TextStyle( style: const TextStyle(
fontSize: 22, fontSize: 22,
shadows: <Shadow>[ shadows: <Shadow>[

View File

@ -269,10 +269,6 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
value: "history", value: "history",
child: Text(t.fetchAndsaveTLHistory), child: Text(t.fetchAndsaveTLHistory),
), ),
PopupMenuItem(
value: "test",
child: Text("Test replay reading"),
),
PopupMenuItem( PopupMenuItem(
value: "/states", value: "/states",
child: Text(t.showStoredData), child: Text(t.showStoredData),
@ -294,11 +290,6 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
case "history": case "history":
changePlayer(_searchFor, fetchHistory: true); changePlayer(_searchFor, fetchHistory: true);
break; break;
case "test":
var replayfile = File("/home/dan63047/Загрузки/659337dd1eef65e513c5dc8d.ttrm").readAsStringSync();
var testObject = ReplayData.fromJson(jsonDecode(replayfile));
print("lol");
break;
default: default:
context.go(value); context.go(value);
} }

View File

@ -201,13 +201,18 @@ class TlMatchResultState extends State<TlMatchResultView> {
case ConnectionState.none: case ConnectionState.none:
case ConnectionState.waiting: case ConnectionState.waiting:
case ConnectionState.active: case ConnectionState.active:
return CircularProgressIndicator(); return const LinearProgressIndicator();
case ConnectionState.done: case ConnectionState.done:
if (!snapshot.hasError){ if (!snapshot.hasError){
var time = framesToTime(snapshot.data!.totalLength); if (roundSelector.isNegative){
return Center(child: Text("Match Length: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}")); var time = framesToTime(snapshot.data!.totalLength);
return Center(child: Text("Match Length: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}", textAlign: TextAlign.center));
}else{
var time = framesToTime(snapshot.data!.roundLengths[roundSelector]);
return Center(child: Text("Round Length: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\nWinner: ${snapshot.data!.roundWinners[roundSelector][1]}", textAlign: TextAlign.center,));
}
}else{ }else{
return Text("skill issue"); return const Text("skill issue", textAlign: TextAlign.center);
} }
} }
@ -247,27 +252,47 @@ class TlMatchResultState extends State<TlMatchResultView> {
case ConnectionState.none: case ConnectionState.none:
case ConnectionState.waiting: case ConnectionState.waiting:
case ConnectionState.active: case ConnectionState.active:
return LinearProgressIndicator(); return const LinearProgressIndicator();
case ConnectionState.done: case ConnectionState.done:
if (!snapshot.hasError){ if (!snapshot.hasError){
var greenSidePlayer = snapshot.data!.endcontext.indexWhere(((element) => element.userId == widget.initPlayerId)); var greenSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId == widget.initPlayerId);
var redSidePlayer = snapshot.data!.endcontext.indexWhere(((element) => element.userId != widget.initPlayerId)); var redSidePlayer = snapshot.data!.endcontext.indexWhere((element) => element.userId != widget.initPlayerId);
return Column(children: [ return Column(children: [
CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].piecesPlaced, CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][greenSidePlayer].piecesPlaced,
redSide: snapshot.data!.totalStats[redSidePlayer].piecesPlaced, redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].piecesPlaced : snapshot.data!.stats[roundSelector][redSidePlayer].piecesPlaced,
label: "Pieces Placed", higherIsBetter: true), label: "Pieces Placed", higherIsBetter: true),
CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].linesCleared, CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][greenSidePlayer].linesCleared,
redSide: snapshot.data!.totalStats[redSidePlayer].linesCleared, redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].linesCleared : snapshot.data!.stats[roundSelector][redSidePlayer].linesCleared,
label: "Lines Cleared", higherIsBetter: true), label: "Lines Cleared", higherIsBetter: true),
CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100, CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][greenSidePlayer].finessePercentage * 100,
redSide: snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100, redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].finessePercentage * 100 : snapshot.data!.stats[roundSelector][redSidePlayer].finessePercentage * 100,
label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true), label: "Finnese", postfix: "%", fractionDigits: 2, higherIsBetter: true),
CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].topCombo, CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topSpike : snapshot.data!.stats[roundSelector][greenSidePlayer].topSpike,
redSide: snapshot.data!.totalStats[redSidePlayer].topCombo, redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topSpike : snapshot.data!.stats[roundSelector][redSidePlayer].topSpike,
label: "Best Spike", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topCombo : snapshot.data!.stats[roundSelector][greenSidePlayer].topCombo,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topCombo : snapshot.data!.stats[roundSelector][redSidePlayer].topCombo,
label: "Best Combo", higherIsBetter: true), label: "Best Combo", higherIsBetter: true),
CompareThingy(greenSide: snapshot.data!.totalStats[greenSidePlayer].topBtB, CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].topBtB : snapshot.data!.stats[roundSelector][greenSidePlayer].topBtB,
redSide: snapshot.data!.totalStats[redSidePlayer].topBtB, redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].topBtB : snapshot.data!.stats[roundSelector][redSidePlayer].topBtB,
label: "Best BtB", higherIsBetter: true), label: "Best BtB", higherIsBetter: true),
const Divider(),
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text("Garbage", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)),
),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.sent,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.sent : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.sent,
label: "Sent", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.recived,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.recived : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.recived,
label: "Recived", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.attack,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.attack : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.attack,
label: "Attack", higherIsBetter: true),
CompareThingy(greenSide: roundSelector.isNegative ? snapshot.data!.totalStats[greenSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][greenSidePlayer].garbage.cleared,
redSide: roundSelector.isNegative ? snapshot.data!.totalStats[redSidePlayer].garbage.cleared : snapshot.data!.stats[roundSelector][redSidePlayer].garbage.cleared,
label: "Cleared", higherIsBetter: true),
],); ],);
}else{ }else{
return Text("skill issue"); return Text("skill issue");
@ -412,17 +437,17 @@ class TlMatchResultState extends State<TlMatchResultView> {
CompareThingy( CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das, greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.das,
redSide: 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", fractionDigits: 1, label: "DAS", fractionDigits: 1, postfix: "F",
higherIsBetter: false), higherIsBetter: false),
CompareThingy( CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr, greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.arr,
redSide: 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", fractionDigits: 1, label: "ARR", fractionDigits: 1, postfix: "F",
higherIsBetter: false), higherIsBetter: false),
CompareThingy( CompareThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf, greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.sdf,
redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf, redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.sdf,
label: "SDF", label: "SDF", prefix: "x",
higherIsBetter: true), higherIsBetter: true),
CompareBoolThingy( CompareBoolThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock,

View File

@ -166,6 +166,7 @@ flutter:
- res/tetrio_badges/sfu_raccoon_1.png - res/tetrio_badges/sfu_raccoon_1.png
- res/tetrio_badges/sfu_raccoon_2.png - res/tetrio_badges/sfu_raccoon_2.png
- res/tetrio_badges/sfu_raccoon_3.png - res/tetrio_badges/sfu_raccoon_3.png
- res/tetrio_badges/streamersuperlobby.png
- res/tetrio_badges/superlobby.png - res/tetrio_badges/superlobby.png
- res/tetrio_badges/superlobby2.png - res/tetrio_badges/superlobby2.png
- res/tetrio_badges/taws_u50_1.png - res/tetrio_badges/taws_u50_1.png
@ -185,6 +186,10 @@ flutter:
- res/tetrio_badges/ttsdtc_1.png - res/tetrio_badges/ttsdtc_1.png
- res/tetrio_badges/ttsdtc_2.png - res/tetrio_badges/ttsdtc_2.png
- res/tetrio_badges/ttsdtc_3.png - res/tetrio_badges/ttsdtc_3.png
- res/tetrio_badges/twc23_1.png
- res/tetrio_badges/twc23_2.png
- res/tetrio_badges/twc23_3.png
- res/tetrio_badges/twc23_4.png
- res/tetrio_badges/ubcea_1.png - res/tetrio_badges/ubcea_1.png
- res/tetrio_badges/ubcea_2.png - res/tetrio_badges/ubcea_2.png
- res/tetrio_badges/ubcea_3.png - res/tetrio_badges/ubcea_3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB