1
0
Fork 0

network: Added Exchange class (for communication between two peers)

This commit is contained in:
Michaël Lemaire 2017-07-24 20:12:41 +02:00
parent 4575bc9eb7
commit c0ee40633a
9 changed files with 262 additions and 26 deletions

View file

@ -78,6 +78,7 @@ Artificial Intelligence
Common UI
---------
* Better fonts, font effects...
* Add caret/focus to text input
* Fix hover being stuck when the cursor exits the window, or the item moves or is hidden
* Add a standard confirm dialog

View file

@ -8,19 +8,20 @@ module TS.SpaceTac {
// Router between game views
export class MainUI extends Phaser.Game {
// Current game session
session: GameSession;
session: GameSession
session_token: string | null
// Audio manager
audio: UI.Audio;
audio: UI.Audio
// Game options
options: UI.GameOptions;
options: UI.GameOptions
// Storage used
storage: Storage;
storage: Storage
// Headless mode
headless: boolean;
headless: boolean
constructor(headless: boolean = false) {
super(1920, 1080, headless ? Phaser.HEADLESS : Phaser.AUTO, '-space-tac');
@ -31,6 +32,7 @@ module TS.SpaceTac {
this.storage = localStorage;
this.session = new GameSession();
this.session_token = null;
if (!headless) {
this.state.onStateChange.add((state: string) => console.log(`View change: ${state}`));
@ -77,6 +79,7 @@ module TS.SpaceTac {
*/
quitGame() {
this.session = new GameSession();
this.session_token = null;
this.state.start('router');
}
@ -97,8 +100,9 @@ module TS.SpaceTac {
/**
* Set the current game session, and redirect to view router
*/
setSession(session: GameSession): void {
setSession(session: GameSession, token?: string): void {
this.session = session;
this.session_token = token || null;
this.state.start("router");
}
@ -110,6 +114,7 @@ module TS.SpaceTac {
var loaded = this.storage.getItem(name);
if (loaded) {
this.session = GameSession.loadFromString(loaded);
this.session_token = null;
console.log("Game loaded");
return true;
} else {

@ -1 +1 @@
Subproject commit 95eb6e8e69bace0fcc40392c04b0af96ffdffd3c
Subproject commit de41d5a99d9df6df6c5183c2e06185df7e2a055d

View file

@ -0,0 +1,64 @@
module TS.SpaceTac.Multi.Specs {
describe("Exchange", function () {
function newExchange(token: string, storage = new FakeRemoteStorage()): [FakeRemoteStorage, Exchange, Exchange] {
let connection = new Connection("test", storage);
let peer1 = new Exchange(connection, token, true, "peer1");
let peer2 = new Exchange(connection, token, false, "peer2");
peer1.timer = new Timer(true);
peer2.timer = new Timer(true);
/*peer1.debug = true;
peer2.debug = true;*/
return [storage, peer1, peer2];
}
beforeEach(function () {
spyOn(console, "log").and.stub();
});
async_it("says hello on start", async function () {
let [storage, peer1, peer2] = newExchange("abc");
spyOn(peer1, "getNextId").and.returnValues("1A", "1B", "1C");
spyOn(peer2, "getNextId").and.returnValues("2A", "2B", "2C");
expect(peer1.next).toEqual("hello");
expect(peer2.next).toEqual("hello");
expect(peer1.remotepeer).toBeNull();
expect(peer2.remotepeer).toBeNull();
expect(peer1.writing).toBe(true);
expect(peer2.writing).toBe(false);
expect(peer1.count).toBe(0);
expect(peer2.count).toBe(0);
await Promise.all([peer1.start(), peer2.start()]);
expect(storage.collections["exchange"]).toEqual([
{ peer: "peer1", current: "hello", next: "1A", count: 0, token: "abc", over: true, data: null },
{ peer: "peer2", current: "1A", next: "2A", count: 1, token: "abc", over: true, data: null },
]);
expect(peer1.next).toEqual("2A");
expect(peer2.next).toEqual("2A");
expect(peer1.remotepeer).toBe("peer2");
expect(peer2.remotepeer).toBe("peer1");
expect(peer1.writing).toBe(true);
expect(peer2.writing).toBe(false);
expect(peer1.count).toBe(2);
expect(peer2.count).toBe(2);
// same peers, new message chain
[storage, peer1, peer2] = newExchange("abc", storage);
spyOn(peer1, "getNextId").and.returnValues("1R", "1S", "1T");
spyOn(peer2, "getNextId").and.returnValues("2R", "2S", "2T");
await Promise.all([peer1.start(), peer2.start()]);
expect(storage.collections["exchange"]).toEqual([
{ peer: "peer1", current: "hello", next: "1R", count: 0, token: "abc", over: true, data: null },
{ peer: "peer2", current: "1A", next: "2A", count: 1, token: "abc", over: true, data: null },
{ peer: "peer2", current: "1R", next: "2R", count: 1, token: "abc", over: true, data: null },
]);
})
})
}

121
src/multi/Exchange.ts Normal file
View file

@ -0,0 +1,121 @@
module TS.SpaceTac.Multi {
/**
* Network communication of two peers around a shared session
*
* An exchange is a sequence of messages
*/
export class Exchange {
connection: Connection
token: string
writing: boolean
localpeer: string
count = 0
remotepeer: string | null = null
next = "hello"
closed = false
timer = new Timer()
debug = false
constructor(connection: Connection, token: string, initiator = false, myid = connection.device_id) {
this.connection = connection;
this.token = token;
this.localpeer = myid;
this.writing = initiator;
}
/**
* Initialize communication with the other peer
*/
async start(): Promise<void> {
if (this.debug) {
console.debug("Exchange started", this.localpeer);
}
if (this.writing) {
await this.writeMessage(null, true);
await this.readMessage();
} else {
await this.readMessage();
await this.writeMessage(null, true);
}
console.log("Echange established", this.token, this.localpeer, this.remotepeer);
}
/**
* Get a next frame id
*/
getNextId(): string {
return `${this.token}${this.count}${RandomGenerator.global.id(8)}`;
}
/**
* Push a raw message
*/
async writeMessage(message: any, over: boolean) {
if (this.writing) {
if (this.debug) {
console.debug("Exchange write", this.localpeer, this.token, this.next);
}
let futurenext = this.getNextId();
let result = await this.connection.storage.upsert("exchange", { token: this.token, current: this.next },
{ peer: this.localpeer, over: over, data: message, next: futurenext, count: this.count });
this.count += 1;
this.next = futurenext;
if (over) {
this.writing = false;
}
return result;
} else {
throw new Error("[Exchange] Tried to write out-of-turn");
}
}
/**
* Wait for a single message
*/
async readMessage(): Promise<any> {
if (this.writing) {
throw new Error("[Exchange] Tried to read out-of-turn");
} else {
let message: any;
do {
if (this.debug) {
console.debug("Exchange read", this.localpeer, this.token, this.next);
}
message = await this.connection.storage.find("exchange", { token: this.token, current: this.next });
if (!message) {
await this.timer.sleep(1000);
}
} while (!message);
if (this.remotepeer) {
if (message.peer != this.remotepeer) {
throw new Error("[Exchange] Bad peer id");
}
} else {
if (message.peer) {
this.remotepeer = message.peer;
} else {
throw new Error("[Exchange] No peer id");
}
}
if (message.count != this.count) {
throw new Error("[Exchange] Bad message count");
} else {
this.count += 1;
}
this.next = message.next;
if (message.over) {
this.writing = true;
}
return message.data;
}
}
}
}

View file

@ -42,7 +42,7 @@ module TS.SpaceTac.Multi {
return Parse.Object.extend("spacetac" + collection);
}
async search(collection: string, fields: any) {
async search(collection: string, fields: any): Promise<any[]> {
let query = new Parse.Query(this.getModel(collection));
iteritems(fields, (key, value) => {
query.equalTo(key, value);
@ -52,7 +52,7 @@ module TS.SpaceTac.Multi {
return results.map(ParseRemoteStorage.unpack);
}
async find(collection: string, fields: any) {
async find(collection: string, fields: any): Promise<any> {
let results = await this.search(collection, fields);
if (results.length == 1) {
return results[0];
@ -61,7 +61,7 @@ module TS.SpaceTac.Multi {
}
}
async upsert(collection: string, unicity: any, additional: any) {
async upsert(collection: string, unicity: any, additional: any): Promise<void> {
let query = new Parse.Query(this.getModel(collection));
iteritems(unicity, (key, value) => {
query.equalTo(key, value);
@ -99,12 +99,12 @@ module TS.SpaceTac.Multi {
return this.collections[name];
}
}
async search(collection: string, fields: any) {
async search(collection: string, fields: any): Promise<any[]> {
let objects = this.getCollection(collection);
let result = objects.filter((obj: any) => !any(items(fields), ([key, value]) => obj[key] != value));
return result;
}
async find(collection: string, fields: any) {
async find(collection: string, fields: any): Promise<any> {
let results = await this.search(collection, fields);
if (results.length == 1) {
return results[0];
@ -112,7 +112,7 @@ module TS.SpaceTac.Multi {
return null;
}
}
async upsert(collection: string, unicity: any, additional: any) {
async upsert(collection: string, unicity: any, additional: any): Promise<void> {
let existing = await this.find(collection, unicity);
let base = existing || copy(unicity);
copyfields(additional, base);

View file

@ -9,6 +9,9 @@ module TS.SpaceTac.UI {
// Interacting player
player: Player
// Exchange (for remote session only)
exchange: Multi.Exchange | null
// Layers
layer_background: Phaser.Group
layer_arena: Phaser.Group
@ -61,6 +64,7 @@ module TS.SpaceTac.UI {
this.battle = battle;
this.ship_hovered = null;
this.background = null;
this.exchange = null;
this.battle.timer = this.timer;
@ -134,6 +138,11 @@ module TS.SpaceTac.UI {
// Start processing the log
this.log_processor.start();
// If we are on a remote session, start the exchange
if (!this.session.primary && this.gameui.session_token) {
this.setupRemoteSession(this.gameui.session_token);
}
}
/**
@ -276,5 +285,15 @@ module TS.SpaceTac.UI {
this.player.revertBattle();
this.game.state.start('router');
}
/**
* Setup exchange for remote session
*/
async setupRemoteSession(token: string) {
this.exchange = new Multi.Exchange(this.getConnection(), token);
await this.exchange.start();
// TODO read actions and apply them
}
}
}

View file

@ -76,7 +76,7 @@ module TS.SpaceTac.UI {
session.primary = false;
session.spectator = true;
this.view.gameui.setSession(session);
this.view.gameui.setSession(session, token);
}
});
}

View file

@ -54,23 +54,49 @@ module TS.SpaceTac.UI {
/**
* Show the invite page
*/
pageInvite() {
async pageInvite() {
this.pageCommon();
let conn = this.view.getConnection();
conn.publish(this.view.session, "Multiplayer invitation").then(token => {
this.addText(this.width / 2, 540, "Give this invite code to your friend:", "#5398e9", 36, false, true);
this.addText(this.width / 2, 620, token, "#d6d6bd", 36, true, true);
this.addText(this.width / 2, 700, "Waiting for a connection...", "#b39256", 36, false, true);
try {
let token = await conn.publish(this.view.session, "Multiplayer invitation");
this.displayMultiplayerToken(token); // TODO On cancel
this.addButton(this.width / 2, 840, () => this.pageMenu(), "options-button");
this.addText(this.width / 2, 840, "Cancel", "#5398e9", 36, true, true);
}).catch(() => {
this.addText(this.width / 2, 620, "Could not establish connection to server", "#b35b56", 36, true, true);
let exchange = new Multi.Exchange(this.view.getConnection(), token, true);
await exchange.start();
this.addButton(this.width / 2, 840, () => this.pageMenu(), "options-button");
this.addText(this.width / 2, 840, "Cancel", "#5398e9", 36, true, true);
});
// TODO Setup the exchange on current view
this.close();
} catch (err) {
this.displayConnectionError();
}
}
/**
* Display a multiplayer token page
*/
private displayMultiplayerToken(token: string) {
this.pageCommon();
this.addText(this.width / 2, 540, "Give this invite code to your friend:", "#5398e9", 36, false, true);
this.addText(this.width / 2, 620, token, "#d6d6bd", 36, true, true);
this.addText(this.width / 2, 700, "Waiting for a connection...", "#b39256", 36, false, true);
this.addButton(this.width / 2, 840, () => this.pageMenu(), "options-button");
this.addText(this.width / 2, 840, "Cancel", "#5398e9", 36, true, true);
}
/**
* Display a connection error
*/
private displayConnectionError() {
this.pageCommon();
this.addText(this.width / 2, 620, "Could not establish connection to server", "#b35b56", 36, true, true);
this.addButton(this.width / 2, 840, () => this.pageMenu(), "options-button");
this.addText(this.width / 2, 840, "Cancel", "#5398e9", 36, true, true);
}
}
}