diff --git a/.scalafmt.conf b/.scalafmt.conf index ba14fb7..786c0a7 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1 +1,4 @@ version = "2.6.4" +rewrite { + rules = [SortImports, RedundantBraces] +} diff --git a/build.sbt b/build.sbt index cd13e29..5715a98 100644 --- a/build.sbt +++ b/build.sbt @@ -61,15 +61,15 @@ lazy val root = (project in file(".")).settings( "org.jmonkeyengine" % "jme3-blender" % jmeVersion, "com.github.stephengold" % "Minie" % "3.0.0", "com.simsilica" % "zay-es" % "1.2.1", - "org.typelevel" %% "cats-core" % "2.1.1", + "org.typelevel" %% "cats-core" % "2.3.0", "com.lihaoyi" % "ammonite" % "2.2.0" cross CrossVersion.full, "org.jetbrains.kotlin" % "kotlin-main-kts" % "1.4.10", "org.jetbrains.kotlin" % "kotlin-scripting-jsr223" % "1.4.10", "org.codehaus.groovy" % "groovy-all" % "3.0.6" pomOnly (), "org.scalafx" %% "scalafx" % "14-R19", "com.typesafe.akka" %% "akka-actor-typed" % "2.6.10", - "org.typelevel" %% "cats-core" % "2.1.1", - "org.typelevel" %% "cats-effect" % "2.1.4", + "org.typelevel" %% "cats-core" % "2.3.0", + "org.typelevel" %% "cats-effect" % "2.3.0", "io.monix" %% "monix" % "3.2.2", "io.monix" %% "monix-bio" % "1.1.0", "io.circe" %% "circe-core" % "0.13.0", @@ -96,7 +96,8 @@ lazy val root = (project in file(".")).settings( "com.badlogicgames.gdx" % "gdx-ai" % "1.8.2", "org.recast4j" % "recast" % "1.2.5", "org.recast4j" % "detour" % "1.2.5", - "com.lihaoyi" %% "pprint" % "0.6.0" + "com.lihaoyi" %% "pprint" % "0.6.0", + "org.scalatest" %% "scalatest" % "3.2.2" % "test" ), // Determine OS version of JavaFX binaries @@ -208,4 +209,7 @@ initialCommands in (console) := """ammonite.Main.main(Array.empty)""" // To learn more about multi-project builds, head over to the official sbt // documentation at http://www.scala-sbt.org/documentation.html addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1") +addCompilerPlugin( + "org.typelevel" %% "kind-projector" % "0.11.1" cross CrossVersion.full +) ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.4.3" diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 0580551..55660c3 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -1,7 +1,7 @@ -# jme-dispatcher { -# type = "Dispatcher" -# name = "JME-Thread" -# executor = "wow.doge.mygame.executors.JMEThreadExecutorServiceConfigurator" -# throughput = 1 -# } +jme-dispatcher { + type = "Dispatcher" + name = "JME-Thread" + executor = "wow.doge.mygame.executors.JMEThreadExecutorServiceConfigurator" + throughput = 1 +} # akka.jvm-exit-on-fatal-error = on \ No newline at end of file diff --git a/src/main/scala/org/slf4j/impl/StaticLoggerBuilder.scala b/src/main/scala/org/slf4j/impl/StaticLoggerBuilder.scala index d09fb49..06c8137 100644 --- a/src/main/scala/org/slf4j/impl/StaticLoggerBuilder.scala +++ b/src/main/scala/org/slf4j/impl/StaticLoggerBuilder.scala @@ -32,13 +32,13 @@ class StaticLoggerBinder extends OdinLoggerBinder[IO] { def apply[A](fa: _root_.monix.bio.Task[A]): IO[A] = fa.to[IO] } - private lazy val (defaultConsoleLogger, release1) = + val (defaultConsoleLogger, release1) = consoleLogger[IO](minLevel = Level.Debug) .withAsync(timeWindow = 1.milliseconds, maxBufferSize = Some(2000)) .allocated .unsafeRunSync() - private lazy val (mainFileLogger, release2) = + val (mainFileLogger, release2) = fileLogger[IO]( "application-log-2.log", Formatter.json, @@ -51,7 +51,7 @@ class StaticLoggerBinder extends OdinLoggerBinder[IO] { lm.copy(message = lm.message.map(s => fansi.Str(s).plainText)) ) - private lazy val (eventBusFileLogger, release3) = + val (eventBusFileLogger, release3) = fileLogger[IO]( "eventbus.log", Formatter.json, diff --git a/src/main/scala/wow/doge/mygame/AppError.scala b/src/main/scala/wow/doge/mygame/AppError.scala new file mode 100644 index 0000000..9ba2fd9 --- /dev/null +++ b/src/main/scala/wow/doge/mygame/AppError.scala @@ -0,0 +1,6 @@ +package wow.doge.mygame + +sealed trait AppError +object AppError { + case class TimeoutError(reason: String) extends AppError +} diff --git a/src/main/scala/wow/doge/mygame/Dispatchers.scala b/src/main/scala/wow/doge/mygame/Dispatchers.scala new file mode 100644 index 0000000..0f57db9 --- /dev/null +++ b/src/main/scala/wow/doge/mygame/Dispatchers.scala @@ -0,0 +1,7 @@ +package wow.doge.mygame + +import akka.actor.typed.DispatcherSelector + +object Dispatchers { + val jmeDispatcher = DispatcherSelector.fromConfig("jme-dispatcher") +} diff --git a/src/main/scala/wow/doge/mygame/Main.scala b/src/main/scala/wow/doge/mygame/Main.scala index 7e8c5df..bba8895 100644 --- a/src/main/scala/wow/doge/mygame/Main.scala +++ b/src/main/scala/wow/doge/mygame/Main.scala @@ -5,8 +5,7 @@ import scala.concurrent.duration._ import _root_.monix.bio.BIOApp import _root_.monix.bio.Task import _root_.monix.bio.UIO -import akka.actor.typed.ActorSystem -import akka.actor.typed.SpawnProtocol +import _root_.monix.execution.Scheduler import akka.util.Timeout import cats.effect.ExitCode import cats.effect.Resource @@ -22,31 +21,24 @@ object Main extends BIOApp with MainModule { JLogger.getLogger("").setLevel(Level.SEVERE) implicit val timeout = Timeout(1.second) + override def scheduler: Scheduler = schedulers.async + def appResource(consoleStream: GenericConsoleStream[TextArea]) = for { logger <- consoleLogger().withAsync( timeWindow = 1.milliseconds, - maxBufferSize = Some(2000) + maxBufferSize = Some(100) ) |+| fileLogger( "application-log-1.log", Formatter.json ).withAsync(timeWindow = 1.milliseconds, maxBufferSize = Some(2000)) jmeScheduler <- jMESchedulerResource - implicit0(actorSystem: ActorSystem[SpawnProtocol.Command]) <- - actorSystemResource(logger) - // gameApp <- { - // // new BulletAppState() - // // bas.setThreadingType(Thr) - // // gameAppResource(new StatsAppState()) - // wire[GameAppResource].get - // } + actorSystem <- actorSystemResource(logger, schedulers.async) _ <- Resource.liftF( new MainApp( logger, - // gameApp, - // actorSystem, jmeScheduler, schedulers, consoleStream @@ -56,11 +48,11 @@ object Main extends BIOApp with MainModule { } yield () def run(args: List[String]): UIO[ExitCode] = { - - lazy val consoleStream = GenericConsoleStream.textAreaStream() + val consoleStream = GenericConsoleStream.textAreaStream() Console.withOut(consoleStream)( appResource(consoleStream) - .use(_ => Task.unit >> Task(consoleStream.close())) + .use(_ => Task.unit) + .flatMap(_ => Task(consoleStream.close())) .onErrorHandleWith(ex => UIO(ex.printStackTrace())) .as(ExitCode.Success) ) diff --git a/src/main/scala/wow/doge/mygame/MainApp.scala b/src/main/scala/wow/doge/mygame/MainApp.scala index 8a197b6..03f6995 100644 --- a/src/main/scala/wow/doge/mygame/MainApp.scala +++ b/src/main/scala/wow/doge/mygame/MainApp.scala @@ -1,5 +1,6 @@ package wow.doge.mygame +import akka.actor.typed.ActorRef import akka.actor.typed.ActorSystem import akka.actor.typed.Scheduler import akka.actor.typed.SpawnProtocol @@ -8,10 +9,12 @@ import cats.effect.Resource import cats.effect.concurrent.Deferred import cats.syntax.eq._ import com.jme3.app.state.AppStateManager -import com.jme3.asset.AssetManager import com.jme3.asset.plugins.ZipLocator import com.jme3.bullet.control.BetterCharacterControl import com.jme3.input.InputManager +import com.jme3.material.Material +import com.jme3.material.MaterialDef +import com.jme3.math.FastMath import com.jme3.renderer.Camera import com.jme3.renderer.RenderManager import com.jme3.renderer.ViewPort @@ -23,18 +26,21 @@ import io.odin.Logger import monix.bio.Fiber import monix.bio.IO import monix.bio.Task -import monix.execution.exceptions.DummyException +import monix.bio.UIO import scalafx.scene.control.TextArea import wow.doge.mygame.executors.Schedulers import wow.doge.mygame.game.GameApp import wow.doge.mygame.game.GameAppActor +import wow.doge.mygame.game.GameAppResource import wow.doge.mygame.game.GameAppTags import wow.doge.mygame.game.entities.EntityIds import wow.doge.mygame.game.entities.NpcActorSupervisor import wow.doge.mygame.game.entities.NpcMovementActor +import wow.doge.mygame.game.entities.PlayerActorSupervisor import wow.doge.mygame.game.entities.PlayerController import wow.doge.mygame.game.entities.PlayerControllerTags import wow.doge.mygame.game.subsystems.input.GameInputHandler +import wow.doge.mygame.game.subsystems.level.DefaultGameLevel import wow.doge.mygame.implicits._ import wow.doge.mygame.launcher.Launcher import wow.doge.mygame.launcher.Launcher.LauncherResult @@ -48,8 +54,14 @@ import wow.doge.mygame.subsystems.scriptsystem.ScriptSystemResource import wow.doge.mygame.utils.AkkaUtils import wow.doge.mygame.utils.GenericConsoleStream import wow.doge.mygame.utils.wrappers.jme.AppNode -import wow.doge.mygame.game.subsystems.level.DefaultGameLevel -import com.jme3.math.FastMath +import wow.doge.mygame.utils.wrappers.jme.AssetManager +import wow.doge.mygame.utils.wrappers.jme.PhysicsSpace +import wow.doge.mygame.subsystems.events.EventBus.ObservableSubscription +import wow.doge.mygame.subsystems.events.PlayerMovementEvent +import monix.reactive.Observable +import monix.eval.Coeval +import wow.doge.mygame.utils.IOUtils +import com.jme3.math.ColorRGBA class MainApp( logger: Logger[Task], @@ -57,37 +69,52 @@ class MainApp( schedulers: Schedulers, consoleStream: GenericConsoleStream[TextArea] )(implicit - spawnProtocol: ActorSystem[SpawnProtocol.Command], - @annotation.unused timeout: Timeout, - @annotation.unused scheduler: Scheduler + spawnProtocol: ActorRef[SpawnProtocol.Command], + timeout: Timeout, + scheduler: Scheduler ) { val scriptSystemInit = new ScriptSystemResource(os.pwd, ScriptInitMode.Eager).init - val eventsModule = new EventsModule(spawnProtocol) + val eventsModule = new EventsModule(scheduler, spawnProtocol) + + class TestClass( + playerEventBus: GameEventBus[PlayerEvent], + tickEventBus: GameEventBus[TickEvent] + ) def gameInit: Resource[Task, Fiber[Throwable, Unit]] = - GameApp.resource(logger, jmeThread, schedulers).evalMap { + wire[GameAppResource].resource.evalMap { case gameApp -> gameAppFib => for { playerEventBus <- eventsModule.playerEventBusTask mainEventBus <- eventsModule.mainEventBusTask tickEventBus <- eventsModule.tickEventBusTask - gameAppActor <- AkkaUtils.spawnActorL( - GameAppActor.Props(tickEventBus).behavior, - "gameAppActor" + obs <- playerEventBus.askL[Observable[PlayerMovementEvent]]( + ObservableSubscription(_) + ) + _ <- IOUtils.toIO( + obs + .doOnNextF(pme => Coeval(pprint.log(s"Received event $pme")).void) + .completedL + .startAndForget ) - _ <- gameAppActor !! GameAppActor.Start inputManager <- gameApp.inputManager - assetManager <- gameApp.assetManager + assetManager <- UIO.pure(gameApp.assetManager) stateManager <- gameApp.stateManager camera <- gameApp.camera - rootNode <- Task.pure(gameApp.rootNode) - enqueueR <- Task(gameApp.enqueue _) + rootNode <- UIO.pure(gameApp.rootNode) + enqueueR <- UIO(gameApp.enqueue _) viewPort <- gameApp.viewPort + physicsSpace <- UIO.pure(gameApp.physicsSpace) _ <- logger.info("before") // jfxUI <- gameApp.jfxUI + gameAppActor <- gameApp.spawnGameActor( + GameAppActor.Props(tickEventBus).behavior, + "gameAppActor" + ) + _ <- gameAppActor !! GameAppActor.Start consoleTextArea <- Task(new TextArea { text = "hello \n" editable = false @@ -98,9 +125,6 @@ class MainApp( // _ <- Task(consoleStream := consoleTextArea) // _ <- Task(jfxUI += consoleTextArea) _ <- logger.info("after") - // bulletAppState <- Task(new BulletAppState()) - // _ <- Task(stateManager.attach(bulletAppState)) - // bulletAppState <- Task.pure(gameApp.bulletAppstate) _ <- logger.info("Initializing console stream") _ <- wire[MainAppDelegate] @@ -109,6 +133,13 @@ class MainApp( } yield gameAppFib } + // val x: Task[Unit] = for { + // tickEventBus <- eventsModule.tickEventBusTask + // playerEventBus <- eventsModule.playerEventBusTask + // _ <- UIO(wire[TestClass]) + // _ <- gameInit(tickEventBus).use(_.join) + // } yield () + val program = for { scriptSystem <- scriptSystemInit launchSignal <- Deferred[Task, Launcher.LauncherResult] @@ -125,6 +156,7 @@ class MainApp( */ else gameInit.use(_.join) + } yield () } @@ -139,47 +171,44 @@ class MainAppDelegate( inputManager: InputManager, assetManager: AssetManager, stateManager: AppStateManager, + physicsSpace: PhysicsSpace[Task], camera: Camera, viewPort: ViewPort, enqueueR: Function1[() => Unit, Unit], rootNode: AppNode[Task] @@ GameAppTags.RootNode - // bulletAppState: BulletAppState )(implicit - spawnProtocol: ActorSystem[SpawnProtocol.Command], - @annotation.unused timeout: Timeout, - @annotation.unused scheduler: Scheduler + spawnProtocol: ActorRef[SpawnProtocol.Command], + timeout: Timeout, + scheduler: Scheduler ) { - // val physicsSpace = bulletAppState.physicsSpace - val physicsSpace = gameApp.physicsSpace + def init( // appScheduler: monix.execution.Scheduler // consoleStream: GenericConsoleStream[TextArea] ) = for { _ <- loggerL.info("Initializing Systems") - _ <- loggerL.debug(physicsSpace.toString()) - _ <- Task( - assetManager.registerLocator( - os.rel / "assets" / "town.zip", - classOf[ZipLocator] - ) + _ <- assetManager.registerLocator( + os.rel / "assets" / "town.zip", + classOf[ZipLocator] ) _ <- loggerL.info("test") // _ <- Task(consoleStream.println("text")) _ <- DefaultGameLevel(assetManager, viewPort) - .addToGame(rootNode, physicsSpace) + .flatMap(_.addToGame(rootNode, physicsSpace).hideErrors) + .onErrorHandleWith(e => loggerL.error(e.toString)) _ <- createPlayerController() - .absorbWith(e => DummyException(e.toString())) + .onErrorHandleWith(e => loggerL.error(e.toString)) // .onErrorRestart(3) _ <- wire[GameInputHandler.Props].begin // .onErrorRestart(3) - // johnActor <- createTestNpc(appScheduler, "John").executeOn(appScheduler) + johnActor <- createTestNpc("John") + .onErrorHandleWith(e => IO.raiseError(new Throwable(e.toString))) // _ <- johnActor !! NpcActorSupervisor.Move(ImVector3f(0, 0, 20)) // _ <- - // (johnActor !! NpcActorSupervisor.Move( - // ImVector3f(-30, 0, 10) - // )).executeAsync + // johnActor + // .tellL(NpcActorSupervisor.Move(ImVector3f(-30, 0, 10))) // .delayExecution(2.seconds) // _ <- // IOUtils @@ -195,53 +224,40 @@ class MainAppDelegate( def createPlayerController( // appScheduler: monix.execution.Scheduler - ): IO[PlayerController.Error, Unit] = { + ): IO[PlayerController.Error, ActorRef[PlayerActorSupervisor.Command]] = { val playerPos = ImVector3f.ZERO val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o" val playerPhysicsControl = PlayerController.Defaults.defaultPlayerPhysicsControl .taggedWith[PlayerControllerTags.PlayerTag] - // lazy val camNode = - // PlayerController.Defaults - // .defaultCamerNode(camera, playerPos) - // .taggedWith[PlayerControllerTags.PlayerCameraNode] - val playerModel = assetManager - .loadModel(modelPath) - .asInstanceOf[Node] - .withRotate(0, FastMath.PI, 0) - val mbPlayerNode = PlayerController.Defaults - .defaultPlayerNode( - playerPos, - playerModel, - playerPhysicsControl - ) - val cameraPivotNode = new Node(EntityIds.CameraPivot.value) - .taggedWith[PlayerControllerTags.PlayerCameraPivotNode] - for { - playerNode <- IO.fromEither(mbPlayerNode) - _ <- IO(cameraPivotNode.addControl(new FollowControl(playerNode))) - .onErrorHandleWith(e => - IO.raiseError(PlayerController.GenericError(e.getMessage())) - ) - camNode <- IO( + playerModel <- + assetManager + .loadModelAs[Node](modelPath) + .map(_.withRotate(0, FastMath.PI, 0)) + .mapError(PlayerController.CouldNotCreatePlayerModel) + playerNode <- UIO( PlayerController.Defaults - .defaultCamerNode(camera, cameraPivotNode, playerNode, playerPos) - ).onErrorHandleWith(e => - IO.raiseError(PlayerController.GenericError(e.getMessage())) - ).map(_.taggedWith[PlayerControllerTags.PlayerCameraNode]) - // _ <- Task { - // val chaseCam = new ChaseCamera(camera, playerNode, inputManager) - // chaseCam.setSmoothMotion(false) - // chaseCam.setLookAtOffset(new Vector3f(0, 1.5f, 10)) - // chaseCam - // } - // .onErrorHandleWith(e => - // IO.raiseError(PlayerController.GenericError(e.getMessage())) - // ) - _ <- wire[PlayerController.Props].create - } yield () + .defaultPlayerNode( + playerPos, + playerModel, + playerPhysicsControl + ) + ) + cameraPivotNode <- UIO( + new Node(EntityIds.CameraPivot.value) + .withControl(new FollowControl(playerNode)) + .taggedWith[PlayerControllerTags.PlayerCameraPivotNode] + ) + camNode <- UIO( + PlayerController.Defaults + .defaultCamerNode(camera, playerPos) + .taggedWith[PlayerControllerTags.PlayerCameraNode] + ) + playerActor <- wire[PlayerController.Props].create + } yield playerActor } + def createTestNpc( // appScheduler: monix.execution.Scheduler, npcName: String @@ -250,12 +266,7 @@ class MainAppDelegate( val npcPhysicsControl = new BetterCharacterControl(1.5f, 6f, 1f) // (1f, 2.1f, 10f) .withJumpForce(ImVector3f(0, 5f, 0)) - val mbNpcNode = PlayerController.Defaults.defaultNpcNode( - assetManager, - initialPos, - npcPhysicsControl, - npcName - ) + val npcActorTask = AkkaUtils.spawnActorL( NpcActorSupervisor .Props( @@ -263,6 +274,7 @@ class MainAppDelegate( enqueueR, initialPos, // tickEventBus, + npcName, npcPhysicsControl ).behavior, npcName, @@ -271,28 +283,35 @@ class MainAppDelegate( .behavior, s"${npcName}-npcActorSupervisor" ) - // .taggedWith[PlayerControllerTags.PlayerTag] for { - npcNode <- IO.fromEither(mbNpcNode) - npcActor <- npcActorTask - // _ <- IO { - // physicsSpace += npcPhysicsControl - // physicsSpace += npcNode - // // rootNode += npcNode - // } - _ <- physicsSpace += npcPhysicsControl - _ <- physicsSpace += npcNode - _ <- rootNode += npcNode + materialDef <- assetManager.loadAssetAs[MaterialDef]( + os.rel / "Common" / "MatDefs" / "Misc" / "Unshaded.j3md" + ) + material = new Material(materialDef) + _ = material.setColor("Color", ColorRGBA.Blue) + mesh = PlayerController.Defaults.defaultMesh.withMaterial(material) + npcNode = PlayerController.Defaults.defaultNpcNode( + mesh, + initialPos, + npcPhysicsControl, + npcName + ) + npcActor <- npcActorTask.hideErrors + _ <- (for { + _ <- physicsSpace += npcPhysicsControl + _ <- physicsSpace += npcNode + _ <- rootNode += npcNode + } yield ()).hideErrors } yield npcActor + } } class FollowControl(playerNode: Node) extends AbstractControl { - override def controlUpdate(tpf: Float): Unit = { + override def controlUpdate(tpf: Float): Unit = this.spatial.setLocalTranslation(playerNode.getLocalTranslation()) - } override def controlRender( rm: RenderManager, vp: ViewPort diff --git a/src/main/scala/wow/doge/mygame/MainModule.scala b/src/main/scala/wow/doge/mygame/MainModule.scala index 431cbbb..6b65566 100644 --- a/src/main/scala/wow/doge/mygame/MainModule.scala +++ b/src/main/scala/wow/doge/mygame/MainModule.scala @@ -5,15 +5,22 @@ import cats.effect.Resource import io.odin.Logger import monix.bio.Task import wow.doge.mygame.executors.ExecutorsModule +import akka.actor.BootstrapSetup +import monix.execution.Scheduler trait MainModule extends ExecutorsModule { def actorSystemResource( - logger: Logger[Task] + logger: Logger[Task], + scheduler: Scheduler ): Resource[Task, ActorSystem[SpawnProtocol.Command]] = Resource.make( logger.info("Creating Actor System") >> Task( - ActorSystem(SpawnProtocol(), name = "GameActorSystem") + ActorSystem( + SpawnProtocol(), + name = "GameActorSystem", + BootstrapSetup().withDefaultExecutionContext(scheduler) + ) ) )(sys => for { diff --git a/src/main/scala/wow/doge/mygame/executors/ExecutorsModule.scala b/src/main/scala/wow/doge/mygame/executors/ExecutorsModule.scala index d88745f..ff3bb9f 100644 --- a/src/main/scala/wow/doge/mygame/executors/ExecutorsModule.scala +++ b/src/main/scala/wow/doge/mygame/executors/ExecutorsModule.scala @@ -1,23 +1,32 @@ package wow.doge.mygame.executors import cats.effect.Resource +import monix.bio.IO import monix.bio.Task +import monix.bio.UIO import monix.execution.Scheduler trait ExecutorsModule { - lazy val schedulers = Schedulers() - // Resource.make( - // Task( - // new Schedulers( - // jme = Scheduler - // .singleThread(name = "JME-Application-Thread", daemonic = false) - // ) - // ) - // )(s => Task(s.jme.shutdown())) - lazy val jMESchedulerResource = Resource.make( + val schedulers = Schedulers() + val acquire: UIO[Either[Error, Int]] = + IO.pure(1).onErrorHandleWith(_ => IO.raiseError(Error)).attempt + // : Resource[IO[Error, Unit], Unit] + val res = Resource.make(acquire)(_ => IO.unit) + val x: Task[Either[Error, Unit]] = res.use { + case Right(value) => Task(Right(println(s"got $value"))) + case Left(value) => Task(Left(value)) + } + val z = x.onErrorHandleWith(ex => UIO(Right(ex.printStackTrace()))) + val y: IO[Error, Unit] = z >> + x.hideErrors.rethrow + + val jMESchedulerResource = Resource.make( Task( Scheduler .singleThread(name = "JME-Application-Thread", daemonic = false) ) )(e => Task(e.shutdown())) } + +sealed trait Error +case object Error extends Error diff --git a/src/main/scala/wow/doge/mygame/executors/GUIExecutor.scala b/src/main/scala/wow/doge/mygame/executors/GUIExecutor.scala index 8738128..d480bff 100644 --- a/src/main/scala/wow/doge/mygame/executors/GUIExecutor.scala +++ b/src/main/scala/wow/doge/mygame/executors/GUIExecutor.scala @@ -42,13 +42,13 @@ object SwingExecutorService extends GUIExecutorService { object JMEExecutorService extends GUIExecutorService { override def execute(command: Runnable) = - JMERunner.runner.enqueue(command) + JMERunner.runner.get.apply(command) // new SingleThreadEventExecutor() sys.addShutdownHook(JMEExecutorService.shutdown()) } object JMERunner { - var runner: com.jme3.app.Application = null + var runner: Option[Runnable => Unit] = None } diff --git a/src/main/scala/wow/doge/mygame/game/GameApp.scala b/src/main/scala/wow/doge/mygame/game/GameApp.scala index b7a1e12..6863ae5 100644 --- a/src/main/scala/wow/doge/mygame/game/GameApp.scala +++ b/src/main/scala/wow/doge/mygame/game/GameApp.scala @@ -1,9 +1,16 @@ 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.app.state.AppStateManager -import com.jme3.asset.AssetManager import com.jme3.bullet.BulletAppState import com.jme3.input.InputManager import com.jme3.scene.Node @@ -12,20 +19,23 @@ import com.jme3.system.AppSettings import com.softwaremill.tagging._ import com.typesafe.scalalogging.{Logger => SLogger} import io.odin.Logger -import monix.bio.IO +import monix.bio.Fiber import monix.bio.Task +import monix.bio.UIO import monix.catnap.ConcurrentChannel import monix.catnap.ConsumerF -import monix.catnap.Semaphore import monix.eval.Coeval +import monix.execution.CancelableFuture +import monix.execution.CancelablePromise import monix.execution.Scheduler +import wow.doge.mygame.executors.JMERunner import wow.doge.mygame.executors.Schedulers +import wow.doge.mygame.utils.GenericTimerActor import wow.doge.mygame.game.subsystems.ui.JFxUI import wow.doge.mygame.implicits._ +import wow.doge.mygame.utils.AkkaUtils import wow.doge.mygame.utils.wrappers.jme._ - -sealed trait Error -case object FlyCamNotExists extends Error +import wow.doge.mygame.Dispatchers object GameAppTags { sealed trait RootNode @@ -33,47 +43,58 @@ object GameAppTags { sealed trait GuiNode } -class GameApp private (logger: Logger[Task], app: SimpleAppExt) { +class GameApp private[game] ( + logger: Logger[Task], + app: SimpleAppExt, + gameActor: ActorRef[TestGameActor.Command], + gameSpawnProtocol: ActorRef[SpawnProtocol.Command] +) { def stateManager: Task[AppStateManager] = Task(app.getStateManager()) def inputManager: Task[InputManager] = Task(app.getInputManager()) - def assetManager: Task[AssetManager] = Task(app.getAssetManager()) + def assetManager = new AssetManager(app.getAssetManager()) def guiNode = Task(app.getGuiNode().taggedWith[GameAppTags.GuiNode]) - def guiNode2 = AppNode[Task](app.getGuiNode()).taggedWith[GameAppTags.GuiNode] - def flyCam = - IO(app.getFlyByCamera()).onErrorHandleWith(_ => - IO.raiseError(FlyCamNotExists) - ) + val guiNode2 = AppNode[Task](app.getGuiNode()).taggedWith[GameAppTags.GuiNode] + def flyCam = Option(app.getFlyByCamera()) def camera = Task(app.getCamera()) def viewPort = Task(app.getViewPort()) // def rootNode = Task(app.getRootNode().taggedWith[GameAppTags.RootNode]) val rootNode = AppNode[Task](app.getRootNode()).taggedWith[GameAppTags.RootNode] - val physicsSpace = new PhysicsSpace[Task](app.bulletAppState.physicsSpace) - def enqueue(cb: () => Unit) = - app.enqueue(new Runnable { - override def run() = cb() - }) + def enqueue(cb: () => Unit) = app.enqueueR(() => cb()) def enqueueL[T](cb: () => T): Task[T] = app.enqueueL(cb) + def spawnGameActor[T]( + behavior: Behavior[T], + actorName: String, + props: Props = Dispatchers.jmeDispatcher + )(implicit scheduler: akka.actor.typed.Scheduler) = + AkkaUtils.spawnActorL(behavior, actorName, props)( + 2.seconds, + scheduler, + gameSpawnProtocol + ) + def scheduler = app.scheduler def jfxUI = JFxUI(app) } -object GameApp { - - def resource( - logger: Logger[Task], - jmeThread: Scheduler, - schedulers: Schedulers - ) = +class GameAppResource( + logger: Logger[Task], + jmeThread: Scheduler, + schedulers: Schedulers +)(implicit + timeout: Timeout, + scheduler: akka.actor.typed.Scheduler, + spawnProtocol: ActorRef[SpawnProtocol.Command] +) { + def resource: Resource[Task, (GameApp, Fiber[Throwable, Unit])] = Resource.make { lazy val bullet = new BulletAppState for { - // bullet <- Task(new BulletAppState) - // startSignal <- Task(CancelablePromise[Unit]()) app <- Task(new SimpleAppExt(schedulers, bullet)) + _ <- UIO(JMERunner.runner = Some(app.enqueue _)) _ <- Task { val settings = new AppSettings(true) settings.setVSync(true) @@ -88,34 +109,76 @@ object GameApp { fib <- Task(app.start).executeOn(jmeThread).start _ <- Task.deferFuture(app.started) - // _ <- Task.fromCancelablePromise(startSignal) - _ <- Task(pprint.log(bullet.toString())) - _ <- Task(println(bullet.physicsSpace.toString())) - gameApp <- Task(new GameApp(logger, app)) - } yield gameApp -> fib - }(_._2.cancel) - - /** - * Synchronization wrapper for a mutable object - * - * @param obj the mutable object - * @param lock lock for synchronization - */ - class SynchedObject[A](obj: A, lock: Semaphore[Task]) { - def modify(f: A => Unit): Task[Unit] = - lock.withPermit(Task(f(obj))) - - def flatModify(f: A => Task[Unit]): Task[Unit] = - lock.withPermit(f(obj)) - - def get: Task[A] = lock.withPermit(Task(obj)) - } + testGameActor <- AkkaUtils.spawnActorL( + new TestGameActor.Props().create, + "testGameActor", + Dispatchers.jmeDispatcher + ) + sp <- testGameActor.askL(TestGameActor.GetSpawnProtocol(_)) + gameApp <- Task(new GameApp(logger, app, testGameActor, sp)) + _ <- Task { + val fut = () => testGameActor.ask(TestGameActor.Stop).flatten + app.cancelToken = Some(fut) + } + } yield (gameApp, fib) + } { + case (gameApp, fib) => + fib.cancel >> UIO(JMERunner.runner = None) + } +} - object SynchedObject { - def apply[A](obj: A) = - Semaphore[Task](1).flatMap(lock => Task(new SynchedObject(obj, lock))) +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 { diff --git a/src/main/scala/wow/doge/mygame/game/GameAppActor.scala b/src/main/scala/wow/doge/mygame/game/GameAppActor.scala index ed170ce..2b5d83a 100644 --- a/src/main/scala/wow/doge/mygame/game/GameAppActor.scala +++ b/src/main/scala/wow/doge/mygame/game/GameAppActor.scala @@ -5,7 +5,7 @@ import scala.concurrent.duration._ import akka.actor.typed.SupervisorStrategy import akka.actor.typed.scaladsl.Behaviors import wow.doge.mygame.game.TickGenerator.Send -import wow.doge.mygame.game.entities.GenericTimerActor +import wow.doge.mygame.utils.GenericTimerActor import wow.doge.mygame.implicits._ import wow.doge.mygame.subsystems.events.EventBus import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus @@ -19,14 +19,7 @@ object GameAppActor { case object Pause extends Command case object Stop extends Command - case class Props( - // app: SimpleAppExt, - // akkaScheduler: Scheduler, - // schedulers: Schedulers, - // spawnProtocol: ActorRef[SpawnProtocol.Command], - // loggerL: Logger[Task] - tickEventBus: GameEventBus[TickEvent] - ) { + case class Props(tickEventBus: GameEventBus[TickEvent]) { def behavior = Behaviors.setup[Command] { ctx => ctx.log.infoP("Hello from GameAppActor") diff --git a/src/main/scala/wow/doge/mygame/game/SimpleAppExt.scala b/src/main/scala/wow/doge/mygame/game/SimpleAppExt.scala index 5e35cd8..a15265c 100644 --- a/src/main/scala/wow/doge/mygame/game/SimpleAppExt.scala +++ b/src/main/scala/wow/doge/mygame/game/SimpleAppExt.scala @@ -1,9 +1,12 @@ package wow.doge.mygame.game import scala.collection.immutable.Queue +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future import com.jme3.app.SimpleApplication import com.jme3.app.state.AppState +import com.jme3.bullet.BulletAppState import monix.bio.Task import monix.execution.CancelableFuture import monix.execution.CancelablePromise @@ -11,7 +14,6 @@ import monix.execution.Scheduler import monix.execution.atomic.Atomic import wow.doge.mygame.executors.GUIExecutorService import wow.doge.mygame.executors.Schedulers -import com.jme3.bullet.BulletAppState // import wow.doge.mygame.implicits._ class SimpleAppExt( schedulers: Schedulers, @@ -25,27 +27,30 @@ class SimpleAppExt( */ private val taskQueue2 = Atomic(Queue.empty[MyTask[_]]) - // lazy val bulletAppState: BulletAppState = new BulletAppState - - // def bulletAppState = synchronized(_bulletAppState) - - // def tickObservable: Observable[Float] = tickSubject - // lazy val bulletAppState = stateManager.state[BulletAppState]() - private val startSignal: CancelablePromise[Unit] = CancelablePromise() + var cancelToken: Option[() => Future[Unit]] = None + def started: CancelableFuture[Unit] = startSignal.future override def simpleInitApp(): Unit = { - // _bulletAppState = new BulletAppState stateManager.attach(bulletAppState) startSignal.success(()) } override def simpleUpdate(tpf: Float): Unit = {} - override def stop(): Unit = { - super.stop() + override def stop(waitFor: Boolean): Unit = { + cancelToken match { + case Some(value) => + value().foreach { _ => + pprint.log("Called cancel in simpleapp") + super.stop(true) + } + case None => + pprint.log("Called cancel in simpleapp") + super.stop(true) + } } def enqueueFuture[T](cb: () => T): CancelableFuture[T] = { @@ -74,7 +79,7 @@ class SimpleAppExt( enqueue(command) } - lazy val scheduler = Scheduler(JMEExecutorService) + val scheduler = Scheduler(JMEExecutorService) } object SimpleAppExt { private[game] case class MyTask[T](p: CancelablePromise[T], cb: () => T) diff --git a/src/main/scala/wow/doge/mygame/game/entities/NpcActorSupervisor.scala b/src/main/scala/wow/doge/mygame/game/entities/NpcActorSupervisor.scala index 5bb477b..512fd86 100644 --- a/src/main/scala/wow/doge/mygame/game/entities/NpcActorSupervisor.scala +++ b/src/main/scala/wow/doge/mygame/game/entities/NpcActorSupervisor.scala @@ -10,6 +10,7 @@ import akka.actor.typed.SupervisorStrategy import akka.actor.typed.scaladsl.ActorContext import akka.actor.typed.scaladsl.Behaviors import akka.util.Timeout +import cats.syntax.show._ import monix.execution.CancelableFuture import monix.execution.CancelablePromise import wow.doge.mygame.game.subsystems.movement.CanMove @@ -26,6 +27,7 @@ import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus import wow.doge.mygame.subsystems.events.TickEvent import wow.doge.mygame.subsystems.events.TickEvent.RenderTick import wow.doge.mygame.subsystems.movement.ImMovementActor +import wow.doge.mygame.utils.GenericTimerActor object NpcActorSupervisor { sealed trait Command @@ -35,10 +37,7 @@ object NpcActorSupervisor { signal: CancelableFuture[NpcMovementActor.DoneMoving.type] ) extends Command private case object DoneMoving extends Command - // private case class MovementResponse(response: CancelableFuture[_]) extends Command private case class LogError(err: Throwable) extends Command - private case object NoOp extends Command - private case class MovementFailed(err: Throwable) extends Command final case class Props( @@ -47,18 +46,19 @@ object NpcActorSupervisor { initialPos: ImVector3f ) { def behavior = - Behaviors.setup[Command] { ctx => - val npcMovementActor = ctx.spawn( - (npcMovementActorBehavior), - s"npc-${npcName}-NpcMovementActor" - ) + Behaviors.withMdc(Map("actorName" -> npcName))( + Behaviors.setup[Command] { ctx => + val npcMovementActor = ctx.spawn( + (npcMovementActorBehavior), + s"npc-${npcName}-NpcMovementActor" + ) - new NpcActorSupervisor(ctx, this, Children(npcMovementActor)) - .idle(State()) - } + new NpcActorSupervisor(ctx, this, Children(npcMovementActor)) + .idle(State()) + } + ) } - final case class State( - ) + final case class State() final case class Children( npcMovementActor: ActorRef[NpcMovementActor.Command] ) @@ -79,12 +79,12 @@ class NpcActorSupervisor( 100.millis ) .behavior, - s"npc-John-NpcActorTimer" + s"npc-${props.npcName}-NpcActorTimer" ) def idle(state: State): Behavior[NpcActorSupervisor.Command] = Behaviors.setup { _ => - ctx.log.debugP("Inside Idle State") + ctx.log.debugP(s"npcActor-${props.npcName}: Entered Idle State") Behaviors.receiveMessage[Command] { case m @ Move(pos) => ctx.ask( @@ -99,7 +99,7 @@ class NpcActorSupervisor( moving(state, move.pos, signal) case LogError(err) => - ctx.log.warnP(err.getMessage()) + logError(err) Behaviors.same case _ => Behaviors.unhandled } @@ -111,19 +111,15 @@ class NpcActorSupervisor( signal: CancelableFuture[NpcMovementActor.DoneMoving.type] ): Behavior[NpcActorSupervisor.Command] = Behaviors.setup { _ => + ctx.log.debugP(s"npcActor-${props.npcName}: Entered Moving State") movementTimer ! GenericTimerActor.Start - - // ctx - // .ask(state.npcMovementActor, NpcMovementActor2.MoveTo(targetPos, _))( - // _.fold(LogError(_), MovementResponse(_)) - // ) ctx.pipeToSelf(signal) { case Success(value) => DoneMoving case Failure(exception) => MovementFailed(exception) } Behaviors.receiveMessagePartial[Command] { case LogError(err) => - ctx.log.error(err.getMessage()) + logError(err) Behaviors.same case MovementFailed(err) => ctx.self ! LogError(err) @@ -132,6 +128,7 @@ class NpcActorSupervisor( case m @ Move(pos) => movementTimer ! GenericTimerActor.Stop children.npcMovementActor ! NpcMovementActor.StopMoving + // new movement request received, cancel previous request signal.cancel() ctx.ask( children.npcMovementActor, @@ -143,15 +140,15 @@ class NpcActorSupervisor( Behaviors.same case InternalMove(move, signal) => moving(state, targetPos, signal) - case NoOp => Behaviors.same - // case MovementResponse(x: CancelableFuture[_]) => - // // ctx.pipeToSelf(x)(_.) case DoneMoving => movementTimer ! GenericTimerActor.Stop idle(state) } } + + def logError(err: Throwable) = + ctx.log.errorP(s"npcActor-${props.npcName}: " + err.getMessage) } object NpcMovementActor { @@ -171,6 +168,7 @@ object NpcMovementActor { val enqueueR: Function1[() => Unit, Unit], val initialPos: ImVector3f, // val tickEventBus: GameEventBus[TickEvent], + val npcName: String, val movable: T ) { def behavior = @@ -189,9 +187,7 @@ class NpcMovementActor[T]( def location = cm.location(props.movable) - def receive( - state: State - ): Behavior[NpcMovementActor.Command] = + def receive(state: State): Behavior[NpcMovementActor.Command] = Behaviors.receiveMessagePartial { case AskPosition(replyTo) => replyTo ! location @@ -214,19 +210,25 @@ class NpcMovementActor[T]( Behaviors.receiveMessagePartial { case StopMoving => ctx.log.debugP( - "Position at Stop = " + location.toString + show"npcActor-${props.npcName}: Position at Stop = " + location ) props.enqueueR(() => cm.stop(props.movable)) receive(state) case MovementTick => - val dst = ImVector3f.dst(targetPos, location) + val dst = ImVector3f.manhattanDst(targetPos, location) if (dst <= 10f) { ctx.self ! StopMoving reachDestination.success(DoneMoving) } else { - ctx.log.traceP("Difference = " + dst.toString()) - ctx.log.traceP("Current pos = " + location.toString()) + ctx.log.traceP( + show"npcActor-${props.npcName}: Difference = " + dst.formatted( + "%.2f" + ) + ) + ctx.log.traceP( + show"npcActor-${props.npcName}: Current pos = " + location + ) } Behaviors.same } @@ -295,7 +297,7 @@ object NpcMovementActorNotUsed { mainEventBus ! EventBus.Subscribe(npcMovementEl) tickEventBus ! EventBus.Subscribe(renderTickEl) - Behaviors.receiveMessage { msg => Behaviors.same } + Behaviors.receiveMessage(msg => Behaviors.same) } } diff --git a/src/main/scala/wow/doge/mygame/game/entities/player/PlayerActorSupervisor.scala b/src/main/scala/wow/doge/mygame/game/entities/player/PlayerActorSupervisor.scala index 491f4cb..63dbfff 100644 --- a/src/main/scala/wow/doge/mygame/game/entities/player/PlayerActorSupervisor.scala +++ b/src/main/scala/wow/doge/mygame/game/entities/player/PlayerActorSupervisor.scala @@ -1,15 +1,11 @@ package wow.doge.mygame.game.entities -import scala.concurrent.duration._ -import scala.util.Random - import akka.actor.typed.ActorRef import akka.actor.typed.Behavior import akka.actor.typed.LogOptions import akka.actor.typed.SupervisorStrategy import akka.actor.typed.scaladsl.ActorContext import akka.actor.typed.scaladsl.Behaviors -import akka.actor.typed.scaladsl.TimerScheduler import com.typesafe.scalalogging.Logger import org.slf4j.event.Level import wow.doge.mygame.game.subsystems.movement.CanMove @@ -19,7 +15,7 @@ import wow.doge.mygame.subsystems.events.PlayerEvent import wow.doge.mygame.subsystems.events.TickEvent import wow.doge.mygame.subsystems.events.TickEvent.RenderTick import wow.doge.mygame.subsystems.movement.ImMovementActor - +import wow.doge.mygame.implicits._ object PlayerActorSupervisor { sealed trait Command final case class Props( @@ -28,15 +24,15 @@ object PlayerActorSupervisor { imMovementActorBehavior: Behavior[ImMovementActor.Command], playerCameraActorBehavior: Behavior[PlayerCameraActor.Command] ) { - def behavior[T: CanMove](movable: T) = + def behavior = Behaviors.logMessages( LogOptions() .withLevel(Level.TRACE) .withLogger( - Logger[PlayerActorSupervisor[T]].underlying + Logger[PlayerActorSupervisor].underlying ), Behaviors.setup[Command] { ctx => - ctx.log.info("Hello from PlayerActor") + ctx.log.infoP("Starting PlayerActor") // spawn children actors val movementActor = @@ -44,7 +40,7 @@ object PlayerActorSupervisor { Behaviors .supervise(imMovementActorBehavior) .onFailure[Exception](SupervisorStrategy.restart), - "playerMovementActorChild" + "playerMovementActor" ) val playerCameraActor = @@ -55,19 +51,27 @@ object PlayerActorSupervisor { "playerCameraActorEl" ) - ctx.spawn( - PlayerMovementActor - .Props( - movementActor, - playerCameraActor, - playerEventBus, - tickEventBus - ) - .behavior, - "playerMovementAcor" + val playerMovementEl = ctx.spawn( + Behaviors + .supervise(PlayerMovementEventListener(movementActor)) + .onFailure[Exception](SupervisorStrategy.restart), + "playerMovementEventHandler" ) - //init actors + val renderTickEl = { + val behavior = + Behaviors.receiveMessage[RenderTick.type] { + case RenderTick => + movementActor ! ImMovementActor.Tick + // playerCameraActor ! PlayerCameraActor.Tick + Behaviors.same + } + ctx.spawn(behavior, "playerMovementTickListener") + } + + //init listeners + playerEventBus ! EventBus.Subscribe(playerMovementEl) + tickEventBus ! EventBus.Subscribe(renderTickEl) playerEventBus ! EventBus.Subscribe(playerCameraEl) new PlayerActorSupervisor( @@ -84,18 +88,19 @@ object PlayerActorSupervisor { movementActor: ActorRef[ImMovementActor.Command] ) } -class PlayerActorSupervisor[T: CanMove]( +class PlayerActorSupervisor( ctx: ActorContext[PlayerActorSupervisor.Command], props: PlayerActorSupervisor.Props, children: PlayerActorSupervisor.Children ) { import PlayerActorSupervisor._ def receive = - Behaviors.receiveMessage[Command] { - case _ => - // children.movementActor ! ImMovementActor.MovedDown(true) - Behaviors.same - } + Behaviors + .receiveMessage[Command] { + case _ => + // children.movementActor ! ImMovementActor.MovedDown(true) + Behaviors.same + } } object PlayerMovementActor { @@ -115,15 +120,17 @@ object PlayerMovementActor { .onFailure[Exception](SupervisorStrategy.restart), "playerMovementEventHandler" ) - val renderTickElBehavior = - Behaviors.receiveMessage[RenderTick.type] { - case RenderTick => - movementActor ! ImMovementActor.Tick - // playerCameraActor ! PlayerCameraActor.Tick - Behaviors.same - } - val renderTickEl = - ctx.spawn(renderTickElBehavior, "playerMovementTickListener") + + val renderTickEl = { + val behavior = + Behaviors.receiveMessage[RenderTick.type] { + case RenderTick => + movementActor ! ImMovementActor.Tick + // playerCameraActor ! PlayerCameraActor.Tick + Behaviors.same + } + ctx.spawn(behavior, "playerMovementTickListener") + } playerEventBus ! EventBus.Subscribe(playerMovementEl) tickEventBus ! EventBus.Subscribe(renderTickEl) @@ -131,47 +138,3 @@ object PlayerMovementActor { } } } - -object GenericTimerActor { - sealed trait Command - final case object Start extends Command - final case object Stop extends Command - private case object Send extends Command - case class TimerKey(seed: Long) - - case class Props[T]( - target: ActorRef[T], - messageToSend: T, - timeInterval: FiniteDuration - ) { - val behavior = Behaviors.withTimers[Command] { timers => - new GenericTimerActor(timers, TimerKey(Random.nextLong()), this).idle - } - } -} -class GenericTimerActor[T]( - timers: TimerScheduler[GenericTimerActor.Command], - timerKey: GenericTimerActor.TimerKey, - props: GenericTimerActor.Props[T] -) { - import GenericTimerActor._ - - val idle: Behavior[Command] = - Behaviors.receiveMessage { - case Start => - timers.startTimerWithFixedDelay(timerKey, Send, props.timeInterval) - active - case _ => Behaviors.unhandled - - } - - val active: Behavior[Command] = - Behaviors.receiveMessagePartial { - case Send => - props.target ! props.messageToSend - Behaviors.same - case Stop => - timers.cancel(timerKey) - idle - } -} diff --git a/src/main/scala/wow/doge/mygame/game/entities/player/PlayerController.scala b/src/main/scala/wow/doge/mygame/game/entities/player/PlayerController.scala index e572f82..b4e2387 100644 --- a/src/main/scala/wow/doge/mygame/game/entities/player/PlayerController.scala +++ b/src/main/scala/wow/doge/mygame/game/entities/player/PlayerController.scala @@ -4,7 +4,6 @@ import akka.actor.typed.ActorRef import akka.actor.typed.Scheduler import akka.actor.typed.SpawnProtocol import akka.util.Timeout -import cats.implicits._ import com.jme3.asset.AssetManager import com.jme3.bullet.BulletAppState import com.jme3.bullet.control.BetterCharacterControl @@ -13,24 +12,22 @@ import com.jme3.renderer.Camera import com.jme3.scene.CameraNode import com.jme3.scene.Geometry import com.jme3.scene.Node +import com.jme3.scene.Spatial import com.jme3.scene.shape.Box import com.softwaremill.tagging._ import io.odin.Logger -import monix.bio.IO import monix.bio.Task +import monix.bio.UIO import wow.doge.mygame.game.GameAppTags import wow.doge.mygame.game.SimpleAppExt import wow.doge.mygame.implicits._ import wow.doge.mygame.math.ImVector3f -import wow.doge.mygame.state.MyMaterial import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus import wow.doge.mygame.subsystems.events.PlayerEvent import wow.doge.mygame.subsystems.events.TickEvent import wow.doge.mygame.subsystems.movement.ImMovementActor import wow.doge.mygame.utils.AkkaUtils import wow.doge.mygame.utils.wrappers.jme._ -import monix.bio.UIO - object PlayerControllerTags { sealed trait PlayerTag sealed trait PlayerCameraNode @@ -40,7 +37,9 @@ object PlayerControllerTags { object PlayerController { sealed trait Error case class CouldNotCreatePlayerNode(reason: String) extends Error - case class GenericError(reason: String) extends Error + case class CouldNotCreatePlayerModel( + reason: wow.doge.mygame.utils.wrappers.jme.AssetManager.Error + ) extends Error class Props( enqueueR: Function1[() => Unit, Unit], @@ -65,9 +64,8 @@ object PlayerController { val playerActorBehavior = { val movementActorBeh = new ImMovementActor.Props( enqueueR, - playerPhysicsControl, camera - ).behavior + ).behavior(playerPhysicsControl) val cameraActorBeh = new PlayerCameraActor.Props( cameraPivotNode, enqueueR, @@ -78,36 +76,19 @@ object PlayerController { tickEventBus, movementActorBeh, cameraActorBeh - ).behavior(playerPhysicsControl) + ).behavior } - val create: IO[Error, Unit] = + val create: UIO[ActorRef[PlayerActorSupervisor.Command]] = (for { - playerActor <- AkkaUtils.spawnActorL( - playerActorBehavior, - "playerActorSupervisor" - ) - // _ <- Task(rootNode += playerNode) - // _ <- Task(pprint.log("Physicsspace = " + physicsSpace.toString())) - // _ <- Task(pprint.log("playerNode = " + playerNode.toString())) + playerActor <- + AkkaUtils.spawnActorL(playerActorBehavior, "playerActorSupervisor") _ <- rootNode += playerNode _ <- physicsSpace += playerNode _ <- physicsSpace += playerPhysicsControl - _ <- IO { - // physicsSpace += playerNode - // physicsSpace += playerPhysicsControl - // rootNode += cameraNode - cameraPivotNode += cameraNode - // playerNode += cameraPivotNode - // rootNode += cameraPivotNode - } + _ = cameraPivotNode += cameraNode _ <- rootNode += cameraPivotNode - } yield ()) - .onErrorHandleWith(e => - UIO(e.printStackTrace()) >> IO.raiseError( - GenericError(e.getMessage()) - ) - ) - // .executeOn(appScheduler) + } yield playerActor).hideErrors + } def apply( @@ -146,11 +127,11 @@ object PlayerController { val geom = Geometry("playerGeom", b) geom } - def defaultTexture(assetManager: AssetManager) = - MyMaterial( - assetManager = assetManager, - path = os.rel / "Common" / "MatDefs" / "Misc" / "Unshaded.j3md" - ) + // def defaultTexture(assetManager: AssetManager) = + // MyMaterial( + // assetManager = assetManager, + // path = os.rel / "Common" / "MatDefs" / "Misc" / "Unshaded.j3md" + // ) // new CameraControl(cam) { // override def controlUpdate(tpf: Float) = { @@ -164,14 +145,11 @@ object PlayerController { def defaultCamerNode( cam: Camera, - playerNode: Node, - cameraPivotNode: Node, + // playerNode: Node, + // cameraPivotNode: Node, playerPos: ImVector3f ) = - new CameraNode( - "CameraNode", - cam - ) + new CameraNode("CameraNode", cam) // .withControlDir(ControlDirection.SpatialToCamera) .withLocalTranslation(ImVector3f(0, 1.5f, 10)) .withLookAt(playerPos, ImVector3f.UNIT_Y) @@ -182,66 +160,32 @@ object PlayerController { // camNode: CameraNode, playerPhysicsControl: BetterCharacterControl ) = - Either - .catchNonFatal( - Node("PlayerNode") - .withChildren( - // camNode, - playerModel - ) - .withLocalTranslation(playerPos) - .withControl(playerPhysicsControl) - ) - .map(_.taggedWith[PlayerControllerTags.PlayerTag]) - .leftMap(e => PlayerController.CouldNotCreatePlayerNode(e.getMessage())) + Node("PlayerNode") + .withChildren(playerModel) + .withLocalTranslation(playerPos) + .withControl(playerPhysicsControl) + .taggedWith[PlayerControllerTags.PlayerTag] def defaultNpcNode( - assetManager: AssetManager, - // modelPath: os.RelPath, + // assetManager: AssetManager, + npcModel: Spatial, initialPos: ImVector3f, npcPhysicsControl: BetterCharacterControl, npcName: String ) = - Either - .catchNonFatal( - Node(npcName) - .withChildren( - // assetManager - // .loadModel(modelPath) - // .asInstanceOf[Node] - // .withRotate(0, FastMath.PI, 0) - defaultMesh.withMaterial(defaultTexture(assetManager)) - ) - .withLocalTranslation(initialPos) - .withControl(npcPhysicsControl) + // Either + // .catchNonFatal( + Node(npcName) + .withChildren( + // defaultMesh.withMaterial(defaultTexture(assetManager)) + npcModel ) - // .map(_.taggedWith[PlayerControllerTags.PlayerTag]) - // .leftMap(e => PlayerController.CouldNotCreatePlayerNode(e.getMessage())) + .withLocalTranslation(initialPos) + .withControl(npcPhysicsControl) + // ) def defaultPlayerPhysicsControl = new BetterCharacterControl(1.5f, 6f, 1f) .withJumpForce(ImVector3f(0, 5f, 0)) } } - -object Methods {} - -// camNode <- IO( -// _cameraNode.getOrElse(defaultCamerNode(camera, initialPlayerPos)) -// ) -// playerPhysicsControl <- IO( -// _playerPhysicsControl -// .getOrElse(defaultPlayerPhysicsControl) -// .taggedWith[PlayerTag] -// ) -// playerNode <- IO.fromEither( -// _playerNode.fold( -// defaultPlayerNode( -// assetManager, -// modelPath, -// initialPlayerPos, -// camNode, -// playerPhysicsControl -// ) -// )(_.asRight) -// ) diff --git a/src/main/scala/wow/doge/mygame/game/entities/player/PlayerEventListeners.scala b/src/main/scala/wow/doge/mygame/game/entities/player/PlayerEventListeners.scala index 310e7e5..123d7da 100644 --- a/src/main/scala/wow/doge/mygame/game/entities/player/PlayerEventListeners.scala +++ b/src/main/scala/wow/doge/mygame/game/entities/player/PlayerEventListeners.scala @@ -11,9 +11,7 @@ import wow.doge.mygame.subsystems.movement.ImMovementActor object PlayerMovementEventListener { import PlayerMovementEvent._ - def apply( - movementActor: ActorRef[ImMovementActor.Command] - ) = + def apply(movementActor: ActorRef[ImMovementActor.Command]) = Behaviors.logMessages( LogOptions() .withLevel(Level.TRACE) @@ -50,9 +48,7 @@ object PlayerMovementEventListener { object PlayerCameraEventListener { import PlayerCameraEvent._ - def apply( - playerCameraActor: ActorRef[PlayerCameraActor.Command] - ) = + def apply(playerCameraActor: ActorRef[PlayerCameraActor.Command]) = Behaviors.logMessages( LogOptions() .withLevel(Level.TRACE) @@ -63,7 +59,6 @@ object PlayerCameraEventListener { Behaviors.receiveMessagePartial { case CameraMovedUp => playerCameraActor ! PlayerCameraActor.RotateUp - Behaviors.same case CameraMovedDown => playerCameraActor ! PlayerCameraActor.RotateDown diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/input/GameInputHandler.scala b/src/main/scala/wow/doge/mygame/game/subsystems/input/GameInputHandler.scala index 14d6ed4..118b6ad 100644 --- a/src/main/scala/wow/doge/mygame/game/subsystems/input/GameInputHandler.scala +++ b/src/main/scala/wow/doge/mygame/game/subsystems/input/GameInputHandler.scala @@ -18,6 +18,7 @@ import wow.doge.mygame.subsystems.events.PlayerCameraEvent import wow.doge.mygame.subsystems.events.PlayerEvent import wow.doge.mygame.subsystems.events.PlayerMovementEvent import wow.doge.mygame.utils.IOUtils._ +import monix.bio.UIO object GameInputHandler { @@ -29,13 +30,13 @@ object GameInputHandler { ) { def begin = for { - _ <- Task(setupMovementKeys(inputManager)) + _ <- UIO(setupMovementKeys(inputManager)) // _ <- UIO(setupAnalogMovementKeys) - _ <- Task(setupCameraKeys()) + _ <- UIO(setupCameraKeys()) _ <- toIO( me.Task.parSequence( Seq( - generateMovementInputEvents( + playerMovementInputEventsGenerator( inputManager, playerEventBus ).completedL, @@ -128,11 +129,14 @@ object GameInputHandler { } - def generateMovementInputEvents( + def methodName(implicit enclosing: sourcecode.Enclosing) = + enclosing.value.split(" ")(0).split("""\.""").last + + def playerMovementInputEventsGenerator( inputManager: InputManager, playerEventBus: GameEventBus[PlayerEvent] ) = { - val name = "playerMovementInputEventsGenerator" + val name = methodName inputManager .enumObservableAction(PlayerMovementInput) // .dump("O") diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/input/InputEnums.scala b/src/main/scala/wow/doge/mygame/game/subsystems/input/InputEnums.scala index 843eef5..ea65998 100644 --- a/src/main/scala/wow/doge/mygame/game/subsystems/input/InputEnums.scala +++ b/src/main/scala/wow/doge/mygame/game/subsystems/input/InputEnums.scala @@ -3,33 +3,33 @@ import enumeratum.EnumEntry._ import enumeratum._ sealed trait PlayerMovementInput extends EnumEntry with UpperSnakecase -final object PlayerMovementInput extends Enum[PlayerMovementInput] { +object PlayerMovementInput extends Enum[PlayerMovementInput] { val values = findValues - final case object WalkForward extends PlayerMovementInput - final case object WalkRight extends PlayerMovementInput - final case object WalkLeft extends PlayerMovementInput - final case object WalkBackward extends PlayerMovementInput - final case object Jump extends PlayerMovementInput + case object WalkForward extends PlayerMovementInput + case object WalkRight extends PlayerMovementInput + case object WalkLeft extends PlayerMovementInput + case object WalkBackward extends PlayerMovementInput + case object Jump extends PlayerMovementInput } sealed trait PlayerAnalogMovementInput extends EnumEntry with UpperSnakecase -final object PlayerAnalogMovementInput extends Enum[PlayerAnalogMovementInput] { +object PlayerAnalogMovementInput extends Enum[PlayerAnalogMovementInput] { val values = findValues - final case object TurnRight extends PlayerAnalogMovementInput - final case object TurnLeft extends PlayerAnalogMovementInput + case object TurnRight extends PlayerAnalogMovementInput + case object TurnLeft extends PlayerAnalogMovementInput } sealed trait PlayerCameraInput extends EnumEntry with UpperSnakecase -final object PlayerCameraInput extends Enum[PlayerCameraInput] { +object PlayerCameraInput extends Enum[PlayerCameraInput] { val values = findValues - final case object CameraRotateLeft extends PlayerCameraInput - final case object CameraRotateRight extends PlayerCameraInput - final case object CameraRotateUp extends PlayerCameraInput - final case object CameraRotateDown extends PlayerCameraInput + case object CameraRotateLeft extends PlayerCameraInput + case object CameraRotateRight extends PlayerCameraInput + case object CameraRotateUp extends PlayerCameraInput + case object CameraRotateDown extends PlayerCameraInput } sealed trait MiscInput extends EnumEntry with UpperSnakecase -final object MiscInput extends Enum[MiscInput] { +object MiscInput extends Enum[MiscInput] { val values = findValues - final case object ToggleCursor extends MiscInput + case object ToggleCursor extends MiscInput } diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/level/DefaultGameLevel.scala b/src/main/scala/wow/doge/mygame/game/subsystems/level/DefaultGameLevel.scala index 98c4631..69a7901 100644 --- a/src/main/scala/wow/doge/mygame/game/subsystems/level/DefaultGameLevel.scala +++ b/src/main/scala/wow/doge/mygame/game/subsystems/level/DefaultGameLevel.scala @@ -8,6 +8,7 @@ import com.jme3.math.ColorRGBA import com.jme3.math.Vector3f import com.jme3.renderer.ViewPort import com.jme3.scene.Spatial +import monix.bio.UIO object DefaultGameLevel { def apply( @@ -22,8 +23,7 @@ object DefaultGameLevel { throw new NotImplementedError("No fallback sceneshape") } ) - val landscape: RigidBodyControl = - new RigidBodyControl(sceneShape, 0) + val landscape: RigidBodyControl = new RigidBodyControl(sceneShape, 0) viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f)) sceneModel.setLocalScale(2f) @@ -47,4 +47,58 @@ object DefaultGameLevel { directionalLight = dl ) } + + def apply( + assetManager: wow.doge.mygame.utils.wrappers.jme.AssetManager, + viewPort: ViewPort + ) = + // for { + // sceneModel <- assetManager.loadModelAs[Node](os.rel / "main.scene") + // sceneShape <- UIO(CollisionShapeFactory.createMeshShape(sceneModel)) + // landscape <- UIO(new RigidBodyControl(sceneShape, 0)) + + // _ <- UIO { + // viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f)) + // sceneModel.setLocalScale(2f) + // sceneModel.addControl(landscape) + // } + + // al = { + // val al = new AmbientLight() + // al.setColor(ColorRGBA.White.mult(1.3f)) + // al + // } + + // dl = { + // val dl = new DirectionalLight() + // dl.setColor(ColorRGBA.White) + // dl.setDirection(new Vector3f(2.8f, -2.8f, -2.8f).normalizeLocal()) + // dl + // } + + // } yield new GameLevel( + // model = sceneModel, + // physicsControl = landscape, + // ambientLight = al, + // directionalLight = dl + // ) + GameLevel( + os.rel / "main.scene", { + val al = new AmbientLight() + al.setColor(ColorRGBA.White.mult(1.3f)) + al + }, { + val dl = new DirectionalLight() + dl.setColor(ColorRGBA.White) + dl.setDirection(new Vector3f(2.8f, -2.8f, -2.8f).normalizeLocal()) + dl + } + )(assetManager).flatMap(gameLevel => + UIO { + viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f)) + gameLevel.model.setLocalScale(2f) + gameLevel.model.addControl(gameLevel.physicsControl) + gameLevel + } + ) } diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/level/GameLevel.scala b/src/main/scala/wow/doge/mygame/game/subsystems/level/GameLevel.scala index 7bf5bb9..d2f7925 100644 --- a/src/main/scala/wow/doge/mygame/game/subsystems/level/GameLevel.scala +++ b/src/main/scala/wow/doge/mygame/game/subsystems/level/GameLevel.scala @@ -1,21 +1,26 @@ package wow.doge.mygame.game.subsystems.level +import cats.effect.Resource import com.jme3.bullet.control.RigidBodyControl import com.jme3.light.AmbientLight import com.jme3.light.DirectionalLight +import com.jme3.scene.Node import com.jme3.scene.Spatial import com.softwaremill.tagging._ +import monix.bio.IO import monix.bio.Task +import monix.bio.UIO import wow.doge.mygame.game.GameAppTags -// import wow.doge.mygame.implicits._ import wow.doge.mygame.utils.wrappers.jme.AppNode +import wow.doge.mygame.utils.wrappers.jme.AssetManager +import wow.doge.mygame.utils.wrappers.jme.CollisionShapeFactory import wow.doge.mygame.utils.wrappers.jme.PhysicsSpace class GameLevel( - model: Spatial, - physicsControl: RigidBodyControl, - ambientLight: AmbientLight, - directionalLight: DirectionalLight + val model: Spatial, + val physicsControl: RigidBodyControl, + val ambientLight: AmbientLight, + val directionalLight: DirectionalLight ) { def addToGame( rootNode: AppNode[Task] @@ GameAppTags.RootNode, @@ -29,4 +34,56 @@ class GameLevel( _ <- physicsSpace += physicsControl } yield () } + def removeFromGame( + rootNode: AppNode[Task] @@ GameAppTags.RootNode, + physicsSpace: PhysicsSpace[Task] + ) = { + for { + _ <- rootNode -= model + _ <- rootNode -= ambientLight + _ <- rootNode -= directionalLight + _ <- physicsSpace -= model + _ <- physicsSpace -= physicsControl + } yield () + } + + def resource( + rootNode: AppNode[Task] @@ GameAppTags.RootNode, + physicsSpace: PhysicsSpace[Task] + ) = + Resource.make(this.addToGame(rootNode, physicsSpace))(_ => + this.removeFromGame(rootNode, physicsSpace) + ) +} + +object GameLevel { + sealed trait Error + case class AssetLoadError(err: AssetManager.Error) extends Error + case class CollisionShapeCreationFailed(err: CollisionShapeFactory.Error) + extends Error + + def apply( + modelPath: os.RelPath, + al: AmbientLight, + dl: DirectionalLight + )(implicit + assetManager: wow.doge.mygame.utils.wrappers.jme.AssetManager + ): IO[Error, GameLevel] = + for { + sceneModel <- + assetManager + .loadModelAs[Node](modelPath) + .mapError(AssetLoadError) + sceneShape <- + CollisionShapeFactory + .createMeshShape(sceneModel) + .mapError(CollisionShapeCreationFailed) + landscape <- UIO(new RigidBodyControl(sceneShape, 0)) + + } yield new GameLevel( + model = sceneModel, + physicsControl = landscape, + ambientLight = al, + directionalLight = dl + ) } diff --git a/src/main/scala/wow/doge/mygame/game/subsystems/movement/MovementActor.scala b/src/main/scala/wow/doge/mygame/game/subsystems/movement/MovementActor.scala index 54f2263..294f433 100644 --- a/src/main/scala/wow/doge/mygame/game/subsystems/movement/MovementActor.scala +++ b/src/main/scala/wow/doge/mygame/game/subsystems/movement/MovementActor.scala @@ -31,16 +31,17 @@ object ImMovementActor { // final case object RotateRight extends Movement // final case object RotateLeft extends Movement - final class Props[T: CanMove]( + final class Props( val enqueueR: Function1[() => Unit, Unit], - val movable: T, // playerMovementEventBus: ActorRef[ // EventBus.Command[PlayerMovementEvent] // ] val camera: Camera ) { - def behavior: Behavior[Command] = - Behaviors.setup(ctx => new ImMovementActor(ctx, this).receive(State())) + def behavior[T: CanMove](movable: T): Behavior[Command] = + Behaviors.setup(ctx => + new ImMovementActor(ctx, this, movable).receive(State()) + ) } /** @@ -54,7 +55,8 @@ object ImMovementActor { class ImMovementActor[T]( ctx: ActorContext[ImMovementActor.Command], - props: ImMovementActor.Props[T] + props: ImMovementActor.Props, + val movable: T ) { import ImMovementActor._ import Methods._ @@ -66,19 +68,19 @@ class ImMovementActor[T]( case m: Movement => m match { case MovedLeft(pressed) => - props.enqueueR(() => stopIfNotPressed(pressed, props.movable)) + props.enqueueR(() => stopIfNotPressed(pressed, movable)) receive(state = state.modify(_.cardinalDir.left).setTo(pressed)) case MovedUp(pressed) => - props.enqueueR(() => stopIfNotPressed(pressed, props.movable)) + props.enqueueR(() => stopIfNotPressed(pressed, movable)) receive(state = state.modify(_.cardinalDir.up).setTo(pressed)) case MovedRight(pressed) => - props.enqueueR(() => stopIfNotPressed(pressed, props.movable)) + props.enqueueR(() => stopIfNotPressed(pressed, movable)) receive(state = state.modify(_.cardinalDir.right).setTo(pressed)) case MovedDown(pressed) => - props.enqueueR(() => stopIfNotPressed(pressed, props.movable)) + props.enqueueR(() => stopIfNotPressed(pressed, movable)) receive(state = state.modify(_.cardinalDir.down).setTo(pressed)) case Jump => - props.enqueueR(() => cm.jump(props.movable)) + props.enqueueR(() => cm.jump(movable)) Behaviors.same } @@ -88,7 +90,7 @@ class ImMovementActor[T]( // if (walkDir != ImVector3f.ZERO) { val tmp = walkDir * 25f * (1f / 144) props.enqueueR { () => - cm.move(props.movable, tmp) + cm.move(movable, tmp) } // } Behaviors.same diff --git a/src/main/scala/wow/doge/mygame/implicits/CatsImplicits.scala b/src/main/scala/wow/doge/mygame/implicits/CatsImplicits.scala new file mode 100644 index 0000000..97fe9af --- /dev/null +++ b/src/main/scala/wow/doge/mygame/implicits/CatsImplicits.scala @@ -0,0 +1,62 @@ +package wow.doge.mygame.implicits + +import cats.data.Kleisli +import monix.bio.IO +import monix.bio.UIO +import cats.effect.Resource +import cats.Functor +import cats.effect.Bracket +import monix.bio.Task +import cats.Show +import cats.syntax.show._ + +trait CatsExtensions { + implicit class KleisliCompanionExt(k: Kleisli.type) { + def io[S, E, A](f: S => IO[E, A]): Kleisli[UIO, S, Either[E, A]] = + k.apply(s => f(s).attempt) + } + implicit class KleisliExt[S, E, A](k: Kleisli[UIO, S, Either[E, A]]) { + def runIO(s: S) = k.run(s).rethrow + } + implicit class ResourceCompanionExt(r: Resource.type) { + // def ioMake[UIO, R](acquire: )(release: ) = r.make()() + def ioMake[E: Show, A]( + acquire: IO[E, A] + )( + release: A => UIO[Unit] + )(implicit F: Functor[UIO]): Resource[UIO, Either[E, A]] = + r.make(acquire.attempt)(a => + IO.fromEither(a) + .onErrorHandleWith(err => IO.terminate(new Exception(err.show))) + .flatMap(release) + ) + + val acq = IO(1).onErrorHandleWith(_ => IO.raiseError("")) + + val res = + ioMake(acq)(_ => IO.unit) + val result = res + .use { + case Left(value) => Task(Left(value)) + case Right(value) => Task(Right(value)) + } + .hideErrors + .rethrow + + // IO.unit.bracket() + } + implicit class ResourceExt[E, A](k: Resource[UIO, Either[E, A]]) { + // def runIO(s: S) = k.run(s).rethrow + // k.use + // : IO[E, B] + // def useIO[B](f: Either[E, A] => IO[E, B]) = + // k.use(f).rethrow + // type test[A] = Tuple2[*, Double] + type IoResource[X, D] = Resource[IO[X, *], D] + val x: Resource[IO[String, *], String] = + Resource.make(IO.raiseError(""))(_ => IO.unit) + // x.use(s => Task.unit) + val x2: IoResource[String, String] = + Resource.make(UIO(""))(_ => IO.unit) + } +} diff --git a/src/main/scala/wow/doge/mygame/implicits/package.scala b/src/main/scala/wow/doge/mygame/implicits/package.scala index d858cf2..8060c52 100644 --- a/src/main/scala/wow/doge/mygame/implicits/package.scala +++ b/src/main/scala/wow/doge/mygame/implicits/package.scala @@ -39,6 +39,8 @@ import com.simsilica.es.EntityComponent import com.simsilica.es.EntityData import com.simsilica.es.EntityId import enumeratum._ +import io.odin.meta.Position +import io.odin.meta.Render import monix.bio.Task import monix.bio.UIO import monix.execution.Ack @@ -797,7 +799,7 @@ package object implicits { } implicit class AkkaLoggerExt(private val logger: Logger) extends AnyVal { - def logP[T]( + private def logP[T]( x: sourcecode.Text[T], tag: String = "", width: Int = 100, @@ -806,12 +808,10 @@ package object implicits { initialOffset: Int = 0 )(implicit line: sourcecode.Line, fileName: sourcecode.FileName) = { - // def joinSeq[T](seq: Seq[T], sep: T): Seq[T] = { - // seq.flatMap(x => Seq(x, sep)).dropRight(1) - // } val tagStrs = if (tag.isEmpty) Seq.empty else Seq(fansi.Color.Cyan(tag), fansi.Str(" ")) + // "".slice(1, -1) val prefix = Seq( fansi.Color.Magenta(fileName.value), fansi.Str(":"), @@ -823,7 +823,6 @@ package object implicits { fansi.Str.join( prefix ++ pprint.tokenize(x.value, width, height, indent).toSeq: _* ) - // x.value } def warnP[T]( @@ -858,4 +857,18 @@ package object implicits { } } + + implicit class odinLoggerExt(private val logger: io.odin.Logger[Task]) + extends AnyVal { + def debugU[M](msg: => M)(implicit render: Render[M], position: Position) = + logger.debug(msg).hideErrors + def infoU[M](msg: => M)(implicit render: Render[M], position: Position) = + logger.info(msg).hideErrors + def traceU[M](msg: => M)(implicit render: Render[M], position: Position) = + logger.trace(msg).hideErrors + def warnU[M](msg: => M)(implicit render: Render[M], position: Position) = + logger.warn(msg).hideErrors + def errorU[M](msg: => M)(implicit render: Render[M], position: Position) = + logger.error(msg).hideErrors + } } diff --git a/src/main/scala/wow/doge/mygame/launcher/Launcher.scala b/src/main/scala/wow/doge/mygame/launcher/Launcher.scala index 0243ee0..9da2904 100644 --- a/src/main/scala/wow/doge/mygame/launcher/Launcher.scala +++ b/src/main/scala/wow/doge/mygame/launcher/Launcher.scala @@ -4,7 +4,6 @@ import cats.effect.Resource import cats.effect.concurrent.Deferred import cats.kernel.Eq import javafx.application.Platform -import javafx.beans.value.ObservableValue import monix.bio.Task import monix.catnap.CancelableF import monix.execution.CancelablePromise @@ -13,7 +12,6 @@ import monix.{eval => me} import scalafx.Includes._ import scalafx.application.JFXApp import scalafx.application.JFXApp.PrimaryStage -import scalafx.beans.property.StringProperty import scalafx.scene.control.Button import scalafx.stage.StageStyle import wow.doge.mygame.executors.Schedulers @@ -47,23 +45,10 @@ class Launcher private (props: Launcher.Props) { .observableAction() .doOnNext(_ => toTask(props.signal.complete(LauncherResult.LaunchGame))) - def testChangeObs( - obs: Observable[(ObservableValue[_ <: String], String, String)] - ) = - obs - .doOnNext { - case (x, y, z) => monix.eval.Task.unit - } - // .subscribe() - private lazy val exitButton = new Button { text = "Exit" - // text <-- testChangeObs } - // exitButton.text.bind - StringProperty("") addListener ((_, _, _) => ()) - private lazy val exitAction = exitButton .observableAction() @@ -103,27 +88,6 @@ class Launcher private (props: Launcher.Props) { ) } - // import cats.syntax.all._ - - // def init(delay: FiniteDuration = 2000.millis) = - // for { - // _ <- Task(Platform.setImplicitExit(false)) - // x <- (Task.unit.start, Task.unit.start).parTupled - // fxAppStartFib <- Task(internal.main(Array.empty)).start - // _ <- Task.sleep(500.millis) - // sceneDragFib <- toIO(sceneDragObservable.completedL).start - // buttonActionsComposedFib <- toIO( - // Observable(launchAction, exitAction).merge - // .doOnNext(_ => - // me.Task(internal.stage.close()).executeOn(props.schedulers.fx) - // ) - // .completedL - // ).start - // c <- CancelableF[Task]( - // fxAppStartFib.cancel >> buttonActionsComposedFib.cancel >> sceneDragFib.cancel - // ) - // } yield (c) - def init = Resource.make(for { _ <- Task(Platform.setImplicitExit(false)) @@ -148,13 +112,7 @@ class Launcher private (props: Launcher.Props) { ) ) .start - c <- CancelableF[Task]( - // Task(println("Cancelling")) >> - // combinedFib.cancel >> - // fxAppStartFib.cancel - // Task.unit - combinedFib.cancel - ) + c <- CancelableF[Task](combinedFib.cancel) } yield c)(_.cancel) } diff --git a/src/main/scala/wow/doge/mygame/math/ImVector3f.scala b/src/main/scala/wow/doge/mygame/math/ImVector3f.scala index cdd0a61..e1b3750 100644 --- a/src/main/scala/wow/doge/mygame/math/ImVector3f.scala +++ b/src/main/scala/wow/doge/mygame/math/ImVector3f.scala @@ -1,9 +1,10 @@ package wow.doge.mygame.math; -import Math.{sqrt, pow} +import cats.Show -case class ImVector3f(x: Float = 0f, y: Float = 0f, z: Float = 0f) +import math.{abs, pow, sqrt} +case class ImVector3f(x: Float, y: Float, z: Float) object ImVector3f { val ZERO = ImVector3f(0, 0, 0) val UNIT_X = ImVector3f(1, 0, 0) @@ -11,5 +12,18 @@ object ImVector3f { val UNIT_Z = ImVector3f(0, 0, 1) def dst(v1: ImVector3f, v2: ImVector3f) = - sqrt(pow(v1.x - v2.x, 2) + pow(v1.y - v2.y, 2) + pow(v1.z - v2.z, 2)) + sqrt( + pow((v1.x - v2.x).toDouble, 2) + pow((v1.y - v2.y).toDouble, 2) + pow( + (v1.z - v2.z).toDouble, + 2 + ) + ) + def manhattanDst(v1: ImVector3f, v2: ImVector3f) = + abs(v1.x - v2.x) + abs(v1.y - v2.y) + abs(v1.z - v2.z) + + implicit val showForImVector3f = new Show[ImVector3f] { + def format(f: Float) = f.formatted("%.2f") + override def show(t: ImVector3f): String = + s"ImVector3f(${format(t.x)},${format(t.y)},${format(t.z)})" + } } diff --git a/src/main/scala/wow/doge/mygame/subsystems/events/EventBus.scala b/src/main/scala/wow/doge/mygame/subsystems/events/EventBus.scala index c5decdb..7d12d31 100644 --- a/src/main/scala/wow/doge/mygame/subsystems/events/EventBus.scala +++ b/src/main/scala/wow/doge/mygame/subsystems/events/EventBus.scala @@ -6,6 +6,20 @@ import akka.actor.typed.ActorRef import akka.actor.typed.Behavior import akka.actor.typed.scaladsl.Behaviors import akka.event.EventStream +import monix.reactive.Observable +import monix.reactive.OverflowStrategy +import monix.execution.cancelables.SingleAssignCancelable +import monix.execution.Ack +import akka.util.Timeout +import akka.actor.typed.Scheduler +import akka.actor.typed.SpawnProtocol +import scala.util.Random +import akka.actor.typed.scaladsl.AskPattern._ +import monix.execution.Cancelable +import wow.doge.mygame.utils.AkkaUtils +import wow.doge.mygame.implicits._ +import monix.bio.UIO +import wow.doge.mygame.subsystems.events.EventBus.ObservableSubscription /** * A (typed) event bus @@ -21,36 +35,129 @@ object EventBus { final case class Subscribe[A, E <: A](subscriber: ActorRef[E])(implicit classTag: ClassTag[E] ) extends Command[A] { - def topic: Class[_] = classTag.runtimeClass } final case class Unsubscribe[A, E <: A](subscriber: ActorRef[E]) extends Command[A] - def apply[A](): Behavior[EventBus.Command[A]] = + final case class ObservableSubscription[A, E <: A]( + replyTo: ActorRef[Observable[E]] + )(implicit + classTag: ClassTag[E] + ) extends Command[A] { + def ct = classTag + } + + def apply[A: ClassTag]()(implicit + timeout: Timeout, + spawnProtocol: ActorRef[SpawnProtocol.Command] + ): Behavior[EventBus.Command[A]] = Behaviors.setup { ctx => val eventStream = new EventStream(ctx.system.classicSystem) + implicit val scheduler = ctx.system.scheduler new EventBus().eventStreamBehavior(eventStream) } + def observable[E](eventBus: ActorRef[EventBus.Command[E]])(implicit + timeout: Timeout, + scheduler: Scheduler, + spawnProtocol: ActorRef[SpawnProtocol.Command], + ct: ClassTag[E] + ) = + Observable.create[E](OverflowStrategy.DropOld(50)) { sub => + implicit val s = sub.scheduler + val c = SingleAssignCancelable() + val behavior = Behaviors.receive[E] { (ctx, msg) => + if (sub.onNext(msg) == Ack.Stop) { + c.cancel() + Behaviors.stopped + } else Behaviors.same + + } + val actor = + AkkaUtils + .spawnActorL( + behavior, + s"eventBusObservable-${ct.toString}-${Random.nextLong()}" + ) + .tapError { + case ex => UIO(sub.onError(ex)) + } + + (for { + a <- actor + _ <- eventBus !! Subscribe(a) + _ <- UIO(c := Cancelable(() => eventBus ! Unsubscribe(a))) + } yield ()).runToFuture + c + } + + def observable2[A, B <: A](eventBus: ActorRef[EventBus.Command[A]])(implicit + timeout: Timeout, + scheduler: Scheduler, + spawnProtocol: ActorRef[SpawnProtocol.Command], + ct: ClassTag[A], + ct2: ClassTag[B] + ) = + Observable.create[B](OverflowStrategy.DropOld(50)) { sub => + implicit val s = sub.scheduler + val c = SingleAssignCancelable() + val behavior = Behaviors.receive[B] { (ctx, msg) => + if (sub.onNext(msg) == Ack.Stop) { + c.cancel() + Behaviors.stopped + } else Behaviors.same + + } + val actor = + AkkaUtils + .spawnActorL( + behavior, + s"eventBusObservable-${ct.toString}-${math.abs(Random.nextLong())}" + ) + .tapError { + case ex => UIO(sub.onError(ex)) + } + + (for { + a <- actor + _ <- eventBus !! Subscribe(a) + _ <- UIO(c := Cancelable(() => eventBus ! Unsubscribe(a))) + } yield ()).runToFuture + c + } + } -class EventBus[B] { +class EventBus[A] { import akka.actor.typed.scaladsl.adapter._ private def eventStreamBehavior( - eventStream: akka.event.EventStream - ): Behavior[EventBus.Command[B]] = - Behaviors.receiveMessage { - case EventBus.Publish(event, name) => - eventStream.publish(event) - Behaviors.same - case s @ EventBus.Subscribe(subscriber) => - eventStream.subscribe(subscriber.toClassic, s.topic) - Behaviors.same - case EventBus.Unsubscribe(subscriber) => - eventStream.unsubscribe(subscriber.toClassic) - Behaviors.same + eventStream: EventStream + )(implicit + timeout: Timeout, + scheduler: Scheduler, + spawnProtocol: ActorRef[SpawnProtocol.Command], + ct: ClassTag[A] + ): Behavior[EventBus.Command[A]] = + Behaviors.setup { ctx => + Behaviors.receiveMessage { + case EventBus.Publish(event, name) => + eventStream.publish(event) + Behaviors.same + case s @ EventBus.Subscribe(subscriber) => + eventStream.subscribe(subscriber.toClassic, s.topic) + Behaviors.same + case EventBus.Unsubscribe(subscriber) => + eventStream.unsubscribe(subscriber.toClassic) + Behaviors.same + case s @ ObservableSubscription(replyTo) => + val obs = EventBus.observable2( + ctx.self + )(timeout, scheduler, spawnProtocol, ct, s.ct) + replyTo ! obs + Behaviors.same + } } } diff --git a/src/main/scala/wow/doge/mygame/subsystems/events/EventsModule.scala b/src/main/scala/wow/doge/mygame/subsystems/events/EventsModule.scala index 9919d3f..200b18d 100644 --- a/src/main/scala/wow/doge/mygame/subsystems/events/EventsModule.scala +++ b/src/main/scala/wow/doge/mygame/subsystems/events/EventsModule.scala @@ -3,7 +3,6 @@ package wow.doge.mygame.subsystems.events import scala.concurrent.duration._ import akka.actor.typed.ActorRef -import akka.actor.typed.ActorSystem import akka.actor.typed.LogOptions import akka.actor.typed.Props import akka.actor.typed.SpawnProtocol @@ -16,10 +15,15 @@ import wow.doge.mygame.implicits._ import wow.doge.mygame.subsystems.events.Event import wow.doge.mygame.subsystems.events.EventBus import wow.doge.mygame.subsystems.events.TickEvent +import scala.reflect.ClassTag +import akka.actor.typed.Scheduler -class EventsModule(spawnProtocol: ActorSystem[SpawnProtocol.Command]) { - implicit val s = spawnProtocol.scheduler - +class EventsModule( + scheduler: Scheduler, + spawnProtocol: ActorRef[SpawnProtocol.Command] +) { + implicit val s = scheduler + implicit val sp = spawnProtocol implicit val timeout = Timeout(1.second) val eventBusLogger = SLogger[EventBus[_]] @@ -35,7 +39,10 @@ class EventsModule(spawnProtocol: ActorSystem[SpawnProtocol.Command]) { val mainEventBusTask = createEventBus[Event]("mainEventBus") - def createEventBus[T](busName: String, logLevel: Level = Level.DEBUG) = + def createEventBus[T: ClassTag]( + busName: String, + logLevel: Level = Level.DEBUG + ) = spawnProtocol.askL( SpawnProtocol.Spawn[EventBus.Command[T]]( Behaviors.logMessages( diff --git a/src/main/scala/wow/doge/mygame/subsystems/moddingsystem/ModdingSystem.scala b/src/main/scala/wow/doge/mygame/subsystems/moddingsystem/ModdingSystem.scala index f54af19..b82e87f 100644 --- a/src/main/scala/wow/doge/mygame/subsystems/moddingsystem/ModdingSystem.scala +++ b/src/main/scala/wow/doge/mygame/subsystems/moddingsystem/ModdingSystem.scala @@ -27,7 +27,7 @@ object Plugin { } object ModdingSystem { - sealed trait Error extends Serializable with Product + sealed trait Error final case class CouldNotDecode(cause: String) extends Error final case class ParseFailure(cause: String) extends Error final case class FileNotFound(fileName: String) extends Error @@ -53,7 +53,7 @@ object ModdingSystem { def findAndReadPluginFiles( dir: os.Path, plugins: ArraySeq[Plugin] - ) = + ): (View[(Plugin, Error)], View[(Plugin, String)]) = plugins .sortBy(_.priority) .view @@ -79,9 +79,8 @@ object ModdingSystem { } } - def readPluginFiles(filePaths: View[os.Path]) = { + def readPluginFiles(filePaths: View[os.Path]) = filePaths.map(path => os.read(path)) - } def parsePluginFiles(files: View[(Plugin, String)]) = files @@ -99,11 +98,10 @@ object ModdingSystem { case (json, value) => json.deepMerge(value) } - def mergePluginData(plugins: View[(Plugin, Json)]) = { + def mergePluginData(plugins: View[(Plugin, Json)]) = foldMerge(plugins.map { case (p, json) => json }) - } def mergePluginDataConsumer = Consumer.foldLeft[Json, Json](Json.fromString("empty")) { @@ -143,13 +141,13 @@ object ModdingSystem { .map { case (p, json) => json } .consumeWith(loadBalancedPluginDataMerger) ) - .onErrorHandle(e => GenericError) + .hideErrors _ <- UIO { println(s"Read Successes = ${readSuccesses.to(Seq)}") println(s"Read Failures = ${readFailures.to(Seq)}") println(s"Parse Successes = ${parseSuccesses.to(Seq)}") println(s"Parse Failures = ${parseFailures.to(Seq)}") - println(s"Merged = $res") + println(show"Merged = $res") } } yield () diff --git a/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptActor.scala b/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptActor.scala index 2b92bb2..a48b617 100644 --- a/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptActor.scala +++ b/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptActor.scala @@ -181,12 +181,11 @@ class ScriptActor( os.Path, Either[wow.doge.mygame.state.ScriptActor.Error, Any] ] - ): LOL = { + ): LOL = paths match { case head :: next => loop(next, scriptsMap + (head -> getScript(head))) case Nil => scriptsMap } - } loop(paths, Map.empty) } diff --git a/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptCachingActor.scala b/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptCachingActor.scala index f2953a5..27a6e80 100644 --- a/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptCachingActor.scala +++ b/src/main/scala/wow/doge/mygame/subsystems/scriptsystem/ScriptCachingActor.scala @@ -99,7 +99,6 @@ class ScriptCachingActor( ) { import com.softwaremill.quicklens._ import ScriptCachingActor._ - import Methods._ def receiveMessage(state: State): Behavior[Command] = Behaviors.receiveMessage { msg => msg match { @@ -108,7 +107,6 @@ class ScriptCachingActor( ctx.self ! DelegateToChild(scriptActor, scriptPath, requester) else getOrCompileScript( - ctx, scriptPath, state.scriptsMap, scriptActor, @@ -165,7 +163,6 @@ class ScriptCachingActor( implicit val timeout = Timeout(15.seconds) // child ! ScriptActor.CompileAny(scriptPath, requester) askChildForScriptCompilation( - ctx, scriptActor, scriptPath, requester @@ -200,29 +197,12 @@ class ScriptCachingActor( } } -} - -object ScriptActorPool { - def apply( - poolSize: Int - ): PoolRouter[ScriptActor.Command] = - Routers.pool(poolSize = poolSize)( - // make sure the workers are restarted if they fail - Behaviors - .supervise(ScriptActor()) - .onFailure[Exception](SupervisorStrategy.restart) - ) -} - -private[scriptsystem] object Methods { - import ScriptCachingActor._ def getOrCompileScript( - ctx: ActorContext[Command], scriptPath: os.Path, scriptsMap: ScriptsMap, scriptActor: ActorRef[ScriptActor.Command], requester: ActorRef[ScriptResult] - ) = { + ) = scriptsMap .get(scriptPath) .fold { @@ -236,10 +216,8 @@ private[scriptsystem] object Methods { ctx.log.debugP("Getting script from cache") requester ! Right(s) } - } def askChildForScriptCompilation( - ctx: ActorContext[Command], scriptActor: ActorRef[ScriptActor.Command], scriptPath: os.Path, requester: ActorRef[ScriptResult] @@ -249,7 +227,7 @@ private[scriptsystem] object Methods { requester ! value value.fold( err => { - ctx.log.error(err.reason) + ctx.log.errorP(err.reason) NoOp }, res => { @@ -257,9 +235,22 @@ private[scriptsystem] object Methods { } ) case Failure(exception) => { - ctx.log.error(exception.getMessage()) + ctx.log.errorP(exception.getMessage) NoOp } } } + +} + +object ScriptActorPool { + def apply( + poolSize: Int + ): PoolRouter[ScriptActor.Command] = + Routers.pool(poolSize = poolSize)( + // make sure the workers are restarted if they fail + Behaviors + .supervise(ScriptActor()) + .onFailure[Exception](SupervisorStrategy.restart) + ) } diff --git a/src/main/scala/wow/doge/mygame/utils/AkkaUtils.scala b/src/main/scala/wow/doge/mygame/utils/AkkaUtils.scala index 2953f01..38f71c1 100644 --- a/src/main/scala/wow/doge/mygame/utils/AkkaUtils.scala +++ b/src/main/scala/wow/doge/mygame/utils/AkkaUtils.scala @@ -7,8 +7,12 @@ import akka.actor.typed.Scheduler import akka.actor.typed.SpawnProtocol import akka.util.Timeout import wow.doge.mygame.implicits._ +import java.util.concurrent.TimeoutException +import monix.bio.IO +import wow.doge.mygame.AppError.TimeoutError object AkkaUtils { + def spawnActorOldL[T]( spawnProtocol: ActorRef[SpawnProtocol.Command], actorName: String, @@ -24,18 +28,23 @@ object AkkaUtils { ) def spawnActorL[T]( behavior: Behavior[T], - actorName: String + actorName: String, + props: Props = Props.empty )(implicit timeout: Timeout, scheduler: Scheduler, spawnProtocol: ActorRef[SpawnProtocol.Command] ) = - spawnProtocol.askL[ActorRef[T]]( - SpawnProtocol.Spawn( - behavior, - actorName, - Props.empty, - _ + spawnProtocol + .askL[ActorRef[T]]( + SpawnProtocol.Spawn( + behavior, + actorName, + props, + _ + ) ) - ) + // .onErrorHandleWith { + // case ex: TimeoutException => IO.raiseError(TimeoutError(ex.getMessage)) + // } } diff --git a/src/main/scala/wow/doge/mygame/utils/GenericConsoleStream.scala b/src/main/scala/wow/doge/mygame/utils/GenericConsoleStream.scala index 318df1a..ca39707 100644 --- a/src/main/scala/wow/doge/mygame/utils/GenericConsoleStream.scala +++ b/src/main/scala/wow/doge/mygame/utils/GenericConsoleStream.scala @@ -16,7 +16,7 @@ class GenericConsoleStream[T]( outputStream: OutputStream, val config: GenericConsoleStream.Config = GenericConsoleStream.Config.default, - // TODO make this atomic + // TODO make this atomic ? private var _streamable: Option[T] = None )(implicit cs: ConsoleStreamable[T] @@ -27,27 +27,26 @@ class GenericConsoleStream[T]( stble.foreach(s => cs.println(s, text)) override def println(text: String): Unit = - if (config.exclusive) { + if (config.exclusive) printToStreamable(_streamable, text) - } else { + else { defaultOut.println(text) printToStreamable(_streamable, text) } override def print(text: String): Unit = - if (config.exclusive) { + if (config.exclusive) printToStreamable(_streamable, text) - } else { + else { defaultOut.println(text) printToStreamable(_streamable, text) } - def :=(s: T) = { + def :=(s: T) = _streamable match { case Some(value) => case None => _streamable = Some(s) } - } } object GenericConsoleStream { diff --git a/src/main/scala/wow/doge/mygame/utils/GenericTimerActor.scala b/src/main/scala/wow/doge/mygame/utils/GenericTimerActor.scala new file mode 100644 index 0000000..96a66e0 --- /dev/null +++ b/src/main/scala/wow/doge/mygame/utils/GenericTimerActor.scala @@ -0,0 +1,66 @@ +package wow.doge.mygame.utils + +import akka.actor.typed.scaladsl.Behaviors +import scala.concurrent.duration.FiniteDuration +import akka.actor.typed.ActorRef +import akka.actor.typed.Behavior +import akka.actor.typed.scaladsl.TimerScheduler +import scala.util.Random +import akka.actor.typed.scaladsl.ActorContext +import wow.doge.mygame.implicits._ + +object GenericTimerActor { + sealed trait Command + final case object Start extends Command + final case object Stop extends Command + private case object Tick extends Command + case class TimerKey(seed: Long) + + case class Props[T]( + target: ActorRef[T], + messageToSend: T, + timeInterval: FiniteDuration + ) { + def behavior = + Behaviors.withTimers[Command] { timers => + Behaviors.setup { ctx => + new GenericTimerActor( + ctx, + timers, + TimerKey(Random.nextLong()), + this + ).idle + } + } + } +} +class GenericTimerActor[T]( + ctx: ActorContext[GenericTimerActor.Command], + timers: TimerScheduler[GenericTimerActor.Command], + timerKey: GenericTimerActor.TimerKey, + props: GenericTimerActor.Props[T] +) { + import GenericTimerActor._ + + val idle: Behavior[Command] = + Behaviors.receiveMessage { + case Start => + timers.startTimerWithFixedDelay(timerKey, Tick, props.timeInterval) + active + case _ => Behaviors.unhandled + + } + + val active: Behavior[Command] = + Behaviors.receiveMessage { + case Start => + ctx.log.warnP(s"Timer-${timerKey.seed}: Timer already started") + Behaviors.same + case Tick => + props.target ! props.messageToSend + Behaviors.same + case Stop => + timers.cancel(timerKey) + idle + } +} diff --git a/src/main/scala/wow/doge/mygame/utils/wrappers/jme/AssetManager.scala b/src/main/scala/wow/doge/mygame/utils/wrappers/jme/AssetManager.scala new file mode 100644 index 0000000..9f63fc1 --- /dev/null +++ b/src/main/scala/wow/doge/mygame/utils/wrappers/jme/AssetManager.scala @@ -0,0 +1,61 @@ +package wow.doge.mygame.utils.wrappers.jme +import scala.reflect.ClassTag + +import com.jme3.asset.AssetLoadException +import com.jme3.asset.AssetLocator +import com.jme3.asset.AssetNotFoundException +import com.jme3.scene.Spatial +import com.jme3.{asset => jmea} +import monix.bio.IO +import monix.bio.UIO + +class AssetManager(assetManager: jmea.AssetManager) { + import AssetManager._ + def loadModel(path: os.RelPath): IO[Error, Spatial] = + IO(assetManager.loadModel(path.toString)).onErrorHandleWith { + case ex: AssetNotFoundException => + IO.raiseError(AssetNotFound(ex.getMessage)) + case ex: AssetLoadException => + IO.raiseError(AssetLoadError(ex.getMessage)) + } + def loadModelAs[T <: Spatial]( + path: os.RelPath + )(implicit ct: ClassTag[T]): IO[Error, T] = + loadModel(path).flatMap(model => + if (model.getClass == ct.runtimeClass) + UIO(model.asInstanceOf[T]) + else IO.raiseError(CouldNotCastError) + ) + def loadAssetAs[T](path: os.RelPath)(implicit ct: ClassTag[T]): IO[Error, T] = + IO(assetManager.loadAsset(path.toString)) + .onErrorHandleWith { + case ex: AssetNotFoundException => + IO.raiseError(AssetNotFound(ex.getMessage)) + case ex: AssetLoadException => + IO.raiseError(AssetLoadError(ex.getMessage)) + } + .flatMap(asset => + if (asset.getClass == ct.runtimeClass) + UIO(asset.asInstanceOf[T]) + else IO.raiseError(CouldNotCastError) + ) + def registerLocator(path: os.RelPath, locator: Class[_ <: AssetLocator]) = + UIO(assetManager.registerLocator(path.toString, locator)) + +} +object AssetManager { + sealed trait Error + case class AssetNotFound(message: String) extends Error + case class AssetLoadError(message: String) extends Error + case object CouldNotCastError extends Error + import cats.data.ReaderT + type IoReaderT[S, E, A] = ReaderT[UIO, S, Either[E, A]] + val IoReaderT = ReaderT + val t = + ReaderT[UIO, String, Either[Error, Unit]](s => UIO.unit.attempt) + .run("s") + .rethrow + val r: IoReaderT[String, Error, Unit] = IoReaderT(s => UIO.unit.attempt) + val t2 = r.run("s").rethrow + // Kleisli[IO, String, Unit](s => IO.unit) +} diff --git a/src/main/scala/wow/doge/mygame/utils/wrappers/jme/CollisionShapeFactory.scala b/src/main/scala/wow/doge/mygame/utils/wrappers/jme/CollisionShapeFactory.scala new file mode 100644 index 0000000..178a201 --- /dev/null +++ b/src/main/scala/wow/doge/mygame/utils/wrappers/jme/CollisionShapeFactory.scala @@ -0,0 +1,18 @@ +package wow.doge.mygame.utils.wrappers.jme + +import com.jme3.bullet.collision.shapes.CollisionShape +import com.jme3.bullet.{util => jmebu} +import com.jme3.scene.Spatial +import monix.bio.IO + +object CollisionShapeFactory { + sealed trait Error + case class WrongArgumentError(reason: String) extends Error + + def createMeshShape(subtree: Spatial): IO[Error, CollisionShape] = + IO(jmebu.CollisionShapeFactory.createMeshShape(subtree)).onErrorHandleWith { + case ex: IllegalArgumentException + if (ex.getMessage.startsWith("The spatial must either be a Node")) => + IO.raiseError(WrongArgumentError(ex.getMessage)) + } +} diff --git a/src/main/scala/wow/doge/mygame/utils/wrappers/jme/Node.scala b/src/main/scala/wow/doge/mygame/utils/wrappers/jme/Node.scala index fdac5d0..4f124cc 100644 --- a/src/main/scala/wow/doge/mygame/utils/wrappers/jme/Node.scala +++ b/src/main/scala/wow/doge/mygame/utils/wrappers/jme/Node.scala @@ -1,28 +1,23 @@ package wow.doge.mygame.utils.wrappers.jme import cats.effect.Sync +import cats.syntax.eq._ +import com.jme3.light.Light import com.jme3.{scene => jmes} +import monix.bio.IO +import monix.bio.Task +import monix.bio.UIO import monix.execution.annotations.UnsafeBecauseImpure import monix.reactive.Observable import wow.doge.mygame.implicits._ -import com.jme3.light.Light - -trait NodeDelegate { - - /** - * Get the underlying wrapped value - */ - @UnsafeBecauseImpure - def unsafeDelegate: jmes.Node -} abstract class NodeWrapper[F[_]: Sync] protected (node: jmes.Node) { + def name = node.getName() def children: Observable[jmes.Spatial] = node.observableChildren def attachChild(n: jmes.Spatial): F[Unit] = Sync[F].delay(node.attachChild(n)) def add(wn: Node[F]): F[Unit] = Sync[F].delay(node.attachChild(wn.unsafeDelegate)) - def remove(n: jmes.Spatial): F[Unit] = - Sync[F].delay(node.detachChild(n)) + def remove(n: jmes.Spatial): F[Unit] = Sync[F].delay(node.detachChild(n)) def remove(wn: Node[F]): F[Unit] = Sync[F].delay(node.detachChild(wn.unsafeDelegate)) def addLight(light: Light) = @@ -52,8 +47,7 @@ object NodeWrapper { } final class Node[F[_]: Sync] private (node: jmes.Node) - extends NodeWrapper[F](node) - with NodeDelegate { + extends NodeWrapper[F](node) { /** * Get the underlying wrapped value @@ -74,3 +68,73 @@ object AppNode { def apply[F[_]: Sync](n: jmes.Node) = new AppNode[F](n) } + +abstract class NodeWrapper2 protected (node: jmes.Node) { + import NodeWrapper2._ + def name = node.getName() + def children: Observable[jmes.Spatial] = node.observableChildren + def attachChild(n: jmes.Spatial): IO[AddNodeToItselfError.type, Unit] = + IO { node.attachChild(n); () }.onErrorHandleWith { + case ex: IllegalArgumentException => + if (ex.getMessage === "Cannot add child to itself") + IO.raiseError(AddNodeToItselfError) + else IO.unit + } + def add(wn: Node2): IO[AddNodeToItselfError.type, Unit] = + IO { node.attachChild(wn.unsafeDelegate); () }.onErrorHandleWith { + case ex: IllegalArgumentException => + if (ex.getMessage === "Cannot add child to itself") + IO.raiseError(AddNodeToItselfError) + else IO.unit + } + def remove(n: jmes.Spatial) = UIO(node.detachChild(n)) + def remove(wn: Node2) = + UIO(node.detachChild(wn.unsafeDelegate)) + def addLight(light: Light) = + UIO { + node.addLight(light) + } + def removeLight(light: Light) = + UIO { + node.removeLight(light) + } + def asSpatial: Task[jmes.Spatial] = UIO(node) +} +object NodeWrapper2 { + sealed trait Error + case object AddNodeToItselfError extends Error + implicit class NodeOps[F[_]](private val nw: NodeWrapper2) extends AnyVal { + def +=(n: jmes.Spatial) = nw.attachChild(n) + def +=(n: Node2) = nw.add(n) + def -=(n: jmes.Spatial) = nw.remove(n) + def -=(wn: Node2) = nw.remove(wn) + def +=(light: Light) = { + nw.addLight(light) + } + + def -=(light: Light) = { + nw.removeLight(light) + } + } +} + +final class Node2 private (node: jmes.Node) extends NodeWrapper2(node) { + + /** + * Get the underlying wrapped value + */ + @UnsafeBecauseImpure + def unsafeDelegate = node +} +object Node2 { + def apply(name: String) = new Node2(new jmes.Node(name)) + def apply(n: jmes.Node) = new Node2(n) +} + +final class AppNode2 private (node: jmes.Node) extends NodeWrapper2(node) +object AppNode2 { + + def apply(name: String) = new AppNode2(new jmes.Node(name)) + def apply(n: jmes.Node) = new AppNode2(n) + +} diff --git a/src/main/scala/wow/doge/mygame/utils/wrappers/jme/PhysicsSpace.scala b/src/main/scala/wow/doge/mygame/utils/wrappers/jme/PhysicsSpace.scala index 22ccedd..671fa5a 100644 --- a/src/main/scala/wow/doge/mygame/utils/wrappers/jme/PhysicsSpace.scala +++ b/src/main/scala/wow/doge/mygame/utils/wrappers/jme/PhysicsSpace.scala @@ -38,6 +38,8 @@ object PhysicsSpace { space } + def -=(anyObject: Any) = space.remove(anyObject) + def +=(spatial: jmes.Spatial) = space.addAll(spatial) def :+(spatial: jmes.Spatial) = { @@ -49,5 +51,7 @@ object PhysicsSpace { space.removeAll(spatial) space } + + def -=(spatial: jmes.Spatial) = space.removeAll(spatial) } } diff --git a/src/test/scala/wow/doge/mygame/ActorTimeoutTest.scala b/src/test/scala/wow/doge/mygame/ActorTimeoutTest.scala new file mode 100644 index 0000000..9d8d673 --- /dev/null +++ b/src/test/scala/wow/doge/mygame/ActorTimeoutTest.scala @@ -0,0 +1,57 @@ +package wow.doge.mygame + +import akka.actor.typed.scaladsl.Behaviors +import akka.actor.typed.scaladsl.ActorContext +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.BeforeAndAfterAll +import akka.actor.typed.ActorSystem +import scala.concurrent.duration._ +import akka.actor.typed.ActorRef +import akka.actor.typed.scaladsl.AskPattern._ +import akka.util.Timeout +import scala.concurrent.Await + +class ActorTimeoutTest extends AnyFunSuite with BeforeAndAfterAll { + import ActorTimeoutTest._ + implicit val as = ActorSystem(new MyActor.Props().create, "system") + implicit val timeout = Timeout(1.millis) + + test("timeoutTest") { + val fut = as.ask(MyActor.GetInt(_)) + val res = Await.result(fut, 1.second) + assert(res == 1) + } + + override protected def afterAll(): Unit = { + as.terminate() + } + +} + +object ActorTimeoutTest { + object MyActor { + sealed trait Command + case class GetInt(replyTo: ActorRef[Int]) extends Command + + class Props() { + def create = + Behaviors.setup[Command] { ctx => + new MyActor(ctx, this).receive + } + } + } + class MyActor( + ctx: ActorContext[MyActor.Command], + props: MyActor.Props + ) { + import MyActor._ + def receive = + Behaviors.receiveMessage[Command] { + case GetInt(replyTo) => + // Thread.sleep(1000) + replyTo ! 1 + Behaviors.same + } + } + +} diff --git a/src/test/scala/wow/doge/mygame/AssetManagerTest.scala b/src/test/scala/wow/doge/mygame/AssetManagerTest.scala new file mode 100644 index 0000000..a1beef3 --- /dev/null +++ b/src/test/scala/wow/doge/mygame/AssetManagerTest.scala @@ -0,0 +1,59 @@ +package wow.doge.mygame + +import org.scalatest.funsuite.AnyFunSuite +import monix.execution.Scheduler.Implicits.global +import cats.syntax.eq._ +import com.jme3.{asset => jmea} +import com.jme3.asset.DesktopAssetManager +import wow.doge.mygame.utils.wrappers.jme.AssetManager +import wow.doge.mygame.utils.wrappers.jme.AssetManager.AssetNotFound +import com.jme3.scene.Geometry +import wow.doge.mygame.utils.wrappers.jme.AssetManager.CouldNotCastError +import com.jme3.scene.Node +import com.jme3.material.MaterialDef +import com.jme3.material.Material + +class AssetManagerTest extends AnyFunSuite { + + val _assetManager: jmea.AssetManager = new DesktopAssetManager(true) + val assetManager = new AssetManager(_assetManager) + + test("Test for AssetNotFound error") { + val res = + assetManager.loadModel(os.rel / "doesnotexist").attempt.runSyncUnsafe() + assert(res === Left(AssetNotFound("doesnotexist"))) + } + + test("Test for Model CouldNotCastError") { + val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o" + val res1 = assetManager + .loadModelAs[Geometry](modelPath) + .attempt + .runSyncUnsafe() + + assert(res1 === Left(CouldNotCastError)) + + val res2 = assetManager + .loadModelAs[Node](modelPath) + .attempt + .runSyncUnsafe() + assert(res2.map(_.getName) === Right("JaimeGeom-ogremesh")) + } + + test("Test for Asset CouldNotCastError") { + val assetPath = os.rel / "Common" / "MatDefs" / "Misc" / "Unshaded.j3md" + + val res1 = assetManager + .loadAssetAs[Material](assetPath) + .attempt + .runSyncUnsafe() + + assert(res1 === Left(CouldNotCastError)) + + val res2 = assetManager + .loadAssetAs[MaterialDef](assetPath) + .attempt + .runSyncUnsafe() + assert(res2.map(_.getName) === Right("Unshaded")) + } +} diff --git a/src/test/scala/wow/doge/mygame/CollisionShapeFactoryTest.scala b/src/test/scala/wow/doge/mygame/CollisionShapeFactoryTest.scala new file mode 100644 index 0000000..76e4203 --- /dev/null +++ b/src/test/scala/wow/doge/mygame/CollisionShapeFactoryTest.scala @@ -0,0 +1,51 @@ +package wow.doge.mygame + +import org.scalatest.funsuite.AnyFunSuite +import com.jme3.scene.Spatial +import com.jme3.collision.{Collidable, CollisionResults} +import com.jme3.bounding.BoundingVolume +import com.jme3.scene.Spatial.DFSMode +import com.jme3.scene.SceneGraphVisitor +import java.util.Queue +import wow.doge.mygame.utils.wrappers.jme.CollisionShapeFactory +import monix.execution.Scheduler.Implicits.global +import cats.syntax.eq._ + +class CollisionShapeFactoryTest extends AnyFunSuite { + test("Test for WrongArgumentError") { + val res = CollisionShapeFactory + .createMeshShape(new TestSpatial) + .attempt + .runSyncUnsafe() + + assert( + res === Left( + CollisionShapeFactory.WrongArgumentError( + "The spatial must either be a Node or a Geometry!" + ) + ) + ) + } +} + +class TestSpatial extends Spatial { + + override def collideWith(x$1: Collidable, x$2: CollisionResults): Int = ??? + + override def updateModelBound(): Unit = ??? + + override def setModelBound(x$1: BoundingVolume): Unit = ??? + + override def getVertexCount(): Int = ??? + + override def getTriangleCount(): Int = ??? + + override def depthFirstTraversal(x$1: SceneGraphVisitor, x$2: DFSMode): Unit = + ??? + + override protected def breadthFirstTraversal( + x$1: SceneGraphVisitor, + x$2: Queue[Spatial] + ): Unit = ??? + +}