Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions frosthaven_assistant/lib/Layout/menus/server_discovery_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:frosthaven_assistant/services/network/client.dart';
import 'package:frosthaven_assistant/services/service_locator.dart';

class ServerDiscoveryDialog extends StatefulWidget {
const ServerDiscoveryDialog({super.key});

@override
State<ServerDiscoveryDialog> createState() => _ServerDiscoveryDialogState();
}

class _ServerDiscoveryDialogState extends State<ServerDiscoveryDialog> {
final _client = getIt<Client>();
List<DiscoveredServer> _servers = [];
bool _isDiscovering = false;

@override
void initState() {
super.initState();
_discoverServers();
}

void _discoverServers() {
setState(() {
_isDiscovering = true;
_servers = [];
});
_client.discoverServers().listen((server) {
setState(() {
if (!_servers.any((s) => s.address == server.address && s.port == server.port)) {
_servers.add(server);
}
});
}).onDone(() {
setState(() {
_isDiscovering = false;
});
});
}

@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Discover Servers'),
content: SizedBox(
width: 400,
height: 300,
child: _isDiscovering && _servers.isEmpty
? const Center(child: CircularProgressIndicator())
: _servers.isEmpty
? const Center(child: Text('No servers found.'))
: ListView.builder(
itemCount: _servers.length,
itemBuilder: (context, index) {
final server = _servers[index];
return ListTile(
title: Text(server.name),
subtitle: Text('${server.address}:${server.port}'),
onTap: () {
Navigator.of(context).pop(server);
},
);
},
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
onPressed: _isDiscovering ? null : _discoverServers,
child: const Text('Refresh'),
),
],
);
}
}
41 changes: 30 additions & 11 deletions frosthaven_assistant/lib/Layout/menus/settings_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:math';

import 'package:flutter/material.dart';
import 'package:frosthaven_assistant/Layout/menus/save_menu.dart';
import 'package:frosthaven_assistant/Layout/menus/server_discovery_dialog.dart';
import 'package:frosthaven_assistant/Resource/commands/clear_unlocked_classes_command.dart';
import 'package:frosthaven_assistant/Resource/commands/set_ally_deck_in_og_gloom_command.dart';
import 'package:frosthaven_assistant/Resource/commands/track_standees_command.dart';
Expand Down Expand Up @@ -443,17 +444,35 @@ class SettingsMenuState extends State<SettingsMenu> {
});
}),

Container(
margin: const EdgeInsets.only(top: 10),
width: 200,
height: 40,
child: TextField(
controller: _serverTextController,
decoration: const InputDecoration(
counterText: "",
helperText: "server ip address",
),
maxLength: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: const EdgeInsets.only(top: 10),
width: 200,
height: 40,
child: TextField(
controller: _serverTextController,
decoration: const InputDecoration(
counterText: "",
helperText: "server ip address",
),
maxLength: 20),
),
IconButton(
icon: const Icon(Icons.search),
onPressed: () async {
final result = await showDialog<DiscoveredServer>(
context: context,
builder: (context) => const ServerDiscoveryDialog(),
);
if (result != null) {
_serverTextController.text = result.address;
_portTextController.text = result.port.toString();
}
},
),
],
),

Container(
Expand Down
39 changes: 39 additions & 0 deletions frosthaven_assistant/lib/services/network/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:frosthaven_assistant/services/network/communication.dart';
import 'package:frosthaven_assistant/services/network/network.dart';
import 'package:multicast_dns/multicast_dns.dart';

import '../../Resource/settings.dart';
import '../../Resource/state/game_state.dart';
Expand Down Expand Up @@ -179,4 +180,42 @@ class Client {
}
_serverResponsive = true;
}

Stream<DiscoveredServer> discoverServers() async* {
final MDnsClient client = MDnsClient();
await client.start();

const String name = '_frosthaven._tcp.local';
await for (final PtrResourceRecord ptr in client.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer(name),
)) {
await for (final SrvResourceRecord srv in client.lookup<SrvResourceRecord>(
ResourceRecordQuery.service(ptr.domainName),
)) {
await for (final IPAddressResourceRecord ip
in client.lookup<IPAddressResourceRecord>(
ResourceRecordQuery.addressIPv4(srv.target),
)) {
yield DiscoveredServer(
name: ptr.domainName,
address: ip.address.address,
port: srv.port,
);
}
}
}
client.stop();
}
}

class DiscoveredServer {
DiscoveredServer({
required this.name,
required this.address,
required this.port,
});

final String name;
final String address;
final int port;
}
1 change: 1 addition & 0 deletions frosthaven_assistant/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies:
format: ^1.4.0
frosthaven_assistant_server:
path: ../frosthaven_assistant_server
multicast_dns: ^0.3.3



Expand Down
148 changes: 148 additions & 0 deletions frosthaven_assistant_server/lib/bonjour.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:convert';

import 'package:multicast_dns/src/constants.dart';
import 'package:multicast_dns/src/resource_record.dart';

class Bonjour {
RawDatagramSocket? _socket;

Future<void> advertise(String ip, int port) async {
// Stop any existing advertising.
stop();

const String serviceName = '_frosthaven._tcp.local';
final String serverName = 'Frosthaven Assistant Server.$serviceName';

_socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, mDnsPort);
_socket!.joinMulticast(mDnsAddressIPv4);

final PtrResourceRecord ptr = PtrResourceRecord(
serviceName,
3600,
domainName: serverName,
);

final SrvResourceRecord srv = SrvResourceRecord(
serverName,
3600,
port: port,
target: ip,
weight: 0,
priority: 0,
);

final IPAddressResourceRecord ipAddress = IPAddressResourceRecord(
ip,
3600,
address: InternetAddress(ip),
);

final List<int> packet = _buildResponse([ptr, srv, ipAddress]);
_socket!.send(packet, mDnsAddressIPv4, mDnsPort);
}

void stop() {
_socket?.close();
_socket = null;
}

List<int> _buildResponse(List<ResourceRecord> records) {
final writer = _MDnsWriter(isQuery: false);
writer.writeRecords(records);
return writer.toUint8List();
}
}

class _MDnsWriter {
_MDnsWriter({this.isQuery = true}) {
// ID is always 0
_byteData.setUint16(0, 0);
// Flags
_byteData.setUint16(2, isQuery ? 0 : 0x8400); // Authoritative answer
// Question count
_byteData.setUint16(4, 0);
// Answer count
_byteData.setUint16(6, 0);
// Authority count
_byteData.setUint16(8, 0);
// Additional count
_byteData.setUint16(10, 0);
}

final bool isQuery;
final Uint8List _data = Uint8List(4096);
late final ByteData _byteData = ByteData.view(_data.buffer);
int _offset = 12;
final Map<String, int> _fqdnOffsets = <String, int>{};

void writeFQDN(String fqdn) {
if (_fqdnOffsets.containsKey(fqdn)) {
final int fqdnOffset = _fqdnOffsets[fqdn]!;
_byteData.setUint16(_offset, 0xc000 | fqdnOffset);
_offset += 2;
return;
}

_fqdnOffsets[fqdn] = _offset;
final List<String> parts = fqdn.split('.');
for (final String part in parts) {
final List<int> partBytes = utf8.encode(part);
_data[_offset++] = partBytes.length;
_data.setRange(_offset, _offset + partBytes.length, partBytes);
_offset += partBytes.length;
}
_data[_offset++] = 0;
}

void writeRecord(ResourceRecord record) {
writeFQDN(record.fullyQualifiedName);
_byteData.setUint16(_offset, record.resourceRecordType);
_offset += 2;
_byteData.setUint16(_offset, ResourceRecordClass.internet | 0x8000); // Cache flush
_offset += 2;
_byteData.setUint32(_offset, record.ttl);
_offset += 4;

final int dataLengthOffset = _offset;
_offset += 2;

final int dataStartOffset = _offset;

if (record is IPAddressResourceRecord) {
final List<int> addressBytes = record.address.rawAddress;
_data.setRange(_offset, _offset + addressBytes.length, addressBytes);
_offset += addressBytes.length;
} else if (record is PtrResourceRecord) {
writeFQDN(record.domainName);
} else if (record is SrvResourceRecord) {
_byteData.setUint16(_offset, record.priority);
_offset += 2;
_byteData.setUint16(_offset, record.weight);
_offset += 2;
_byteData.setUint16(_offset, record.port);
_offset += 2;
writeFQDN(record.target);
} else if (record is TxtResourceRecord) {
final List<int> textBytes = utf8.encode(record.text);
_data[_offset++] = textBytes.length;
_data.setRange(_offset, _offset + textBytes.length, textBytes);
_offset += textBytes.length;
}

final int dataLength = _offset - dataStartOffset;
_byteData.setUint16(dataLengthOffset, dataLength);
}

void writeRecords(List<ResourceRecord> records) {
_byteData.setUint16(6, records.length);
for (final ResourceRecord record in records) {
writeRecord(record);
}
}

Uint8List toUint8List() {
return Uint8List.view(_data.buffer, 0, _offset);
}
}
4 changes: 4 additions & 0 deletions frosthaven_assistant_server/lib/game_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ abstract class GameServer {
void send(String data);
String currentStateMessage(String commandDescription);
Future<String> getConnectToIP();
void startAdvertising(String ip, int port);
void stopAdvertising();

void sendPing();
void addClientConnection(Socket client);
Expand Down Expand Up @@ -84,6 +86,7 @@ abstract class GameServer {
'Server Online: IP: $connectTo, Port: ${server.port.toString()}';
log(info);
setNetworkMessage(info);
startAdvertising(connectTo, server.port);
resetState();
send(currentStateMessage(""));
var subscriptions = server.listen((Socket client) {
Expand Down Expand Up @@ -119,6 +122,7 @@ abstract class GameServer {
serverEnabled = false;
leftOverMessage = "";

stopAdvertising();
resetState();
}

Expand Down
Loading