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.
 
 

230 lines
6.8 KiB

package wow.doge.mygame.subsystems.moddingsystem
import java.io.FileNotFoundException
import java.nio.file.NoSuchFileException
import scala.collection.View
import scala.collection.immutable.ArraySeq
import cats.Show
import cats.implicits._
import cats.kernel.Eq
import io.circe._
import io.circe.generic.JsonCodec
import io.circe.generic.semiauto._
import io.circe.parser._
import monix.bio.IO
import monix.bio.UIO
import monix.reactive.Consumer
import monix.reactive.Observable
import wow.doge.mygame.utils.IOUtils
import IOUtils.toIO
@JsonCodec
final case class Test1(hello1: String, hello2: String)
@JsonCodec
final case class Test2(hello1: String)
final case class Plugin(name: String, priority: Int)
object Plugin {
implicit val decoder: Decoder[Plugin] = deriveDecoder
implicit val show = Show.fromToString[Plugin]
implicit val eq = Eq.fromUniversalEquals[Plugin]
}
object ModdingSystem {
sealed trait Error
final case class CouldNotDecode(cause: String) extends Error
final case class ParseFailure(cause: io.circe.ParsingFailure) extends Error
final case class DecodingFailure(cause: io.circe.DecodingFailure)
extends Error
final case class FileNotFound(path: os.Path) extends Error
object Error {
implicit val show = Show.fromToString[Error]
implicit val eq = Eq.fromUniversalEquals[Error]
}
def readPluginsList(dir: os.Path): IO[Error, ArraySeq[Plugin]] =
IO(parse(os.read(dir / "plugins.json")))
.onErrorHandleWith {
case _: FileNotFoundException =>
IO.raiseError(FileNotFound(dir / "plugins.json"))
case _: NoSuchFileException =>
IO.raiseError(FileNotFound(dir / "plugins.json"))
}
.flatMap(files =>
IO.fromEither(files)
.map(_.as[ArraySeq[Plugin]])
.mapError(ParseFailure)
)
.flatMap(result => IO.fromEither(result).mapError(DecodingFailure))
def findPluginFiles(dir: os.Path): View[os.Path] =
os.list(dir)
.view
.filter(f => f.ext == "json" && f.baseName.endsWith("plugin"))
def findAndReadPluginFiles(
dir: os.Path,
plugins: ArraySeq[Plugin]
): UIO[(View[(Plugin, Error)], View[(Plugin, String)])] =
UIO(
plugins
.sortBy(_.priority)
.view
.map { p =>
val path = dir / os.RelPath(p.name + ".plugin.json")
p ->
Either
.catchNonFatal(os.read(path))
.leftMap {
case _: FileNotFoundException => FileNotFound(path)
case _: NoSuchFileException => FileNotFound(path)
}
}
.partitionMap {
case (p, either) =>
either match {
case Left(value) => Left(p -> value)
case Right(value) => Right(p -> value)
}
}
)
// : (View[(Plugin, Error)], View[(Plugin, String)])
def findAndReadPluginFiles2(
dir: os.Path,
plugins: ArraySeq[Plugin]
) =
// IO.parTraverse(plugins.sortBy(_.priority))(p =>
// IO {
// val path = dir / os.RelPath(p.name + ".plugin.json")
// os.read(path)
// }
// .onErrorHandleWith {
// case _: FileNotFoundException =>
// IO.raiseError(FileNotFound(dir.toString))
// case _: NoSuchFileException =>
// IO.raiseError(FileNotFound(dir.toString))
// }
// .flatMap(r => UIO(p -> r))
// ).map {
// _.partitionMap {
// case (p, either) =>
// either match {
// case Left(value) => Left(p -> value)
// case Right(value) => Right(p -> value)
// }
// }
// }
plugins
.sortBy(_.priority)
.view
.map { p =>
val path = dir / os.RelPath(p.name + ".plugin.json")
p -> IO(os.read(path))
.onErrorHandleWith {
case _: FileNotFoundException => IO.raiseError(FileNotFound(path))
case _: NoSuchFileException => IO.raiseError(FileNotFound(path))
}
// .map(r => p -> r)
}
.map {
case (p, io) =>
io.attempt.map {
case Left(value) => Left(p -> value)
case Right(value) => Right(p -> value)
}
}
.to(List)
.parSequence
// .partitionMap {
// _.map {
// case l @ Left(value) => l
// case r @ Right(value) => r
// }
// }
// .sequence
// def readPluginFiles(filePaths: View[os.Path]) =
// filePaths.map(path => os.read(path))
// def readPluginFiles2(filePaths: View[os.Path]) =
// filePaths
// .map(path =>
// IO(os.read(path)).onErrorHandleWith {
// case _: FileNotFoundException =>
// IO.raiseError(FileNotFound(path))
// case _: NoSuchFileException =>
// IO.raiseError(FileNotFound(path))
// }
// )
// .to(List)
// .parSequence
def parsePluginFiles(files: View[(Plugin, String)]) =
files
.map {
case (p, s) => p -> parse(s)
}
.partitionMap {
case (p, Left(value)) => Left(p -> value)
case (p, Right(value)) => Right(p -> value)
}
val emptyJson = Json.fromString("empty")
val foldFn: (Json, Json) => Json = {
case (json, Json.Null) => json //ignore null values
case (json, value) => json.deepMerge(value)
}
def mergePluginDataConsumer =
Consumer.foldLeft[Json, Json](emptyJson)(foldFn)
def loadBalancedPluginDataMerger =
Consumer
.loadBalance(parallelism = 2, mergePluginDataConsumer)
.map(_.foldLeft(emptyJson)(foldFn))
def run(wd: os.Path = os.pwd) =
for {
plugins <- readPluginsList(wd)
(readFailures, readSuccesses) <- findAndReadPluginFiles(wd, plugins)
(parseFailures, parseSuccesses) <- UIO(parsePluginFiles(readSuccesses))
res <- UIO.parMap5(
UIO(readFailures.to(List)),
UIO(readSuccesses.to(List)),
UIO(parseFailures.to(List)),
UIO(parseSuccesses.to(List)),
toIO(
Observable
.fromIterable(parseSuccesses)
.map { case (p, json) => json }
.consumeWith(loadBalancedPluginDataMerger)
).hideErrors
)(Result.apply)
} yield res
def log(res: Result) =
UIO {
pprint.log(show"Read Successes = ${res.readSuccesses}")
pprint.log(show"Read Failures = ${res.readFailures}")
pprint.log(show"Parse Successes = ${res.parseSuccesses}")
pprint.log(show"Parse Failures = ${res.parseFailures}")
pprint.log(show"Merged = ${res.pluginJson}")
}
case class Result(
readFailures: List[(Plugin, Error)],
readSuccesses: List[(Plugin, String)],
parseFailures: List[(Plugin, ParsingFailure)],
parseSuccesses: List[(Plugin, Json)],
pluginJson: Json
)
object Result {
implicit val show = Show.fromToString[Result]
// implicit val eq = Eq.fromUniversalEquals[Error]
}
}