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

3 years ago
  1. package wow.doge.mygame.subsystems.scriptsystem
  2. import javax.script.ScriptEngine
  3. import javax.script.ScriptEngineManager
  4. import javax.script.ScriptException
  5. import scala.concurrent.duration._
  6. import ammonite.main.Defaults
  7. import ammonite.runtime.Storage.Folder
  8. import ammonite.util.Res
  9. import ammonite.util.Res.Failure
  10. import cats.effect.Resource
  11. import cats.effect.concurrent.Deferred
  12. import cats.syntax.either._
  13. import cats.syntax.flatMap._
  14. import com.softwaremill.macwire._
  15. import com.softwaremill.tagging._
  16. import groovy.util.GroovyScriptEngine
  17. import io.odin.Logger
  18. import monix.bio.IO
  19. import monix.bio.Task
  20. import monix.bio.UIO
  21. import monix.catnap.ConcurrentQueue
  22. import monix.reactive.Observable
  23. import wow.doge.mygame.implicits._
  24. import monix.{eval => me}
  25. import groovy.util.ResourceException
  26. import monix.catnap.MVar
  27. trait Requestable[A] {
  28. protected def queue: ConcurrentQueue[Task, A]
  29. def request[T](
  30. compileRequest: Deferred[Task, T] => A
  31. )(implicit timeout: FiniteDuration) =
  32. for {
  33. d <- Deferred[Task, T]
  34. req = compileRequest(d)
  35. _ <- queue.offer(req)
  36. res <- d.get.timeout(timeout).map(_.get)
  37. } yield res
  38. }
  39. class ScriptCompiler private (
  40. _queue: ConcurrentQueue[Task, ScriptCompiler.Command]
  41. ) extends Requestable[ScriptCompiler.Command] {
  42. override protected def queue = _queue
  43. // def tell(item: Command) = queue.offer(item)
  44. }
  45. object ScriptCompiler {
  46. sealed trait State
  47. case object Idle extends State
  48. case object Active extends State
  49. /**
  50. * script representation
  51. */
  52. sealed trait ScriptTag
  53. type ScriptObject = Any @@ ScriptTag
  54. sealed trait KotlinEngineTag
  55. type KotlinScriptEngine = ScriptEngine @@ KotlinEngineTag
  56. sealed trait Error
  57. final case class AmmoniteFailure(error: Res.Failure) extends Error
  58. final case class AmmoniteException(error: Res.Exception) extends Error
  59. final case class ScriptExceptionError(error: ScriptException) extends Error
  60. final case class ResourceExceptionError(error: ResourceException)
  61. extends Error
  62. final case class GroovyScriptExceptionError(
  63. error: groovy.util.ScriptException
  64. ) extends Error
  65. final case class SomeError(reason: String) extends Error
  66. sealed trait Command
  67. final case class Get(
  68. path: os.Path,
  69. result: Deferred[Task, ScriptResult],
  70. force: Boolean
  71. ) extends Command
  72. final case class GetData(result: Deferred[Task, Data])
  73. final case class ObservableData(result: Deferred[Task, Observable[Data]])
  74. extends Command
  75. // extends Command
  76. // final case class CompileAll(paths: Seq[os.Path]) extends Command
  77. type ScriptsMap = Map[os.Path, Any]
  78. type ScriptResult = Either[Error, Any]
  79. sealed trait ScriptType
  80. case object ScalaType extends ScriptType
  81. case object KotlinType extends ScriptType
  82. case object GroovyType extends ScriptType
  83. val defaultScalaRunner =
  84. ammonite
  85. .Main(
  86. storageBackend = new Folder(
  87. // os.pwd / "target"
  88. Defaults.ammoniteHome,
  89. isRepl = false
  90. )
  91. )
  92. val defaultKotlinRunner: KotlinScriptEngine = {
  93. val manager = new ScriptEngineManager()
  94. val engine = manager.getEngineByExtension("main.kts")
  95. engine.taggedWith[KotlinEngineTag]
  96. }
  97. val defaultGroovyRunner: GroovyScriptEngine =
  98. new GroovyScriptEngine(os.pwd.toString)
  99. case class Data(scriptsMap: ScriptsMap)
  100. class SourceMaker(
  101. queue: ConcurrentQueue[Task, Command],
  102. worker: ScriptCompilerWorker
  103. ) {
  104. import com.softwaremill.quicklens._
  105. def get =
  106. for {
  107. dataVar <- MVar[Task].of(Data(Map.empty))
  108. obs <- Task.deferAction(implicit s =>
  109. Task(
  110. Observable
  111. .repeatEvalF(queue.poll)
  112. .scanEval0(me.Task.pure((Active: State) -> Data(Map.empty))) {
  113. case state -> data -> command =>
  114. val nextState: IO[Error, (State, Data)] = state match {
  115. case Idle => IO.pure(Idle -> data)
  116. case Active =>
  117. command match {
  118. case Get(path, result, force) =>
  119. def getAndUpdate =
  120. worker
  121. .request(
  122. ScriptCompilerWorker.CompileAny(path, _)
  123. )(
  124. 20.seconds
  125. )
  126. .flatTap(result.complete)
  127. .hideErrors
  128. .rethrow
  129. .flatMap(res =>
  130. UIO(pprint.log(res)) >>
  131. UIO.pure(
  132. data
  133. .modify(_.scriptsMap)
  134. .using(_ + (path -> res))
  135. )
  136. )
  137. for {
  138. nextData <-
  139. if (force) getAndUpdate
  140. else
  141. data.scriptsMap.get(path) match {
  142. case Some(e) =>
  143. result
  144. .complete(e.asRight[Error])
  145. .hideErrors >> UIO.pure(data)
  146. case None => getAndUpdate
  147. }
  148. } yield Active -> nextData
  149. case ObservableData(result) =>
  150. result
  151. .complete(Observable.repeatEvalF(dataVar.take))
  152. .hideErrors >> IO.pure(Active -> data)
  153. }
  154. }
  155. nextState
  156. .flatTap { case (_, data) => dataVar.put(data).hideErrors }
  157. .tapError(err => UIO(pprint.log(err.toString)))
  158. .attempt
  159. // .mapFilter(_.toOption)
  160. .map(_.getOrElse(state -> data))
  161. .toTask
  162. }
  163. )
  164. )
  165. } yield obs
  166. }
  167. class ScriptCompileFns(
  168. val scalaRunner: ammonite.Main,
  169. val kotlinRunner: KotlinScriptEngine,
  170. val groovyRunner: GroovyScriptEngine
  171. ) {
  172. def runScala(path: os.Path): Either[Error, Any] =
  173. scalaRunner
  174. .runScript(path, Seq.empty)
  175. ._1 match {
  176. case e @ Res.Exception(t, msg) => Left(AmmoniteException(e))
  177. case f @ Failure(msg) => Left(AmmoniteFailure(f))
  178. case Res.Success(obj) => Right(obj)
  179. case _ => Left(SomeError("Failed to run script"))
  180. }
  181. def runKotlin(path: os.Path): Either[Error, Any] =
  182. Either
  183. .catchNonFatal(kotlinRunner.eval(os.read(path)))
  184. .leftMap {
  185. case ex: ScriptException => ScriptExceptionError(ex)
  186. }
  187. def runGroovy(path: os.Path): Either[Error, Any] =
  188. Either
  189. .catchNonFatal(groovyRunner.run(path.relativeTo(os.pwd).toString, ""))
  190. .leftMap {
  191. case ex: ResourceException => ResourceExceptionError(ex)
  192. case ex: groovy.util.ScriptException => GroovyScriptExceptionError(ex)
  193. }
  194. def ensureReturnedObjectNotNull(scriptObject: Any): Either[Error, Any] =
  195. Either.fromOption(Option(scriptObject), SomeError("unknown object"))
  196. }
  197. class ScriptCompileSource(
  198. fns: ScriptCompileFns,
  199. logger: Logger[Task],
  200. queue: ConcurrentQueue[Task, ScriptCompilerWorker.CompileRequest]
  201. ) {
  202. import fns._
  203. val source =
  204. Task.deferAction(implicit s =>
  205. Task(
  206. Observable
  207. .repeatEvalF(queue.poll)
  208. .doOnNextF(el => logger.debug(s"Got $el"))
  209. .mapParallelUnorderedF(4) {
  210. case ScriptCompilerWorker.CompileAny(path, result) =>
  211. for {
  212. mbRes <- Task(
  213. runScala(path)
  214. .flatMap(ensureReturnedObjectNotNull)
  215. // .map(_.taggedWith[ScriptTag])
  216. )
  217. _ <- result.complete(mbRes)
  218. } yield mbRes
  219. }
  220. )
  221. )
  222. }
  223. // override private val
  224. final class ScriptCompilerWorker(
  225. logger: Logger[Task],
  226. _queue: ConcurrentQueue[Task, ScriptCompilerWorker.CompileRequest]
  227. ) extends Requestable[ScriptCompilerWorker.CompileRequest] {
  228. override def queue = _queue
  229. }
  230. object ScriptCompilerWorker {
  231. sealed trait CompileRequest
  232. final case class CompileAny(
  233. path: os.Path,
  234. result: Deferred[Task, ScriptResult]
  235. ) extends CompileRequest
  236. def apply(
  237. logger: Logger[Task],
  238. scalaRunner: ammonite.Main = defaultScalaRunner,
  239. kotlinRunner: KotlinScriptEngine = defaultKotlinRunner,
  240. groovyRunner: GroovyScriptEngine = defaultGroovyRunner
  241. ) = {
  242. val acquire = for {
  243. queue <- ConcurrentQueue[Task].bounded[CompileRequest](10)
  244. fns <- UIO.pure(wire[ScriptCompileFns])
  245. worker = wire[ScriptCompilerWorker]
  246. fib <- wire[ScriptCompileSource].source.flatMap(_.completedL.toIO.start)
  247. // resource = Concurrent[Task].background(
  248. // wire[ScriptCompileSource].source.flatMap(_.completedL.toIO)
  249. // )
  250. } yield worker -> fib
  251. Resource
  252. .make(acquire) { case worker -> fib => fib.cancel }
  253. .map(_._1)
  254. }
  255. }
  256. def apply(logger: Logger[Task]) = {
  257. def acquire(worker: ScriptCompilerWorker) =
  258. for {
  259. queue <- ConcurrentQueue.bounded[Task, Command](10)
  260. fib <- wire[SourceMaker].get.flatMap(_.completedL.toIO.start)
  261. } yield new ScriptCompiler(queue) -> fib
  262. ScriptCompilerWorker(logger)
  263. .flatMap(worker =>
  264. Resource.make(acquire(worker)) { case (_, fib) => fib.cancel }
  265. )
  266. .map(_._1)
  267. }
  268. }