From 4a354cd7a4edb203dd5e66355dbed0e62994e09b Mon Sep 17 00:00:00 2001 From: Mike Vink Date: Thu, 29 Jul 2021 03:34:50 +0200 Subject: feat(): handshaker part1 --- api/src/main/java/akkamon/api/MessagingEngine.java | 8 ++- client/src/akkamon/client/Client.ts | 10 ++- client/src/akkamon/client/InteractionEngine.ts | 43 +++++++++++- client/src/akkamon/scenes/DemoScene.ts | 12 +++- client/src/akkamon/scenes/UIElement.ts | 47 +++++++++++-- client/src/akkamon/scenes/WorldScene.ts | 22 +++--- .../java/akkamon/domain/AkkamonMessageEngine.java | 4 +- .../src/main/java/akkamon/domain/AkkamonNexus.java | 32 +++++++-- .../java/akkamon/domain/InteractionHandshaker.java | 80 ++++++++++++++++++++-- 9 files changed, 220 insertions(+), 38 deletions(-) diff --git a/api/src/main/java/akkamon/api/MessagingEngine.java b/api/src/main/java/akkamon/api/MessagingEngine.java index f1c4a78..c315a0b 100644 --- a/api/src/main/java/akkamon/api/MessagingEngine.java +++ b/api/src/main/java/akkamon/api/MessagingEngine.java @@ -6,6 +6,7 @@ import akkamon.api.models.*; import akkamon.domain.AkkamonMessageEngine; import akkamon.domain.AkkamonNexus; import akkamon.domain.AkkamonSession; +import akkamon.domain.InteractionHandshaker; import com.google.gson.Gson; import java.util.*; @@ -16,7 +17,7 @@ public class MessagingEngine implements AkkamonMessageEngine { private Map> sceneIdToAkkamonSessions = new HashMap<>(); private Map trainerIdToAkkamonSessions = new HashMap<>(); - private Set pendingInteractioRequestNameSet = new HashSet<>(); + private Map> pendingInteractioRequestToHandshaker = new HashMap<>(); private Gson gson = new Gson(); private ActorRef system; @@ -65,9 +66,10 @@ public class MessagingEngine implements AkkamonMessageEngine { } @Override - public void broadCastInteractionRequestToSessionWithTrainerIds(List trainerIds, String type, String trainerId, String requestName) { + public void broadCastInteractionRequestToSessionWithTrainerIds(List trainerIds, String type, String trainerId, String requestName, ActorRef handshaker) { System.out.println("Sending interaction request " + requestName); - this.pendingInteractioRequestNameSet.add(requestName); + this.pendingInteractioRequestToHandshaker.put(requestName, handshaker); + trainerIds.add(trainerId); for (String id : trainerIds) { AkkamonSession session = trainerIdToAkkamonSessions.get(id); if (session != null) { diff --git a/client/src/akkamon/client/Client.ts b/client/src/akkamon/client/Client.ts index 2cc5ee8..1591347 100644 --- a/client/src/akkamon/client/Client.ts +++ b/client/src/akkamon/client/Client.ts @@ -32,6 +32,7 @@ import { import type { IncomingEvent, + IncomingInteractionRequest } from './IncomingEvents'; import { @@ -91,7 +92,7 @@ export class Client implements AkkamonClient case EventType.INTERACTION_REQUEST: console.log("Received an interaction request!"); console.log(event); - // this.interactionEngine!.push(event); + this.interactionEngine!.push(event as IncomingInteractionRequest); break; default: console.log("ignored incoming event, doesn't match EventType interface."); @@ -116,6 +117,7 @@ export class Client implements AkkamonClient } setUIControls(input: Phaser.Input.InputPlugin, menu: any) { + console.log("setting ui controls!"); this.controls = new UIControls(input, menu); } @@ -201,10 +203,12 @@ export class Client implements AkkamonClient } sendInteractionRequest(interaction: Interaction) { - console.log("sent a battle request!"); + console.log("sent an interaction request!"); console.log(this.getCurrentSceneKey()); console.log(JSON.stringify(interaction)); - this.interactionEngine!.setAwaitingResponse(); + + this.interactionEngine!.setAwaitingInteractionRequestInitiation(true); + this.send(new OutgoingInteractionRequestEvent( this.getCurrentSceneKey(), interaction diff --git a/client/src/akkamon/client/InteractionEngine.ts b/client/src/akkamon/client/InteractionEngine.ts index 92b6b81..cf0c947 100644 --- a/client/src/akkamon/client/InteractionEngine.ts +++ b/client/src/akkamon/client/InteractionEngine.ts @@ -4,6 +4,11 @@ import type { WorldScene } from '../scenes/WorldScene'; import { baseQueue } from '../DataWrappers'; +import { + InteractionRequestDialogue, + WaitingDialogue +} from '../scenes/UIElement'; + import type { IncomingInteractionRequest } from './IncomingEvents'; @@ -11,18 +16,52 @@ import type { export class InteractionEngine extends AkkamonEngine { + private scene: WorldScene; + private messageQueue = baseQueue(); + private awaitingInit: boolean = false; + + private waitingForResponseOf: String | undefined; + + private requestBackLog = baseQueue(); + + private answering: boolean = false; + constructor(scene: WorldScene) { super(); + this.scene = scene; + } + + playerIsBusy() { + return this.waitingForResponseOf || this.awaitingInit || this.answering } update() { + if (!this.requestBackLog.isEmpty() + && !this.playerIsBusy() + && this.scene.menus.isEmpty()) { + let message = this.requestBackLog.pop(); + this.scene.pushMenu(new InteractionRequestDialogue(this.scene, ["YES", "NO"], {name: message!.trainerId, requestType: message!.type})); + } } - setAwaitingResponse() { + setAwaitingInteractionRequestInitiation(value: boolean) { + this.awaitingInit = value; } - push() { + push(event: IncomingInteractionRequest) { + // check trainerId + if (this.awaitingInit) { + this.waitingForResponseOf = event.requestName; + this.scene.clearMenus(); + + this.scene.pushMenu(new WaitingDialogue(this.scene, new Phaser.GameObjects.Group(this.scene), 20, 'Awaiting player response...')); + + this.awaitingInit = false; + } else { + this.requestBackLog.push(event); + } } + } diff --git a/client/src/akkamon/scenes/DemoScene.ts b/client/src/akkamon/scenes/DemoScene.ts index 9544df3..b6b94e5 100644 --- a/client/src/akkamon/scenes/DemoScene.ts +++ b/client/src/akkamon/scenes/DemoScene.ts @@ -1,5 +1,7 @@ import Phaser from 'phaser'; +import { client } from '../../app'; + import type { BasePhaserScene } from '../PhaserTypes'; @@ -9,6 +11,14 @@ import { createWorldScene } from './WorldScene'; -let DemoScene = createWorldScene(Phaser.Scene, "DemoScene", "map", "akkamon-demo-extruded"); +function updatable(scene: Scene) { + return class DemoScene extends scene { + update(time: number, delta: number) { + client.updateScene(delta); + } + } +} + +let DemoScene = updatable(createWorldScene(Phaser.Scene, "DemoScene", "map", "akkamon-demo-extruded")); export default DemoScene; diff --git a/client/src/akkamon/scenes/UIElement.ts b/client/src/akkamon/scenes/UIElement.ts index 03cce88..f1d5dd3 100644 --- a/client/src/akkamon/scenes/UIElement.ts +++ b/client/src/akkamon/scenes/UIElement.ts @@ -24,7 +24,9 @@ class MenuText extends Phaser.GameObjects.Text { scene.add.existing(this); group.add(this); this.setDepth(groupDepth); + } + } class WrappedMenuText extends MenuText { @@ -67,6 +69,7 @@ export interface AkkamonMenu { destroyAndGoBack: () => void confirm: () => void destroyGroup: () => void + setMenuVisible: (v: boolean) => void } @@ -91,6 +94,10 @@ class Menu extends Phaser.GameObjects.Image implements AkkamonMenu { xOffsetFromRight?: number pickerOffset?:number + setMenuVisible(value: boolean) { + this.group!.setVisible(value); + } + destroyGroup() { this.group!.destroy(true); } @@ -297,7 +304,7 @@ class ConfirmationDialogue extends Menu implements AkkamonMenu { options?: Array dialogueBox?: Dialogue - constructor(scene: WorldScene, options: Array, dialogueData: {[key: string]: string}) { + constructor(scene: WorldScene, options: Array, dialogueData: {[key: string]: string} | string) { super(scene, "confirmation-dialogue"); let camera = scene.cameras.main; this.setDisplaySize(200, 0.83 * 200) @@ -347,6 +354,10 @@ class Dialogue extends Phaser.GameObjects.Image implements AkkamonMenu { ); } + setMenuVisible(value: boolean) { + this.group!.setVisible(value); + } + selectButton() { } confirm() { @@ -416,7 +427,7 @@ class ChallengeDialogue extends ConfirmationDialogue implements AkkamonMenu { if (this.buttons![this.index!].text === "YES") { this.akkamonScene.requestBattle(this.challengedTrainerName); this.akkamonScene.clearMenus(); - this.akkamonScene.pushMenu(new WaitingDialogue(this.akkamonScene, new Phaser.GameObjects.Group(this.scene), 20)); + this.akkamonScene.pushMenu(new WaitingDialogue(this.akkamonScene, new Phaser.GameObjects.Group(this.scene), 20, 'Awaiting request initialisation...')); } else { this.destroyAndGoBack(); } @@ -428,14 +439,37 @@ class ChallengeDialogue extends ConfirmationDialogue implements AkkamonMenu { } } -class WaitingDialogue extends Dialogue { +export class InteractionRequestDialogue extends ConfirmationDialogue implements AkkamonMenu { + + constructor(scene: WorldScene, options: Array, dialogueData: {name: string, requestType: string}) { + super(scene, options, dialogueData); + this.dialogueBox!.push( + `Do you want to ${dialogueData.requestType} with ${dialogueData.name}?` + ); + this.dialogueBox!.displayNextDialogue(); + } + + confirm() { + if (this.buttons![this.index!].text === "YES") { + } else { + this.destroyAndGoBack(); + } + } + + destroy() { + this.scene.time.removeAllEvents(); + super.destroy(); + } +} + +export class WaitingDialogue extends Dialogue { waitingPrinter: any - constructor(scene: WorldScene, group: Phaser.GameObjects.Group, depth: number) { + constructor(scene: WorldScene, group: Phaser.GameObjects.Group, depth: number, text: string) { super(scene, group, depth); - this.typewriteText("Waiting on reponse..."); + this.typewriteText(text); this.waitingPrinter = setInterval(() => { this.displayedText.text = ''; - this.typewriteText("Waiting on reponse..."); + this.typewriteText(text); }, 3000); } @@ -449,4 +483,5 @@ class WaitingDialogue extends Dialogue { this.scene.time.removeAllEvents(); super.destroy(); } + } diff --git a/client/src/akkamon/scenes/WorldScene.ts b/client/src/akkamon/scenes/WorldScene.ts index fd866aa..3e8db40 100644 --- a/client/src/akkamon/scenes/WorldScene.ts +++ b/client/src/akkamon/scenes/WorldScene.ts @@ -17,8 +17,6 @@ export let TILE_SIZE = 32; export interface WorldScene extends Phaser.Scene { - client: typeof client; - map?: Phaser.Tilemaps.Tilemap spawnPoint?: Phaser.Types.Tilemaps.TiledObject; spawnPointTilePos?: { @@ -47,15 +45,14 @@ export interface WorldScene extends Phaser.Scene { requestBattle: (remotePlayerData: string | string[]) => void clearMenus: () => void + } export function createWorldScene(scene: PhaserScene, sceneKey: string, mapKey: string, tileSetKey: string) { - return class WorldScene extends scene implements WorldScene { + return class WorldScene extends scene { map?: Phaser.Tilemaps.Tilemap; - client = client; - menus = baseStack(); spawnPoint?: Phaser.Types.Tilemaps.TiledObject; @@ -90,7 +87,7 @@ export function createWorldScene(scene: Pha ); this.spawnPointTilePos = tilePos; - this.client.requestInitWorldScene( + client.requestInitWorldScene( this ); @@ -104,11 +101,11 @@ export function createWorldScene(scene: Pha } menuTakesUIControl(input: Phaser.Input.InputPlugin, menu: AkkamonMenu): void { - this.client.setUIControls(input, menu); + client.setUIControls(input, menu); } isUsingGridControls() { - this.client.setGridControls(); + client.setGridControls(); } pushMenu(menu: AkkamonMenu) { @@ -126,6 +123,7 @@ export function createWorldScene(scene: Pha this.popMenu(); if (!this.menus.isEmpty()) { this.menuTakesUIControl(this.input, this.menus.peek()!); + this.menus.peek()!.setMenuVisible(true); } else { this.isUsingGridControls(); } @@ -134,11 +132,11 @@ export function createWorldScene(scene: Pha } getPlayerPixelPosition(): Phaser.Math.Vector2 { - return this.client.requestPlayerPixelPosition(); + return client.requestPlayerPixelPosition(); } getRemotePlayerNames(): Array { - let remotePlayerData = this.client.requestRemotePlayerData(); + let remotePlayerData = client.requestRemotePlayerData(); if (remotePlayerData.size === 0) { return ['Nobody Online']; } else { @@ -147,9 +145,9 @@ export function createWorldScene(scene: Pha } requestBattle(remotePlayerName: string | string[]): void { - this.client.sendInteractionRequest({ + client.sendInteractionRequest({ type: "battle", - requestingTrainerId: this.client.getSessionTrainerId()!, + requestingTrainerId: client.getSessionTrainerId()!, receivingTrainerIds: Array.isArray(remotePlayerName) ? remotePlayerName : [remotePlayerName] }); } diff --git a/domain/src/main/java/akkamon/domain/AkkamonMessageEngine.java b/domain/src/main/java/akkamon/domain/AkkamonMessageEngine.java index b3ab9b2..075726d 100644 --- a/domain/src/main/java/akkamon/domain/AkkamonMessageEngine.java +++ b/domain/src/main/java/akkamon/domain/AkkamonMessageEngine.java @@ -1,5 +1,7 @@ package akkamon.domain; +import akka.actor.typed.ActorRef; + import java.util.List; import java.util.Map; @@ -7,7 +9,7 @@ public interface AkkamonMessageEngine { // broadcasts position info to WebSocket Clients void broadCastHeartBeatToScene(String sceneId, Map trainerPositions); - void broadCastInteractionRequestToSessionWithTrainerIds(List trainerIds, String type, String trainerId, String requestName); + void broadCastInteractionRequestToSessionWithTrainerIds(List trainerIds, String type, String trainerId, String requestName, ActorRef handshaker); void registerTrainerSessionToSceneAndTrainerIdMaps(String sceneId, AkkamonSession session); diff --git a/domain/src/main/java/akkamon/domain/AkkamonNexus.java b/domain/src/main/java/akkamon/domain/AkkamonNexus.java index 003630f..9099617 100644 --- a/domain/src/main/java/akkamon/domain/AkkamonNexus.java +++ b/domain/src/main/java/akkamon/domain/AkkamonNexus.java @@ -15,6 +15,22 @@ public class AkkamonNexus extends AbstractBehavior { public interface Command {} + + public static class RespondInteractionHandshaker implements Command { + public String requestName; + public boolean allRepliedInTime; + + public RespondInteractionHandshaker(String requestName, boolean allRepliedInTime) { + this.requestName = requestName; + this.allRepliedInTime = allRepliedInTime; + } + + } + + public static class InteractionReply implements Command { + public String trainerId; + } + public static class RequestInteraction implements Command { @@ -239,24 +255,32 @@ public class AkkamonNexus extends AbstractBehavior { .onMessage(RequestStopMoving.class, this::onStopMoving) .onMessage(RequestNewTilePos.class, this::onNewTilePos) .onMessage(RequestInteraction.class, this::onInteractionRequest) + .onMessage(RespondInteractionHandshaker.class, this::onInteractionHandshakerResponse) .build(); } + private Behavior onInteractionHandshakerResponse(RespondInteractionHandshaker r) { + + return this; + } + private AkkamonNexus onInteractionRequest(RequestInteraction interactionRequest) { List needConfirmation = interactionRequest.forwardTo; getContext().getLog().info("Creating interactionHandshaker of type {} from {} to {} ", interactionRequest.type, interactionRequest.trainerId, interactionRequest.forwardTo); String requestName = "interaction-handshaker-" + interactionRequest.type + "-" + interactionRequest.trainerId + "-" + interactionRequest.requestId; - getContext().spawn(InteractionHandshaker.create( + + ActorRef handshaker = getContext().spawn(InteractionHandshaker.create( interactionRequest.trainerId, + interactionRequest.type, interactionRequest.forwardTo, - interactionRequest.requestId, + requestName, interactionRequest.replyTo, - Duration.ofSeconds(20) + Duration.ofSeconds(60) ), requestName); - messageEngine.broadCastInteractionRequestToSessionWithTrainerIds(needConfirmation, interactionRequest.type, interactionRequest.trainerId, requestName); + messageEngine.broadCastInteractionRequestToSessionWithTrainerIds(needConfirmation, interactionRequest.type, interactionRequest.trainerId, requestName, handshaker); return this; } diff --git a/domain/src/main/java/akkamon/domain/InteractionHandshaker.java b/domain/src/main/java/akkamon/domain/InteractionHandshaker.java index d6c35ea..6f47282 100644 --- a/domain/src/main/java/akkamon/domain/InteractionHandshaker.java +++ b/domain/src/main/java/akkamon/domain/InteractionHandshaker.java @@ -1,6 +1,5 @@ package akkamon.domain; -import akka.actor.Timers; import akka.actor.typed.ActorRef; import akka.actor.typed.Behavior; import akka.actor.typed.javadsl.*; @@ -8,6 +7,7 @@ import akka.actor.typed.javadsl.*; import java.time.Duration; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; public class InteractionHandshaker extends AbstractBehavior { @@ -15,11 +15,27 @@ public class InteractionHandshaker extends AbstractBehavior create( String trainerId, + String type, List needingConfirmation, - long requestId, + String requestName, ActorRef replyTo, Duration timeout) { @@ -28,8 +44,9 @@ public class InteractionHandshaker extends AbstractBehavior new InteractionHandshaker( context, trainerId, + type, new HashSet(needingConfirmation), - requestId, + requestName, replyTo, timeout, timers @@ -37,19 +54,70 @@ public class InteractionHandshaker extends AbstractBehavior stillWaiting; + + private ActorRef replyTo; + + private String requestName; + private String type; + + private Set alreadyWaitingForInteraction = new HashSet<>(); + public InteractionHandshaker(ActorContext context, String trainerId, - Set needingConfirmation, - long requestId, + String type, + Set needingToShakeHands, + String requestName, ActorRef replyTo, Duration timeout, TimerScheduler timers) { + super(context); + + timers.startSingleTimer(HandshakeTimeout.INSTANCE, timeout); + + this.replyTo = replyTo; + this.requestName = requestName; + this.type = type; + + ActorRef respondTrainerPositionAdapter = context.messageAdapter(AkkamonNexus.InteractionReply.class, WrappedReply::new); + + + stillWaiting = needingToShakeHands; } @Override public Receive createReceive() { - return null; + return newReceiveBuilder() + .onMessage(WrappedReply.class, this::onReply) + .onMessage(HandshakeTimeout.class, this::onHandshakeTimeOut) + .build(); + } + + private Behavior onHandshakeTimeOut(HandshakeTimeout timeoutInstance) { + getContext().getLog().info("Received {}", timeoutInstance); + replyTo.tell( + new AkkamonNexus.RespondInteractionHandshaker( + requestName, + false + ) + ); + return Behaviors.stopped(); + } + + + private Behavior onReply(WrappedReply w) { + getContext().getLog().info("received reply from {}!", w.reply.trainerId); + return respondIfAllRepliesReceived(); + } + + private Behavior respondIfAllRepliesReceived() { + if (this.stillWaiting.isEmpty()) { + // send response + return Behaviors.stopped(); + } else { + return this; + } } } -- cgit v1.2.3