Fix rejoining #13

Merged
marco merged 5 commits from fix-rejoining into master 2024-05-21 21:56:36 +00:00
9 changed files with 201 additions and 190 deletions

View File

@ -3,43 +3,33 @@ import 'package:uuid/uuid.dart';
class GameInfo { class GameInfo {
final UuidValue? playerID; final UuidValue? playerID;
final UuidValue? lobbyID;
final String? passphrase; final String? passphrase;
const GameInfo({ const GameInfo({
required this.playerID, required this.playerID,
required this.lobbyID,
required this.passphrase, required this.passphrase,
}); });
factory GameInfo.empty() { factory GameInfo.empty() {
return const GameInfo(playerID: null, lobbyID: null, passphrase: null); return const GameInfo(playerID: null, passphrase: null);
} }
factory GameInfo.fromJson(Map<String, dynamic> json) { factory GameInfo.fromJson(Map<String, dynamic> json) {
final playerid = UuidValue.fromString(json['playerID']); final playerid = UuidValue.fromString(json['playerID']);
final lobbyid = UuidValue.fromString(json['lobbyID']);
final passphrase = json['passphrase']; final passphrase = json['passphrase'];
return GameInfo( return GameInfo(playerID: playerid, passphrase: passphrase);
playerID: playerid, lobbyID: lobbyid, passphrase: passphrase);
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
String? pid; String? pid;
String? lid;
if (playerID != null) { if (playerID != null) {
pid = playerID.toString(); pid = playerID.toString();
} }
if (lobbyID != null) {
lid = lobbyID.toString();
}
return { return {
'playerID': pid, 'playerID': pid,
'lobbyID': lid,
'passphrase': passphrase, 'passphrase': passphrase,
}; };
} }
@ -47,51 +37,29 @@ class GameInfo {
void store() async { void store() async {
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool("contains", true); await prefs.setString(passphrase!, playerID.toString());
await prefs.setString("playerID", playerID.toString());
await prefs.setString("lobbyID", lobbyID.toString());
await prefs.setString("passphrase", passphrase.toString());
} }
void delete() async { static Future<GameInfo?> get(String phrase) async {
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
var playerID = prefs.getString(phrase);
await prefs.setBool("contains", false); if (playerID == null) return null;
}
static Future<GameInfo?> get() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
var contains = prefs.getBool("contains");
var playerID = prefs.getString("playerID");
var lobbyID = prefs.getString("lobbyID");
var passphrase = prefs.getString("passphrase");
if (contains == null ||
!contains ||
playerID == null ||
lobbyID == null ||
passphrase == null) {
return null;
}
return GameInfo( return GameInfo(
playerID: UuidValue.fromString(playerID), playerID: UuidValue.fromString(playerID), passphrase: phrase);
lobbyID: UuidValue.fromString(lobbyID),
passphrase: passphrase);
} }
} }
class WebsocketMessageIdentifyPlayer { class WebsocketMessageIdentifyPlayer {
final String playerID; final String playerID;
final String lobbyID;
final String? passphrase; final String? passphrase;
const WebsocketMessageIdentifyPlayer({ const WebsocketMessageIdentifyPlayer({
required this.playerID, required this.playerID,
required this.lobbyID,
required this.passphrase, required this.passphrase,
}); });
Map<String, dynamic> toJson() => Map<String, dynamic> toJson() =>
{'lobbyID': lobbyID, 'playerID': playerID, 'passphrase': passphrase}; {'playerID': playerID, 'passphrase': passphrase};
} }

View File

@ -31,7 +31,7 @@ class ChessApp extends StatelessWidget {
useMaterial3: true, useMaterial3: true,
), ),
routerConfig: ChessAppRouter.getInstance().router, routerConfig: ChessAppRouter.getInstance().router,
title: 'mChess 1.0.6', title: 'mChess 1.0.7',
), ),
); );
} }

View File

@ -40,29 +40,35 @@ class ServerConnection {
channel!.sink.add(message); channel!.sink.add(message);
} }
void connect(String playerID, lobbyID, String? passphrase) { Future? connect(String playerID, String? passphrase) {
disconnectExistingConnection(); if (channel != null) return null;
channel = WebSocketChannel.connect(Uri.parse(config.getWebsocketURL())); channel = WebSocketChannel.connect(Uri.parse(config.getWebsocketURL()));
send( channel!.ready.then((val) {
jsonEncode( send(
WebsocketMessageIdentifyPlayer( jsonEncode(
playerID: (playerID), WebsocketMessageIdentifyPlayer(
lobbyID: (lobbyID), playerID: (playerID),
passphrase: (passphrase), passphrase: (passphrase),
),
), ),
), );
);
log(channel!.closeCode.toString()); log(channel!.closeCode.toString());
broadcast = channel!.stream.asBroadcastStream(); broadcast = channel!.stream.asBroadcastStream();
broadcast.listen(handleIncomingData); broadcast.listen(handleIncomingData);
});
return channel!.ready;
} }
void disconnectExistingConnection() { Future disconnectExistingConnection() async {
if (channel == null) return; if (channel == null) return;
channel!.sink.close();
await channel!.sink.close();
channel = null;
broadcast = const Stream.empty(); broadcast = const Stream.empty();
} }

View File

@ -15,21 +15,64 @@ class ConnectionCubit extends Cubit<ConnectionCubitState> {
return _instance; return _instance;
} }
void connect(String playerID, lobbyID, String? passphrase) { void connect(String playerID, String? passphrase) {
ServerConnection.getInstance().connect(playerID, lobbyID, passphrase); var connectedFuture =
ServerConnection.getInstance().connect(playerID, passphrase);
connectedFuture?.then((val) {
emit(ConnectionCubitState(
iAmConnected: true,
connectedToPhrase: passphrase,
opponentConnected: false));
});
}
void connectIfNotConnected(String playerID, String? passphrase) {
if (state.iAmConnected && state.connectedToPhrase == passphrase) {
return;
}
if (state.iAmConnected && state.connectedToPhrase != passphrase) {
disonnect().then((val) {
connect(playerID, passphrase);
});
}
connect(playerID, passphrase);
}
Future disonnect() async {
var disconnectFuture =
ServerConnection.getInstance().disconnectExistingConnection();
disconnectFuture.then(
(val) {
emit(ConnectionCubitState.init());
},
);
return disconnectFuture;
} }
void opponentConnected() { void opponentConnected() {
emit(ConnectionCubitState(true)); emit(ConnectionCubitState(
iAmConnected: state.iAmConnected,
connectedToPhrase: state.connectedToPhrase,
opponentConnected: true));
} }
} }
class ConnectionCubitState { class ConnectionCubitState {
final bool iAmConnected;
final String? connectedToPhrase;
final bool opponentConnected; final bool opponentConnected;
ConnectionCubitState(this.opponentConnected); ConnectionCubitState(
{required this.iAmConnected,
required this.connectedToPhrase,
required this.opponentConnected});
factory ConnectionCubitState.init() { factory ConnectionCubitState.init() {
return ConnectionCubitState(false); return ConnectionCubitState(
iAmConnected: false, connectedToPhrase: null, opponentConnected: false);
} }
} }

View File

@ -24,34 +24,21 @@ class CreateGameWidget extends StatefulWidget {
class _CreateGameWidgetState extends State<CreateGameWidget> { class _CreateGameWidgetState extends State<CreateGameWidget> {
late Future<GameInfo?> registerResponse; late Future<GameInfo?> registerResponse;
late Future disconnectFuture;
late ChessGameArguments chessGameArgs; late ChessGameArguments chessGameArgs;
@override @override
void initState() { void initState() {
registerResponse = hostPrivateGame();
registerResponse.then((args) {
if (args == null) return;
chessGameArgs = ChessGameArguments(
lobbyID: args.lobbyID!,
playerID: args.playerID!,
passphrase: args.passphrase);
});
connectToWebsocket(registerResponse);
super.initState(); super.initState();
} disconnectFuture = ConnectionCubit().disonnect();
disconnectFuture.then((val) {
void connectToWebsocket(Future<GameInfo?> resp) { registerResponse = createPrivateGame();
resp.then((value) { registerResponse.then((val) {
if (value == null) return; ConnectionCubit().connectIfNotConnected(
val!.playerID.toString(),
ConnectionCubit.getInstance().connect( val.passphrase,
value.playerID!.uuid, );
value.lobbyID!.uuid, });
value.passphrase,
);
}); });
} }
@ -72,69 +59,80 @@ class _CreateGameWidgetState extends State<CreateGameWidget> {
return Scaffold( return Scaffold(
floatingActionButton: fltnBtn, floatingActionButton: fltnBtn,
body: Center( body: Center(
child: FutureBuilder<GameInfo?>( child: FutureBuilder(
future: registerResponse, future: disconnectFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) { if (snapshot.connectionState != ConnectionState.done) {
return const SizedBox( return Container();
height: 100, } else {
width: 100, return FutureBuilder<GameInfo?>(
child: CircularProgressIndicator(), future: registerResponse,
); builder: (context, snapshot) {
} else { if (snapshot.connectionState != ConnectionState.done) {
String passphrase = snapshot.data?.passphrase ?? "no passphrase"; return const SizedBox(
return BlocListener<ConnectionCubit, ConnectionCubitState>( height: 100,
listener: (context, state) { width: 100,
// We wait for our opponent to connect child: CircularProgressIndicator(),
if (state.opponentConnected) { );
//TODO: is goNamed the correct way to navigate? } else {
context.goNamed('game', var passphrase =
pathParameters: {'phrase': passphrase.toURL()}, snapshot.data?.passphrase ?? "no passphrase";
extra: chessGameArgs); return BlocListener<ConnectionCubit,
} ConnectionCubitState>(
}, listener: (context, state) {
child: Column( // We wait for our opponent to connect
mainAxisAlignment: MainAxisAlignment.center, if (state.opponentConnected) {
children: [ //TODO: is goNamed the correct way to navigate?
Text( context.goNamed('game', pathParameters: {
'Give this phrase to your friend and sit tight:', 'phrase': passphrase.toURL(),
style: TextStyle( });
color: Theme.of(context).colorScheme.primary), }
), },
const SizedBox(height: 25), child: Column(
Row( mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: [
children: [ Text(
SelectableText( 'Give this phrase to your friend and sit tight:',
passphrase, style: TextStyle(
style: const TextStyle(fontWeight: FontWeight.bold), color: Theme.of(context).colorScheme.primary),
),
const SizedBox(height: 25),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SelectableText(
passphrase,
style: const TextStyle(
fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.copy),
onPressed: () async {
await Clipboard.setData(
ClipboardData(text: passphrase));
},
)
],
),
const SizedBox(height: 25),
const CircularProgressIndicator()
],
), ),
IconButton( );
icon: const Icon(Icons.copy), }
onPressed: () async { },
await Clipboard.setData( );
ClipboardData(text: passphrase)); }
}, }),
)
],
),
const SizedBox(height: 25),
const CircularProgressIndicator()
],
),
);
}
},
),
), ),
); );
} }
Future<GameInfo?> hostPrivateGame() async { Future<GameInfo?> createPrivateGame() async {
Response response; Response response;
try { try {
response = await http.get(Uri.parse(config.getHostURL()), response = await http.get(Uri.parse(config.getCreateGameURL()),
headers: {"Accept": "application/json"}); headers: {"Accept": "application/json"});
} catch (e) { } catch (e) {
log('Exception: ${e.toString()}'); log('Exception: ${e.toString()}');

View File

@ -3,7 +3,7 @@ import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:mchess/api/game_info.dart'; import 'package:mchess/api/game_info.dart';
import 'package:mchess/connection_cubit/connection_cubit.dart'; import 'package:mchess/connection_cubit/connection_cubit.dart';
@ -23,65 +23,62 @@ class _JoinGameHandleWidgetState extends State<JoinGameHandleWidget> {
@override @override
void initState() { void initState() {
joinGameFuture = joinPrivateGame(widget.passphrase);
joinGameFuture.then(
(value) {
if (value != null) {
switchToGame(value);
}
},
);
super.initState(); super.initState();
joinGameFuture = joinPrivateGame(widget.passphrase);
joinGameFuture.then((val) {
ConnectionCubit.getInstance().connectIfNotConnected(
val!.playerID!.uuid,
val.passphrase,
);
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const ChessGame(); var loadingIndicator = const SizedBox(
} height: 100,
width: 100,
void switchToGame(GameInfo info) { child: CircularProgressIndicator(),
var chessGameArgs = ChessGameArguments(
lobbyID: info.lobbyID!,
playerID: info.playerID!,
passphrase: info.passphrase);
ConnectionCubit.getInstance().connect(
info.playerID!.uuid,
info.lobbyID!.uuid,
info.passphrase,
); );
if (!chessGameArgs.isValid()) { return Scaffold(
context.goNamed('lobbySelector'); body: Center(
const snackBar = SnackBar( child: FutureBuilder(
backgroundColor: Colors.amberAccent, future: joinGameFuture,
content: Text("Game information is corrupted"), builder: (context, snapshot) {
); if (snapshot.connectionState != ConnectionState.done) {
ScaffoldMessenger.of(context).clearSnackBars(); return loadingIndicator;
ScaffoldMessenger.of(context).showSnackBar(snackBar); } else {
return BlocBuilder<ConnectionCubit, ConnectionCubitState>(
return; builder: (context, state) {
} if (state.iAmConnected) {
return const ChessGame();
} else {
return loadingIndicator;
}
});
}
}),
),
);
} }
Future<GameInfo?> joinPrivateGame(String phrase) async { Future<GameInfo?> joinPrivateGame(String phrase) async {
http.Response response; http.Response response;
var existingInfo = await GameInfo.get();
log('lobbyID: ${existingInfo?.lobbyID} and playerID: ${existingInfo?.playerID} and passphrase: "${existingInfo?.passphrase}"'); var existingInfo = await GameInfo.get(phrase);
log('playerID: ${existingInfo?.playerID} and passphrase: "${existingInfo?.passphrase}"');
GameInfo info; GameInfo info;
if (existingInfo?.passphrase == phrase) { if (existingInfo?.passphrase == phrase) {
// We have player info for this exact passphrase // We have player info for this exact passphrase
info = GameInfo( info = GameInfo(playerID: existingInfo?.playerID, passphrase: phrase);
playerID: existingInfo?.playerID,
lobbyID: existingInfo?.lobbyID,
passphrase: phrase);
} else { } else {
info = GameInfo(playerID: null, lobbyID: null, passphrase: phrase); info = GameInfo(playerID: null, passphrase: phrase);
} }
try { try {
response = await http.post(Uri.parse(config.getJoinURL()), response = await http.post(Uri.parse(config.getJoinGameURL()),
body: jsonEncode(info), headers: {"Accept": "application/json"}); body: jsonEncode(info), headers: {"Accept": "application/json"});
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
@ -114,7 +111,6 @@ class _JoinGameHandleWidgetState extends State<JoinGameHandleWidget> {
var info = GameInfo.fromJson(jsonDecode(response.body)); var info = GameInfo.fromJson(jsonDecode(response.body));
info.store(); info.store();
log('Player info received from server: '); log('Player info received from server: ');
log('lobbyID: ${info.lobbyID}');
log('playerID: ${info.playerID}'); log('playerID: ${info.playerID}');
log('passphrase: ${info.passphrase}'); log('passphrase: ${info.passphrase}');

View File

@ -20,7 +20,9 @@ class _LobbySelectorState extends State<LobbySelector> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
ElevatedButton( ElevatedButton(
onPressed: () => context.goNamed('createGame'), onPressed: () {
context.goNamed('createGame');
},
child: const Row( child: const Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -34,7 +36,9 @@ class _LobbySelectorState extends State<LobbySelector> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
ElevatedButton( ElevatedButton(
onPressed: () => buildEnterPassphraseDialog(context), onPressed: () {
buildEnterPassphraseDialog(context);
},
child: const Row( child: const Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@ -2,7 +2,6 @@ import 'dart:developer';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:mchess/connection/ws_connection.dart';
import 'package:mchess/pages/join_game_handle_widget.dart'; import 'package:mchess/pages/join_game_handle_widget.dart';
import 'package:mchess/pages/lobby_selector.dart'; import 'package:mchess/pages/lobby_selector.dart';
import 'package:mchess/pages/create_game_widget.dart'; import 'package:mchess/pages/create_game_widget.dart';
@ -40,8 +39,6 @@ class ChessAppRouter {
path: 'game/:phrase', path: 'game/:phrase',
name: 'game', name: 'game',
builder: (context, state) { builder: (context, state) {
ServerConnection.getInstance().disconnectExistingConnection();
var urlPhrase = state.pathParameters['phrase']; var urlPhrase = state.pathParameters['phrase'];
if (urlPhrase == null) { if (urlPhrase == null) {
log('in /game route builder: url phrase null'); log('in /game route builder: url phrase null');
@ -49,8 +46,7 @@ class ChessAppRouter {
} }
return JoinGameHandleWidget( return JoinGameHandleWidget(
passphrase: urlPhrase.toPhraseWithSpaces(), passphrase: urlPhrase.toPhraseWithSpaces());
);
}, },
) )
], ],

View File

@ -3,7 +3,7 @@ const debugURL = 'localhost:8080';
const useDbgUrl = false; const useDbgUrl = false;
String getHostURL() { String getCreateGameURL() {
var prot = 'https'; var prot = 'https';
var domain = prodURL; var domain = prodURL;
if (useDbgUrl) { if (useDbgUrl) {
@ -13,7 +13,7 @@ String getHostURL() {
return '$prot://$domain/api/hostPrivate'; return '$prot://$domain/api/hostPrivate';
} }
String getJoinURL() { String getJoinGameURL() {
var prot = 'https'; var prot = 'https';
var domain = prodURL; var domain = prodURL;
if (useDbgUrl) { if (useDbgUrl) {