blob: 46b1f9503c8fe516d9715eb56d2dcd96120e1c5c [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/// Since this file includes Sky/Mojo, it will need to be mocked out for unit
/// tests.
/// Unfortunately, imports can't be replaced, so the best thing to do is to swap
/// out the whole file.
///
/// The goal of the SettingsManager is to handle viewing and editing of the
/// Croupier Settings.
/// loadSettings: Get the settings of the current player or specified userID.
/// saveSettings: For the current player and their userID, save settings.
/// In the background, these values will be synced.
/// When setting up a syncgroup, the userIDs are very important.
import '../../settings/client.dart' as settings_client;
import '../../logic/game/game.dart' as logic_game;
import '../../logic/croupier_settings.dart' show CroupierSettings;
import 'croupier_client.dart' show CroupierClient;
import 'discovery_client.dart' show DiscoveryClient;
import 'util.dart' as util;
import 'dart:async';
import 'dart:convert' show UTF8, JSON;
import 'package:v23discovery/discovery.dart' as discovery;
import 'package:syncbase/syncbase_client.dart' as sc;
class SettingsManager {
final util.KeyValueCallback updateSettingsCallback;
final util.KeyValueCallback updateGamesCallback;
final util.KeyValueCallback updatePlayerFoundCallback;
final util.KeyValueCallback updateGameStatusCallback;
final util.AsyncKeyValueCallback updateGameLogCallback;
final CroupierClient _cc;
sc.SyncbaseTable tb;
static const String _discoveryGameAdKey = "discovery-game-ad";
// The game subscription. Cancel when done listening.
StreamSubscription<sc.WatchChange> _gameSubscription;
SettingsManager(
settings_client.AppSettings appSettings,
this.updateSettingsCallback,
this.updateGamesCallback,
this.updatePlayerFoundCallback,
this.updateGameStatusCallback,
this.updateGameLogCallback)
: _cc = new CroupierClient(appSettings);
Future _prepareSettingsTable() async {
if (tb != null) {
return; // Then we're already prepared.
}
sc.SyncbaseDatabase db = await _cc.createDatabase();
tb = await _cc.createTable(db, util.tableNameSettings);
// Start to watch the stream for the shared settings table.
await _cc.watchEverything(db, util.tableNameSettings,
util.settingsWatchSyncPrefix, _onSettingsChange);
}
// In the case of the settings manager, we're checking for any changes to
// any person's Croupier Settings.
Future _onSettingsChange(String key, String value, bool duringScan) async {
this.updateSettingsCallback(util.userIDFromSettingsDataKey(key), value);
}
// Guaranteed to be called when the program starts.
// If no Croupier Settings exist, then random ones are created.
Future<String> load() async {
util.log('SettingsManager.load');
await _prepareSettingsTable();
int userID = await _getUserID();
if (userID == null) {
CroupierSettings settings = new CroupierSettings.random();
String jsonStr = settings.toJSONString();
await this.save(settings.userID, jsonStr);
return jsonStr;
} else {
return await _tryReadData(tb, util.settingsDataKeyFromUserID(userID));
}
}
Future<String> _tryReadData(sc.SyncbaseTable st, String rowkey) async {
var row = st.row(rowkey);
if (!(await row.exists())) {
print("$rowkey did not exist");
return null;
}
return UTF8.decode(await row.get());
}
// Note: only the current user is allowed to save settings.
// This means we can also save their user id.
// All other settings will be synced instead.
Future save(int userID, String jsonString) async {
util.log('SettingsManager.save');
await _prepareSettingsTable();
await tb.row(util.settingsPersonalKey).put(UTF8.encode("$userID"));
await tb
.row(util.settingsDataKeyFromUserID(userID))
.put(UTF8.encode(jsonString));
}
// Best called after load(), to ensure that there are settings in the table.
Future createSettingsSyncgroup() async {
int id = await _getUserID();
_cc.createSyncgroup(
await _mySettingsSyncgroupName(), util.tableNameSettings,
prefix: util.settingsDataKeyFromUserID(id));
}
Future<String> _mySettingsSyncgroupName() async {
return _cc.makeSyncgroupName(await _syncSettingsSuffix());
}
// Forward any player changes and game status signals to Croupier's logic.
Future _onGameChange(String key, String value, bool duringScan) async {
if (key.indexOf("/players") != -1) {
if (this.updatePlayerFoundCallback != null) {
String type = util.playerUpdateTypeFromPlayerKey(key);
switch (type) {
case "player_number":
// Update the player number for this player.
this.updatePlayerFoundCallback(key, value);
break;
case "settings_sg":
// Join this player's settings syncgroup.
_cc.joinSyncgroup(value);
// Also, signal that this player has been found.
this.updatePlayerFoundCallback(key, null);
break;
default:
print("Unexpected key: $key with value $value");
assert(false);
}
}
} else if (key.indexOf("/status") != -1) {
if (this.updateGameStatusCallback != null) {
this.updateGameStatusCallback(key, value);
}
} else if (key.indexOf("/log") != -1) {
if (this.updateGameLogCallback != null) {
await this.updateGameLogCallback(key, value, duringScan);
}
}
}
Future<logic_game.GameStartData> createGameSyncgroup(
String type, int gameID) async {
print("Creating game syncgroup for $type and $gameID");
sc.SyncbaseDatabase db = await _cc.createDatabase();
sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
// Watch all the data in the game.
assert(_gameSubscription == null);
_gameSubscription = await _cc.watchEverything(
db, util.tableNameGames, util.syncgamePrefix(gameID), _onGameChange,
sorter: (sc.WatchChange a, sc.WatchChange b) {
return a.rowKey.compareTo(b.rowKey);
});
print("Now writing to some rows of $gameID");
// Start up the table and write yourself as player 0.
await gameTable.row(util.gameTypeKey(gameID)).put(UTF8.encode("$type"));
int id = await _getUserID();
await gameTable.row(util.gameOwnerKey(gameID)).put(UTF8.encode("$id"));
await gameTable
.row(util.playerSettingsKeyFromData(gameID, id))
.put(UTF8.encode(await _mySettingsSyncgroupName()));
logic_game.GameStartData gsd =
new logic_game.GameStartData(type, 0, gameID, id);
String sgName = _cc.makeSyncgroupName(util.syncgameSuffix("${gsd.gameID}"));
await gameTable.row(util.gameSyncgroupKey(gameID)).put(UTF8.encode(sgName));
await _cc.createSyncgroup(sgName, util.tableNameGames,
prefix: util.syncgamePrefix(gameID));
return gsd;
}
void quitGame() {
if (_gameSubscription != null) {
_gameSubscription.cancel();
_gameSubscription = null;
}
}
Future joinGameSyncgroup(String sgName, int gameID) async {
print("Now joining game syncgroup at $sgName and $gameID");
sc.SyncbaseDatabase db = await _cc.createDatabase();
sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
// Watch for the players in the game.
_gameSubscription = await _cc.watchEverything(
db, util.tableNameGames, util.syncgamePrefix(gameID), _onGameChange,
sorter: (sc.WatchChange a, sc.WatchChange b) {
return a.rowKey.compareTo(b.rowKey);
});
await _cc.joinSyncgroup(sgName);
int id = await _getUserID();
await gameTable
.row(util.playerSettingsKeyFromData(gameID, id))
.put(UTF8.encode(await _mySettingsSyncgroupName()));
}
Future setPlayerNumber(int gameID, int userID, int playerNumber) async {
sc.SyncbaseDatabase db = await _cc.createDatabase();
sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
await gameTable
.row(util.playerNumberKeyFromData(gameID, userID))
.put(UTF8.encode("$playerNumber"));
}
Future setGameStatus(int gameID, String status) async {
sc.SyncbaseDatabase db = await _cc.createDatabase();
sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
await gameTable.row(util.gameStatusKey(gameID)).put(UTF8.encode(status));
}
Future<String> getGameStatus(int gameID) async {
sc.SyncbaseDatabase db = await _cc.createDatabase();
sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
return _tryReadData(gameTable, util.gameStatusKey(gameID));
}
Future<String> getGameSyncgroup(int gameID) async {
sc.SyncbaseDatabase db = await _cc.createDatabase();
sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
return _tryReadData(gameTable, util.gameSyncgroupKey(gameID));
}
Future<logic_game.GameStartData> getGameStartData(int gameID) async {
sc.SyncbaseDatabase db = await _cc.createDatabase();
sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
String owner = await _tryReadData(gameTable, util.gameOwnerKey(gameID));
String type = await _tryReadData(gameTable, util.gameTypeKey(gameID));
int id = await _getUserID();
String playerNumber =
await _tryReadData(gameTable, util.playerNumberKeyFromData(gameID, id));
int pn = playerNumber != null ? int.parse(playerNumber) : null;
return new logic_game.GameStartData(type, pn, gameID, int.parse(owner));
}
// TODO(alexfandrianto): It is possible that the more efficient way of
// scanning is to do it for only short bursts. In that case, we should call
// stopScanSettings a few seconds after starting it.
// Someone who is creating a game should scan for players who wish to join.
Future scanSettings() async {
SettingsScanHandler ssh =
new SettingsScanHandler(_cc, this.updateGamesCallback);
return _cc.discoveryClient.scan(
_discoveryGameAdKey,
'v.InterfaceName="${util.discoveryInterfaceName}"',
ssh.found,
ssh.lost);
}
Future stopScanSettings() {
return _cc.discoveryClient.stopScan(_discoveryGameAdKey);
}
// Someone who wants to join a game should advertise their presence.
Future advertiseSettings(logic_game.GameStartData gsd) async {
String settingsSuffix = await _syncSettingsSuffix();
String gameSuffix = util.syncgameSuffix("${gsd.gameID}");
return _cc.discoveryClient.advertise(
_discoveryGameAdKey,
DiscoveryClient.advertisementMaker(
interfaceName: util.discoveryInterfaceName,
attrs: <String, String>{
util.syncgameSettingsAttr: _cc.makeSyncgroupName(settingsSuffix),
util.syncgameGameStartDataAttr: gsd.toJSONString()
},
addrs: <String>[
_cc.makeSyncgroupName(gameSuffix)
]));
}
Future stopAdvertiseSettings() {
return _cc.discoveryClient.stopAdvertise(_discoveryGameAdKey);
}
Future<int> _getUserID() async {
String result = await _tryReadData(tb, util.settingsPersonalKey);
if (result == null) {
return null;
}
return int.parse(result);
}
Future<String> _syncSettingsSuffix([int userID]) async {
int id = userID;
if (id == null) {
id = await _getUserID();
}
return "${util.sgSuffix}-$id";
}
}
// Manages found and lost settings advertisements.
// Upon finding a settings advertisement, you want to join the syncgroup that
// they're advertising.
class SettingsScanHandler {
CroupierClient _cc;
Map<String, String> settingsAddrs;
Map<String, String> gameAddrs;
util.KeyValueCallback updateGamesCallback;
SettingsScanHandler(this._cc, this.updateGamesCallback) {
settingsAddrs = new Map<String, String>();
gameAddrs = new Map<String, String>();
}
void found(discovery.Update s) {
util.log(
"SettingsScanHandler Found ${s.id} ${s.interfaceName} ${s.addresses}");
if (s.addresses.length == 1 && s.attributes != null) {
// Note: Assumes 1 address and attributes for the game.
String id = s.id.toString();
settingsAddrs[id] = s.attributes[util.syncgameSettingsAttr];
gameAddrs[id] = s.addresses[0];
String gameSettingsJSON = s.attributes[util.syncgameGameStartDataAttr];
updateGamesCallback(gameAddrs[id], gameSettingsJSON);
_cc.joinSyncgroup(settingsAddrs[id]);
} else {
// An unexpected service was found. Who is advertising it?
// https://github.com/vanadium/issues/issues/846
util.log("Unexpected service found: ${s.toString()}");
}
}
void lost(List<int> idList) {
util.log("SettingsScanHandler Lost $idList");
// TODO(alexfandrianto): Leave the syncgroup?
// Looks like leave isn't actually implemented, so we can't do this.
String id = idList.toString();
String addr = gameAddrs[id];
if (addr != null) {
updateGamesCallback(addr, null);
}
settingsAddrs.remove(id);
gameAddrs.remove(id);
}
}