package wow.doge.mygame.game import scala.concurrent.duration._ import akka.actor.typed.ActorRef import akka.actor.typed.Behavior import akka.actor.typed.Props import akka.actor.typed.SpawnProtocol import akka.actor.typed.scaladsl.AskPattern._ import akka.util.Timeout import cats.effect.Resource import cats.effect.concurrent.Deferred import com.jme3.bullet.BulletAppState import com.jme3.input.InputManager import com.jme3.scene.Node import com.jme3.scene.Spatial import com.jme3.system.AppSettings import com.softwaremill.tagging._ import com.typesafe.scalalogging.{Logger => SLogger} import io.odin.Logger import monix.bio.Fiber import monix.bio.IO import monix.bio.Task import monix.bio.UIO import monix.catnap.ConcurrentChannel import monix.catnap.ConsumerF import monix.eval.Coeval import monix.execution.CancelableFuture import monix.execution.CancelablePromise import monix.execution.Scheduler import wow.doge.mygame.AppError import wow.doge.mygame.AppError.TimeoutError import wow.doge.mygame.Dispatchers import wow.doge.mygame.executors.JMERunner import wow.doge.mygame.executors.Schedulers import wow.doge.mygame.game.subsystems.ui.JFxUI import wow.doge.mygame.implicits._ import wow.doge.mygame.utils.AkkaUtils import wow.doge.mygame.utils.GenericTimerActor import wow.doge.mygame.utils.wrappers.jme._ import wow.doge.mygame.types._ object GameAppTags { sealed trait RootNode sealed trait GuiNode } class GameApp private[game] ( logger: Logger[Task], app: SimpleAppExt, gameActor: ActorRef[TestGameActor.Command], gameSpawnProtocol: ActorRef[SpawnProtocol.Command], scheduler: akka.actor.typed.Scheduler ) { def inputManager: UIO[InputManager] = UIO(app.getInputManager()) val assetManager = new AssetManager(app.getAssetManager()) val guiNode: GuiNode = AppNode2(app.getGuiNode()).taggedWith[GameAppTags.GuiNode] // def flyCam = Option(app.getFlyByCamera()) def camera = UIO(app.getCamera()) def viewPort = UIO(app.getViewPort()) val rootNode: RootNode = AppNode2(app.getRootNode()).taggedWith[GameAppTags.RootNode] val physicsSpace = new PhysicsSpace(app.bulletAppState.physicsSpace) def enqueue(cb: () => Unit) = app.enqueueR(() => cb()) def enqueueL[T](cb: () => T): Task[T] = app.enqueueL(cb) def whenTerminated: IO[AppError, Unit] = IO.deferFuture(app.whenTerminated).onErrorHandleWith(TimeoutError.from) def spawnGameActor[T]( behavior: Behavior[T], actorName: Option[String] = None, props: Props = Dispatchers.jmeDispatcher )(implicit name: sourcecode.Name) = AkkaUtils.spawnActorL(behavior, actorName, props)( 2.seconds, scheduler, gameSpawnProtocol, name ) def scheduler = JmeScheduler(app.scheduler) def jfxUI = JFxUI(app) } class GameAppResource( logger: Logger[Task], jmeThread: Scheduler, schedulers: Schedulers )(implicit timeout: Timeout, scheduler: akka.actor.typed.Scheduler, spawnProtocol: ActorRef[SpawnProtocol.Command] ) { def resource : Resource[UIO, Either[AppError, (GameApp, Fiber[Nothing, Unit])]] = Resource.make( (for { app <- UIO(new SimpleAppExt(schedulers, new BulletAppState)) _ <- UIO(JMERunner.runner = Some(app.enqueue _)) _ <- UIO { val settings = new AppSettings(true) settings.setVSync(true) /** * disables the launcher * We'll be making our own launcher anyway */ app.setShowSettings(false) app.setSettings(settings) } fib <- UIO(app.start).executeOn(jmeThread).start _ <- Task.deferFuture(app.started).onErrorHandleWith(TimeoutError.from) testGameActor <- AkkaUtils.spawnActorL( new TestGameActor.Props().create, Some("testGameActor"), props = Dispatchers.jmeDispatcher ) sp <- testGameActor .askL(TestGameActor.GetSpawnProtocol) .onErrorHandleWith(TimeoutError.from) gameApp <- UIO(new GameApp(logger, app, testGameActor, sp, scheduler)) _ <- UIO { val fut = () => testGameActor.ask(TestGameActor.Stop).flatten app.cancelToken = Some(fut) } } yield (gameApp, fib)).attempt ) { case Right(gameApp -> fib) => fib.cancel case Left(error) => IO.terminate(new Exception(error.toString)) } } object GameApp {} import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.ActorContext object TestGameActor { sealed trait Command case object Ping extends Command case class Stop(stopSignal: ActorRef[CancelableFuture[Unit]]) extends Command case class GetSpawnProtocol( replyTo: ActorRef[ActorRef[SpawnProtocol.Command]] ) extends Command import scala.concurrent.duration._ class Props() { def create = Behaviors.setup[Command] { ctx => ctx.spawn( GenericTimerActor .Props(ctx.self, Ping, 1000.millis) .behavior, "pingTimer" ) ! GenericTimerActor.Start new TestGameActor(ctx, this).receive } } } class TestGameActor( ctx: ActorContext[TestGameActor.Command], props: TestGameActor.Props ) { import TestGameActor._ val stopPromise = CancelablePromise[Unit]() def receive = Behaviors .receiveMessage[Command] { case Stop(replyTo) => ctx.log.infoP("stopping") replyTo ! stopPromise.future Behaviors.stopped case Ping => ctx.log.debugP("ping") Behaviors.same case GetSpawnProtocol(replyTo) => val sp = ctx.spawn(SpawnProtocol(), "gameSpawnProtocol") replyTo ! sp Behaviors.same } .receiveSignal { case (_, akka.actor.typed.PostStop) => stopPromise.success(()) Behaviors.same } } object Ops { final class AddToNode[T <: Node](private val node: T) extends AnyVal { /** * Pure version */ def apply(spatial: Spatial)(implicit logger: Logger[Task]) = logger.debug( s"Request to add spatial with name ${spatial.getName()} to node ${node.getName()}" ) >> Task(node.attachChild(spatial)) /** * Impure version */ def apply(spatial: Spatial)(implicit logger: SLogger) = Coeval { logger.debug( s"Request to add spatial with name ${spatial.getName()} to node ${node.getName()}" ) node.attachChild(spatial) } } } object SpawnSystem { sealed trait Result case object Ok extends Result sealed trait Complete case object Complete extends Complete sealed trait SpawnRequest final case class SpawnSpatial(nodeTask: Task[Node]) extends SpawnRequest final case class SpawnRequestWrapper( spawnRequest: SpawnRequest, result: Deferred[Task, Result] ) def apply(logger: Logger[Task]) = for { spawnChannel <- ConcurrentChannel[Task].of[Complete, SpawnRequestWrapper] spawnSystem <- Task(new SpawnSystem(logger, spawnChannel)) consumer <- spawnChannel.consume .use(consumer => spawnSystem.receive(consumer)) .startAndForget } yield (spawnSystem) } class SpawnSystem( logger: Logger[Task], spawnChannel: ConcurrentChannel[ Task, SpawnSystem.Complete, SpawnSystem.SpawnRequestWrapper ] ) { import SpawnSystem._ for { spawnSystem <- SpawnSystem(logger) res <- spawnSystem.request(SpawnSpatial(Task(new Node("Test")))) } yield () // val spawnChannel = ConcurrentChannel[Task].of[Result, SpawnRequest] private def receive( consumer: ConsumerF[Task, Complete, SpawnRequestWrapper] ): Task[Unit] = consumer.pull.flatMap { case Right(message) => for { _ <- logger .debug(s"Received spawn request $message") _ <- handleSpawn(message) } yield receive(consumer) case Left(r) => logger.info("Closing Spawn System") } private def handleSpawn(spawnRequestWrapper: SpawnRequestWrapper) = spawnRequestWrapper match { case SpawnRequestWrapper(spawnRequest, result) => spawnRequest match { case SpawnSpatial(spatialTask) => spatialTask.flatMap(spatial => logger.debug( s"Spawning spatial with name ${spatial.getName()}" ) >> result .complete(Ok) ) } } def request(spawnRequest: SpawnRequest) = for { d <- Deferred[Task, Result] _ <- spawnChannel.push(SpawnRequestWrapper(spawnRequest, d)) res <- d.get } yield (res) def stop = spawnChannel.halt(Complete) }