Testing out JmonkeyEngine to make a game in Scala with Akka Actors within a pure FP layer
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

310 lines
9.7 KiB

package wow.doge.mygame.subsystems.scriptsystem
import javax.script.ScriptEngine
import javax.script.ScriptEngineManager
import javax.script.ScriptException
import scala.concurrent.duration._
import ammonite.main.Defaults
import ammonite.runtime.Storage.Folder
import ammonite.util.Res
import ammonite.util.Res.Failure
import cats.effect.Resource
import cats.effect.concurrent.Deferred
import cats.syntax.either._
import cats.syntax.flatMap._
import com.softwaremill.macwire._
import com.softwaremill.tagging._
import groovy.util.GroovyScriptEngine
import io.odin.Logger
import monix.bio.IO
import monix.bio.Task
import monix.bio.UIO
import monix.catnap.ConcurrentQueue
import monix.reactive.Observable
import wow.doge.mygame.implicits._
import monix.{eval => me}
import groovy.util.ResourceException
import monix.catnap.MVar
trait Requestable[A] {
protected def queue: ConcurrentQueue[Task, A]
def request[T](
compileRequest: Deferred[Task, T] => A
)(implicit timeout: FiniteDuration) =
for {
d <- Deferred[Task, T]
req = compileRequest(d)
_ <- queue.offer(req)
res <- d.get.timeout(timeout).map(_.get)
} yield res
}
class ScriptCompiler private (
_queue: ConcurrentQueue[Task, ScriptCompiler.Command]
) extends Requestable[ScriptCompiler.Command] {
override protected def queue = _queue
// def tell(item: Command) = queue.offer(item)
}
object ScriptCompiler {
sealed trait State
case object Idle extends State
case object Active extends State
/**
* script representation
*/
sealed trait ScriptTag
type ScriptObject = Any @@ ScriptTag
sealed trait KotlinEngineTag
type KotlinScriptEngine = ScriptEngine @@ KotlinEngineTag
sealed trait Error
final case class AmmoniteFailure(error: Res.Failure) extends Error
final case class AmmoniteException(error: Res.Exception) extends Error
final case class ScriptExceptionError(error: ScriptException) extends Error
final case class ResourceExceptionError(error: ResourceException)
extends Error
final case class GroovyScriptExceptionError(
error: groovy.util.ScriptException
) extends Error
final case class SomeError(reason: String) extends Error
sealed trait Command
final case class Get(
path: os.Path,
result: Deferred[Task, ScriptResult],
force: Boolean
) extends Command
final case class GetData(result: Deferred[Task, Data])
final case class ObservableData(result: Deferred[Task, Observable[Data]])
extends Command
// extends Command
// final case class CompileAll(paths: Seq[os.Path]) extends Command
type ScriptsMap = Map[os.Path, Any]
type ScriptResult = Either[Error, Any]
sealed trait ScriptType
case object ScalaType extends ScriptType
case object KotlinType extends ScriptType
case object GroovyType extends ScriptType
val defaultScalaRunner =
ammonite
.Main(
storageBackend = new Folder(
// os.pwd / "target"
Defaults.ammoniteHome,
isRepl = false
)
)
val defaultKotlinRunner: KotlinScriptEngine = {
val manager = new ScriptEngineManager()
val engine = manager.getEngineByExtension("main.kts")
engine.taggedWith[KotlinEngineTag]
}
val defaultGroovyRunner: GroovyScriptEngine =
new GroovyScriptEngine(os.pwd.toString)
case class Data(scriptsMap: ScriptsMap)
class SourceMaker(
queue: ConcurrentQueue[Task, Command],
worker: ScriptCompilerWorker
) {
import com.softwaremill.quicklens._
def get =
for {
dataVar <- MVar[Task].of(Data(Map.empty))
obs <- Task.deferAction(implicit s =>
Task(
Observable
.repeatEvalF(queue.poll)
.scanEval0(me.Task.pure((Active: State) -> Data(Map.empty))) {
case state -> data -> command =>
val nextState: IO[Error, (State, Data)] = state match {
case Idle => IO.pure(Idle -> data)
case Active =>
command match {
case Get(path, result, force) =>
def getAndUpdate =
worker
.request(
ScriptCompilerWorker.CompileAny(path, _)
)(
20.seconds
)
.flatTap(result.complete)
.hideErrors
.rethrow
.flatMap(res =>
UIO(pprint.log(res)) >>
UIO.pure(
data
.modify(_.scriptsMap)
.using(_ + (path -> res))
)
)
for {
nextData <-
if (force) getAndUpdate
else
data.scriptsMap.get(path) match {
case Some(e) =>
result
.complete(e.asRight[Error])
.hideErrors >> UIO.pure(data)
case None => getAndUpdate
}
} yield Active -> nextData
case ObservableData(result) =>
result
.complete(Observable.repeatEvalF(dataVar.take))
.hideErrors >> IO.pure(Active -> data)
}
}
nextState
.flatTap { case (_, data) => dataVar.put(data).hideErrors }
.tapError(err => UIO(pprint.log(err.toString)))
.attempt
// .mapFilter(_.toOption)
.map(_.getOrElse(state -> data))
.toTask
}
)
)
} yield obs
}
class ScriptCompileFns(
val scalaRunner: ammonite.Main,
val kotlinRunner: KotlinScriptEngine,
val groovyRunner: GroovyScriptEngine
) {
def runScala(path: os.Path): Either[Error, Any] =
scalaRunner
.runScript(path, Seq.empty)
._1 match {
case e @ Res.Exception(t, msg) => Left(AmmoniteException(e))
case f @ Failure(msg) => Left(AmmoniteFailure(f))
case Res.Success(obj) => Right(obj)
case _ => Left(SomeError("Failed to run script"))
}
def runKotlin(path: os.Path): Either[Error, Any] =
Either
.catchNonFatal(kotlinRunner.eval(os.read(path)))
.leftMap {
case ex: ScriptException => ScriptExceptionError(ex)
}
def runGroovy(path: os.Path): Either[Error, Any] =
Either
.catchNonFatal(groovyRunner.run(path.relativeTo(os.pwd).toString, ""))
.leftMap {
case ex: ResourceException => ResourceExceptionError(ex)
case ex: groovy.util.ScriptException => GroovyScriptExceptionError(ex)
}
def ensureReturnedObjectNotNull(scriptObject: Any): Either[Error, Any] =
Either.fromOption(Option(scriptObject), SomeError("unknown object"))
}
class ScriptCompileSource(
fns: ScriptCompileFns,
logger: Logger[Task],
queue: ConcurrentQueue[Task, ScriptCompilerWorker.CompileRequest]
) {
import fns._
val source =
Task.deferAction(implicit s =>
Task(
Observable
.repeatEvalF(queue.poll)
.doOnNextF(el => logger.debug(s"Got $el"))
.mapParallelUnorderedF(4) {
case ScriptCompilerWorker.CompileAny(path, result) =>
for {
mbRes <- Task(
runScala(path)
.flatMap(ensureReturnedObjectNotNull)
// .map(_.taggedWith[ScriptTag])
)
_ <- result.complete(mbRes)
} yield mbRes
}
)
)
}
// override private val
final class ScriptCompilerWorker(
logger: Logger[Task],
_queue: ConcurrentQueue[Task, ScriptCompilerWorker.CompileRequest]
) extends Requestable[ScriptCompilerWorker.CompileRequest] {
override def queue = _queue
}
object ScriptCompilerWorker {
sealed trait CompileRequest
final case class CompileAny(
path: os.Path,
result: Deferred[Task, ScriptResult]
) extends CompileRequest
def apply(
logger: Logger[Task],
scalaRunner: ammonite.Main = defaultScalaRunner,
kotlinRunner: KotlinScriptEngine = defaultKotlinRunner,
groovyRunner: GroovyScriptEngine = defaultGroovyRunner
) = {
val acquire = for {
queue <- ConcurrentQueue[Task].bounded[CompileRequest](10)
fns <- UIO.pure(wire[ScriptCompileFns])
worker = wire[ScriptCompilerWorker]
fib <- wire[ScriptCompileSource].source.flatMap(_.completedL.toIO.start)
// resource = Concurrent[Task].background(
// wire[ScriptCompileSource].source.flatMap(_.completedL.toIO)
// )
} yield worker -> fib
Resource
.make(acquire) { case worker -> fib => fib.cancel }
.map(_._1)
}
}
def apply(logger: Logger[Task]) = {
def acquire(worker: ScriptCompilerWorker) =
for {
queue <- ConcurrentQueue.bounded[Task, Command](10)
fib <- wire[SourceMaker].get.flatMap(_.completedL.toIO.start)
} yield new ScriptCompiler(queue) -> fib
ScriptCompilerWorker(logger)
.flatMap(worker =>
Resource.make(acquire(worker)) { case (_, fib) => fib.cancel }
)
.map(_._1)
}
}