...
This commit is contained in:
@@ -7,11 +7,12 @@ import 'package:bonsoir/bonsoir.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../manager/notification_manager.dart';
|
||||
|
||||
import '../model/user_info.dart';
|
||||
import '../model/play_packet.dart';
|
||||
import '../manager/global_chat_manager.dart';
|
||||
import '../manager/media_manager.dart';
|
||||
import '../manager/notification_manager.dart';
|
||||
|
||||
enum NetworkRole { none, host, guest }
|
||||
|
||||
@@ -26,9 +27,7 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
// ------------------------------------------------------------------------
|
||||
// 상수 설정
|
||||
// ------------------------------------------------------------------------
|
||||
// [핵심] 패킷 구분자 (End Of Packet) - Base64와 겹치지 않는 고유 문자열
|
||||
static const String PACKET_DELIMITER = "|||EOP|||";
|
||||
|
||||
static const int HEARTBEAT_INTERVAL_SEC = 3;
|
||||
static const int TIMEOUT_SEC = 10;
|
||||
static const int RECONNECT_WAIT_SEC = 5;
|
||||
@@ -47,8 +46,6 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
|
||||
final Map<Socket, UserInfo?> _connectedGuests = {};
|
||||
final List<UserInfo> guestList = [];
|
||||
|
||||
// [버퍼] 소켓별로 들어오다 만 데이터를 저장
|
||||
final Map<Socket, String> _packetBuffers = {};
|
||||
|
||||
BonsoirService? _bonsoirService;
|
||||
@@ -66,21 +63,21 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
DateTime? _lastPongTime;
|
||||
bool _isReconnecting = false;
|
||||
|
||||
// [추가] 현재 선택된 게임 ID 및 설정
|
||||
String selectedGameId = 'quiz_mix';
|
||||
Map<String, dynamic> selectedGameConfig = {}; // 난이도 등 저장
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 초기화
|
||||
// ------------------------------------------------------------------------
|
||||
void initialize({
|
||||
required String nickname,
|
||||
String? profileImage, // [추가] 선택적 파라미터
|
||||
}) {
|
||||
void initialize({required String nickname, String? profileImage}) {
|
||||
final uuid = const Uuid().v4().substring(0, 8);
|
||||
final randomColor = 0xFF000000 | (nickname.hashCode & 0xFFFFFF);
|
||||
|
||||
me = UserInfo(
|
||||
id: uuid,
|
||||
nickname: nickname,
|
||||
colorValue: randomColor,
|
||||
profileImageBase64: profileImage, // [적용]
|
||||
profileImageBase64: profileImage
|
||||
);
|
||||
_log("초기화 완료: ${me.nickname}");
|
||||
}
|
||||
@@ -103,6 +100,14 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
// ------------------------------------------------------------------------
|
||||
// 레디 시스템
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
// [수정] 게임 선택 함수 (Config 추가)
|
||||
void selectGame(String gameId, {Map<String, dynamic>? config}) {
|
||||
selectedGameId = gameId;
|
||||
selectedGameConfig = config ?? {};
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleReady() {
|
||||
me = me.copyWith(isReady: !me.isReady);
|
||||
notifyListeners();
|
||||
@@ -128,7 +133,12 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
if (allGuestsReady) {
|
||||
_log("🚀 전원 준비 완료! 3초 후 게임 시작...");
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
final startPayload = {'type': 'GAME_START', 'gameId': 'quiz_ox'};
|
||||
// [수정] 게임 시작 패킷에 Config 포함
|
||||
final startPayload = {
|
||||
'type': 'GAME_START',
|
||||
'gameId': selectedGameId,
|
||||
'config': selectedGameConfig
|
||||
};
|
||||
sendMessage(startPayload);
|
||||
_messageController.add(startPayload);
|
||||
_resetAllReadyState();
|
||||
@@ -174,7 +184,6 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
await _bonsoirBroadcast!.start();
|
||||
|
||||
await MediaManager().initialize(roomName);
|
||||
|
||||
_startHeartbeat();
|
||||
notifyListeners();
|
||||
|
||||
@@ -187,8 +196,12 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
void _handleNewGuest(Socket client) {
|
||||
_log("🎉 연결됨: ${client.remoteAddress.address}");
|
||||
_connectedGuests[client] = null;
|
||||
_packetBuffers[client] = ""; // 버퍼 초기화
|
||||
_packetBuffers[client] = "";
|
||||
|
||||
final myHandshake = {'type': 'HANDSHAKE', 'payload': me.toJson()};
|
||||
final jsonString = jsonEncode(myHandshake);
|
||||
client.add(utf8.encode('$jsonString$PACKET_DELIMITER'));
|
||||
|
||||
client.listen(
|
||||
(Uint8List data) => _onDataReceived(client, data),
|
||||
onError: (e) => _removeGuest(client),
|
||||
@@ -243,6 +256,32 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
// [수정] 싱글 모드 시작 시 Config 추가
|
||||
Future<void> startSoloMode(String gameId, {Map<String, dynamic>? config}) async {
|
||||
stopNetwork(force: true);
|
||||
|
||||
role = NetworkRole.host;
|
||||
hostIp = "Solo Mode";
|
||||
hostPort = 0;
|
||||
selectedGameId = gameId;
|
||||
selectedGameConfig = config ?? {}; // Config 저장
|
||||
|
||||
await MediaManager().initialize("solo_session");
|
||||
|
||||
_log("👤 싱글 플레이 모드 시작: $gameId");
|
||||
notifyListeners();
|
||||
|
||||
// 바로 시작 신호
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_messageController.add({
|
||||
'type': 'GAME_START',
|
||||
'gameId': gameId,
|
||||
'config': selectedGameConfig
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Future<void> joinRoom(String ip, int port) async {
|
||||
if (role != NetworkRole.guest) stopNetwork(force: true);
|
||||
role = NetworkRole.guest;
|
||||
@@ -254,7 +293,7 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
_clientSocket = await Socket.connect(ip, port, timeout: const Duration(seconds: 5));
|
||||
_log("✅ 접속 성공!");
|
||||
|
||||
_packetBuffers[_clientSocket!] = ""; // 내 버퍼 초기화
|
||||
_packetBuffers[_clientSocket!] = "";
|
||||
|
||||
sendMessage({'type': 'HANDSHAKE', 'payload': me.toJson()});
|
||||
|
||||
@@ -279,7 +318,7 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 데이터 송수신 (버퍼링 및 구분자 로직 강화)
|
||||
// 데이터 송수신
|
||||
// ------------------------------------------------------------------------
|
||||
void sendPacket(PlayPacket packet) {
|
||||
sendMessage(packet.toJson());
|
||||
@@ -289,8 +328,6 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
if (role == NetworkRole.guest && _clientSocket == null) return;
|
||||
|
||||
final jsonString = jsonEncode(messageMap);
|
||||
|
||||
// 로그 필터링
|
||||
if (messageMap['type'] != 'PING' && messageMap['type'] != 'PONG') {
|
||||
if (messageMap['type'] == 'chat') {
|
||||
_log("📤 전송: [CHAT]");
|
||||
@@ -301,7 +338,6 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
// [핵심] 메시지 뒤에 고유 구분자를 붙여서 전송
|
||||
final fullMessage = '$jsonString$PACKET_DELIMITER';
|
||||
final List<int> data = utf8.encode(fullMessage);
|
||||
|
||||
@@ -316,30 +352,17 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
|
||||
void _onDataReceived(Socket socket, Uint8List data) {
|
||||
try {
|
||||
// 1. 기존 버퍼 가져오기
|
||||
String buffer = _packetBuffers[socket] ?? "";
|
||||
// 2. 새 데이터 추가
|
||||
buffer += utf8.decode(data, allowMalformed: true);
|
||||
|
||||
// 3. 구분자(PACKET_DELIMITER)를 기준으로 메시지 추출
|
||||
while (buffer.contains(PACKET_DELIMITER)) {
|
||||
final int delimiterIndex = buffer.indexOf(PACKET_DELIMITER);
|
||||
|
||||
// 완성된 메시지 하나 추출
|
||||
final String msg = buffer.substring(0, delimiterIndex);
|
||||
|
||||
// 버퍼에서 추출한 부분과 구분자 제거
|
||||
buffer = buffer.substring(delimiterIndex + PACKET_DELIMITER.length);
|
||||
|
||||
// 메시지 처리
|
||||
if (msg.trim().isNotEmpty) {
|
||||
_processMessage(socket, msg);
|
||||
}
|
||||
if (msg.trim().isNotEmpty) _processMessage(socket, msg);
|
||||
}
|
||||
|
||||
// 4. 남은 찌꺼기(다음 패킷의 일부)를 다시 버퍼에 저장
|
||||
_packetBuffers[socket] = buffer;
|
||||
|
||||
} catch (e) {
|
||||
_log("데이터 수신 에러: $e");
|
||||
}
|
||||
@@ -361,7 +384,11 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
|
||||
if (jsonMap['type'] == 'HANDSHAKE') {
|
||||
final guestInfo = UserInfo.fromJson(jsonMap['payload']);
|
||||
_connectedGuests[socket] = guestInfo;
|
||||
|
||||
if (role == NetworkRole.host) {
|
||||
_connectedGuests[socket] = guestInfo;
|
||||
}
|
||||
|
||||
guestList.removeWhere((u) => u.id == guestInfo.id);
|
||||
guestList.add(guestInfo);
|
||||
notifyListeners();
|
||||
@@ -386,6 +413,13 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
if (jsonMap['type'] == 'GAME_START') {
|
||||
if (jsonMap['gameId'] != null) {
|
||||
selectedGameId = jsonMap['gameId'];
|
||||
}
|
||||
// [추가] Config 동기화
|
||||
if (jsonMap['config'] != null) {
|
||||
selectedGameConfig = jsonMap['config'];
|
||||
}
|
||||
_resetAllReadyState();
|
||||
_messageController.add(jsonMap);
|
||||
return;
|
||||
@@ -410,52 +444,23 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 연결 관리
|
||||
// ------------------------------------------------------------------------
|
||||
void _handleConnectionLost(dynamic reason) {
|
||||
if (role != NetworkRole.guest) return;
|
||||
|
||||
_log("⚠️ 연결 끊김: $reason");
|
||||
|
||||
_clientSocket?.destroy();
|
||||
_clientSocket = null;
|
||||
|
||||
if (_disconnectWaitTimer != null && _disconnectWaitTimer!.isActive) return;
|
||||
|
||||
// 5초 카운트다운 시작
|
||||
_disconnectWaitTimer = Timer(const Duration(seconds: RECONNECT_WAIT_SEC), () {
|
||||
_log("💀 복구 실패. 종료.");
|
||||
|
||||
// [추가] 재접속 실패 알림 발송
|
||||
_sendDisconnectionNotification();
|
||||
|
||||
stopNetwork(force: true);
|
||||
});
|
||||
|
||||
_attemptReconnection();
|
||||
}
|
||||
|
||||
// [신규] 연결 끊김 알림 메서드
|
||||
void _sendDisconnectionNotification() {
|
||||
// 앱이 현재 화면에 떠있지 않을 때만(백그라운드) 알림
|
||||
final state = WidgetsBinding.instance.lifecycleState;
|
||||
if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive || state == AppLifecycleState.detached) {
|
||||
NotificationManager().showNotification(
|
||||
id: 9999, // 고정 ID (시스템 알림용)
|
||||
title: "연결 끊김 ⚠️",
|
||||
body: "방과의 연결이 종료되었습니다. 앱을 실행해 확인해주세요.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _attemptReconnection() async {
|
||||
if (hostIp == null || hostPort == null) return;
|
||||
_isReconnecting = true;
|
||||
while (_disconnectWaitTimer != null && _disconnectWaitTimer!.isActive) {
|
||||
try {
|
||||
await joinRoom(hostIp!, hostPort!);
|
||||
_log("✅ 재접속 성공!");
|
||||
_isReconnecting = false;
|
||||
return;
|
||||
} catch (e) {
|
||||
@@ -499,10 +504,7 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
|
||||
void stopNetwork({bool force = false}) {
|
||||
if (!force && _disconnectWaitTimer != null) return;
|
||||
|
||||
_log("🛑 종료");
|
||||
MediaManager().cleanup();
|
||||
|
||||
_heartbeatTimer?.cancel();
|
||||
_disconnectWaitTimer?.cancel();
|
||||
_disconnectWaitTimer = null;
|
||||
@@ -510,12 +512,10 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
_bonsoirDiscovery?.stop();
|
||||
_serverSocket?.close();
|
||||
_clientSocket?.close();
|
||||
|
||||
for (var s in _connectedGuests.keys) s.close();
|
||||
_connectedGuests.clear();
|
||||
_packetBuffers.clear(); // 버퍼 정리
|
||||
_packetBuffers.clear();
|
||||
guestList.clear();
|
||||
|
||||
role = NetworkRole.none;
|
||||
_serverSocket = null;
|
||||
_clientSocket = null;
|
||||
|
||||
Reference in New Issue
Block a user