feat(calls): Я ЕБЛАН ХАХАХХА Я ШАШКИ ЧЕРЕЗ ЗВОНКИ СДЕЛАЛ

This commit is contained in:
Jganenok
2026-06-13 00:42:14 +07:00
parent 34cd52335e
commit b9a89a4437
8 changed files with 999 additions and 12 deletions
+72
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart'
show TargetPlatform, defaultTargetPlatform;
@@ -35,6 +36,14 @@ class CallParticipant {
});
}
class CallChatMessage {
final String text;
final bool mine;
final DateTime time;
CallChatMessage({required this.text, required this.mine, required this.time});
}
class CallSession {
final Ws2Config ws2Config;
@@ -90,6 +99,17 @@ class CallSession {
static const String _probeQuestion = 'AreYouKomet?';
static const String _probeAnswer = 'YesImKomet😎';
final List<CallChatMessage> _chat = [];
final _chatController = StreamController<CallChatMessage>.broadcast();
final _gameController = StreamController<Map<String, dynamic>>.broadcast();
List<CallChatMessage> get chatLog => List.unmodifiable(_chat);
Stream<CallChatMessage> get chatMessages => _chatController.stream;
Stream<Map<String, dynamic>> get gameMessages => _gameController.stream;
int get selfUserId => ws2Config.userId;
int? get peerUserId => _peerId;
bool get localVideo => _localVideo;
bool get localScreen => _localScreen;
MediaStream? get localVideoStream => _localVideoStream;
@@ -160,6 +180,7 @@ class CallSession {
Future<void> _sampleLevels() async {
final pc = _pc;
if (pc == null || _ended) return;
if (!_mediaConnected || _current != CallSessionState.active) return;
var local = 0.0;
var remote = 0.0;
@@ -571,6 +592,21 @@ class CallSession {
void _onProbeMessage(RTCDataChannel channel, RTCDataChannelMessage message) {
if (message.isBinary) return;
final text = message.text;
final frame = _decodeFrame(text);
if (frame != null && frame['t'] == 'chat') {
final body = frame['text'];
if (body is String && body.isNotEmpty) {
_addChat(CallChatMessage(text: body, mine: false, time: DateTime.now()));
}
return;
}
if (frame != null && frame['t'] == 'game') {
final data = Map<String, dynamic>.of(frame)..remove('t');
if (!_gameController.isClosed) _gameController.add(data);
return;
}
if (text == _probeQuestion) {
_sendProbe(channel, _probeAnswer);
} else if (text == _probeAnswer) {
@@ -578,12 +614,46 @@ class CallSession {
}
}
Map<String, dynamic>? _decodeFrame(String text) {
try {
final v = jsonDecode(text);
return v is Map<String, dynamic> ? v : null;
} catch (_) {
return null;
}
}
void _sendProbe(RTCDataChannel channel, String text) {
try {
channel.send(RTCDataChannelMessage(text));
} catch (_) {}
}
void sendChatMessage(String text) {
final body = text.trim();
final channel = _probeChannel;
if (body.isEmpty || channel == null) return;
try {
channel.send(RTCDataChannelMessage(jsonEncode({'t': 'chat', 'text': body})));
} catch (_) {
return;
}
_addChat(CallChatMessage(text: body, mine: true, time: DateTime.now()));
}
void sendGame(Map<String, dynamic> data) {
final channel = _probeChannel;
if (channel == null) return;
try {
channel.send(RTCDataChannelMessage(jsonEncode({'t': 'game', ...data})));
} catch (_) {}
}
void _addChat(CallChatMessage message) {
_chat.add(message);
if (!_chatController.isClosed) _chatController.add(message);
}
void _markPeerKomet() {
if (_peerIsKomet) return;
_peerIsKomet = true;
@@ -1097,6 +1167,8 @@ class CallSession {
if (!_remoteStream.isClosed) await _remoteStream.close();
if (!_info.isClosed) await _info.close();
if (!_kometDetected.isClosed) await _kometDetected.close();
if (!_chatController.isClosed) await _chatController.close();
if (!_gameController.isClosed) await _gameController.close();
}
void _applyConnectionInfo(Map<String, dynamic> msg, List iceServers) {
+209
View File
@@ -0,0 +1,209 @@
enum CheckersSide { white, black }
class Checkers {
static const int size = 8;
static const int empty = 0;
static const int whiteMan = 1;
static const int whiteKing = 2;
static const int blackMan = 3;
static const int blackKing = 4;
static const List<List<int>> _dirs = [
[-1, -1],
[-1, 1],
[1, -1],
[1, 1],
];
static List<int> initial() {
final board = List<int>.filled(size * size, empty);
for (var r = 0; r < size; r++) {
for (var c = 0; c < size; c++) {
if ((r + c) % 2 == 0) continue;
final i = r * size + c;
if (r <= 2) board[i] = blackMan;
if (r >= 5) board[i] = whiteMan;
}
}
return board;
}
static CheckersSide? sideOf(int piece) {
if (piece == whiteMan || piece == whiteKing) return CheckersSide.white;
if (piece == blackMan || piece == blackKing) return CheckersSide.black;
return null;
}
static bool isKing(int piece) => piece == whiteKing || piece == blackKing;
static CheckersSide opponent(CheckersSide side) =>
side == CheckersSide.white ? CheckersSide.black : CheckersSide.white;
static int _row(int i) => i ~/ size;
static int _col(int i) => i % size;
static bool _inB(int r, int c) => r >= 0 && r < size && c >= 0 && c < size;
static int _idx(int r, int c) => r * size + c;
static int _lastRow(CheckersSide side) =>
side == CheckersSide.white ? 0 : size - 1;
static int _kingOf(CheckersSide side) =>
side == CheckersSide.white ? whiteKing : blackKing;
static List<List<int>> legalMoves(List<int> board, CheckersSide side) {
final captures = <List<int>>[];
for (var i = 0; i < board.length; i++) {
if (sideOf(board[i]) != side) continue;
_collectCaptures(List<int>.of(board), i, side, [i], <int>{}, captures);
}
if (captures.isNotEmpty) return captures;
final quiet = <List<int>>[];
for (var i = 0; i < board.length; i++) {
if (sideOf(board[i]) != side) continue;
_collectQuiet(board, i, side, quiet);
}
return quiet;
}
static void _collectCaptures(List<int> work, int at, CheckersSide side,
List<int> path, Set<int> captured, List<List<int>> out) {
final steps = _captureSteps(work, at, captured);
if (steps.isEmpty) {
if (path.length > 1) out.add(List<int>.of(path));
return;
}
final piece = work[at];
for (final step in steps) {
final landing = step[0];
final victim = step[1];
final promote = !isKing(piece) && _row(landing) == _lastRow(side);
final moved = promote ? _kingOf(side) : piece;
work[at] = empty;
work[landing] = moved;
captured.add(victim);
path.add(landing);
_collectCaptures(work, landing, side, path, captured, out);
path.removeLast();
captured.remove(victim);
work[landing] = empty;
work[at] = piece;
}
}
static List<List<int>> _captureSteps(
List<int> work, int at, Set<int> captured) {
final piece = work[at];
final side = sideOf(piece);
if (side == null) return const [];
final king = isKing(piece);
final r0 = _row(at);
final c0 = _col(at);
final result = <List<int>>[];
for (final d in _dirs) {
var r = r0 + d[0];
var c = c0 + d[1];
if (king) {
while (_inB(r, c) && work[_idx(r, c)] == empty) {
r += d[0];
c += d[1];
}
if (!_inB(r, c)) continue;
final vi = _idx(r, c);
if (sideOf(work[vi]) == side || captured.contains(vi)) continue;
var lr = r + d[0];
var lc = c + d[1];
while (_inB(lr, lc) && work[_idx(lr, lc)] == empty) {
result.add([_idx(lr, lc), vi]);
lr += d[0];
lc += d[1];
}
} else {
if (!_inB(r, c)) continue;
final vi = _idx(r, c);
if (work[vi] == empty ||
sideOf(work[vi]) == side ||
captured.contains(vi)) {
continue;
}
final lr = r + d[0];
final lc = c + d[1];
if (_inB(lr, lc) && work[_idx(lr, lc)] == empty) {
result.add([_idx(lr, lc), vi]);
}
}
}
return result;
}
static void _collectQuiet(
List<int> board, int at, CheckersSide side, List<List<int>> out) {
final piece = board[at];
final r0 = _row(at);
final c0 = _col(at);
if (isKing(piece)) {
for (final d in _dirs) {
var r = r0 + d[0];
var c = c0 + d[1];
while (_inB(r, c) && board[_idx(r, c)] == empty) {
out.add([at, _idx(r, c)]);
r += d[0];
c += d[1];
}
}
} else {
final forward = side == CheckersSide.white ? -1 : 1;
for (final dc in const [-1, 1]) {
final r = r0 + forward;
final c = c0 + dc;
if (_inB(r, c) && board[_idx(r, c)] == empty) {
out.add([at, _idx(r, c)]);
}
}
}
}
static List<int> applyMove(List<int> board, List<int> path) {
final next = List<int>.of(board);
if (path.length < 2) return next;
final from = path.first;
final side = sideOf(board[from]);
if (side == null) return next;
final piece = board[from];
next[from] = empty;
var promoted = isKing(piece);
for (var k = 0; k < path.length - 1; k++) {
final a = path[k];
final b = path[k + 1];
final dr = (_row(b) - _row(a)).sign;
final dc = (_col(b) - _col(a)).sign;
var r = _row(a) + dr;
var c = _col(a) + dc;
while (r != _row(b) || c != _col(b)) {
final vi = _idx(r, c);
if (next[vi] != empty && sideOf(next[vi]) != side) {
next[vi] = empty;
}
r += dr;
c += dc;
}
if (_row(b) == _lastRow(side)) promoted = true;
}
next[path.last] = promoted ? _kingOf(side) : piece;
return next;
}
static bool _hasPieces(List<int> board, CheckersSide side) =>
board.any((p) => sideOf(p) == side);
static CheckersSide? winner(List<int> board, CheckersSide toMove) {
if (!_hasPieces(board, toMove) || legalMoves(board, toMove).isEmpty) {
return opponent(toMove);
}
return null;
}
}
+28 -2
View File
@@ -1,8 +1,10 @@
import 'dart:async';
import 'dart:io';
import 'package:komet/core/storage/app_instance.dart';
import 'package:komet/core/utils/logger.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
class ProfileData {
@@ -155,10 +157,34 @@ class AppDatabase {
return _db!;
}
static Future<String> _databasesDir() async {
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
final dir = await getApplicationSupportDirectory();
return dir.path;
}
return getDatabasesPath();
}
static Future<void> _migrateLegacyDb(String target) async {
if (AppInstance.isNamed) return;
if (!(Platform.isLinux || Platform.isWindows || Platform.isMacOS)) return;
try {
if (await File(target).exists()) return;
final legacy = File(join(await getDatabasesPath(), 'komet.db'));
if (legacy.path == target) return;
if (await legacy.exists()) {
await legacy.copy(target);
logger.i('[db] перенёс komet.db -> $target');
}
} catch (_) {}
}
static Future<Database> _open() async {
final dbPath = await getDatabasesPath();
final dbPath = await _databasesDir();
final target = join(dbPath, 'komet${AppInstance.suffix}.db');
await _migrateLegacyDb(target);
return openDatabase(
join(dbPath, 'komet.db'),
target,
version: 11,
onOpen: (db) => db.execute('PRAGMA foreign_keys = ON'),
onCreate: (db, _) => _createTables(db),
+9
View File
@@ -0,0 +1,9 @@
class AppInstance {
AppInstance._();
static const String id = String.fromEnvironment('KOMET_INSTANCE');
static bool get isNamed => id.isNotEmpty;
static String get suffix => isNamed ? '_$id' : '';
}
+2 -1
View File
@@ -4,6 +4,7 @@ import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../config/app_media_cache.dart';
import '../storage/app_instance.dart';
/// Постоянный дисковый кэш скачанных медиа (файлы, видео).
///
@@ -21,7 +22,7 @@ class MediaCache {
final cached = _dir;
if (cached != null) return cached;
final base = await getApplicationSupportDirectory();
final dir = Directory(p.join(base.path, 'media_cache'));
final dir = Directory(p.join(base.path, 'media_cache${AppInstance.suffix}'));
if (!await dir.exists()) {
await dir.create(recursive: true);
}