blob: c7ef2392e5a126eb78648f05f08918763de95a7b [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 '../../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:discovery/discovery.dart' as discovery;
import 'package:syncbase/syncbase_client.dart' as sc;
class SettingsManager {
final util.updateCallbackT updateCallback;
final CroupierClient _cc;
sc.SyncbaseTable tb;
static const String _discoverySettingsKey = "settings";
static const String _personalKey = "personal";
static const String _settingsWatchSyncPrefix = "users";
SettingsManager([this.updateCallback]) : _cc = new CroupierClient();
String _settingsDataKey(int userID) {
return "${_settingsWatchSyncPrefix}/${userID}/settings";
}
Future _prepareSettingsTable() async {
if (tb != null) {
return; // Then we're already prepared.
}
sc.SyncbaseNoSqlDatabase db = await _cc.createDatabase();
tb = await _cc.createTable(db, util.tableNameSettings);
// Start to watch the stream for the shared settings table.
Stream<sc.WatchChange> watchStream = db.watch(util.tableNameSettings,
_settingsWatchSyncPrefix, await db.getResumeMarker());
_startWatch(watchStream); // Don't wait for this future.
_loadSettings(tb); // Don't wait for this future.
}
// 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, this._settingsDataKey(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(_personalKey).put(UTF8.encode("${userID}"));
await tb.row(this._settingsDataKey(userID)).put(UTF8.encode(jsonString));
}
// This watch method ensures that any changes are propagated to the caller.
// In the case of the settings manager, we're checking for any changes to
// any person's Croupier Settings.
Future _startWatch(Stream<sc.WatchChange> watchStream) async {
util.log('Settings watching for changes...');
// This stream never really ends, so I guess we'll watch forever.
await for (sc.WatchChange wc in watchStream) {
assert(wc.tableName == util.tableNameSettings);
util.log('Watch Key: ${wc.rowKey}');
util.log('Watch Value ${UTF8.decode(wc.valueBytes)}');
String key = wc.rowKey;
String value;
switch (wc.changeType) {
case sc.WatchChangeTypes.put:
value = UTF8.decode(wc.valueBytes);
break;
case sc.WatchChangeTypes.delete:
value = null;
break;
default:
assert(false);
}
if (this.updateCallback != null) {
this.updateCallback(key, value);
}
}
}
// Best called after load(), to ensure that there are settings in the table.
Future createSyncgroup() async {
int id = await _getUserID();
_cc.createSyncgroup(
_cc.makeSyncgroupName(await _syncSuffix()), util.tableNameSettings,
prefix: this._settingsDataKey(id));
}
// When starting the settings manager, there may be settings already in the
// store. Make sure to load those.
Future _loadSettings(sc.SyncbaseTable tb) async {
tb
.scan(new sc.RowRange.prefix(_settingsWatchSyncPrefix))
.forEach((sc.KeyValue kv) {
if (kv.key.endsWith("/settings")) {
// Then we can process the value as if it were settings data.
this.updateCallback(kv.key, UTF8.decode(kv.value));
}
});
}
// 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);
_cc.discoveryClient.scan(_discoverySettingsKey, "CroupierSettings", ssh);
}
void stopScanSettings() {
_cc.discoveryClient.stopScan(_discoverySettingsKey);
}
// Someone who wants to join a game should advertise their presence.
Future advertiseSettings() async {
String suffix = await _syncSuffix();
_cc.discoveryClient.advertise(
_discoverySettingsKey,
DiscoveryClient.serviceMaker(
interfaceName: "CroupierSettings",
addrs: <String>[_cc.makeSyncgroupName(suffix)]));
}
void stopAdvertiseSettings() {
_cc.discoveryClient.stopAdvertise(_discoverySettingsKey);
}
Future<int> _getUserID() async {
String result = await _tryReadData(tb, _personalKey);
if (result == null) {
return null;
}
return int.parse(result);
}
Future<String> _syncSuffix() async {
int id = await _getUserID();
return "${util.sgSuffix}${id}";
}
}
// Implementation of the ScanHandler for Settings information.
// Upon finding a settings advertiser, you want to join the syncgroup that
// they're advertising.
class SettingsScanHandler extends discovery.ScanHandler {
CroupierClient _cc;
SettingsScanHandler(this._cc);
void found(discovery.Service s) {
util.log(
"SettingsScanHandler Found ${s.instanceUuid} ${s.instanceName} ${s.addrs}");
// TODO(alexfandrianto): Filter based on instanceName?
if (s.addrs.length > 0) {
_cc.joinSyncgroup(s.addrs[0]);
} 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> instanceId) {
util.log("SettingsScanHandler Lost ${instanceId}");
// TODO(alexfandrianto): Leave the syncgroup?
// Looks like leave isn't actually implemented, so we can't do this.
}
}