network: Added Exchange class (for communication between two peers)
This commit is contained in:
parent
4575bc9eb7
commit
c0ee40633a
1
TODO.md
1
TODO.md
|
@ -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
|
||||
|
|
|
@ -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
|
64
src/multi/Exchange.spec.ts
Normal file
64
src/multi/Exchange.spec.ts
Normal 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
121
src/multi/Exchange.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue