├── .scalafmt.conf ├── project ├── build.properties ├── assembly.sbt ├── release.sbt ├── scalafmt.sbt └── publishToBintray.sbt ├── version.sbt ├── MetricConfig.scala ├── src ├── main │ ├── resources │ │ ├── textures │ │ │ └── org │ │ │ │ └── konstructs │ │ │ │ ├── brick.png │ │ │ │ ├── dirt.png │ │ │ │ ├── glass.png │ │ │ │ ├── grass.png │ │ │ │ ├── sand.png │ │ │ │ ├── snow.png │ │ │ │ ├── stick.png │ │ │ │ ├── stone.png │ │ │ │ ├── test.png │ │ │ │ ├── torch.png │ │ │ │ ├── water.png │ │ │ │ ├── wood.png │ │ │ │ ├── cobble.png │ │ │ │ ├── leaves.png │ │ │ │ ├── planks.png │ │ │ │ ├── vacuum.png │ │ │ │ ├── flower-red.png │ │ │ │ ├── grass-dirt.png │ │ │ │ ├── snow-dirt.png │ │ │ │ ├── sunflower.png │ │ │ │ ├── tool-sack.png │ │ │ │ ├── work-table.png │ │ │ │ ├── flower-blue.png │ │ │ │ ├── flower-purple.png │ │ │ │ ├── flower-white.png │ │ │ │ ├── flower-yellow.png │ │ │ │ ├── hammerstone.png │ │ │ │ ├── space │ │ │ │ └── vacuum.png │ │ │ │ ├── stone-brick.png │ │ │ │ ├── gray-framed-stone.png │ │ │ │ └── white-framed-stone.png │ │ └── reference.conf │ ├── scala │ │ └── konstructs │ │ │ ├── utils │ │ │ ├── utils.scala │ │ │ ├── compress.scala │ │ │ └── diamondsquare.scala │ │ │ ├── protocol │ │ │ ├── commands.scala │ │ │ ├── server.scala │ │ │ └── client.scala │ │ │ ├── metrics │ │ │ ├── PrintMetricPlugin.scala │ │ │ ├── MetricPlugin.scala │ │ │ └── GraphiteMetricPlugin.scala │ │ │ ├── shard │ │ │ ├── ShardPosition.scala │ │ │ ├── ChunkPosition.scala │ │ │ ├── BlockData.scala │ │ │ ├── ChunkData.scala │ │ │ ├── ShardActor.scala │ │ │ └── Light.scala │ │ │ ├── plugin │ │ │ ├── tools.scala │ │ │ ├── toolsack │ │ │ │ └── sack.scala │ │ │ └── plugin.scala │ │ │ ├── api │ │ │ └── api.scala │ │ │ ├── generator.scala │ │ │ ├── main.scala │ │ │ ├── storage.scala │ │ │ ├── inventory.scala │ │ │ ├── world.scala │ │ │ ├── db.scala │ │ │ ├── universe.scala │ │ │ ├── konstructing.scala │ │ │ ├── player.scala │ │ │ └── blocks.scala │ └── java │ │ └── konstructs │ │ └── api │ │ └── messages │ │ ├── Said.java │ │ └── DamageBlockWithBlock.java └── test │ └── scala │ └── konstructs │ ├── DbSpec.scala │ └── GeometrySpec.scala ├── .gitignore ├── .travis.yml ├── LICENSE └── README.md /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | maxColumn = 120 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.10 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.1.25-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /project/assembly.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3") 2 | -------------------------------------------------------------------------------- /project/release.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.3") 2 | -------------------------------------------------------------------------------- /project/scalafmt.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.4.8") 2 | -------------------------------------------------------------------------------- /project/publishToBintray.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.3.0") 2 | -------------------------------------------------------------------------------- /MetricConfig.scala: -------------------------------------------------------------------------------- 1 | package konstructs.metric 2 | 3 | import akka.actor.ActorRef 4 | 5 | case class MetricConfig(interval: Int, listeners: Seq[ActorRef]) 6 | -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/brick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/brick.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/dirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/dirt.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/glass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/glass.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/grass.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/sand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/sand.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/snow.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/stick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/stick.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/stone.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/test.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/torch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/torch.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/water.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/water.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/wood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/wood.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/cobble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/cobble.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/leaves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/leaves.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/planks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/planks.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/vacuum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/vacuum.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/flower-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/flower-red.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/grass-dirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/grass-dirt.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/snow-dirt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/snow-dirt.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/sunflower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/sunflower.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/tool-sack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/tool-sack.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/work-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/work-table.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/flower-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/flower-blue.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/flower-purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/flower-purple.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/flower-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/flower-white.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/flower-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/flower-yellow.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/hammerstone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/hammerstone.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/space/vacuum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/space/vacuum.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/stone-brick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/stone-brick.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/gray-framed-stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/gray-framed-stone.png -------------------------------------------------------------------------------- /src/main/resources/textures/org/konstructs/white-framed-stone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructs/server/HEAD/src/main/resources/textures/org/konstructs/white-framed-stone.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache/ 6 | .history/ 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | /meta/ 19 | /db/ 20 | /binary/ 21 | /lib/ 22 | /players/ 23 | /world/ 24 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/utils/utils.scala: -------------------------------------------------------------------------------- 1 | package konstructs.utils 2 | 3 | import java.util.concurrent.TimeUnit 4 | import scala.util.Random 5 | import scala.concurrent.duration.Duration 6 | import akka.actor.Actor 7 | 8 | trait Scheduled { actor: Actor => 9 | def schedule(millis: Int, msg: Any) { 10 | val init = Duration(new Random().nextInt(millis), TimeUnit.MILLISECONDS) 11 | val freq = Duration(millis, TimeUnit.MILLISECONDS) 12 | context.system.scheduler.schedule(init, freq, self, msg)(context.dispatcher) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/utils/compress.scala: -------------------------------------------------------------------------------- 1 | package konstructs.utils 2 | 3 | import java.util.zip.{Inflater, Deflater} 4 | 5 | package object compress { 6 | 7 | def deflate(data: Array[Byte], buffer: Array[Byte], offset: Int): Int = { 8 | val compresser = new Deflater() 9 | compresser.setInput(data) 10 | compresser.finish() 11 | val size = compresser.deflate(buffer, offset, buffer.size - offset) 12 | compresser.end() 13 | size + offset 14 | } 15 | 16 | def inflate(data: Array[Byte], buffer: Array[Byte], offset: Int, length: Int): Int = { 17 | val decompresser = new Inflater() 18 | decompresser.setInput(data, offset, length) 19 | val size = decompresser.inflate(buffer) 20 | decompresser.end() 21 | size 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/protocol/commands.scala: -------------------------------------------------------------------------------- 1 | package konstructs.protocol 2 | 3 | case class SendBlock(p: Int, q: Int, x: Int, y: Int, z: Int, w: Int) 4 | case class You(pid: Int, p: Int, q: Int, x: Int, y: Int, z: Int) 5 | case class Nick(pid: Int, name: String) 6 | case class Authenticate(version: Int, name: String, token: String) 7 | case class Talk(message: String) 8 | case class Disconnect(pid: Int) 9 | case class Sign(x: Int, y: Int, z: Int, face: Int, text: String) 10 | case class Version(version: Int) 11 | case class Position(x: Float, y: Float, z: Float, rx: Float, ry: Float) { 12 | def toApiPosition = new konstructs.api.Position(x.toInt, y.toInt, z.toInt) 13 | } 14 | case class Say(message: String) 15 | case class Time(seconds: Long) 16 | case class ChunkUpdate(p: Int, q: Int, k: Int) 17 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/metrics/PrintMetricPlugin.scala: -------------------------------------------------------------------------------- 1 | package konstructs.metric 2 | 3 | import scala.collection.mutable 4 | import akka.actor.{Actor, ActorRef, Props} 5 | import konstructs.plugin.PluginConstructor 6 | import konstructs.api.messages.SetMetric 7 | import konstructs.api.MetricId 8 | 9 | class PrintMetricPlugin extends Actor { 10 | 11 | val oldMetrics = mutable.Map[MetricId, Long]() 12 | 13 | def receive = { 14 | case s: SetMetric => 15 | val old = oldMetrics.getOrElse(s.getId, 0.toLong) 16 | oldMetrics += s.getId -> s.getValue 17 | val v = s.getValue - old 18 | println(s"${s.getId.toMetricString}: $v") 19 | } 20 | 21 | } 22 | 23 | object PrintMetricPlugin { 24 | @PluginConstructor 25 | def props(name: String, universe: ActorRef) = 26 | Props(classOf[PrintMetricPlugin]) 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/konstructs/api/messages/Said.java: -------------------------------------------------------------------------------- 1 | package konstructs.api.messages; 2 | 3 | public class Said { 4 | private final String text; 5 | 6 | public Said(String text) { 7 | this.text = text; 8 | } 9 | 10 | public String getText() { 11 | return text; 12 | } 13 | 14 | @Override 15 | public boolean equals(Object o) { 16 | if (this == o) return true; 17 | if (o == null || getClass() != o.getClass()) return false; 18 | 19 | Said said = (Said) o; 20 | 21 | return text.equals(said.text); 22 | 23 | } 24 | 25 | @Override 26 | public int hashCode() { 27 | return text.hashCode(); 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return "Said(" + 33 | "text='" + text + '\'' + 34 | ')'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/shard/ShardPosition.scala: -------------------------------------------------------------------------------- 1 | package konstructs.shard 2 | 3 | import konstructs.api.Position 4 | import konstructs.Db 5 | 6 | case class ShardPosition(m: Int, n: Int, o: Int) { 7 | def local(chunk: ChunkPosition): ChunkPosition = 8 | ChunkPosition( 9 | chunk.p - m * Db.ShardSize, 10 | chunk.q - n * Db.ShardSize, 11 | chunk.k - o * Db.ShardSize 12 | ) 13 | } 14 | 15 | object ShardPosition { 16 | def apply(c: ChunkPosition): ShardPosition = { 17 | // For negative values we need to "round down", i.e. -0.01 should be -1 and not 0 18 | val m = (if (c.p < 0) (c.p - Db.ShardSize + 1) else c.p) / Db.ShardSize 19 | val n = (if (c.q < 0) (c.q - Db.ShardSize + 1) else c.q) / Db.ShardSize 20 | val o = (if (c.k < 0) (c.k - Db.ShardSize + 1) else c.k) / Db.ShardSize 21 | ShardPosition(m, n, o) 22 | } 23 | 24 | def apply(p: Position): ShardPosition = 25 | ShardPosition(ChunkPosition(p)) 26 | 27 | } 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.12.1 4 | jdk: 5 | - oraclejdk8 6 | 7 | notifications: 8 | webhooks: 9 | urls: 10 | - https://webhooks.gitter.im/e/d7d25eec49bfe73a4f9b 11 | on_success: always # options: [always|never|change] default: always 12 | on_failure: always # options: [always|never|change] default: always 13 | on_start: always # options: [always|never|change] default: always 14 | 15 | before_script: 16 | - "sbt scalafmtTest || ( sbt scalafmt; git diff --exit-code )" 17 | - "grep $(date +%Y) LICENSE" 18 | 19 | script: 20 | - sbt test assembly doc 21 | 22 | deploy: 23 | provider: releases 24 | api_key: 25 | secure: EgJ7RuEBzNjcKBuQP4jtwPU1hp8LzoacGufAaimOfG4+3pTfbPFttXjMXjb/8HP4QF1vcFH3BemPyZng20qTTP1LuoNvU3Ae4ZakCFDS0F9KfajibcmJRCqoJ7LSI6olFS/zyMWNVWkEUiBcOcLIafBTz1HB6M0ucxanN6/dUS4= 26 | file: "target/scala-*/konstructs-server-*.jar" 27 | file_glob: true 28 | on: 29 | repo: konstructs/server 30 | tags: true 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 Petter Arvidsson and Stefan Berggren 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/metrics/MetricPlugin.scala: -------------------------------------------------------------------------------- 1 | package konstructs.metric 2 | 3 | import scala.collection.mutable 4 | 5 | import akka.actor.{Actor, ActorRef, Props} 6 | 7 | import konstructs.plugin.{PluginConstructor, Config, ListConfig} 8 | import konstructs.api.MetricId 9 | import konstructs.api.messages.{IncreaseMetric, SetMetric} 10 | import konstructs.utils.Scheduled 11 | 12 | class MetricPlugin(universe: ActorRef, config: MetricConfig) extends Actor with Scheduled { 13 | import MetricPlugin.SendMetrics 14 | 15 | val metrics = mutable.Map[MetricId, Long]() 16 | 17 | schedule(config.interval, SendMetrics) 18 | 19 | def receive = { 20 | case u: IncreaseMetric => 21 | val id = u.getId 22 | val v = metrics.getOrElse(id, 0.toLong) + u.getIncrease 23 | metrics += id -> v 24 | case u: SetMetric => 25 | val id = u.getId 26 | val v = u.getValue 27 | metrics += id -> v 28 | case SendMetrics => 29 | for ((k, v) <- metrics; l <- config.listeners) { 30 | l ! new SetMetric(k, v.toInt) 31 | } 32 | } 33 | } 34 | 35 | object MetricPlugin { 36 | import konstructs.plugin.Plugin.nullAsEmpty 37 | case object SendMetrics 38 | 39 | @PluginConstructor 40 | def props( 41 | name: String, 42 | universe: ActorRef, 43 | @Config(key = "interval") interval: Int, 44 | @ListConfig( 45 | key = "listeners", 46 | elementType = classOf[ActorRef], 47 | optional = true 48 | ) listeners: Seq[ActorRef] 49 | ) = 50 | Props( 51 | classOf[MetricPlugin], 52 | universe, 53 | MetricConfig(interval, nullAsEmpty(listeners)) 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/plugin/tools.scala: -------------------------------------------------------------------------------- 1 | package konstructs.tools 2 | 3 | import java.util.UUID 4 | import akka.actor.{Actor, Props, ActorRef} 5 | import konstructs.plugin.PluginConstructor 6 | import konstructs.KonstructingViewActor 7 | import konstructs.api._ 8 | import konstructs.api.messages._ 9 | import konstructs.plugin.toolsack.ToolSackActor 10 | 11 | class WorkTableActor(universe: ActorRef) extends Actor { 12 | import WorkTableActor._ 13 | 14 | def receive = { 15 | case f: InteractTertiaryFilter => 16 | f.getMessage match { 17 | case i: InteractTertiary 18 | if i.isWorldPhase && i.getBlockAtPosition != null && i.getBlockAtPosition.getType == BlockId => 19 | if (i.getBlock != null && i.getBlock.getType == ToolSackActor.BlockId && i.getBlock.getId() != null) { 20 | context.actorOf( 21 | KonstructingViewActor 22 | .props(i.getSender, universe, i.getBlock.getId, ToolSackActor.SackView, KonstructingView, ResultView)) 23 | } else { 24 | context.actorOf( 25 | KonstructingViewActor.props(i.getSender, universe, null, EmptyView, KonstructingView, ResultView)) 26 | } 27 | f.skip(self) 28 | case _ => 29 | f.next(self) 30 | } 31 | } 32 | } 33 | 34 | object WorkTableActor { 35 | val BlockId = new BlockTypeId("org/konstructs", "work-table") 36 | val EmptyView = new InventoryView(0, 0, 0, 0) 37 | val KonstructingView = new InventoryView(9, 4, 3, 3) 38 | val ResultView = new InventoryView(11, 9, 1, 1) 39 | 40 | @PluginConstructor 41 | def props(name: String, universe: ActorRef) = Props(classOf[WorkTableActor], universe) 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/protocol/server.scala: -------------------------------------------------------------------------------- 1 | package konstructs.protocol 2 | 3 | import akka.actor.{Actor, ActorRef, Props, ActorLogging, Stash} 4 | import akka.io._ 5 | import java.net.InetSocketAddress 6 | import konstructs.plugin.PluginConstructor 7 | import konstructs.api.{BlockFactory, GetTextures, Textures} 8 | import konstructs.api.messages.GetBlockFactory 9 | 10 | class Server(name: String, universe: ActorRef) extends Actor with ActorLogging with Stash { 11 | import Tcp._ 12 | import context.system 13 | 14 | IO(Tcp) ! Bind(self, new InetSocketAddress("0.0.0.0", 4080)) 15 | 16 | universe ! GetTextures 17 | 18 | def receive = { 19 | case Textures(textures) => 20 | context.become(getFactory(textures)) 21 | universe ! GetBlockFactory.MESSAGE 22 | unstashAll() 23 | case _ => 24 | stash() 25 | } 26 | 27 | def getFactory(textures: Array[Byte]): Receive = { 28 | case b: BlockFactory => 29 | context.become(waiting(b, textures)) 30 | unstashAll() 31 | case _ => 32 | stash() 33 | } 34 | 35 | def waiting(factory: BlockFactory, textures: Array[Byte]): Receive = { 36 | case _: Bound => 37 | context.become(bound(sender, factory, textures)) 38 | case CommandFailed(_: Bind) => context stop self 39 | } 40 | 41 | def bound(listener: ActorRef, factory: BlockFactory, textures: Array[Byte]): Receive = { 42 | case Connected(remote, _) => 43 | val connection = sender 44 | val handler = context.actorOf(ClientActor.props(universe, factory, textures)) 45 | println(s"$remote connected!") 46 | connection ! Tcp.Register(handler) 47 | } 48 | } 49 | 50 | object Server { 51 | @PluginConstructor 52 | def props(name: String, universe: ActorRef) = 53 | Props(classOf[Server], name, universe) 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/api/api.scala: -------------------------------------------------------------------------------- 1 | package konstructs.api 2 | 3 | import java.util.UUID 4 | import akka.actor.ActorRef 5 | import akka.util.ByteString 6 | import com.google.gson.JsonElement 7 | 8 | /* Manage blocks */ 9 | case object GetTextures 10 | case class Textures(textures: Array[Byte]) 11 | 12 | case class CreateInventory(blockId: UUID, size: Int) 13 | case class GetInventory(blockId: UUID) 14 | case class GetInventoryResponse(blockId: UUID, inventory: Option[Inventory]) 15 | case class PutStack(blockId: UUID, slot: Int, stack: Stack) 16 | case class RemoveStack(blockId: UUID, slot: Int, amount: StackAmount) 17 | case class GetStack(blockId: UUID, slot: Int) 18 | case class GetStackResponse(blockId: UUID, slot: Int, stack: Stack) 19 | case class DeleteInventory(blockId: UUID) 20 | case class ReceiveStack(stack: Stack) 21 | 22 | /* Manage konstructing */ 23 | case class MatchPattern(pattern: Pattern) 24 | case class PatternMatched(result: Stack) 25 | case class KonstructPattern(pattern: Pattern) 26 | case class PatternKonstructed(pattern: PatternTemplate, result: Stack, number: Int) 27 | case object PatternNotKonstructed 28 | 29 | /* Manage player */ 30 | case class ConnectView(manager: ActorRef, view: View) 31 | case class UpdateView(view: View) 32 | case class PutViewStack(stack: Stack, to: Int) 33 | case class RemoveViewStack(from: Int, amount: StackAmount) 34 | case object CloseInventory 35 | 36 | /* Messages for binary storage */ 37 | case class StoreBinary(id: String, ns: String, data: ByteString) 38 | case class LoadBinary(id: String, ns: String) 39 | case class BinaryLoaded(id: String, data: Option[ByteString]) 40 | 41 | /* Messages for JSON storage */ 42 | case class StoreGson(id: String, ns: String, data: JsonElement) 43 | case class LoadGson(id: String, ns: String) 44 | case class GsonLoaded(id: String, data: JsonElement) 45 | -------------------------------------------------------------------------------- /src/test/scala/konstructs/DbSpec.scala: -------------------------------------------------------------------------------- 1 | package konstructs.shard 2 | 3 | import org.scalatest.{ Matchers, WordSpec } 4 | 5 | class DbSpec extends WordSpec with Matchers { 6 | 7 | "A ChunkPosition" should { 8 | "Return local index 0 for chunk 0, 0, 0" in { 9 | ShardActor.index(ChunkPosition(0, 0, 0), ShardPosition(0,0,0)) shouldEqual 0 10 | } 11 | 12 | "Return local index 8*8*8 - 1 for chunk 7, 7, 7" in { 13 | ShardActor.index(ChunkPosition(7, 7, 7), ShardPosition(0,0,0)) shouldEqual (8*8*8 - 1) 14 | } 15 | 16 | "Return local index 0 for chunk 8, 8, 8" in { 17 | ShardActor.index(ChunkPosition(8, 8, 8), ShardPosition(1,1,1)) shouldEqual 0 18 | } 19 | 20 | "Return local index 8*8*8 - 1 for chunk 15, 15, 15" in { 21 | ShardActor.index(ChunkPosition(15, 15, 15), ShardPosition(1,1,1)) shouldEqual (8*8*8 - 1) 22 | } 23 | 24 | "Return local index 0 for chunk -8, -8, -8" in { 25 | ShardActor.index(ChunkPosition(-8, -8, -8), ShardPosition(-1,-1,-1)) shouldEqual 0 26 | } 27 | 28 | "Return local index 8*8*8 - 1 for chunk -1, -1, -1" in { 29 | ShardActor.index(ChunkPosition(-1, -1, -1), ShardPosition(-1,-1,-1)) shouldEqual (8*8*8 - 1) 30 | } 31 | 32 | "Return ShardPosition(-1,-1,-1) for ChunkPosition(-1,-1,-1)" in { 33 | ShardPosition(ChunkPosition(-1,-1,-1)) shouldEqual ShardPosition(-1,-1,-1) 34 | } 35 | 36 | "Return ShardPosition(-1,-1,-1) for ChunkPosition(-8,-8,-8)" in { 37 | ShardPosition(ChunkPosition(-8,-8,-8)) shouldEqual ShardPosition(-1,-1,-1) 38 | } 39 | 40 | "Return ShardPosition(-2,-2,-2) for ChunkPosition(-9,-9,-9)" in { 41 | ShardPosition(ChunkPosition(-9,-9,-9)) shouldEqual ShardPosition(-2,-2,-2) 42 | } 43 | 44 | "Return ShardPosition(-1,-2,2) for ChunkPosition(-7,-9,16)" in { 45 | ShardPosition(ChunkPosition(-7,-9,16)) shouldEqual ShardPosition(-1,-2,2) 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/konstructs/api/messages/DamageBlockWithBlock.java: -------------------------------------------------------------------------------- 1 | package konstructs.api.messages; 2 | 3 | import konstructs.api.Block; 4 | import konstructs.api.Position; 5 | 6 | /** 7 | * DamageBlockWithBlock is a message that is used to damage a block with another block. 8 | * This means that it will damage the health of both blocks with the other blocks damage value. 9 | * @see konstructs.api.Health 10 | */ 11 | public class DamageBlockWithBlock { 12 | private final Position toDamage; 13 | private final Block using; 14 | 15 | /** 16 | * Create a new immutable DamageBlockWithBlock instance 17 | * @param toDamage The position in the world to damage 18 | * @param using The block to be used to damage the position 19 | */ 20 | public DamageBlockWithBlock(Position toDamage, Block using) { 21 | this.toDamage = toDamage; 22 | this.using = using; 23 | } 24 | 25 | /** 26 | * Returns the position to damage 27 | * @return the position to damage 28 | */ 29 | public Position getToDamage() { 30 | return toDamage; 31 | } 32 | 33 | /** 34 | * Returns the block to damage the position with 35 | * @return Block to deal damage with 36 | */ 37 | public Block getUsing() { 38 | return using; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) return true; 44 | if (o == null || getClass() != o.getClass()) return false; 45 | 46 | DamageBlockWithBlock that = (DamageBlockWithBlock) o; 47 | 48 | if (!toDamage.equals(that.toDamage)) return false; 49 | return using.equals(that.using); 50 | 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | int result = toDamage.hashCode(); 56 | result = 31 * result + using.hashCode(); 57 | return result; 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return "DamageBlockWithBlock(" + 63 | "toDamage=" + toDamage + 64 | ", using=" + using + 65 | ')'; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/plugin/toolsack/sack.scala: -------------------------------------------------------------------------------- 1 | package konstructs.plugin.toolsack 2 | 3 | import java.util.UUID 4 | import akka.actor.{Actor, Props, ActorRef} 5 | import konstructs.plugin.PluginConstructor 6 | import konstructs.KonstructingViewActor 7 | import konstructs.api._ 8 | import konstructs.api.messages._ 9 | class ToolSackActor(universe: ActorRef) extends Actor { 10 | import ToolSackActor._ 11 | 12 | def receive = { 13 | case f: InteractTertiaryFilter => 14 | f.getMessage match { 15 | case i: InteractTertiary if !i.isWorldPhase && i.getBlock != null && i.getBlock.getType == BlockId => 16 | val b = if (i.getBlock.getId == null) { 17 | val newBlock = i.getBlock.withId(UUID.randomUUID) 18 | universe ! CreateInventory(newBlock.getId, 16) 19 | newBlock 20 | } else { 21 | i.getBlock 22 | } 23 | context.actorOf( 24 | KonstructingViewActor.props(i.getSender, universe, b.getId, SackView, KonstructingView, ResultView)) 25 | f.skipWith(self, i.withBlock(b)) 26 | case i: InteractTertiary 27 | if i.isWorldPhase && i.getBlockAtPosition != null && i.getBlockAtPosition.getType == BlockId => 28 | val b = if (i.getBlockAtPosition.getId == null) { 29 | val newBlock = i.getBlockAtPosition.withId(UUID.randomUUID) 30 | universe ! CreateInventory(newBlock.getId, 16) 31 | newBlock 32 | } else { 33 | i.getBlockAtPosition 34 | } 35 | context.actorOf( 36 | KonstructingViewActor.props(i.getSender, universe, b.getId, SackView, KonstructingView, ResultView)) 37 | f.skipWith(self, i.withBlockAtPosition(b)) 38 | case _ => 39 | f.next(self) 40 | } 41 | } 42 | } 43 | 44 | object ToolSackActor { 45 | val BlockId = new BlockTypeId("org/konstructs", "tool-sack") 46 | val SackView = new InventoryView(2, 4, 4, 4) 47 | val KonstructingView = new InventoryView(7, 5, 2, 2) 48 | val ResultView = new InventoryView(8, 9, 1, 1) 49 | 50 | @PluginConstructor 51 | def props(name: String, universe: ActorRef) = Props(classOf[ToolSackActor], universe) 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/generator.scala: -------------------------------------------------------------------------------- 1 | package konstructs 2 | 3 | import akka.actor.{Actor, ActorRef, Props} 4 | 5 | import konstructs.api._ 6 | 7 | import konstructs.shard.{ChunkPosition, BoxChunking, BlockData} 8 | 9 | class GeneratorActor(jsonStorage: ActorRef, binaryStorage: ActorRef, factory: BlockFactory) extends Actor { 10 | import GeneratorActor._ 11 | 12 | val worlds = Seq[WorldEntry]( 13 | WorldEntry( 14 | new Box(new Position(-1536, 0, -1536), new Position(1536, 512, 1536)), 15 | context.actorOf( 16 | FlatWorldActor.props("Terra", new Position(3072, 1024, 3072), factory, jsonStorage, binaryStorage)) 17 | ) 18 | ) 19 | 20 | val SpaceVacuum = factory.getW(BlockTypeId.fromString("org/konstructs/space/vacuum")) 21 | 22 | val Pristine = Health.PRISTINE.getHealth() 23 | 24 | val EmptyChunk = { 25 | val data = new Array[Byte](Db.ChunkSize * Db.ChunkSize * Db.ChunkSize * BlockData.Size) 26 | for (i <- 0 until Db.ChunkSize * Db.ChunkSize * Db.ChunkSize) { 27 | BlockData.write(data, 28 | i, 29 | SpaceVacuum, 30 | Pristine, 31 | Direction.UP_ENCODING, 32 | Rotation.IDENTITY_ENCODING, 33 | LightLevel.FULL_ENCODING, 34 | 0, 35 | 0, 36 | 0, 37 | LightLevel.DARK_ENCODING) 38 | } 39 | data 40 | } 41 | 42 | def receive = { 43 | case Generate(chunk) => 44 | worlds.filter { b => 45 | BoxChunking.contains(b.box, chunk) 46 | }.headOption match { 47 | case Some(entry) => 48 | entry.actor forward World.Generate(chunk, BoxChunking.translate(entry.box, chunk)) 49 | case None => 50 | sender ! Generated(chunk, EmptyChunk) 51 | } 52 | } 53 | 54 | } 55 | 56 | object GeneratorActor { 57 | case class Generate(position: ChunkPosition) 58 | case class Generated(position: ChunkPosition, blocks: Array[Byte]) 59 | 60 | case class WorldEntry(box: Box, actor: ActorRef) 61 | def props(jsonStorage: ActorRef, binaryStorage: ActorRef, factory: BlockFactory) = 62 | Props(classOf[GeneratorActor], jsonStorage, binaryStorage, factory) 63 | } 64 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/metrics/GraphiteMetricPlugin.scala: -------------------------------------------------------------------------------- 1 | package konstructs.metric 2 | 3 | import java.net.InetSocketAddress 4 | import scala.concurrent.duration.{Duration, FiniteDuration} 5 | 6 | import akka.actor.{Actor, ActorRef, Props, Stash} 7 | import akka.io.{IO, Tcp} 8 | import akka.util.ByteString 9 | import java.util.concurrent.TimeUnit 10 | 11 | import konstructs.plugin.{Config, PluginConstructor} 12 | import konstructs.api.messages.SetMetric 13 | 14 | class GraphiteMetricPlugin(name: String, remote: InetSocketAddress, namespacePrefix: String, reconnectDelay: Int) 15 | extends Actor 16 | with Stash { 17 | import GraphiteMetricPlugin.AttemptConnect 18 | import Tcp._ 19 | import context.system 20 | val duration = FiniteDuration(reconnectDelay, TimeUnit.MILLISECONDS) 21 | 22 | def connect(delay: FiniteDuration) { 23 | context.system.scheduler.scheduleOnce(delay, self, AttemptConnect)(context.dispatcher) 24 | } 25 | 26 | connect(Duration.Zero) 27 | 28 | def receive = { 29 | case AttemptConnect => 30 | println(s"$name: Attempting to connect $remote") 31 | IO(Tcp) ! Connect(remote) 32 | case CommandFailed(_: Connect) => 33 | println(s"$name: Failed to connect $remote") 34 | connect(duration) 35 | case c @ Connected(remote, local) => 36 | unstashAll() 37 | println(s"$name: Connected to $remote") 38 | val connection = sender 39 | connection ! Register(self) 40 | context become { 41 | case s: SetMetric => 42 | val unixTime = System.currentTimeMillis() / 1000L 43 | val msg = s"${namespacePrefix}.${s.getId.toMetricString} ${s.getValue} $unixTime\n" 44 | connection ! Write(ByteString(msg, "ascii")) 45 | case CommandFailed(w: Write) => 46 | // O/S buffer was full 47 | println(s"$name: Failed to write") 48 | case _: ConnectionClosed => 49 | println(s"$name: Connection closed") 50 | connect(duration) 51 | context.unbecome() 52 | } 53 | case _ => 54 | stash() 55 | } 56 | 57 | } 58 | 59 | object GraphiteMetricPlugin { 60 | case object AttemptConnect 61 | 62 | @PluginConstructor 63 | def props( 64 | name: String, 65 | universe: ActorRef, 66 | @Config(key = "host") host: String, 67 | @Config(key = "port") port: Int, 68 | @Config(key = "namespace-prefix") namespacePrefix: String, 69 | @Config(key = "reconnect-delay") reconnectDelay: Int 70 | ) = { 71 | Props(classOf[GraphiteMetricPlugin], 72 | name, 73 | new InetSocketAddress(host, port), 74 | namespacePrefix: String, 75 | reconnectDelay) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Konstructs Server 2 | ================= 3 | 4 | [![Join the chat at https://gitter.im/konstructs/server](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/konstructs/server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | [![Build Status](https://travis-ci.org/konstructs/server.svg?branch=master)](https://travis-ci.org/konstructs/server) 7 | 8 | This is a Infiniminer/Minecraft inspired game server. It has four overall goals: 9 | 10 | - Scalability 11 | - Huge worlds 12 | - Extendability 13 | - Server driven game logic 14 | 15 | ## Scalability - actors 16 | 17 | The server is implemented using [Akka](http://akka.io/) actors and the new IO subsystem ported from the [Spray](http://spray.io/) project. We try to keep everything nicely isolated in or behind an actor to make the game easy to scale up (run on multiple cores/CPUs). We hope that this in the long run also will help with scaling out (running the same world on several servers). 18 | 19 | ## Huge worlds - separated game logic and world 20 | 21 | At the core we try to have very simple block format that is shared with the client. We store all blocks compressed using [DEFLATE](http://en.wikipedia.org/wiki/DEFLATE), also when in memory or being sent to the client. This means that we can keep a huge amount of blocks in memory which helps fight IO latency as well as keeping disk IO to a minimum. Actually, one of the unimplemented features is block unloading. So far we never needed it! 22 | 23 | Since we can manage a huge amount of blocks, it makes sense to not limit world interaction to where the player is. Therefore we have completely separated all world logic from the world itself. Therefore automatic world interaction is not dependent on the part of the world being loaded, but is always done for all of the world via plugins. 24 | 25 | ## Extendability - plugins are actors 26 | 27 | Since the server is at is core just a set of actors exchanging messages every plugin is created equal. Message interfaces are provided to interact with the block world as well as with players. Persistence are provide by actors capable of loading and storing JSON data on behalf of other actors. It is therefore easy to create a plugin that keeps out of the way of other plugins. 28 | 29 | ## Server driven game logic - true multiplayer game 30 | 31 | To keep the game extremely extensible, while still keeping the client simple, all game logic is implemented server side. This means that for the player there is no need to download different plugins or versions of the client to play different worlds. Plugins mainly interact with the block world and/or using one of the simple features implemented in the client (like the inventory). We are also planning on adding some kind of crafting support. 32 | 33 | ## Building 34 | Get [SBT](http://www.scala-sbt.org/download.html)! 35 | 36 | ```sbt test``` to run unit tests. 37 | 38 | ## Running 39 | 40 | ```sbt run``` starts the server and binds port 4080. 41 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/shard/ChunkPosition.scala: -------------------------------------------------------------------------------- 1 | package konstructs.shard 2 | 3 | import konstructs.api.{Position, Box} 4 | 5 | import konstructs.Db 6 | 7 | case class ChunkPosition(p: Int, q: Int, k: Int) { 8 | def translate(pd: Int, qd: Int, kd: Int) = 9 | copy(p = p + pd, q = q + qd, k = k + kd) 10 | def distance(c: ChunkPosition): Double = { 11 | val dp = p - c.p 12 | val dq = q - c.q 13 | val dk = k - c.k 14 | math.pow(dp * dp + dq * dq + dk * dk, 1d / 2d) 15 | } 16 | 17 | def position(x: Int, y: Int, z: Int): Position = 18 | new Position( 19 | p * Db.ChunkSize + x, 20 | k * Db.ChunkSize + y, 21 | q * Db.ChunkSize + z 22 | ) 23 | 24 | def contains(pos: Position): Boolean = { 25 | val pp = (if (pos.getX < 0) (pos.getX - Db.ChunkSize + 1) else pos.getX) / Db.ChunkSize 26 | val pq = (if (pos.getZ < 0) (pos.getZ - Db.ChunkSize + 1) else pos.getZ) / Db.ChunkSize 27 | val pk = (if (pos.getY < 0) (pos.getY - Db.ChunkSize + 1) else pos.getY) / Db.ChunkSize 28 | return p == pp && q == pq && k == pk 29 | } 30 | } 31 | 32 | object ChunkPosition { 33 | def apply(pos: Position): ChunkPosition = { 34 | // For negative values we need to "round down", i.e. -0.01 should be -1 and not 0 35 | val p = (if (pos.getX < 0) (pos.getX - Db.ChunkSize + 1) else pos.getX) / Db.ChunkSize 36 | val q = (if (pos.getZ < 0) (pos.getZ - Db.ChunkSize + 1) else pos.getZ) / Db.ChunkSize 37 | val k = (if (pos.getY < 0) (pos.getY - Db.ChunkSize + 1) else pos.getY) / Db.ChunkSize 38 | ChunkPosition(p, q, k) 39 | } 40 | } 41 | 42 | object BoxChunking { 43 | 44 | def contains(box: Box, chunk: ChunkPosition): Boolean = 45 | box.contains(chunk.position(0, 0, 0)) 46 | 47 | def translate(box: Box, chunk: ChunkPosition): ChunkPosition = { 48 | ChunkPosition(chunk.position(0, 0, 0).subtract(box.getFrom)) 49 | } 50 | 51 | def chunked(box: Box): Set[Box] = { 52 | val start = box.getFrom 53 | val end = box.getUntil 54 | val startChunk = ChunkPosition(start) 55 | val endChunk = ChunkPosition(end) 56 | 57 | val xrange = startChunk.p to endChunk.p 58 | val yrange = startChunk.k to endChunk.k 59 | val zrange = startChunk.q to endChunk.q 60 | 61 | (for (xi <- xrange; yi <- yrange; zi <- zrange) yield { 62 | val xs = if (xi == startChunk.p) start.getX else xi * Db.ChunkSize 63 | val xe = if (xi == endChunk.p) end.getX else xi * Db.ChunkSize + Db.ChunkSize 64 | val ys = if (yi == startChunk.k) start.getY else yi * Db.ChunkSize 65 | val ye = if (yi == endChunk.k) end.getY else yi * Db.ChunkSize + Db.ChunkSize 66 | val zs = if (zi == startChunk.q) start.getZ else zi * Db.ChunkSize 67 | val ze = if (zi == endChunk.q) end.getZ else zi * Db.ChunkSize + Db.ChunkSize 68 | new Box(new Position(xs, ys, zs), new Position(xe, ye, ze)) 69 | }).filter { e => 70 | e.getFrom != e.getUntil // Remove all empty queries 71 | } toSet 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/main.scala: -------------------------------------------------------------------------------- 1 | package konstructs 2 | 3 | import java.util.UUID 4 | import java.io.{FileReader, FileNotFoundException, File} 5 | import org.apache.commons.io.FileUtils 6 | import scala.collection.JavaConverters._ 7 | import akka.actor.ActorSystem 8 | import com.typesafe.config.ConfigFactory 9 | import com.google.gson.reflect.TypeToken 10 | import konstructs.api.{GsonDefault, Position} 11 | import konstructs.plugin.PluginLoaderActor 12 | import konstructs.shard.{ShardPosition, ShardActor} 13 | 14 | object Main extends App { 15 | val conf = 16 | ConfigFactory.parseFile(new java.io.File("konstructs.conf")).withFallback(ConfigFactory.load()) 17 | implicit val system = ActorSystem("main", conf) 18 | // Run any global backwards compatibility scripts 19 | // Plugins should handle their own backwards compatibility 20 | Compatibility.process() 21 | val loader = system.actorOf(PluginLoaderActor.props(conf), "plugin-loader") 22 | loader ! PluginLoaderActor.Start 23 | } 24 | 25 | object Compatibility { 26 | import ShardActor.{shardId, positionMappingFile} 27 | val TypeOfPositionMapping = new TypeToken[java.util.Map[String, UUID]]() {}.getType 28 | 29 | def process() { 30 | println("STARTING MIGRATION PROCESS, DON'T INTERRUPT!") 31 | handleOldUuidDB() 32 | println("MIGRATION PROCESS FINISHED") 33 | } 34 | 35 | val PositionRegex = """(-?\d*)-(-?\d*)-(-?\d*)""".r 36 | 37 | def parsePositionStr(posStr: String): Position = posStr match { 38 | case PositionRegex(x, y, z) => new Position(x.toInt, y.toInt, z.toInt) 39 | } 40 | 41 | /* 42 | * This "migration" only works if disk storage on default location was used 43 | */ 44 | def handleOldUuidDB() { 45 | val gson = GsonDefault.getDefaultGson 46 | try { 47 | val fileName = s"meta/org/konstructs/block-manager/position-mapping.json" 48 | val reader = new FileReader(fileName) 49 | val mapping: java.util.Map[String, UUID] = gson.fromJson(reader, TypeOfPositionMapping) 50 | println("Read old position mappings file, will migrate") 51 | val grouped = mapping.asScala.groupBy { 52 | case (positionStr, uuid) => 53 | val position = parsePositionStr(positionStr) 54 | ShardPosition(position) 55 | } 56 | val meta = new File("meta") 57 | for ((shard, positions) <- grouped) { 58 | val file = Storage.file(meta, positionMappingFile(shardId(shard)), "chunks", "json") 59 | if (file.exists()) 60 | throw new IllegalStateException("Shard position mapping already exists!") 61 | Storage.write(meta, 62 | positionMappingFile(shardId(shard)), 63 | "chunks", 64 | "json", 65 | gson.toJson(positions.asJava, TypeOfPositionMapping).getBytes()) 66 | println(s"Migrated mapping for $shard to $file") 67 | } 68 | println("Position mapping migration finished, will delete original mappings file") 69 | FileUtils.forceDelete(new File(fileName)) 70 | println("Successfully deleted original mappings file") 71 | } catch { 72 | case e: FileNotFoundException => 73 | println("No old position mapping file found, skipping position mapping migration.") 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/storage.scala: -------------------------------------------------------------------------------- 1 | package konstructs 2 | 3 | import java.io.File 4 | import scala.util.Try 5 | import akka.actor.{Actor, ActorRef, Props} 6 | import akka.util.ByteString 7 | import org.apache.commons.io.FileUtils 8 | import konstructs.plugin.{PluginConstructor, Config} 9 | import konstructs.api.GsonDefault 10 | import com.google.gson.{Gson, JsonParser, JsonElement} 11 | 12 | class BinaryStorageActor(name: String, directory: File) extends Actor { 13 | import konstructs.api.{StoreBinary, LoadBinary, BinaryLoaded} 14 | import Storage._ 15 | 16 | val Suffix = "binary" 17 | 18 | def receive = { 19 | case StoreBinary(id, ns, data) => 20 | write(directory, id, ns, Suffix, data.toArray) 21 | case LoadBinary(id, ns) => 22 | sender ! BinaryLoaded(id, load(directory, id, ns, Suffix).map(ByteString(_))) 23 | } 24 | } 25 | 26 | trait BinaryStorage { 27 | import konstructs.api.{StoreBinary, LoadBinary} 28 | 29 | def ns: String 30 | def binaryStorage: ActorRef 31 | 32 | def loadBinary(id: String)(implicit sender: ActorRef) = binaryStorage ! LoadBinary(id, ns) 33 | def storeBinary(id: String, data: ByteString)(implicit sender: ActorRef) = binaryStorage ! StoreBinary(id, ns, data) 34 | } 35 | 36 | object BinaryStorageActor { 37 | @PluginConstructor 38 | def props(name: String, universe: ActorRef, @Config(key = "directory") directory: File) = 39 | Props(classOf[BinaryStorageActor], name, directory) 40 | } 41 | 42 | class JsonStorageActor(name: String, directory: File) extends Actor { 43 | import konstructs.api.{StoreGson, LoadGson, GsonLoaded} 44 | import Storage._ 45 | 46 | private val Suffix = "json" 47 | private val gson = new Gson() 48 | private val parser = new JsonParser() 49 | 50 | def receive = { 51 | case StoreGson(id, ns, data) => 52 | write(directory, id, ns, Suffix, gson.toJson(data).getBytes()) 53 | case LoadGson(id, ns) => 54 | val data = load(directory, id, ns, Suffix).flatMap { d => 55 | Try(parser.parse(new String(d))).toOption 56 | } 57 | sender ! GsonLoaded(id, data.orNull) 58 | } 59 | } 60 | 61 | object JsonStorageActor { 62 | @PluginConstructor 63 | def props(name: String, universe: ActorRef, @Config(key = "directory") directory: File) = 64 | Props(classOf[JsonStorageActor], name, directory) 65 | } 66 | 67 | trait JsonStorage { 68 | import konstructs.api.{StoreGson, LoadGson} 69 | 70 | val gson = GsonDefault.getDefaultGson 71 | 72 | def ns: String 73 | def jsonStorage: ActorRef 74 | 75 | def loadGson(id: String)(implicit sender: ActorRef) = jsonStorage ! LoadGson(id, ns) 76 | def storeGson(id: String, data: JsonElement)(implicit sender: ActorRef) = jsonStorage ! StoreGson(id, ns, data) 77 | 78 | } 79 | 80 | object Storage { 81 | def file(directory: File, id: String, ns: String, suffix: String) = new File(directory, s"$ns/$id.$suffix") 82 | 83 | def write(directory: File, id: String, ns: String, suffix: String, data: Array[Byte]) = 84 | FileUtils.writeByteArrayToFile(file(directory, id, ns, suffix), data) 85 | 86 | def load(directory: File, id: String, ns: String, suffix: String): Option[Array[Byte]] = { 87 | val f = file(directory, id, ns, suffix) 88 | if (f.exists) { 89 | Some(FileUtils.readFileToByteArray(f)) 90 | } else { 91 | None 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/inventory.scala: -------------------------------------------------------------------------------- 1 | package konstructs 2 | 3 | import java.util.UUID 4 | 5 | import scala.collection.mutable 6 | import scala.collection.JavaConverters._ 7 | 8 | import akka.actor.{Actor, Props, ActorRef, Stash} 9 | 10 | import com.google.gson.reflect.TypeToken 11 | import konstructs.plugin.{PluginConstructor, Config} 12 | import konstructs.api._ 13 | 14 | class InventoryActor(val ns: String, val jsonStorage: ActorRef) 15 | extends Actor 16 | with Stash 17 | with JsonStorage 18 | with utils.Scheduled { 19 | import InventoryActor._ 20 | 21 | schedule(5000, StoreData) 22 | 23 | loadGson(InventoriesFile) 24 | 25 | val typeOfInventories = new TypeToken[java.util.Map[String, Inventory]]() {}.getType 26 | var inventories: java.util.Map[String, Inventory] = null 27 | 28 | private def put(blockId: UUID, slot: Int, stack: Stack): Stack = { 29 | if (inventories.containsKey(blockId.toString)) { 30 | val inventory = inventories.get(blockId.toString) 31 | val oldStack = inventory.getStack(slot) 32 | 33 | if (oldStack != null) { 34 | if (oldStack.canAcceptPartOf(stack)) { 35 | val r = oldStack.acceptPartOf(stack) 36 | inventories.put(blockId.toString, inventory.withSlot(slot, r.getAccepting)) 37 | r.getGiving 38 | } else { 39 | inventories.put(blockId.toString, inventory.withSlot(slot, stack)) 40 | oldStack 41 | } 42 | } else { 43 | inventories.put(blockId.toString, inventory.withSlot(slot, stack)) 44 | null; 45 | } 46 | } else { 47 | stack 48 | } 49 | } 50 | 51 | private def get(blockId: UUID, slot: Int): Stack = 52 | if (inventories.containsKey(blockId.toString)) { 53 | inventories.get(blockId.toString).getStack(slot) 54 | } else { 55 | null 56 | } 57 | 58 | private def remove(blockId: UUID, slot: Int, amount: StackAmount): Stack = 59 | if (inventories.containsKey(blockId.toString)) { 60 | val i = inventories.get(blockId.toString) 61 | val s = i.getStack(slot) 62 | if (s == null) 63 | return null 64 | inventories.put(blockId.toString, i.withSlot(slot, s.drop(amount))) 65 | s.take(amount) 66 | } else { 67 | null 68 | } 69 | 70 | def receive = { 71 | case GsonLoaded(_, json) if json != null => 72 | inventories = gson.fromJson(json, typeOfInventories) 73 | // This handles old inventories where empty stacks wasn't null 74 | val updatedInventories = new java.util.HashMap[String, Inventory]() 75 | inventories.asScala.toMap foreach { 76 | case (pos, inventory) => 77 | updatedInventories.put(pos, Inventory.convertPre0_1(inventory)) 78 | } 79 | inventories = updatedInventories 80 | context.become(ready) 81 | unstashAll() 82 | case GsonLoaded(_, _) => 83 | inventories = new java.util.HashMap() 84 | context.become(ready) 85 | unstashAll() 86 | case _ => 87 | stash() 88 | } 89 | 90 | def ready: Receive = { 91 | case CreateInventory(blockId, size) => 92 | if (!inventories.containsKey(blockId.toString)) { 93 | inventories.put(blockId.toString, Inventory.createEmpty(size)) 94 | } 95 | 96 | case GetInventory(blockId) => 97 | sender ! GetInventoryResponse(blockId, Option(inventories.get(blockId.toString))) 98 | 99 | case PutStack(blockId, slot, stack) => 100 | val leftovers = put(blockId, slot, stack) 101 | sender ! new ReceiveStack(leftovers) 102 | 103 | case RemoveStack(blockId, slot, amount) => 104 | sender ! new ReceiveStack(remove(blockId, slot, amount)) 105 | 106 | case GetStack(blockId, slot) => 107 | sender ! GetStackResponse(blockId, slot, get(blockId, slot)) 108 | 109 | case DeleteInventory(blockId) => 110 | inventories.remove(blockId.toString) 111 | 112 | case StoreData => 113 | storeGson(InventoriesFile, gson.toJsonTree(inventories, typeOfInventories)) 114 | 115 | } 116 | } 117 | 118 | object InventoryActor { 119 | case object StoreData 120 | val InventoriesFile = "inventories" 121 | 122 | @PluginConstructor 123 | def props(name: String, universe: ActorRef, @Config(key = "json-storage") jsonStorage: ActorRef): Props = 124 | Props(classOf[InventoryActor], name, jsonStorage) 125 | } 126 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/shard/BlockData.scala: -------------------------------------------------------------------------------- 1 | package konstructs.shard 2 | 3 | import java.util.UUID 4 | 5 | import konstructs.api.{Position, Block, BlockTypeId, Direction, Rotation, Orientation, LightLevel, Colour, Health} 6 | import konstructs.Db 7 | 8 | case class BlockData(w: Int, 9 | health: Int, 10 | direction: Int, 11 | rotation: Int, 12 | ambient: Int, 13 | red: Int, 14 | green: Int, 15 | blue: Int, 16 | light: Int) { 17 | def write(data: Array[Byte], i: Int) { 18 | BlockData.write(data, i, w, health, direction, rotation, ambient, red, green, blue, light) 19 | } 20 | 21 | def block(id: UUID, blockTypeId: BlockTypeId) = 22 | new Block(id, blockTypeId, Health.get(health), Orientation.get(direction, rotation)) 23 | } 24 | 25 | object BlockData { 26 | 27 | val Size = 7 28 | 29 | def w(data: Array[Byte], i: Int): Int = 30 | (data(i * Size) & 0xFF) + ((data(i * Size + 1) & 0xFF) << 8) 31 | 32 | def hp(data: Array[Byte], i: Int): Int = 33 | (data(i * Size + 2) & 0xFF) + ((data(i * Size + 3) & 0x07) << 8) 34 | 35 | def direction(data: Array[Byte], i: Int): Int = 36 | ((data(i * Size + 3) & 0xE0) >> 5) 37 | 38 | def rotation(data: Array[Byte], i: Int): Int = 39 | ((data(i * Size + 3) & 0x18) >> 3) 40 | 41 | def ambientLight(data: Array[Byte], i: Int): Int = 42 | ((data(i * Size + 4) & 0x0F)) 43 | 44 | def red(data: Array[Byte], i: Int): Int = 45 | ((data(i * Size + 4) & 0xF0) >> 4) 46 | 47 | def green(data: Array[Byte], i: Int): Int = 48 | ((data(i * Size + 5) & 0x0F)) 49 | 50 | def blue(data: Array[Byte], i: Int): Int = 51 | ((data(i * Size + 5) & 0xF0) >> 4) 52 | 53 | def light(data: Array[Byte], i: Int): Int = 54 | ((data(i * Size + 6) & 0x0F)) 55 | 56 | def apply(data: Array[Byte], i: Int): BlockData = { 57 | apply(w(data, i), 58 | hp(data, i), 59 | direction(data, i), 60 | rotation(data, i), 61 | ambientLight(data, i), 62 | red(data, i), 63 | green(data, i), 64 | blue(data, i), 65 | light(data, i)) 66 | } 67 | 68 | def apply(w: Int, block: Block, ambient: Int, colour: Colour, level: LightLevel): BlockData = { 69 | apply( 70 | w, 71 | block.getHealth.getHealth, 72 | block.getOrientation.getDirection.getEncoding, 73 | block.getOrientation.getRotation.getEncoding, 74 | ambient, 75 | colour.getRed, 76 | colour.getGreen, 77 | colour.getBlue, 78 | level.getLevel 79 | ) 80 | } 81 | 82 | def write(data: Array[Byte], 83 | i: Int, 84 | w: Int, 85 | health: Int, 86 | direction: Int, 87 | rotation: Int, 88 | ambient: Int, 89 | red: Int, 90 | green: Int, 91 | blue: Int, 92 | light: Int) { 93 | writeW(data, i, w) 94 | writeHealthAndOrientation(data, i, health, direction, rotation) 95 | writeLight(data, i, ambient, red, green, blue, light) 96 | } 97 | 98 | def write(data: Array[Byte], 99 | i: Int, 100 | w: Int, 101 | block: Block, 102 | ambientLight: LightLevel, 103 | colour: Colour, 104 | light: LightLevel) { 105 | write(data, 106 | i, 107 | w, 108 | block.getHealth.getHealth, 109 | block.getOrientation.getDirection.getEncoding, 110 | block.getOrientation.getRotation.getEncoding, 111 | ambientLight.getLevel, 112 | colour.getRed, 113 | colour.getGreen, 114 | colour.getBlue, 115 | light.getLevel) 116 | } 117 | 118 | def writeW(data: Array[Byte], i: Int, w: Int) { 119 | data(i * Size) = (w & 0xFF).toByte 120 | data(i * Size + 1) = ((w >> 8) & 0xFF).toByte 121 | } 122 | 123 | def writeHealthAndOrientation(data: Array[Byte], i: Int, health: Int, direction: Int, rotation: Int) { 124 | data(i * Size + 2) = (health & 0xFF).toByte 125 | val b = (((direction << 5) & 0xE0) + ((rotation << 3) & 0x18) + ((health >> 8) & 0x07)).toByte 126 | data(i * Size + 3) = b 127 | } 128 | 129 | def writeLight(data: Array[Byte], i: Int, ambient: Int, red: Int, green: Int, blue: Int, light: Int) { 130 | val b4 = ((ambient & 0x0F) + ((red << 4) & 0xF0)).toByte 131 | data(i * Size + 4) = b4 132 | val b5 = ((green & 0x0F) + ((blue << 4) & 0xF0)).toByte 133 | data(i * Size + 5) = b5 134 | data(i * Size + 6) = (light & 0x0F).toByte 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/test/scala/konstructs/GeometrySpec.scala: -------------------------------------------------------------------------------- 1 | package konstructs 2 | 3 | import org.scalatest.{ Matchers, WordSpec } 4 | 5 | import konstructs.api._ 6 | import konstructs.shard.{ ChunkPosition, BoxChunking } 7 | 8 | class GeometrySpec extends WordSpec with Matchers { 9 | 10 | "A Position" should { 11 | "return chunk 0, 0 for 0, 0, 0" in { 12 | ChunkPosition(new Position(0, 0, 0)) shouldEqual ChunkPosition(0, 0, 0) 13 | } 14 | 15 | "return chunk 0, 1 for 0, 0, 32" in { 16 | ChunkPosition(new Position(0, 0, 32)) shouldEqual ChunkPosition(0, 1, 0) 17 | } 18 | 19 | "return chunk 1, 0 for 32, 0, 0" in { 20 | ChunkPosition(new Position(32, 0, 0)) shouldEqual ChunkPosition(1, 0, 0) 21 | } 22 | 23 | "return chunk 1, 1 for 32, 0, 32" in { 24 | ChunkPosition(new Position(32, 0, 32)) shouldEqual ChunkPosition(1, 1, 0) 25 | } 26 | 27 | "return chunk 0, 0 for 31, 0, 31" in { 28 | ChunkPosition(new Position(31, 0, 31)) shouldEqual ChunkPosition(0, 0, 0) 29 | } 30 | 31 | "return chunk 0, -1 for 0, 0, -1" in { 32 | ChunkPosition(new Position(0, 0, -1)) shouldEqual ChunkPosition(0, -1, 0) 33 | } 34 | 35 | "return chunk -1, -1 for -1, 0, -1" in { 36 | ChunkPosition(new Position(-1, 0, -1)) shouldEqual ChunkPosition(-1, -1, 0) 37 | } 38 | 39 | "return chunk -1, -1 for -32, 0, -32" in { 40 | ChunkPosition(new Position(-32, 0, -32)) shouldEqual ChunkPosition(-1, -1, 0) 41 | } 42 | 43 | "return chunk -2, -2 for -33, 0, -33" in { 44 | ChunkPosition(new Position(-33, 0, -33)) shouldEqual ChunkPosition(-2, -2, 0) 45 | } 46 | 47 | "return chunk -1, 0 for -1, 0, 0" in { 48 | ChunkPosition(new Position(-1, 0, 0)) shouldEqual ChunkPosition(-1, 0, 0) 49 | } 50 | 51 | } 52 | 53 | "A Box" should { 54 | 55 | "contain ChunkPosition(0, 0, 0) in (-32, 0, -32) (32, 0, 32)" in { 56 | BoxChunking.contains(new Box(new Position(-32, 0, -32), new Position(32, 1, 32)), ChunkPosition(0, 0, 0)) shouldEqual true 57 | } 58 | 59 | "single block query (boundary)" in { 60 | BoxChunking.chunked(new Box(new Position(0, 0, 31), new Position(0, 0, 32))) shouldEqual Set( 61 | new Box(new Position(0, 0, 31), new Position(0, 0, 32)) 62 | ) 63 | } 64 | 65 | "match two block query (boundary)" in { 66 | BoxChunking.chunked(new Box(new Position(0, 0, 31), new Position(0, 0, 33))) shouldEqual Set( 67 | new Box(new Position(0, 0, 31), new Position(0, 0, 32)), 68 | new Box(new Position(0, 0, 32), new Position(0, 0, 33)) 69 | ) 70 | } 71 | 72 | "match single block query (negative boundary)" in { 73 | BoxChunking.chunked(new Box(new Position(0, 0, -33), new Position(0, 0, -32))) shouldEqual Set( 74 | new Box(new Position(0, 0, -33), new Position(0, 0, -32)) 75 | ) 76 | } 77 | 78 | "match two block query (negative boundary)" in { 79 | BoxChunking.chunked(new Box(new Position(0, 0, -33), new Position(0, 0, -31))) shouldEqual Set( 80 | new Box(new Position(0, 0, -33), new Position(0, 0, -32)), 81 | new Box(new Position(0, 0, -32), new Position(0, 0, -31)) 82 | ) 83 | } 84 | 85 | "split in two chunks (one dimension)" in { 86 | BoxChunking.chunked(new Box(new Position(0, 0, 1), new Position(0, 0, 33))) shouldEqual Set( 87 | new Box(new Position(0, 0, 1), new Position(0, 0, 32)), 88 | new Box(new Position(0, 0, 32), new Position(0, 0, 33)) 89 | ) 90 | } 91 | 92 | "split in three chunks (one dimension)" in { 93 | BoxChunking.chunked(new Box(new Position(0, 0, -1), new Position(0, 0, 33))) shouldEqual Set( 94 | new Box(new Position(0, 0, -1), new Position(0, 0, 0)), 95 | new Box(new Position(0, 0, 0), new Position(0, 0, 32)), 96 | new Box(new Position(0, 0, 32), new Position(0, 0, 33)) 97 | ) 98 | } 99 | 100 | "split in four chunks (two dimensions)" in { 101 | BoxChunking.chunked(new Box(new Position(0, 1, 1), new Position(0, 33,33))) shouldEqual Set( 102 | new Box(new Position(0,1,1),new Position(0,32,32)), 103 | new Box(new Position(0,1,32),new Position(0,32,33)), 104 | new Box(new Position(0,32,1),new Position(0,33,32)), 105 | new Box(new Position(0,32,32),new Position(0,33,33))) 106 | } 107 | 108 | "within one chunk" in { 109 | BoxChunking.chunked(new Box(new Position(1, 1, 1), new Position(12, 12, 12))) shouldEqual Set( 110 | new Box(new Position(1, 1, 1),new Position(12, 12, 12))) 111 | } 112 | 113 | "negative only (one dimension)" in { 114 | BoxChunking.chunked(new Box(new Position(0, 0, -67), new Position(0, 0, -1))) shouldEqual Set( 115 | new Box(new Position(0, 0, -67), new Position(0, 0, -64)), 116 | new Box(new Position(0, 0, -64), new Position(0, 0, -32)), 117 | new Box(new Position(0, 0, -32), new Position(0, 0, -1)) 118 | ) 119 | } 120 | 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/shard/ChunkData.scala: -------------------------------------------------------------------------------- 1 | package konstructs.shard 2 | 3 | import akka.util.ByteString 4 | 5 | import konstructs.api.{Position, LightLevel} 6 | import konstructs.utils.compress 7 | import konstructs.Db 8 | 9 | case class ChunkData(version: Int, data: ByteString) { 10 | import ChunkData._ 11 | 12 | val revision = readRevision(data, 2) 13 | 14 | def unpackTo(blockBuffer: Array[Byte], compressionBuffer: Array[Byte]) { 15 | data.copyToArray(compressionBuffer) 16 | val size = compress.inflate(compressionBuffer, blockBuffer, Header, data.size - Header) 17 | } 18 | 19 | def block(c: ChunkPosition, p: Position, blockBuffer: Array[Byte], compressionBuffer: Array[Byte]): BlockData = { 20 | unpackTo(blockBuffer, compressionBuffer) 21 | BlockData(blockBuffer, index(c, p)) 22 | } 23 | 24 | } 25 | 26 | object ChunkData { 27 | import BlockData._ 28 | import Db._ 29 | 30 | val InitialRevision = 1 31 | val Size = ChunkSize * ChunkSize * ChunkSize * BlockData.Size 32 | val RevisionSize = 4 33 | val Version2Header = 2 + RevisionSize 34 | val Version1Header = 2 35 | val Header = Version2Header 36 | val Version = 3.toByte 37 | 38 | /* Writes revision as a Little Endian 4 byte unsigned integer 39 | * Long is required since all Java types are signed 40 | */ 41 | def writeRevision(data: Array[Byte], revision: Long, offset: Int) { 42 | if (revision > 4294967295L) { 43 | throw new IllegalArgumentException("Must be smaller than 4294967295") 44 | } 45 | data(offset + 0) = (revision & 0xFF).toByte 46 | data(offset + 1) = ((revision >> 8) & 0xFF).toByte 47 | data(offset + 2) = ((revision >> 16) & 0xFF).toByte 48 | data(offset + 3) = ((revision >> 24) & 0xFF).toByte 49 | } 50 | 51 | /* Read revision as a Little Endian 4 byte unsigned integer 52 | * Returns long as all Java types are signed 53 | */ 54 | def readRevision(data: ByteString, offset: Int): Long = { 55 | (data(offset + 0) & 0xFF).toLong + 56 | ((data(offset + 1) & 0xFF) << 8).toLong + 57 | ((data(offset + 2) & 0xFF) << 16).toLong + 58 | ((data(offset + 3) & 0x7F) << 24).toLong 59 | } 60 | 61 | def apply(revision: Long, blocks: Array[Byte], buffer: Array[Byte]): ChunkData = { 62 | val size = compress.deflate(blocks, buffer, Header) 63 | buffer(0) = Version 64 | buffer(1) = 0.toByte 65 | writeRevision(buffer, revision, 2) 66 | apply(Version, ByteString.fromArray(buffer, 0, size)) 67 | } 68 | 69 | def loadOldFormat(version: Int, 70 | data: ByteString, 71 | blockBuffer: Array[Byte], 72 | compressionBuffer: Array[Byte], 73 | chunk: ChunkPosition, 74 | spaceVacuum: Int): ChunkData = { 75 | 76 | data.copyToArray(compressionBuffer) 77 | if (version == 1) { 78 | val size = compress.inflate(compressionBuffer, blockBuffer, Version1Header, data.size - Version1Header) 79 | convertFromOldFormat1(blockBuffer, size) 80 | } else { 81 | val size = compress.inflate(compressionBuffer, blockBuffer, Header, data.size - Header) 82 | convertFromOldFormat2(blockBuffer, size) 83 | } 84 | if (!inOldWorld(chunk)) { 85 | updateVacuumToSpaceVacuum(blockBuffer, spaceVacuum) 86 | } 87 | apply(0, blockBuffer, compressionBuffer) 88 | } 89 | 90 | private def inOldWorld(chunk: ChunkPosition): Boolean = 91 | chunk.p >= -48 && chunk.p < 48 && 92 | chunk.q >= -48 && chunk.q < 48 && 93 | chunk.k >= 0 && chunk.k < 16 94 | 95 | private def updateVacuumToSpaceVacuum(buf: Array[Byte], spaceVacuum: Int) { 96 | for (i <- 0 until ChunkSize * ChunkSize * ChunkSize) { 97 | if (BlockData.w(buf, i) == 0) { 98 | BlockData.writeW(buf, i, spaceVacuum) 99 | BlockData.writeLight(buf, i, LightLevel.FULL_ENCODING, 0, 0, 0, LightLevel.DARK_ENCODING) 100 | } 101 | } 102 | } 103 | 104 | private def convertFromOldFormat1(buf: Array[Byte], size: Int) { 105 | val tmp = java.util.Arrays.copyOf(buf, size) 106 | for (i <- 0 until size) { 107 | buf(i * BlockData.Size) = tmp(i) 108 | buf(i * BlockData.Size + 1) = 0.toByte 109 | buf(i * BlockData.Size + 2) = 0xFF.toByte 110 | buf(i * BlockData.Size + 3) = 0x07.toByte 111 | buf(i * BlockData.Size + 4) = 0x00.toByte 112 | buf(i * BlockData.Size + 5) = 0x00.toByte 113 | buf(i * BlockData.Size + 6) = 0x00.toByte 114 | } 115 | } 116 | 117 | private def convertFromOldFormat2(buf: Array[Byte], size: Int) { 118 | val tmp = java.util.Arrays.copyOf(buf, size) 119 | for (i <- 0 until (size / 4)) { 120 | buf(i * BlockData.Size) = tmp(i * 4) 121 | buf(i * BlockData.Size + 1) = tmp(i * 4 + 1) 122 | buf(i * BlockData.Size + 2) = tmp(i * 4 + 2) 123 | buf(i * BlockData.Size + 3) = tmp(i * 4 + 3) 124 | buf(i * BlockData.Size + 4) = 0x00.toByte 125 | buf(i * BlockData.Size + 5) = 0x00.toByte 126 | buf(i * BlockData.Size + 6) = 0x00.toByte 127 | } 128 | } 129 | 130 | def index(x: Int, y: Int, z: Int): Int = 131 | x + y * ChunkSize + z * ChunkSize * ChunkSize 132 | 133 | def index(c: ChunkPosition, p: Position): Int = { 134 | val x = p.getX - c.p * ChunkSize 135 | val y = p.getY - c.k * ChunkSize 136 | val z = p.getZ - c.q * ChunkSize 137 | index(x, y, z) 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/utils/diamondsquare.scala: -------------------------------------------------------------------------------- 1 | package konstructs.utils 2 | 3 | import java.io.{DataInputStream, ByteArrayInputStream, DataOutputStream, ByteArrayOutputStream} 4 | import com.sksamuel.scrimage.Image 5 | import konstructs.api._ 6 | 7 | trait HeightMap extends PartialFunction[Position, Int] 8 | 9 | trait LocalHeightMap { 10 | def get(local: Position): Int 11 | def sizeX: Int 12 | def sizeZ: Int 13 | } 14 | 15 | case class ArrayHeightMap(data: Array[Int], sizeX: Int, sizeZ: Int) extends LocalHeightMap { 16 | private val SizeOfInt = 4 17 | def get(pos: Position) = data(pos.getX + pos.getZ * sizeZ) 18 | def toByteArray = { 19 | val bytes = new ByteArrayOutputStream(sizeX * sizeZ * SizeOfInt) 20 | val dataWriter = new DataOutputStream(bytes) 21 | for (x <- 0 until sizeX; z <- 0 until sizeZ) { 22 | dataWriter.writeInt(data(x + z * sizeZ)) 23 | } 24 | bytes.toByteArray 25 | } 26 | } 27 | 28 | object ArrayHeightMap { 29 | def fromExistingHeightMap(map: PartialFunction[Position, Int], sizeX: Int, sizeZ: Int): ArrayHeightMap = { 30 | val local = for (x <- 0 until sizeX; z <- 0 until sizeZ) yield { 31 | map(new Position(x, 0, z)) 32 | } 33 | ArrayHeightMap(local.toArray, sizeX, sizeZ) 34 | } 35 | def fromByteArray(data: Array[Byte], sizeX: Int, sizeZ: Int): ArrayHeightMap = { 36 | val dataReader = new DataInputStream(new ByteArrayInputStream(data)) 37 | val mapData = new Array[Int](sizeX * sizeZ) 38 | for (x <- 0 until sizeX; z <- 0 until sizeZ) { 39 | mapData(x + z * sizeZ) = dataReader.readInt() 40 | } 41 | ArrayHeightMap(mapData, sizeX, sizeZ) 42 | } 43 | } 44 | 45 | case class FlatHeightMap(height: Int) extends HeightMap { 46 | def apply(pos: Position) = height 47 | def isDefinedAt(pos: Position) = true 48 | } 49 | 50 | case object EmptyHeightMap extends HeightMap { 51 | def apply(pos: Position) = ??? 52 | def isDefinedAt(pos: Position) = false 53 | } 54 | 55 | case class ImageHeightMap(img: Image, range: Int = 128) extends LocalHeightMap { 56 | private val scale: Double = (256 * 256 * 256) / range 57 | def get(local: Position) = { 58 | val v: Double = (img.pixel(local.getX, local.getZ).argb & 0x00FFFFFF) 59 | (v / scale).toInt 60 | } 61 | def sizeX = img.width 62 | def sizeZ = img.height 63 | } 64 | 65 | case class GlobalHeightMap(placement: Position, map: LocalHeightMap) extends HeightMap { 66 | def apply(position: Position) = { 67 | map.get(position.subtract(placement).withY(0)) 68 | } 69 | def isDefinedAt(position: Position) = 70 | (position.getX >= placement.getX && 71 | position.getZ >= placement.getZ && 72 | position.getX < placement.getX + map.sizeX && 73 | position.getZ < placement.getZ + map.sizeZ) 74 | } 75 | 76 | class DiamondSquareHeightMap(roughness: Float, 77 | baseSize: Int, 78 | placement: Position, 79 | global: PartialFunction[Position, Int]) 80 | extends HeightMap { 81 | import DiamondSquareHeightMap._ 82 | private val size = findNearestPowerOfTwo(baseSize) + 1 83 | private val offset = size / 4 + 1 84 | private val localPlacement = placement.subtractX(offset).subtractZ(offset) 85 | private val max = size - 1 86 | private val array = Array.fill[Float](size * size)(Float.NaN) 87 | private val random = new scala.util.Random() 88 | 89 | private val local = new PartialFunction[Position, Float] { 90 | def apply(local: Position) = { 91 | array(local.getX + size * local.getZ) 92 | } 93 | def isDefinedAt(local: Position) = 94 | array.isDefinedAt(local.getX + size * local.getZ) 95 | } 96 | 97 | val g = new PartialFunction[Position, Float] { 98 | def apply(local: Position) = { 99 | val noise = (1.0f - (random.nextFloat() * 2.0f)) 100 | global(localPlacement.add(local).withY(0)).toFloat + noise 101 | } 102 | def isDefinedAt(local: Position) = 103 | global.isDefinedAt(localPlacement.add(local).withY(0)) 104 | } 105 | 106 | val result = GlobalHeightMap(placement, new LocalHeightMap { 107 | 108 | def get(local: Position): Int = 109 | math.round(array((local.getX + offset) + size * (local.getZ + offset))) 110 | 111 | def sizeX = baseSize 112 | def sizeZ = baseSize 113 | }) 114 | 115 | private val map = g orElse local 116 | 117 | { 118 | val dist = offset / 2 119 | setPoint(0, 0, diamond(0, 0, dist)) 120 | setPoint(0, max, diamond(0, max, dist)) 121 | setPoint(max, 0, diamond(max, 0, dist)) 122 | setPoint(max, max, diamond(max, max, dist)) 123 | divide(max) 124 | } 125 | 126 | private def getPoint(x: Int, z: Int): Option[Float] = { 127 | val pos = new Position(x, 0, z) 128 | if (map.isDefinedAt(pos)) { 129 | val point = map(pos) 130 | if (point.isNaN) None 131 | else Some(point) 132 | } else None 133 | } 134 | 135 | private def setPoint(x: Int, z: Int, value: Float) { 136 | array(x + size * z) = value 137 | } 138 | 139 | def square(x: Int, y: Int, size: Int) = 140 | average( 141 | getPoint(x - size, y - size), // upper left 142 | getPoint(x + size, y - size), // upper right 143 | getPoint(x + size, y + size), // lower right 144 | getPoint(x - size, y + size) // lower left 145 | ) 146 | 147 | def diamond(x: Int, y: Int, size: Int) = 148 | average( 149 | getPoint(x, y - size), // top 150 | getPoint(x + size, y), // right 151 | getPoint(x, y + size), // bottom 152 | getPoint(x - size, y) // left 153 | ) 154 | 155 | def average(values: Option[Float]*): Float = { 156 | val v = values.flatten 157 | if (v.size > 0) v.sum / v.size 158 | else 0.0f 159 | } 160 | 161 | def divide(size: Int) { 162 | val half = size / 2 163 | val scale = roughness * half 164 | 165 | if (half < 1) return 166 | 167 | for (y <- half until max by size; x <- half until max by size) { 168 | val v = square(x, y, half) + random.nextFloat() * scale * 2 - scale 169 | setPoint(x, y, v) 170 | } 171 | for (y <- 0 to max by half; x <- ((y + half) % size) to max by size) { 172 | val v = diamond(x, y, half) + random.nextFloat() * scale * 2 - scale 173 | setPoint(x, y, v) 174 | } 175 | divide(size / 2) 176 | } 177 | 178 | def apply(pos: Position): Int = result(pos) 179 | def isDefinedAt(pos: Position) = result.isDefinedAt(pos) 180 | 181 | } 182 | 183 | object DiamondSquareHeightMap { 184 | def findNearestPowerOfTwo(number: Int): Int = { 185 | if (number < 0) throw new IllegalArgumentException("Number must be positive") 186 | var n = number 187 | var i = 0 188 | while (n != 0) { 189 | i += 1 190 | n = n >> 1 191 | } 192 | 1 << i 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/world.scala: -------------------------------------------------------------------------------- 1 | package konstructs 2 | 3 | import scala.util.Random 4 | import akka.actor.{Actor, ActorRef, Props, Stash} 5 | import akka.util.ByteString 6 | import konstructs.api._ 7 | import konstructs.utils._ 8 | import konstructs.shard.{BlockData, ChunkPosition, ChunkData} 9 | 10 | case class FlatWorld(sizeX: Int, sizeZ: Int) 11 | 12 | class FlatWorldActor(name: String, 13 | end: Position, 14 | factory: BlockFactory, 15 | val jsonStorage: ActorRef, 16 | val binaryStorage: ActorRef) 17 | extends Actor 18 | with Stash 19 | with JsonStorage 20 | with BinaryStorage { 21 | import World._ 22 | import GeneratorActor.Generated 23 | import Db.ChunkSize 24 | 25 | val ns = "worlds" 26 | 27 | loadGson(name) 28 | 29 | val random = new Random 30 | val size = 1024 31 | 32 | def receive = { 33 | case GsonLoaded(_, json) if json != null => 34 | val world = gson.fromJson(json, classOf[FlatWorld]) 35 | loadBinary(name) 36 | context.become(loadHeightMap(world)) 37 | unstashAll() 38 | case GsonLoaded(_, _) => 39 | val world = FlatWorld(size * 3, size * 3) 40 | storeGson(name, gson.toJsonTree(world)) 41 | loadBinary(name) 42 | context.become(loadHeightMap(world)) 43 | unstashAll() 44 | case _ => 45 | stash() 46 | } 47 | 48 | def loadHeightMap(world: FlatWorld): Receive = { 49 | case BinaryLoaded(_, Some(data)) => 50 | val localMap = ArrayHeightMap.fromByteArray(data.toArray, world.sizeX, world.sizeZ) 51 | val map = GlobalHeightMap(new Position(0, 0, 0), localMap) 52 | context.become(ready(world, map)) 53 | case BinaryLoaded(_, None) => 54 | val newMap = { 55 | val diamond0 = new DiamondSquareHeightMap(0.1f, size, new Position(0, 0, 0), EmptyHeightMap) 56 | val diamond1 = new DiamondSquareHeightMap(0.2f, size, new Position(0, 0, size), diamond0) 57 | val diamond2 = new DiamondSquareHeightMap(0.1f, size, new Position(0, 0, 2 * size), diamond0 orElse diamond1) 58 | val diamond3 = 59 | new DiamondSquareHeightMap(0.3f, size, new Position(size, 0, 0), diamond0 orElse diamond1 orElse diamond2) 60 | val diamond4 = new DiamondSquareHeightMap(0.7f, 61 | size, 62 | new Position(size, 0, size), 63 | diamond0 orElse diamond1 orElse diamond2 orElse diamond3) 64 | val diamond5 = new DiamondSquareHeightMap( 65 | 0.1f, 66 | size, 67 | new Position(size, 0, 2 * size), 68 | diamond0 orElse diamond1 orElse diamond2 orElse diamond3 orElse diamond4) 69 | val diamond6 = new DiamondSquareHeightMap( 70 | 0.3f, 71 | size, 72 | new Position(2 * size, 0, 0), 73 | diamond0 orElse diamond1 orElse diamond2 orElse diamond3 orElse diamond4 orElse diamond5) 74 | val diamond7 = new DiamondSquareHeightMap( 75 | 0.2f, 76 | size, 77 | new Position(2 * size, 0, size), 78 | diamond0 orElse diamond1 orElse diamond2 orElse diamond3 orElse diamond4 orElse diamond5 orElse diamond6) 79 | val diamond8 = new DiamondSquareHeightMap( 80 | 0.1f, 81 | size, 82 | new Position(2 * size, 0, 2 * size), 83 | diamond0 orElse diamond1 orElse diamond2 orElse diamond3 orElse diamond4 orElse diamond5 orElse diamond6 orElse diamond7) 84 | diamond0 orElse diamond1 orElse diamond2 orElse diamond3 orElse diamond4 orElse diamond5 orElse diamond6 orElse diamond7 orElse diamond8 85 | } 86 | val localMap = ArrayHeightMap.fromExistingHeightMap(newMap, size * 3, size * 3) 87 | storeBinary(name, ByteString(localMap.toByteArray)) 88 | val map = GlobalHeightMap(new Position(0, 0, 0), localMap) 89 | context.become(ready(world, map)) 90 | unstashAll() 91 | case _ => 92 | stash() 93 | } 94 | 95 | private def w(ns: String, name: String) = 96 | factory.getW(new BlockTypeId(ns, name)) 97 | 98 | val Pristine = Health.PRISTINE.getHealth() 99 | 100 | private val Konstructs = "org/konstructs" 101 | private val Flowers = Seq("flower-yellow", "flower-red", "flower-purple", "sunflower", "flower-white", "flower-blue") 102 | 103 | private def blockSeq(chunk: ChunkPosition, map: HeightMap): Seq[BlockData] = { 104 | for (z <- 0 until ChunkSize; 105 | y <- 0 until ChunkSize; 106 | x <- 0 until ChunkSize) yield { 107 | val global = chunk.position(x, y, z) 108 | val height = map(global) + 32 109 | val gy = global.getY 110 | if (gy < height - (3 + random.nextInt(1))) { 111 | BlockData(w(Konstructs, "stone"), 112 | Pristine, 113 | Direction.UP_ENCODING, 114 | Rotation.IDENTITY_ENCODING, 115 | LightLevel.DARK_ENCODING, 116 | 0, 117 | 0, 118 | 0, 119 | LightLevel.DARK_ENCODING) 120 | } else if (gy < height) { 121 | BlockData(w(Konstructs, "dirt"), 122 | Pristine, 123 | Direction.UP_ENCODING, 124 | Rotation.IDENTITY_ENCODING, 125 | LightLevel.DARK_ENCODING, 126 | 0, 127 | 0, 128 | 0, 129 | LightLevel.DARK_ENCODING) 130 | } else if (gy < 10) { 131 | BlockData(w(Konstructs, "water"), 132 | Pristine, 133 | Direction.UP_ENCODING, 134 | Rotation.IDENTITY_ENCODING, 135 | LightLevel.FULL_ENCODING, 136 | 0, 137 | 0, 138 | 0, 139 | LightLevel.DARK_ENCODING) 140 | } else { 141 | BlockData(w(Konstructs, "vacuum"), 142 | Pristine, 143 | Direction.UP_ENCODING, 144 | Rotation.IDENTITY_ENCODING, 145 | LightLevel.FULL_ENCODING, 146 | 0, 147 | 0, 148 | 0, 149 | LightLevel.DARK_ENCODING) 150 | } 151 | } 152 | } 153 | 154 | private def blocks(chunk: ChunkPosition, map: HeightMap): Array[Byte] = { 155 | val data = new Array[Byte](ChunkSize * ChunkSize * ChunkSize * BlockData.Size) 156 | val bs = blockSeq(chunk, map) 157 | for (i <- 0 until ChunkSize * ChunkSize * ChunkSize) { 158 | bs(i).write(data, i) 159 | } 160 | data 161 | } 162 | 163 | def ready(world: FlatWorld, map: HeightMap): Receive = { 164 | case Generate(real, chunk) => 165 | sender ! Generated(real, blocks(chunk, map)) 166 | } 167 | 168 | } 169 | 170 | object FlatWorldActor { 171 | def props(name: String, end: Position, factory: BlockFactory, jsonStorage: ActorRef, binaryStorage: ActorRef) = 172 | Props(classOf[FlatWorldActor], name, end, factory, jsonStorage, binaryStorage) 173 | } 174 | 175 | object World { 176 | case class Generate(real: ChunkPosition, position: ChunkPosition) 177 | } 178 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/db.scala: -------------------------------------------------------------------------------- 1 | package konstructs 2 | 3 | import scala.collection.JavaConverters._ 4 | import scala.collection.mutable 5 | import akka.actor.{Actor, ActorRef, Props} 6 | 7 | import konstructs.api.{Position, BlockFactory, Box, BlockTypeId, Block, Orientation} 8 | import konstructs.api.messages.{ 9 | BoxQuery, 10 | BoxShapeQuery, 11 | BoxQueryResult, 12 | BoxShapeQueryResult, 13 | ViewBlock, 14 | ReplaceBlocks, 15 | ReplaceBlock, 16 | DamageBlockWithBlock, 17 | InteractTertiary 18 | } 19 | import konstructs.shard.{ShardActor, ShardPosition, ChunkPosition, ChunkData, BoxChunking, Light} 20 | 21 | object Db { 22 | val ChunkSize = 32 23 | val ShardSize = 8 24 | } 25 | 26 | class DbActor(universe: ActorRef, 27 | generator: ActorRef, 28 | binaryStorage: ActorRef, 29 | jsonStorage: ActorRef, 30 | blockUpdateEvents: Seq[ActorRef], 31 | blockFactory: BlockFactory, 32 | tertiaryInteractionFilters: Seq[ActorRef]) 33 | extends Actor { 34 | import DbActor._ 35 | 36 | def shardActorId(r: ShardPosition) = s"shard-${r.m}-${r.n}-${r.o}" 37 | 38 | def getShardActor(pos: Position): ActorRef = 39 | getShardActor(ShardPosition(pos)) 40 | 41 | def getShardActor(chunk: ChunkPosition): ActorRef = 42 | getShardActor(ShardPosition(chunk)) 43 | 44 | def getShardActor(shard: ShardPosition): ActorRef = { 45 | val rid = shardActorId(shard) 46 | context.child(rid) match { 47 | case Some(a) => a 48 | case None => 49 | context.actorOf(ShardActor.props(self, 50 | shard, 51 | binaryStorage, 52 | jsonStorage, 53 | blockUpdateEvents, 54 | generator, 55 | blockFactory, 56 | tertiaryInteractionFilters, 57 | universe), 58 | rid) 59 | } 60 | } 61 | 62 | def receive = { 63 | case i: InteractPrimaryUpdate => 64 | getShardActor(i.position) forward i 65 | case i: InteractSecondaryUpdate => 66 | getShardActor(i.position) forward i 67 | case i: InteractTertiaryUpdate => 68 | getShardActor(i.message.getPosition) forward i 69 | case r: ReplaceBlock => 70 | getShardActor(r.getPosition) forward r 71 | case v: ViewBlock => 72 | getShardActor(v.getPosition) forward v 73 | case s: SendBlocks => 74 | getShardActor(s.chunk) forward s 75 | case d: DamageBlockWithBlock => 76 | getShardActor(d.getToDamage) forward d 77 | case q: BoxQuery => 78 | val chunkBoxes = BoxChunking.chunked(q.getBox) 79 | val resultActor = context.actorOf(BoxQueryResultActor.props(sender, Left(q), chunkBoxes, blockFactory)) 80 | chunkBoxes.foreach { box => 81 | getShardActor(box.getFrom).tell(new BoxQuery(box), resultActor) 82 | } 83 | case q: BoxShapeQuery => 84 | val chunkBoxes = BoxChunking.chunked(q.getBox.getBox) 85 | val resultActor = context.actorOf(BoxQueryResultActor.props(sender, Right(q), chunkBoxes, blockFactory)) 86 | chunkBoxes.foreach { box => 87 | getShardActor(box.getFrom).tell(new BoxQuery(box), resultActor) 88 | } 89 | case r: ReplaceBlocks => 90 | for ((chunk, blocks) <- splitList[BlockTypeId](r.getBlocks)) { 91 | getShardActor(chunk) forward ShardActor.ReplaceBlocks(chunk, r.getFilter, blocks) 92 | } 93 | case b: BlockList => 94 | universe ! b 95 | case c: ChunkUpdate => 96 | universe ! c 97 | case r: Light.RefreshLight => 98 | getShardActor(r.chunk) forward r 99 | case r: Light.RefreshAmbientLight => 100 | getShardActor(r.chunk) forward r 101 | case f: Light.FloodLight => 102 | getShardActor(f.chunk) forward f 103 | case f: Light.FloodAmbientLight => 104 | getShardActor(f.chunk) forward f 105 | case l: Light.RemoveLight => 106 | getShardActor(l.chunk) forward l 107 | case l: Light.RemoveAmbientLight => 108 | getShardActor(l.chunk) forward l 109 | } 110 | } 111 | 112 | object DbActor { 113 | case class SendBlocks(chunk: ChunkPosition) 114 | case class BlockList(chunk: ChunkPosition, data: ChunkData) 115 | case class ChunkUpdate(chunk: ChunkPosition, data: ChunkData) 116 | 117 | case class InteractPrimaryUpdate(position: Position, block: Block) 118 | case class InteractSecondaryUpdate(position: Position, orientation: Orientation, block: Block) 119 | case class InteractTertiaryUpdate(filters: Seq[ActorRef], message: InteractTertiary) 120 | 121 | def splitList[T](placed: java.util.Map[Position, T]): Map[ChunkPosition, Map[Position, T]] = { 122 | val shards = mutable.HashMap[ChunkPosition, mutable.Map[Position, T]]() 123 | 124 | for ((position, i) <- placed.asScala) { 125 | val pos = ChunkPosition(position) 126 | val map: mutable.Map[Position, T] = 127 | shards.getOrElse(pos, mutable.HashMap[Position, T]()) 128 | map += position -> i 129 | shards += pos -> map 130 | } 131 | (shards.map { 132 | case (k, v) => 133 | k -> v.toMap 134 | }).toMap 135 | } 136 | 137 | def props(universe: ActorRef, 138 | generator: ActorRef, 139 | binaryStorage: ActorRef, 140 | jsonStorage: ActorRef, 141 | blockUpdateEvents: Seq[ActorRef], 142 | blockFactory: BlockFactory, 143 | tertiaryInteractionFilters: Seq[ActorRef]) = 144 | Props(classOf[DbActor], 145 | universe, 146 | generator, 147 | binaryStorage, 148 | jsonStorage, 149 | blockUpdateEvents, 150 | blockFactory, 151 | tertiaryInteractionFilters) 152 | } 153 | 154 | class BoxQueryResultActor(initiator: ActorRef, 155 | blockFactory: BlockFactory, 156 | query: Either[BoxQuery, BoxShapeQuery], 157 | boxes: Set[Box]) 158 | extends Actor { 159 | var receivedBoxes: Set[BoxQueryResult] = Set() 160 | 161 | private val box = query match { 162 | case Left(q) => q.getBox 163 | case Right(q) => q.getBox.getBox 164 | } 165 | 166 | def receive = { 167 | case r: BoxQueryResult => 168 | receivedBoxes += r 169 | if (receivedBoxes.map(_.getBox) == boxes) { 170 | val data = new Array[BlockTypeId](box.getNumberOfBlocks) 171 | for (subData <- receivedBoxes) { 172 | for ((position, typeId) <- subData.getAsMap.asScala) { 173 | data(box.arrayIndex(position)) = typeId 174 | } 175 | } 176 | query match { 177 | case Left(q) => 178 | initiator ! new BoxQueryResult(q.getBox, data) 179 | case Right(q) => 180 | initiator ! new BoxShapeQueryResult(q.getBox, data) 181 | } 182 | context.stop(self) 183 | } 184 | } 185 | 186 | } 187 | 188 | object BoxQueryResultActor { 189 | def props(initiator: ActorRef, query: Either[BoxQuery, BoxShapeQuery], boxes: Set[Box], blockFactory: BlockFactory) = 190 | Props(classOf[BoxQueryResultActor], initiator, blockFactory, query, boxes) 191 | } 192 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/universe.scala: -------------------------------------------------------------------------------- 1 | package konstructs 2 | 3 | import akka.actor.{Actor, ActorRef, Props, Stash} 4 | import konstructs.plugin.{PluginConstructor, Config, ListConfig, PluginRef} 5 | import konstructs.api._ 6 | import konstructs.api.messages._ 7 | import konstructs.metric.MetricPlugin 8 | import collection.JavaConverters._ 9 | 10 | class UniverseActor( 11 | name: String, 12 | jsonStorage: ActorRef, 13 | binaryStorage: ActorRef, 14 | inventoryManager: ActorRef, 15 | konstructing: ActorRef, 16 | blockManager: ActorRef, 17 | metrics: ActorRef, 18 | chatFilters: Seq[ActorRef], 19 | blockUpdateEvents: Seq[ActorRef], 20 | primaryInteractionFilters: Seq[ActorRef], 21 | secondaryInteractionFilters: Seq[ActorRef], 22 | tertiaryInteractionFilters: Seq[ActorRef], 23 | listeners: Map[EventTypeId, Seq[ActorRef]] 24 | ) extends Actor 25 | with Stash { 26 | 27 | import UniverseActor._ 28 | 29 | var generator: ActorRef = null 30 | var db: ActorRef = null 31 | 32 | private var nextPid = 0 33 | 34 | def playerActorId(pid: Int) = s"player-$pid" 35 | 36 | def allPlayers(except: Option[Int] = None) = { 37 | val players = context.children.filter(_.path.name.startsWith("player-")) 38 | except match { 39 | case Some(pid) => 40 | players.filter(_.path.name != playerActorId(pid)) 41 | case None => players 42 | } 43 | } 44 | 45 | def player(nick: String, password: String) { 46 | val player = context.actorOf( 47 | PlayerActor.props(nextPid, nick, password, sender, db, self, jsonStorage, protocol.Position(0, 512f, 0, 0, 0)), 48 | playerActorId(nextPid) 49 | ) 50 | allPlayers(except = Some(nextPid)).foreach(_ ! PlayerActor.SendInfo(player)) 51 | allPlayers(except = Some(nextPid)).foreach(player ! PlayerActor.SendInfo(_)) 52 | nextPid = nextPid + 1 53 | } 54 | 55 | blockManager ! GetBlockFactory.MESSAGE 56 | 57 | def receive = { 58 | case factory: BlockFactory => 59 | generator = context.actorOf(GeneratorActor.props(jsonStorage, binaryStorage, factory)) 60 | db = context.actorOf( 61 | DbActor 62 | .props(self, generator, binaryStorage, jsonStorage, blockUpdateEvents, factory, tertiaryInteractionFilters)) 63 | context.become(ready) 64 | unstashAll() 65 | case _ => 66 | stash() 67 | } 68 | 69 | def ready: Receive = { 70 | case s: SendEvent => 71 | listeners getOrElse (s.getId, Seq()) foreach (_ forward s.getMessage) 72 | case CreatePlayer(nick, password) => 73 | player(nick, password) 74 | case m: PlayerActor.PlayerMovement => 75 | allPlayers(except = Some(m.pid)).foreach(_ ! m) 76 | case l: PlayerActor.PlayerLogout => 77 | allPlayers(except = Some(l.pid)).foreach(_ ! l) 78 | case c: DbActor.ChunkUpdate => 79 | allPlayers().foreach(_ ! c) 80 | case s: Say => 81 | val filters = chatFilters :+ self 82 | filters.head.forward(new SayFilter(filters.tail.toArray, s)) 83 | case s: SayFilter => 84 | allPlayers().foreach(_.forward(new Said(s.getMessage.getText))) 85 | case s: Said => 86 | allPlayers().foreach(_.forward(s)) 87 | case i: InteractPrimary => 88 | val filters = primaryInteractionFilters :+ self 89 | filters.head.forward(new InteractPrimaryFilter(filters.tail.toArray, i)) 90 | case i: InteractPrimaryFilter => 91 | val message = i.getMessage 92 | if (message.getPosition != null) { 93 | db.tell(DbActor.InteractPrimaryUpdate(message.getPosition, message.getBlock), message.getSender) 94 | } else { 95 | message.getSender ! new InteractResult(message.getPosition, message.getBlock, null) 96 | } 97 | case i: InteractSecondary => 98 | val filters = secondaryInteractionFilters :+ self 99 | filters.head.forward(new InteractSecondaryFilter(filters.tail.toArray, i)) 100 | case i: InteractSecondaryFilter => 101 | val message = i.getMessage 102 | if (message.getPosition != null && message.getBlock != null) { 103 | db.tell(DbActor.InteractSecondaryUpdate(message.getPosition, message.getOrientation, message.getBlock), 104 | message.getSender) 105 | } else { 106 | message.getSender ! new InteractResult(message.getPosition, message.getBlock, null) 107 | } 108 | case i: InteractTertiary => 109 | if (i.getPosition != null) { 110 | db ! DbActor.InteractTertiaryUpdate(tertiaryInteractionFilters, i) 111 | } else { 112 | val filters = tertiaryInteractionFilters :+ self 113 | filters.head ! new InteractTertiaryFilter(filters.tail.toArray, i) 114 | } 115 | case i: InteractTertiaryFilter if !i.getMessage.isWorldPhase => 116 | val message = i.getMessage 117 | message.getSender ! new InteractResult(message.getPosition, message.getBlock, message.getBlockAtPosition) 118 | case p: ReplaceBlock => 119 | db.forward(p) 120 | case v: ViewBlock => 121 | db.forward(v) 122 | case r: ReplaceBlocks => 123 | db forward r 124 | case c: CreateInventory => 125 | inventoryManager.forward(c) 126 | case g: GetInventory => 127 | inventoryManager.forward(g) 128 | case p: PutStack => 129 | inventoryManager.forward(p) 130 | case r: RemoveStack => 131 | inventoryManager.forward(r) 132 | case g: GetStack => 133 | inventoryManager.forward(g) 134 | case d: DeleteInventory => 135 | inventoryManager.forward(d) 136 | case m: MatchPattern => 137 | konstructing.forward(m) 138 | case k: KonstructPattern => 139 | konstructing.forward(k) 140 | case q: BoxQuery => 141 | db forward q 142 | case q: BoxShapeQuery => 143 | db forward q 144 | case d: DamageBlockWithBlock => 145 | db forward d 146 | case GetBlockFactory.MESSAGE => 147 | blockManager.forward(GetBlockFactory.MESSAGE) 148 | case GetTextures => 149 | blockManager.forward(GetTextures) 150 | case i: IncreaseMetric => 151 | metrics.forward(i) 152 | case s: SetMetric => 153 | metrics.forward(s) 154 | } 155 | } 156 | 157 | object UniverseActor { 158 | case class CreatePlayer(nick: String, password: String) 159 | 160 | import konstructs.plugin.Plugin.nullAsEmpty 161 | 162 | def parseEventId(plugin: PluginRef)(id: String): (EventTypeId, PluginRef) = 163 | (EventTypeId.fromString(id), plugin) 164 | 165 | def parseEventIds(plugin: PluginRef): Seq[(EventTypeId, PluginRef)] = 166 | plugin.getConfig.root.keySet.asScala map parseEventId(plugin) toSeq 167 | 168 | def tag(entry: (EventTypeId, PluginRef)): EventTypeId = entry._1 169 | 170 | def getActorRef(plugins: Seq[(EventTypeId, konstructs.plugin.PluginRef)]): Seq[ActorRef] = 171 | plugins map { 172 | case (_, plugin) => plugin.getPlugin 173 | } 174 | 175 | def parseListeners(listeners: Seq[PluginRef]): Map[EventTypeId, Seq[ActorRef]] = { 176 | val tagged: Seq[(EventTypeId, PluginRef)] = listeners map parseEventIds flatten 177 | val grouped = tagged groupBy tag 178 | grouped mapValues getActorRef 179 | } 180 | 181 | @PluginConstructor 182 | def props( 183 | name: String, 184 | notUsed: ActorRef, 185 | @Config(key = "binary-storage") binaryStorage: ActorRef, 186 | @Config(key = "json-storage") jsonStorage: ActorRef, 187 | @Config(key = "inventory-manager") inventoryManager: ActorRef, 188 | @Config(key = "konstructing") konstructing: ActorRef, 189 | @Config(key = "block-manager") blockManager: ActorRef, 190 | @Config(key = "metrics") metrics: ActorRef, 191 | @ListConfig( 192 | key = "chat-filters", 193 | elementType = classOf[ActorRef], 194 | optional = true 195 | ) chatFilters: Seq[ActorRef], 196 | @ListConfig( 197 | key = "block-update-events", 198 | elementType = classOf[ActorRef], 199 | optional = true 200 | ) blockUpdateEvents: Seq[ActorRef], 201 | @ListConfig( 202 | key = "primary-interaction-listeners", 203 | elementType = classOf[ActorRef], 204 | optional = true 205 | ) primaryListeners: Seq[ActorRef], 206 | @ListConfig( 207 | key = "secondary-interaction-listeners", 208 | elementType = classOf[ActorRef], 209 | optional = true 210 | ) secondaryListeners: Seq[ActorRef], 211 | @ListConfig( 212 | key = "tertiary-interaction-listeners", 213 | elementType = classOf[ActorRef], 214 | optional = true 215 | ) tertiaryListeners: Seq[ActorRef], 216 | @ListConfig( 217 | key = "listeners", 218 | elementType = classOf[PluginRef], 219 | optional = true 220 | ) listeners: Seq[PluginRef] 221 | ): Props = Props( 222 | classOf[UniverseActor], 223 | name, 224 | jsonStorage, 225 | binaryStorage, 226 | inventoryManager, 227 | konstructing, 228 | blockManager, 229 | metrics, 230 | nullAsEmpty(chatFilters), 231 | nullAsEmpty(blockUpdateEvents), 232 | nullAsEmpty(primaryListeners), 233 | nullAsEmpty(secondaryListeners), 234 | nullAsEmpty(tertiaryListeners), 235 | parseListeners(nullAsEmpty(listeners)) 236 | ) 237 | } 238 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/konstructing.scala: -------------------------------------------------------------------------------- 1 | package konstructs 2 | 3 | import java.util 4 | import java.util.UUID 5 | 6 | import scala.collection.JavaConverters._ 7 | import scala.math.min 8 | 9 | import com.typesafe.config.{Config => TypesafeConfig, ConfigValueType} 10 | import com.typesafe.config.ConfigException.BadValue 11 | import akka.actor.{Actor, Props, ActorRef, Stash} 12 | import konstructs.api._ 13 | import konstructs.api.messages.GetBlockFactory 14 | import konstructs.plugin.{PluginConstructor, Config, ListConfig} 15 | 16 | class KonstructingActor(universe: ActorRef, konstructs: Set[Konstruct]) extends Actor with Stash { 17 | 18 | universe ! GetBlockFactory.MESSAGE 19 | 20 | def bestMatch(pattern: Pattern, factory: BlockFactory): Option[Konstruct] = { 21 | konstructs.filter { k => 22 | pattern.contains(k.getPattern, factory) > 0 23 | }.toSeq.sortWith(_.getPattern.getComplexity > _.getPattern.getComplexity).headOption 24 | } 25 | 26 | def receive = { 27 | case factory: BlockFactory => 28 | context.become(initialized(factory)) 29 | unstashAll() 30 | case _ => 31 | stash() 32 | } 33 | 34 | def initialized(factory: BlockFactory): Receive = { 35 | case MatchPattern(pattern: Pattern) => 36 | bestMatch(pattern, factory).map { k => 37 | sender ! PatternMatched(k.getResult) 38 | } 39 | case KonstructPattern(pattern: Pattern) => 40 | bestMatch(pattern, factory) match { 41 | case Some(k) => 42 | val maxNumberOfStacks = min(pattern.contains(k.getPattern, factory), Stack.MAX_SIZE / k.getResult.size) 43 | sender ! PatternKonstructed(k.getPattern, k.getResult, maxNumberOfStacks) 44 | case None => 45 | sender ! PatternNotKonstructed 46 | } 47 | } 48 | } 49 | 50 | object KonstructingActor { 51 | 52 | import konstructs.plugin.Plugin.nullAsEmpty 53 | 54 | def parseStack(config: TypesafeConfig): Stack = { 55 | val blockId = config.getString("id") 56 | val amount = if (config.hasPath("amount")) { 57 | config.getInt("amount") 58 | } else { 59 | 1 60 | } 61 | new Stack(((0 until amount).map { i => 62 | Block.create(blockId) 63 | }).toArray) 64 | } 65 | 66 | def parseStackTemplate(config: TypesafeConfig): StackTemplate = { 67 | if (config.hasPath("id")) { 68 | val blockId = config.getString("id") 69 | val amount = if (config.hasPath("amount")) { 70 | config.getInt("amount") 71 | } else { 72 | 1 73 | } 74 | new StackTemplate(BlockOrClassId.fromString(blockId), amount) 75 | } else { 76 | null 77 | } 78 | } 79 | 80 | def parsePatternTemplate(config: TypesafeConfig): PatternTemplate = { 81 | if (config.hasPath("stack")) { 82 | new PatternTemplate(Array(parseStackTemplate(config.getConfig("stack"))), 1, 1) 83 | } else { 84 | if (config.getValue("stacks").valueType() == ConfigValueType.OBJECT) { 85 | throw new BadValue("stacks", "'stacks' does not expect object. Did you mean 'stack'?") 86 | } 87 | val rows = config.getInt("rows") 88 | val columns = config.getInt("columns") 89 | val stacks = util.Arrays.copyOf( 90 | config.getConfigList("stacks").asScala.map(parseStackTemplate).toArray, 91 | rows * columns 92 | ) 93 | new PatternTemplate(stacks, rows, columns) 94 | } 95 | } 96 | 97 | def parseKonstructs(config: TypesafeConfig): Set[Konstruct] = { 98 | val konstructs = config.root.entrySet.asScala.map { e => 99 | config.getConfig(e.getKey) 100 | } 101 | (for (konstruct <- konstructs) yield { 102 | val pattern = parsePatternTemplate(konstruct.getConfig("match")) 103 | val result = parseStack(konstruct.getConfig("result")) 104 | new Konstruct(pattern, result) 105 | }) toSet 106 | } 107 | 108 | @PluginConstructor 109 | def props( 110 | name: String, 111 | universe: ActorRef, 112 | @Config(key = "konstructs") konstructs: TypesafeConfig 113 | ): Props = 114 | Props(classOf[KonstructingActor], universe, parseKonstructs(konstructs)) 115 | } 116 | 117 | class KonstructingViewActor(player: ActorRef, 118 | universe: ActorRef, 119 | inventoryId: UUID, 120 | inventoryView: InventoryView, 121 | konstructingView: InventoryView, 122 | resultView: InventoryView) 123 | extends Actor 124 | with Stash { 125 | 126 | universe ! GetBlockFactory.MESSAGE 127 | 128 | val EmptyInventory = Inventory.createEmpty(0) 129 | var konstructing = Inventory.createEmpty(konstructingView.getRows * konstructingView.getColumns) 130 | var result = Inventory.createEmpty(resultView.getRows * resultView.getColumns) 131 | var factory: BlockFactory = null 132 | 133 | private def view(inventory: Inventory) = 134 | View.EMPTY.add(inventoryView, inventory).add(konstructingView, konstructing).add(resultView, result) 135 | 136 | private def updateKonstructing(k: Inventory) { 137 | konstructing = k 138 | result = Inventory.createEmpty(1) 139 | val p = konstructing.getPattern(konstructingView) 140 | if (p != null) 141 | universe ! MatchPattern(p) 142 | } 143 | 144 | def receive = { 145 | case f: BlockFactory => 146 | factory = f 147 | if (inventoryId != null) { 148 | universe ! GetInventory(inventoryId) 149 | } else { 150 | context.become(ready(EmptyInventory)) 151 | player ! ConnectView(self, view(EmptyInventory)) 152 | } 153 | case GetInventoryResponse(_, Some(inventory)) => 154 | context.become(ready(inventory)) 155 | player ! ConnectView(self, view(inventory)) 156 | case _ => 157 | context.stop(self) 158 | } 159 | 160 | def awaitInventory: Receive = { 161 | case GetInventoryResponse(_, Some(inventory)) => 162 | context.become(ready(inventory)) 163 | player ! UpdateView(view(inventory)) 164 | unstashAll() 165 | case CloseInventory => 166 | context.stop(self) 167 | case _ => 168 | stash() 169 | } 170 | 171 | def awaitKonstruction(inventory: Inventory, amount: StackAmount): Receive = { 172 | case PatternKonstructed(pattern, stack, number) => 173 | context.become(ready(inventory)) 174 | val toKonstruct = amount match { 175 | case StackAmount.ALL => 176 | number 177 | case StackAmount.HALF => 178 | Math.max(number / 2, 1) 179 | case StackAmount.ONE => 180 | 1 181 | } 182 | 183 | val newKonstructing = konstructing.remove(pattern, factory, toKonstruct) 184 | if (newKonstructing != null) 185 | updateKonstructing(newKonstructing) 186 | player ! ReceiveStack(Stack.createOfSize(stack.getTypeId, toKonstruct * stack.size)) 187 | player ! UpdateView(view(inventory)) 188 | unstashAll() 189 | case CloseInventory => 190 | context.stop(self) 191 | case _ => 192 | stash() 193 | } 194 | 195 | def ready(inventory: Inventory): Receive = { 196 | case r: ReceiveStack => 197 | player ! r 198 | case PatternMatched(stack) => 199 | result = result.withSlot(0, stack) 200 | player ! UpdateView(view(inventory)) 201 | case PutViewStack(stack, to) => 202 | if (inventoryView.contains(to)) { 203 | context.become(awaitInventory) 204 | universe.forward(PutStack(inventoryId, inventoryView.translate(to), stack)) 205 | universe ! GetInventory(inventoryId) 206 | } else if (konstructingView.contains(to)) { 207 | val index = konstructingView.translate(to) 208 | val oldStack = konstructing.getStack(index) 209 | if (oldStack != null) { 210 | if (oldStack.acceptsPartOf(stack)) { 211 | val r = oldStack.acceptPartOf(stack) 212 | sender ! ReceiveStack(r.getGiving) 213 | updateKonstructing(konstructing.withSlot(index, r.getAccepting())) 214 | } else { 215 | updateKonstructing(konstructing.withSlot(index, stack)) 216 | sender ! ReceiveStack(oldStack) 217 | } 218 | } else { 219 | updateKonstructing(konstructing.withSlot(index, stack)) 220 | sender ! ReceiveStack(null) 221 | } 222 | player ! UpdateView(view(inventory)) 223 | } else { 224 | sender ! ReceiveStack(stack) 225 | } 226 | case RemoveViewStack(from, amount) => 227 | if (inventoryView.contains(from)) { 228 | context.become(awaitInventory) 229 | universe.forward(RemoveStack(inventoryId, inventoryView.translate(from), amount)) 230 | universe ! GetInventory(inventoryId) 231 | } else if (konstructingView.contains(from)) { 232 | val stack = konstructing.getStack(konstructingView.translate(from)) 233 | updateKonstructing(konstructing.withSlot(konstructingView.translate(from), stack.drop(amount))) 234 | sender ! ReceiveStack(stack.take(amount)) 235 | player ! UpdateView(view(inventory)) 236 | } else if (resultView.contains(from)) { 237 | val pattern = konstructing.getPattern(konstructingView) 238 | if (pattern != null) { 239 | if (!result.isEmpty) { 240 | context.become(awaitKonstruction(inventory, amount)) 241 | universe ! KonstructPattern(pattern) 242 | } else { 243 | sender ! ReceiveStack(null) 244 | } 245 | } else { 246 | sender ! ReceiveStack(null) 247 | } 248 | } 249 | case CloseInventory => 250 | context.stop(self) 251 | } 252 | 253 | } 254 | 255 | object KonstructingViewActor { 256 | def props(player: ActorRef, 257 | universe: ActorRef, 258 | inventoryId: UUID, 259 | inventoryView: InventoryView, 260 | konstructingView: InventoryView, 261 | resultView: InventoryView): Props = 262 | Props(classOf[KonstructingViewActor], player, universe, inventoryId, inventoryView, konstructingView, resultView) 263 | } 264 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/protocol/client.scala: -------------------------------------------------------------------------------- 1 | package konstructs.protocol 2 | 3 | import scala.collection.JavaConverters._ 4 | import akka.actor.{Actor, Props, ActorRef, Stash, PoisonPill} 5 | import akka.io.Tcp 6 | import akka.util.{ByteString, ByteStringBuilder} 7 | import konstructs.{PlayerActor, UniverseActor, DbActor} 8 | import konstructs.api._ 9 | import konstructs.api.messages.Said 10 | import konstructs.shard.ChunkPosition 11 | 12 | class ClientActor(universe: ActorRef, factory: BlockFactory, textures: Array[Byte]) extends Actor with Stash { 13 | import DbActor.BlockList 14 | import UniverseActor.CreatePlayer 15 | import ClientActor._ 16 | import PlayerActor._ 17 | 18 | implicit val bo = java.nio.ByteOrder.BIG_ENDIAN 19 | 20 | private var readBuffer = ByteString.empty 21 | 22 | private val writeBuffer = new ByteStringBuilder() 23 | 24 | private var player: PlayerInfo = null 25 | 26 | private var canWrite = true 27 | 28 | private def readData[T](conv: String => T, data: String): List[T] = { 29 | val comma = data.indexOf(',') 30 | if (comma > 0) { 31 | val i = conv(data.take(comma)) 32 | i :: readData(conv, data.drop(comma + 1)) 33 | } else { 34 | val i = conv(data) 35 | i :: Nil 36 | } 37 | } 38 | 39 | private def handle(data: ByteString) = { 40 | val command = data.decodeString("ascii") 41 | if (command.startsWith("P,")) { 42 | val floats = readData(_.toFloat, command.drop(2)) 43 | player.actor ! Position(floats(0), floats(1), floats(2), floats(3), floats(4)) 44 | } else if (command.startsWith("C,")) { 45 | val ints = readData(_.toInt, command.drop(2)) 46 | player.actor ! DbActor.SendBlocks(ChunkPosition(ints(0), ints(1), ints(2))) 47 | } else if (command.startsWith("M,")) { 48 | val ints = readData(_.toInt, command.drop(2)) 49 | if (ints(0) != 0) { 50 | player.actor ! Action(new konstructs.api.Position(ints(1), ints(2), ints(3)), 51 | Orientation.get(Direction.get(ints(6)), Rotation.get(ints(7))), 52 | ints(4), 53 | ints(5)) 54 | } else { 55 | player.actor ! Action(null, null, ints(4), ints(5)) 56 | } 57 | } else if (command.startsWith("T,")) { 58 | val message = command.substring(2) 59 | player.actor ! Say(message) 60 | } else if (command.startsWith("I")) { 61 | player.actor ! CloseInventory 62 | } else if (command.startsWith("R,")) { 63 | val ints = readData(_.toInt, command.drop(2)) 64 | player.actor ! SelectItem(ints(0), ints(1)) 65 | } else if (command.startsWith("D,")) { 66 | val ints = readData(_.toInt, command.drop(2)) 67 | player.actor ! SetViewDistance(ints(0)) 68 | } 69 | true 70 | } 71 | 72 | private def read(data: ByteString)(handle: ByteString => Boolean) { 73 | readBuffer = readBuffer ++ data 74 | try { 75 | while (!readBuffer.isEmpty) { 76 | val size = readBuffer.iterator.getInt 77 | val length = readBuffer.length - 4 78 | if (size <= length) { 79 | readBuffer = readBuffer.drop(4) 80 | val result = handle(readBuffer.take(size)) 81 | readBuffer = readBuffer.drop(size) 82 | if (!result) 83 | return 84 | } else { 85 | return 86 | } 87 | } 88 | } catch { 89 | case _: java.util.NoSuchElementException => 90 | /* Packet was not complete yet */ 91 | } 92 | } 93 | 94 | def handleAck(pipe: ActorRef) { 95 | canWrite = true 96 | send(sender) 97 | } 98 | 99 | def receive = { 100 | case Tcp.Received(data) => 101 | read(data) { data => 102 | val command = data.decodeString("ascii") 103 | 104 | if (command.startsWith(s"V,$Version,")) { 105 | val strings = readData(s => s, command.drop(2)) 106 | val auth = Authenticate(strings(0) toInt, strings(1), strings(2)) 107 | println(s"Player ${auth.name} connected with protocol version ${auth.version}") 108 | universe ! CreatePlayer(auth.name, auth.token) 109 | context.become(waitForPlayer(sender)) 110 | } else { 111 | sendError(sender, s"This server only supports protocol version $Version") 112 | } 113 | false 114 | } 115 | case _: Tcp.ConnectionClosed => 116 | context.stop(self) 117 | case Ack => 118 | handleAck(sender) 119 | } 120 | 121 | def waitForPlayer(pipe: ActorRef): Receive = { 122 | case p: PlayerInfo => 123 | player = p 124 | send(pipe, s"U,${p.pid},${p.pos.x},${p.pos.y},${p.pos.z},${p.pos.rx},${p.pos.ry}") 125 | sendPlayerNick(pipe, p.pid, p.nick) 126 | sendBlockTypes(pipe) 127 | sendTextures(pipe) 128 | unstashAll() 129 | context.become(ready(pipe)) 130 | // Process any data left over from processing of version packet 131 | read(ByteString.empty)(handle) 132 | case Tcp.Received(data) => 133 | stash() 134 | case Ack => 135 | handleAck(sender) 136 | } 137 | 138 | def ready(pipe: ActorRef): Receive = { 139 | case Tcp.Received(data) => 140 | read(data)(handle) 141 | case BlockList(chunk, data) => 142 | sendBlocks(pipe, chunk, data.data) 143 | case ChunkUpdate(p, q, k) => 144 | sendChunkUpdate(pipe, p, q, k) 145 | case b: SendBlock => 146 | sendBlock(pipe, b) 147 | case BeltUpdate(items) => 148 | sendBelt(pipe, items) 149 | case InventoryUpdate(view) => 150 | sendInventory(pipe, view.getItems.asScala.toMap) 151 | case p: PlayerMovement => 152 | sendPlayerMovement(pipe, p) 153 | case PlayerNick(pid, nick) => 154 | sendPlayerNick(pipe, pid, nick) 155 | case PlayerLogout(pid) => 156 | sendPlayerLogout(pipe, pid) 157 | case s: Said => 158 | sendSaid(pipe, s.getText) 159 | case HeldStack(stack) => 160 | if (stack != null) 161 | sendHeldStack(pipe, stack.size, factory.getW(stack), stack.getHead.getHealth.getHealth) 162 | else 163 | sendHeldStack(pipe, 0, -1, 0) 164 | case Time(t) => 165 | sendTime(pipe, t) 166 | case _: Tcp.ConnectionClosed => 167 | context.stop(self) 168 | case Ack => 169 | handleAck(sender) 170 | } 171 | 172 | override def postStop { 173 | if (player != null) 174 | player.actor ! PoisonPill 175 | } 176 | 177 | def sendError(pipe: ActorRef, error: String) { 178 | send(pipe, s"E,$error") 179 | context.stop(self) 180 | } 181 | 182 | def sendPlayerNick(pipe: ActorRef, pid: Int, nick: String) { 183 | send(pipe, s"N,$pid,$nick") 184 | } 185 | 186 | def sendTime(pipe: ActorRef, t: Long) { 187 | send(pipe, s"T,$t") 188 | } 189 | 190 | def sendChunkUpdate(pipe: ActorRef, p: Int, q: Int, k: Int) { 191 | send(pipe, s"c,$p,$q,$k") 192 | } 193 | 194 | def sendSaid(pipe: ActorRef, msg: String) { 195 | send(pipe, s"t,$msg") 196 | } 197 | 198 | def sendPlayerLogout(pipe: ActorRef, pid: Int) { 199 | send(pipe, s"D,$pid") 200 | } 201 | 202 | def sendPlayerMovement(pipe: ActorRef, p: PlayerMovement) { 203 | send(pipe, s"P,${p.pid},${p.pos.x},${p.pos.y},${p.pos.z},${p.pos.rx},${p.pos.ry}") 204 | } 205 | 206 | def sendBelt(pipe: ActorRef, items: Array[Stack]) { 207 | for ((stack, i) <- items.zipWithIndex) { 208 | if (stack != null) { 209 | send(pipe, s"G,${i},${stack.size},${factory.getW(stack)},${stack.getHead.getHealth.getHealth}") 210 | } else { 211 | send(pipe, s"G,${i},0,0,0") 212 | } 213 | } 214 | } 215 | 216 | def sendHeldStack(pipe: ActorRef, size: Int, w: Int, health: Int) { 217 | send(pipe, s"i,$size,$w,$health") 218 | } 219 | 220 | def sendInventory(pipe: ActorRef, items: Map[Integer, Stack]) { 221 | for ((p, stack) <- items) { 222 | if (stack != null) { 223 | send(pipe, s"I,${p},${stack.size},${factory.getW(stack)},${stack.getHead.getHealth.getHealth}") 224 | } else { 225 | send(pipe, s"I,${p},0,0,0") 226 | } 227 | } 228 | } 229 | 230 | def sendBlock(pipe: ActorRef, b: SendBlock) { 231 | send(pipe, s"B,${b.p},${b.q},${b.x},${b.y},${b.z},${b.w}") 232 | } 233 | 234 | def sendBlocks(pipe: ActorRef, chunk: ChunkPosition, blocks: ByteString) { 235 | val data = 236 | ByteString.createBuilder.putByte(C).putInt(chunk.p).putInt(chunk.q).putInt(chunk.k).append(blocks).result 237 | writeBuffer.putInt(data.length).append(data) 238 | send(pipe) 239 | } 240 | 241 | def sendTextures(pipe: ActorRef) { 242 | val data = ByteString.createBuilder.putByte(M).putBytes(textures).result 243 | writeBuffer.putInt(data.length).append(data) 244 | send(pipe) 245 | } 246 | 247 | def sendBlockTypes(pipe: ActorRef) { 248 | val types = factory.getBlockTypes().asScala.map { 249 | case (id, t) => factory.getW(id) -> t 250 | } 251 | for ((w, t) <- types) { 252 | sendBlockType(pipe, w, t) 253 | } 254 | } 255 | 256 | def booleanToInt(b: Boolean): Int = if (b) 1 else 0 257 | 258 | def sendBlockType(pipe: ActorRef, w: Int, t: BlockType) { 259 | val isObstacle = booleanToInt(t.isObstacle) 260 | val isTransparent = booleanToInt(t.isTransparent) 261 | val isOrientable = booleanToInt(t.isOrientable) 262 | val faces = t.getFaces 263 | send(pipe, 264 | s"W,$w,${t.getBlockShape.getShape},${t.getBlockState.getState},$isObstacle,$isTransparent,${faces(0)},${faces( 265 | 1)},${faces(2)},${faces(3)},${faces(4)},${faces(5)},${isOrientable}") 266 | } 267 | 268 | def send(pipe: ActorRef, msg: String) { 269 | val data = ByteString(msg, "ascii") 270 | writeBuffer.putInt(data.length).append(data) 271 | send(pipe) 272 | } 273 | 274 | def send(pipe: ActorRef) { 275 | if (!writeBuffer.isEmpty) { 276 | if (canWrite) { 277 | pipe ! Tcp.Write(writeBuffer.result(), Ack) 278 | writeBuffer.clear() 279 | canWrite = false 280 | } 281 | } 282 | } 283 | 284 | } 285 | 286 | object ClientActor { 287 | val C = 'C'.toByte 288 | val B = 'B'.toByte 289 | val V = 'V'.toByte 290 | val P = 'P'.toByte 291 | val M = 'M'.toByte 292 | 293 | val Version = 10 294 | case object Ack extends Tcp.Event 295 | def props(universe: ActorRef, factory: BlockFactory, textures: Array[Byte]) = 296 | Props(classOf[ClientActor], universe, factory, textures) 297 | } 298 | -------------------------------------------------------------------------------- /src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | konstructs { 2 | org/konstructs/meta { 3 | class = "konstructs.JsonStorageActor" 4 | directory = "meta" 5 | } 6 | org/konstructs/binary { 7 | class = "konstructs.BinaryStorageActor" 8 | directory = "binary" 9 | } 10 | //org/konstructs/printmetric { 11 | // class = "konstructs.metric.PrintMetricPlugin" 12 | //} 13 | //org/konstructs/graphitemetric { 14 | // class = "konstructs.metric.GraphiteMetricPlugin" 15 | // host = "your.graphite.server.org" 16 | // port = 2003 17 | // namespace-prefix = "konstructs" 18 | // reconnect-delay = 1000 19 | //} 20 | org/konstructs/metric { 21 | class = "konstructs.metric.MetricPlugin" 22 | interval = 1000 23 | listeners { 24 | //org/konstructs/printmetric {} 25 | //org/konstructs/graphitemetric {} 26 | } 27 | } 28 | org/konstructs/konstructing { 29 | class = "konstructs.KonstructingActor" 30 | konstructs { 31 | org/konstructs/wood { 32 | match.stack { 33 | id = org/konstructs/dirt 34 | amount = 8 35 | } 36 | result.id = org/konstructs/wood 37 | } 38 | org/konstructs/stone-brick { 39 | match { 40 | stacks = [ 41 | { id = org/konstructs/stone }, 42 | { id = org/konstructs/stone } 43 | ] 44 | rows = 2 45 | columns = 1 46 | } 47 | result { 48 | id = org/konstructs/stone-brick 49 | amount = 2 50 | } 51 | } 52 | org/konstructs/brick { 53 | match { 54 | stacks = [ 55 | { id = org/konstructs/wood }, 56 | { id = org/konstructs/dirt } 57 | ] 58 | rows = 2 59 | columns = 1 60 | } 61 | result.id = org/konstructs/brick 62 | } 63 | org/konstructs/glass { 64 | match { 65 | stacks = [ 66 | { id = org/konstructs/wood }, 67 | { id = org/konstructs/sand } 68 | ] 69 | rows = 2 70 | columns = 1 71 | } 72 | result.id = org/konstructs/glass 73 | } 74 | org/konstructs/planks { 75 | match.stack.id = org/konstructs/class/Wood 76 | result { 77 | id = org/konstructs/planks 78 | amount = 2 79 | } 80 | } 81 | org/konstructs/stick { 82 | match { 83 | stacks = [ 84 | { id = org/konstructs/class/Wood }, 85 | { id = org/konstructs/class/Wood } 86 | ] 87 | rows = 2 88 | columns = 1 89 | } 90 | result { 91 | id = org/konstructs/stick 92 | amount = 8 93 | } 94 | } 95 | org/konstructs/torch { 96 | match { 97 | stacks = [ 98 | { id = org/konstructs/stick }, 99 | { id = org/konstructs/class/Wood } 100 | ] 101 | rows = 2 102 | columns = 1 103 | } 104 | result { 105 | id = org/konstructs/torch 106 | } 107 | } 108 | org/konstructs/work-table { 109 | match { 110 | stacks = [ 111 | { id = org/konstructs/class/Planks }, 112 | { id = org/konstructs/class/Planks }, 113 | { id = org/konstructs/stone }, 114 | { id = org/konstructs/stone } 115 | ] 116 | rows = 2 117 | columns = 2 118 | } 119 | result.id = org/konstructs/work-table 120 | } 121 | org/konstructs/stone { 122 | match.stack.id = org/konstructs/cobble 123 | result.id = org/konstructs/stone 124 | } 125 | org/konstructs/white-framed-stone { 126 | match { 127 | stacks = [ 128 | { id = org/konstructs/stone }, 129 | { id = org/konstructs/stone }, 130 | { id = org/konstructs/stone }, 131 | { id = org/konstructs/stone } 132 | ] 133 | rows = 2 134 | columns = 2 135 | } 136 | result { 137 | id = org/konstructs/white-framed-stone 138 | amount = 4 139 | } 140 | } 141 | org/konstructs/gray-framed-stone { 142 | match { 143 | stacks = [ 144 | { id = org/konstructs/cobble }, 145 | { id = org/konstructs/cobble }, 146 | { id = org/konstructs/cobble }, 147 | { id = org/konstructs/cobble } 148 | ] 149 | rows = 2 150 | columns = 2 151 | } 152 | result { 153 | id = org/konstructs/gray-framed-stone 154 | amount = 4 155 | } 156 | } 157 | org/konstructs/hammerstone { 158 | match { 159 | stacks = [ 160 | { id = org/konstructs/class/Stone }, 161 | { id = org/konstructs/class/Stone } 162 | ] 163 | rows = 1 164 | columns = 2 165 | } 166 | result.id = org/konstructs/hammerstone 167 | } 168 | # Test block konstruct, uncomment for testing 169 | # org/konstructs/test { 170 | # match { 171 | # stacks = [ 172 | # { 173 | # id = org/konstructs/dirt 174 | # amount = 2 175 | # }, 176 | # { id = org/konstructs/dirt } 177 | # ] 178 | # rows = 1 179 | # columns = 2 180 | # } 181 | # result.id = org/konstructs/test 182 | # } 183 | } 184 | } 185 | org/konstructs/block-manager { 186 | class = "konstructs.BlockMetaActor" 187 | json-storage = org/konstructs/meta 188 | classes { 189 | org/konstructs/class/Tool { 190 | durability = 40 191 | } 192 | org/konstructs/class/Stone { 193 | durability = 40 194 | destroyed-as = org/konstructs/cobble 195 | } 196 | org/konstructs/class/Brick { 197 | destroyed-as = org/konstructs/self 198 | } 199 | org/konstructs/class/GrassDirt { 200 | destroyed-as = org/konstructs/dirt 201 | } 202 | org/konstructs/class/Granular { 203 | durability = 5 204 | } 205 | org/konstructs/class/Grass { 206 | durability = 3 207 | } 208 | org/konstructs/class/Flower { 209 | durability = 1 210 | } 211 | org/konstructs/class/Foilage { 212 | durability = 3 213 | destroyed-as = org/konstructs/vacuum 214 | } 215 | org/konstructs/class/Torch { 216 | durability = 1 217 | } 218 | } 219 | blocks { 220 | org/konstructs/vacuum { 221 | obstacle = false 222 | state = "gas" 223 | } 224 | org/konstructs/space/vacuum { 225 | obstacle = false 226 | state = "gas" 227 | } 228 | org/konstructs/test { 229 | orientable = true 230 | faces = [0, 1, 2, 3, 4, 5] 231 | durability = 1 232 | } 233 | org/konstructs/dirt { 234 | classes = { 235 | org/konstructs/class/Granular {} 236 | } 237 | } 238 | org/konstructs/tool-sack { 239 | faces = [0, 0, 1, 1, 0, 0] 240 | } 241 | org/konstructs/torch { 242 | shape = "plant" 243 | obstacle = false 244 | light-colour = ffd090 245 | light-level = 15 246 | classes { 247 | org/konstructs/class/Torch {} 248 | } 249 | } 250 | org/konstructs/hammerstone { 251 | shape = "plant" 252 | damage-multipliers { 253 | org/konstructs/class/Stone = 2.0 254 | org/konstructs/class/Wood = 1.5 255 | } 256 | classes { 257 | org/konstructs/class/Tool {} 258 | } 259 | } 260 | org/konstructs/grass-dirt { 261 | faces = [1, 1, 2, 0, 1, 1] 262 | classes { 263 | org/konstructs/class/GrassDirt {} 264 | org/konstructs/class/Granular {} 265 | } 266 | } 267 | org/konstructs/sand { 268 | classes { 269 | org/konstructs/class/Granular {} 270 | } 271 | } 272 | org/konstructs/stone-brick { 273 | classes { 274 | org/konstructs/class/Brick.order = 1 275 | org/konstructs/class/Stone.order = 2 276 | } 277 | } 278 | org/konstructs/brick { 279 | classes { 280 | org/konstructs/class/Brick.order = 1 281 | org/konstructs/class/Stone.order = 2 282 | } 283 | } 284 | org/konstructs/wood { 285 | faces = [1, 1, 2, 0, 1, 1] 286 | classes { 287 | org/konstructs/class/Wood {} 288 | } 289 | } 290 | org/konstructs/stick { 291 | shape = "plant" 292 | classes { 293 | org/konstructs/class/Stick {} 294 | } 295 | } 296 | org/konstructs/work-table { 297 | faces = [0, 0, 1, 2, 0, 0] 298 | } 299 | org/konstructs/stone { 300 | classes { 301 | org/konstructs/class/Stone {} 302 | } 303 | } 304 | org/konstructs/planks { 305 | classes { 306 | org/konstructs/class/Planks {} 307 | } 308 | } 309 | org/konstructs/snow-dirt { 310 | faces = [1, 1, 2, 0, 1, 1] 311 | classes { 312 | org/konstructs/class/Granular {} 313 | } 314 | } 315 | org/konstructs/glass { 316 | } 317 | org/konstructs/cobble { 318 | classes { 319 | org/konstructs/class/Stone {} 320 | } 321 | } 322 | org/konstructs/white-framed-stone { 323 | classes { 324 | org/konstructs/class/Stone {} 325 | } 326 | } 327 | org/konstructs/gray-framed-stone { 328 | classes { 329 | org/konstructs/class/Stone {} 330 | } 331 | } 332 | org/konstructs/snow { 333 | classes { 334 | org/konstructs/class/Granular {} 335 | } 336 | } 337 | org/konstructs/leaves { 338 | classes { 339 | org/konstructs/class/Foilage {} 340 | } 341 | } 342 | org/konstructs/water { 343 | state = "liquid" 344 | obstacle = false 345 | } 346 | org/konstructs/grass { 347 | shape = "plant" 348 | obstacle = false 349 | classes { 350 | org/konstructs/class/Grass {} 351 | } 352 | } 353 | org/konstructs/flower-yellow { 354 | obstacle = false 355 | shape = "plant" 356 | classes { 357 | org/konstructs/class/Flower {} 358 | } 359 | } 360 | org/konstructs/flower-red { 361 | obstacle = false 362 | shape = "plant" 363 | classes { 364 | org/konstructs/class/Flower {} 365 | } 366 | } 367 | org/konstructs/flower-purple { 368 | obstacle = false 369 | shape = "plant" 370 | classes { 371 | org/konstructs/class/Flower {} 372 | } 373 | } 374 | org/konstructs/sunflower { 375 | obstacle = false 376 | shape = "plant" 377 | classes { 378 | org/konstructs/class/Flower {} 379 | } 380 | } 381 | org/konstructs/flower-white { 382 | obstacle = false 383 | shape = "plant" 384 | classes { 385 | org/konstructs/class/Flower {} 386 | } 387 | } 388 | org/konstructs/flower-blue { 389 | obstacle = false 390 | shape = "plant" 391 | classes { 392 | org/konstructs/class/Flower {} 393 | } 394 | } 395 | } 396 | } 397 | org/konstructs/sack { 398 | class = "konstructs.plugin.toolsack.ToolSackActor" 399 | } 400 | org/konstructs/work-table { 401 | class = "konstructs.tools.WorkTableActor" 402 | } 403 | org/konstructs/inventory-manager { 404 | class = "konstructs.InventoryActor" 405 | json-storage = org/konstructs/meta 406 | } 407 | universe { 408 | class = "konstructs.UniverseActor" 409 | binary-storage = org/konstructs/binary 410 | json-storage = org/konstructs/meta 411 | inventory-manager = org/konstructs/inventory-manager 412 | konstructing = org/konstructs/konstructing 413 | block-manager = org/konstructs/block-manager 414 | metrics = org/konstructs/metric 415 | tertiary-interaction-listeners { 416 | org/konstructs/sack {} 417 | org/konstructs/work-table {} 418 | } 419 | } 420 | server { 421 | class = "konstructs.protocol.Server" 422 | block-manager = org/konstructs/block-manager 423 | } 424 | } 425 | globals { 426 | simulation-speed = 1 427 | } 428 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/player.scala: -------------------------------------------------------------------------------- 1 | package konstructs 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import scala.concurrent.duration.Duration 6 | 7 | import konstructs.plugin.toolsack.ToolSackActor 8 | 9 | import akka.actor.{Actor, Props, ActorRef, Stash, PoisonPill} 10 | 11 | import konstructs.api._ 12 | import konstructs.api.messages._ 13 | import konstructs.shard.ChunkPosition 14 | 15 | case class Player(nick: String, password: String, position: protocol.Position, inventory: Inventory) 16 | 17 | class PlayerActor( 18 | pid: Int, 19 | nick: String, 20 | password: String, 21 | client: ActorRef, 22 | db: ActorRef, 23 | universe: ActorRef, 24 | override val jsonStorage: ActorRef, 25 | startingPosition: protocol.Position 26 | ) extends Actor 27 | with Stash 28 | with utils.Scheduled 29 | with JsonStorage { 30 | 31 | import PlayerActor._ 32 | import DbActor.{BlockList, ChunkUpdate} 33 | 34 | val Stone = BlockTypeId.fromString("org/konstructs/stone") 35 | val ns = "players" 36 | 37 | var data: Player = null 38 | var viewDistance = 10 39 | 40 | schedule(5000, StoreData) 41 | 42 | loadGson(nick) 43 | 44 | def receive = { 45 | case GsonLoaded(_, json) if json != null => 46 | val newData = gson.fromJson(json, classOf[Player]) 47 | if (newData.password == password) { 48 | data = newData 49 | if (data.inventory.isEmpty) { 50 | val inventoryBlock = Block.createWithId(ToolSackActor.BlockId) 51 | universe ! CreateInventory(inventoryBlock.getId, 16) 52 | val inventory = Inventory.createEmpty(9).withSlot(0, Stack.createFromBlock(inventoryBlock)) 53 | data = data.copy(inventory = inventory) 54 | } else { 55 | data = data.copy(inventory = Inventory.convertPre0_1(data.inventory)) 56 | } 57 | client ! PlayerInfo(pid, nick, self, data.position) 58 | context.become(sendBelt) 59 | unstashAll() 60 | } else { 61 | println(s"Stop player and client actors for ${newData.nick}, incorrect password provided."); 62 | context.stop(self) 63 | } 64 | case GsonLoaded(_, _) => 65 | val inventoryBlock = Block.create(ToolSackActor.BlockId) 66 | val inventory = Inventory 67 | .createEmpty(9) 68 | .withSlot(8, Stack.createFromBlock(inventoryBlock)) 69 | .withSlot(7, Stack.createOfSize(Stone, 1)) 70 | .withSlot(6, Stack.createOfSize(Stone, 1)) 71 | data = Player(nick, password, startingPosition, inventory) 72 | client ! PlayerInfo(pid, nick, self, data.position) 73 | context.become(sendBelt) 74 | /* Send welcome message */ 75 | sendWelcomeText(1, s"Welcome $nick!") 76 | sendWelcomeText(3, "The yellow item on the rightmost position of your belt is your tool sack.") 77 | sendWelcomeText(3, "You can use your tool sack to craft different type of blocks and tools.") 78 | sendWelcomeText(3, "It can also be used as a storage space.") 79 | sendWelcomeText( 80 | 4, 81 | "Activate it by first selecting it (press 9 or use the scroll wheel) and then press E or the middle mouse button.") 82 | sendWelcomeText( 83 | 4, 84 | "By placing the two stone blocks in the 2x2 crafting area of the tool sack you can craft a hand axe which is a simple tool.") 85 | sendWelcomeText( 86 | 4, 87 | "Use it to destroy other blocks and explore what can be crafted by placing other blocks into the 2x2 crafting area.") 88 | sendWelcomeText(0, "Happy playing!") 89 | unstashAll() 90 | case _ => 91 | stash() 92 | } 93 | 94 | def update(position: protocol.Position) { 95 | data = data.copy(position = position) 96 | } 97 | 98 | def update(inventory: Inventory) { 99 | data = data.copy(inventory = inventory) 100 | client ! BeltUpdate(inventory.getStacks) 101 | } 102 | 103 | val random = new scala.util.Random 104 | 105 | def getBeltBlock(active: Int): Block = { 106 | val inventory = data.inventory 107 | val block = inventory.stackHead(active) 108 | if (block != null) { 109 | update(inventory.stackTail(active)) 110 | } 111 | block 112 | } 113 | 114 | def actionPrimary(pos: Position, block: Block, active: Int): Receive = { 115 | case i: InteractResult => 116 | if (i.getPosition == pos) { 117 | if (i.getBlock != null && block != null) { 118 | /* We got our tool block back, possibly damaged */ 119 | val stack = data.inventory.getStack(active) 120 | update(data.inventory.withSlot(active, stack.replaceHead(i.getBlock))) 121 | } else if (block != null && i.getBlock == null) { 122 | /* We didn't get our tool block back, it was destroyed */ 123 | update(data.inventory.stackTail(active)) 124 | } else { 125 | /* We used a null tool, no need to update the stack (it's empty) */ 126 | } 127 | context.become(ready orElse handleBasics) 128 | unstashAll() 129 | } else { 130 | throw new IllegalStateException(s"Invalid primary interaction result position: ${i.getPosition}") 131 | } 132 | } 133 | 134 | def actionSecondary(pos: Position, block: Block, active: Int): Receive = { 135 | case i: InteractResult => 136 | if (i.getPosition == pos) { 137 | if (i.getBlock == null && block != null) { 138 | /* We didn't get our block back, it was placed */ 139 | update(data.inventory.stackTail(active)) 140 | } else if (i.getBlock != null) { 141 | /* We got our block back, it couldn't be placed, possibly updated */ 142 | val stack = data.inventory.getStack(active) 143 | update(data.inventory.withSlot(active, stack.replaceHead(i.getBlock))) 144 | } 145 | context.become(ready orElse handleBasics) 146 | unstashAll() 147 | } else { 148 | throw new IllegalStateException(s"Invalid secondary interaction result position: ${i.getPosition}") 149 | } 150 | } 151 | 152 | def actionTertiary(pos: Position, block: Block, active: Int): Receive = { 153 | case i: InteractResult => 154 | if (i.getPosition == pos) { 155 | if (i.getBlock == null && block != null) { 156 | /* We didn't get our block back, server needed it */ 157 | update(data.inventory.stackTail(active)) 158 | } else if (i.getBlock != null) { 159 | /* We got our block back, possibly updated */ 160 | val stack = data.inventory.getStack(active) 161 | update(data.inventory.withSlot(active, stack.replaceHead(i.getBlock))) 162 | } 163 | context.become(ready orElse handleBasics) 164 | unstashAll() 165 | } else { 166 | throw new IllegalStateException(s"Invalid tertiary interaction result position: ${i.getPosition}") 167 | } 168 | } 169 | 170 | def action(pos: Position, orientation: Orientation, button: Int, active: Int): Receive = { 171 | val block = data.inventory.stackHead(active) 172 | button match { 173 | case 1 => 174 | val responseHandler = actionPrimary(pos, block, active) 175 | universe ! new InteractPrimary(self, nick, pos, orientation, block) 176 | responseHandler 177 | case 2 => 178 | val responseHandler = actionSecondary(pos, block, active) 179 | universe ! new InteractSecondary(self, nick, pos, orientation, block) 180 | responseHandler 181 | case 3 => 182 | val responseHandler = actionTertiary(pos, block, active) 183 | universe ! new InteractTertiary(self, nick, pos, orientation, block, null, false) 184 | responseHandler 185 | } 186 | } 187 | 188 | def putInBelt(stack: Stack) { 189 | if (stack != null && stack.getTypeId != BlockTypeId.VACUUM) { 190 | val inventory = data.inventory 191 | val r = inventory.acceptPartOf(stack) 192 | if (r.getGiving == null) { 193 | update(r.getAccepting) 194 | } else { 195 | update(r.getAccepting) 196 | println(s"The following stack was destroyed: ${r.getGiving}") 197 | } 198 | } 199 | } 200 | 201 | def moveInBelt(from: Int, to: Int) { 202 | val inventory = data.inventory 203 | update(inventory.swapSlot(from, to)) 204 | } 205 | 206 | override def postStop { 207 | if (data != null) 208 | storeGson(nick, gson.toJsonTree(data)) 209 | universe ! PlayerLogout(pid) 210 | client ! PoisonPill 211 | } 212 | 213 | var delay = 0 214 | 215 | def sendWelcomeText(d: Int, text: String) { 216 | context.system.scheduler 217 | .scheduleOnce(Duration.create(delay, TimeUnit.SECONDS), client, new Said(text))(context.dispatcher) 218 | delay = delay + d 219 | } 220 | 221 | def sendBelt: Receive = { 222 | /* Send belt*/ 223 | client ! BeltUpdate(data.inventory.getStacks) 224 | /* Send time */ 225 | client ! protocol.Time((new java.util.Date().getTime / 1000L) % 600) 226 | 227 | ready orElse handleBasics 228 | } 229 | 230 | def stashAll: Receive = { 231 | case _ => 232 | stash() 233 | } 234 | 235 | def handleBasics: Receive = { 236 | case p: protocol.Position => 237 | update(p) 238 | universe ! PlayerMovement(pid, data.position) 239 | case p: PlayerMovement => 240 | client ! p 241 | case p: PlayerNick => 242 | client ! p 243 | case SendInfo(to) => 244 | to ! PlayerMovement(pid, data.position) 245 | to ! PlayerNick(pid, data.nick) 246 | case StoreData => 247 | storeGson(nick, gson.toJsonTree(data)) 248 | case l: PlayerLogout => 249 | client ! l 250 | case protocol.Say(msg) => 251 | universe ! new Say(nick, msg) 252 | case s: Said => 253 | client.forward(s) 254 | case bl: BlockList => 255 | client ! bl 256 | case c: ChunkUpdate => 257 | val distance = c.chunk.distance(ChunkPosition(data.position.toApiPosition)) 258 | if (distance < 2) { 259 | /* Force update chunks nearby */ 260 | client ! BlockList(c.chunk, c.data) 261 | } else if (distance < viewDistance) { 262 | client ! protocol.ChunkUpdate(c.chunk.p, c.chunk.q, c.chunk.k) 263 | } else { 264 | /* Discard any chunk update that is too far away from the client */ 265 | } 266 | case s: DbActor.SendBlocks => 267 | db ! s 268 | case SetViewDistance(d) => 269 | viewDistance = d 270 | } 271 | 272 | def ready: Receive = { 273 | case Action(pos, orientation, button, active) => 274 | context.become(action(pos, orientation, button, active) orElse handleBasics orElse stashAll) 275 | case r: ReplaceBlockResult => 276 | if (!r.getBlock.getType.equals(BlockTypeId.VACUUM)) { 277 | putInBelt(Stack.createFromBlock(r.getBlock)) 278 | } 279 | case ReceiveStack(stack) => 280 | putInBelt(stack) 281 | case ConnectView(inventoryActor, view) => 282 | context.become(manageInventory(inventoryActor, view) orElse handleBasics orElse stashAll) 283 | client ! InventoryUpdate(addBelt(view)) 284 | } 285 | 286 | val BeltView = new InventoryView(0, 4, 1, 9) 287 | 288 | def addBelt(view: View) = view.add(BeltView, data.inventory) 289 | 290 | def stackSelected(inventoryActor: ActorRef, view: View, stack: Stack): Receive = { 291 | 292 | client ! HeldStack(stack) 293 | 294 | val f: Receive = { 295 | case SelectItem(index, button) => 296 | if (BeltView.contains(index)) { 297 | val beltIndex = BeltView.translate(index) 298 | val oldStack = data.inventory.getStack(beltIndex) 299 | if (oldStack != null) { 300 | if (oldStack.canAcceptPartOf(stack)) { 301 | val r = oldStack.acceptPartOf(stack) 302 | if (r.getGiving != null) { 303 | context.become(stackSelected(inventoryActor, view, r.getGiving) orElse handleBasics orElse stashAll) 304 | } else { 305 | context.become(manageInventory(inventoryActor, view) orElse handleBasics orElse stashAll) 306 | } 307 | update(data.inventory.withSlot(beltIndex, r.getAccepting)) 308 | } else { 309 | context.become(stackSelected(inventoryActor, view, oldStack) orElse handleBasics orElse stashAll) 310 | if (stack != null) 311 | update(data.inventory.withSlot(beltIndex, stack)) 312 | } 313 | } else { 314 | update(data.inventory.withSlot(beltIndex, stack)) 315 | context.become(manageInventory(inventoryActor, view) orElse handleBasics orElse stashAll) 316 | } 317 | client ! InventoryUpdate(addBelt(view)) 318 | } else { 319 | context.become(manageInventory(inventoryActor, view) orElse handleBasics orElse stashAll) 320 | inventoryActor ! PutViewStack(stack, index) 321 | } 322 | unstashAll() 323 | case UpdateView(view) => 324 | context.become(stackSelected(inventoryActor, view, stack) orElse handleBasics orElse stashAll) 325 | unstashAll() 326 | client ! InventoryUpdate(addBelt(view)) 327 | case CloseInventory => 328 | context.become(ready orElse handleBasics) 329 | inventoryActor ! CloseInventory 330 | unstashAll() 331 | } 332 | f 333 | } 334 | 335 | def stackAmount(button: Int): StackAmount = button match { 336 | case 1 => StackAmount.ALL 337 | case 3 => StackAmount.HALF 338 | case 2 => StackAmount.ONE 339 | case i => throw new IllegalStateException(s"Undefined button: $i") 340 | } 341 | 342 | def manageInventory(inventoryActor: ActorRef, view: View): Receive = { 343 | client ! HeldStack(null) 344 | 345 | val f: Receive = { 346 | case SelectItem(index, button) => 347 | val amount = stackAmount(button) 348 | if (BeltView.contains(index)) { 349 | val beltIndex = BeltView.translate(index) 350 | val stack = data.inventory.getStack(beltIndex) 351 | if (stack != null) { 352 | update(data.inventory.withSlot(beltIndex, stack.drop(amount))) 353 | context.become(stackSelected(inventoryActor, view, stack.take(amount)) orElse handleBasics orElse stashAll) 354 | client ! InventoryUpdate(addBelt(view)) 355 | } 356 | } else { 357 | inventoryActor ! RemoveViewStack(index, amount) 358 | } 359 | case UpdateView(view) => 360 | context.become(manageInventory(inventoryActor, view) orElse handleBasics orElse stashAll) 361 | client ! InventoryUpdate(addBelt(view)) 362 | case ReceiveStack(stack) => 363 | if (stack != null) 364 | context.become(stackSelected(inventoryActor, view, stack) orElse handleBasics orElse stashAll) 365 | case CloseInventory => 366 | context.become(ready orElse handleBasics) 367 | inventoryActor ! CloseInventory 368 | unstashAll() 369 | } 370 | f 371 | } 372 | 373 | } 374 | 375 | object PlayerActor { 376 | case object StoreData 377 | case class PlayerMovement(pid: Int, pos: protocol.Position) 378 | case class PlayerLogout(pid: Int) 379 | case class PlayerInfo(pid: Int, nick: String, actor: ActorRef, pos: protocol.Position) 380 | case class PlayerNick(pid: Int, nick: String) 381 | case class BeltUpdate(items: Array[Stack]) 382 | case class Action(pos: Position, orientation: Orientation, button: Int, active: Int) 383 | case class SendInfo(to: ActorRef) 384 | case class InventoryUpdate(view: View) 385 | case class SelectItem(index: Int, button: Int) 386 | case class HeldStack(held: Stack) 387 | case class SetViewDistance(distance: Int) 388 | 389 | def props(pid: Int, 390 | nick: String, 391 | password: String, 392 | client: ActorRef, 393 | db: ActorRef, 394 | universe: ActorRef, 395 | store: ActorRef, 396 | startingPosition: protocol.Position) = 397 | Props(classOf[PlayerActor], pid, nick, password, client, db, universe, store, startingPosition) 398 | 399 | } 400 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/plugin/plugin.scala: -------------------------------------------------------------------------------- 1 | package konstructs.plugin 2 | 3 | import java.lang.reflect.{Method, Modifier} 4 | import java.io.File 5 | import scala.concurrent.Future 6 | import scala.language.existentials 7 | import scala.util.Try 8 | 9 | import akka.util.Timeout 10 | import akka.actor.{Props, ActorRef, Actor, ActorSelection, Stash} 11 | import com.typesafe.config.{Config => TypesafeConfig, ConfigException, ConfigValueType} 12 | 13 | import konstructs.api.messages.GlobalConfig 14 | 15 | object Plugin { 16 | val StaticParameters = 2 17 | def nullAsEmpty[T](seq: Seq[T]): Seq[T] = 18 | if (seq == null) { 19 | Seq.empty[T] 20 | } else { 21 | seq 22 | } 23 | def nullAsEmpty[T](list: java.util.List[T]): java.util.List[T] = 24 | if (list == null) { 25 | java.util.Collections.emptyList[T] 26 | } else { 27 | list 28 | } 29 | } 30 | 31 | case class PluginConfigParameterMeta(name: String, 32 | configType: Class[_], 33 | optional: Boolean, 34 | listType: Option[Class[_]] = None) 35 | 36 | case class PluginConfigMeta(method: Method, parameters: Seq[PluginConfigParameterMeta], staticParameters: Int) 37 | 38 | object PluginConfigMeta { 39 | def apply(m: Method, staticParameters: Int): PluginConfigMeta = { 40 | val annotations = m.getParameterAnnotations.flatMap(_.filter { a => 41 | a.isInstanceOf[Config] || a.isInstanceOf[ListConfig] 42 | }) 43 | val parameters = m.getParameterTypes.drop(staticParameters).zip(annotations).map { 44 | case (t, c: Config) => 45 | PluginConfigParameterMeta(c.key, t, c.optional) 46 | case (t, c: ListConfig) => 47 | PluginConfigParameterMeta(c.key, c.elementType, c.optional, Some(t)) 48 | } 49 | apply(m, parameters, staticParameters) 50 | } 51 | } 52 | 53 | case class PluginMeta(configs: Seq[PluginConfigMeta]) 54 | 55 | object PluginMeta { 56 | private def validationError(m: Method) { 57 | println(s"Error validating: $m") 58 | } 59 | 60 | private def validateReturnType(m: Method): Boolean = 61 | if (m.getReturnType == classOf[Props]) { 62 | return true 63 | } else { 64 | validationError(m) 65 | println(s"The return type must be: ${classOf[Props]}") 66 | return false 67 | } 68 | 69 | private def validateStatic(m: Method): Boolean = { 70 | if (Modifier.isStatic(m.getModifiers)) { 71 | return true 72 | } else { 73 | validationError(m) 74 | println("The method must be static") 75 | return false 76 | } 77 | } 78 | 79 | private def validateFirstArgString(m: Method): Boolean = { 80 | if (m.getParameterTypes()(0) == classOf[String]) { 81 | return true 82 | } else { 83 | validationError(m) 84 | println(s"The first argument must be: ${classOf[String]}") 85 | return false 86 | } 87 | } 88 | 89 | private def validateSecondArgActorRef(m: Method): Boolean = { 90 | if (m.getParameterTypes()(1) == classOf[ActorRef]) { 91 | return true 92 | } else { 93 | validationError(m) 94 | println(s"The second argument must be: ${classOf[ActorRef]}") 95 | return false 96 | } 97 | } 98 | 99 | private def thirdArgIsConfig(m: Method): Boolean = { 100 | val paramTypes = m.getParameterTypes() 101 | if (paramTypes.size > 2 && paramTypes(2) == classOf[TypesafeConfig] && !m.getParameterAnnotations()(2).exists { 102 | a => 103 | a.isInstanceOf[Config] || a.isInstanceOf[ListConfig] 104 | }) { 105 | return true 106 | } else { 107 | return false 108 | } 109 | } 110 | 111 | private def allParametersAreAnnotated(m: Method, staticParameters: Int): Boolean = 112 | if (m.getParameterAnnotations 113 | .filter(_.exists { a => 114 | a.isInstanceOf[Config] || a.isInstanceOf[ListConfig] 115 | }) 116 | .size == m.getParameterTypes.size - staticParameters) { 117 | return true 118 | } else { 119 | validationError(m) 120 | println( 121 | s"Only the first three arguments, name, universe and optionally config, are left without an @Config annotation.") 122 | println("All other arguments must be annotated.") 123 | return false 124 | } 125 | 126 | def apply(className: String): PluginMeta = { 127 | val clazz = Class.forName(className) 128 | 129 | val filteredMethods = clazz.getMethods 130 | .filter(_.getAnnotations.exists(_.isInstanceOf[PluginConstructor])) 131 | .filter(validateReturnType) 132 | .filter(validateStatic) 133 | .filter(validateFirstArgString) 134 | .filter(validateSecondArgActorRef) 135 | 136 | val methods = for (m <- filteredMethods) yield { 137 | val staticParameters = if (thirdArgIsConfig(m)) { 138 | Plugin.StaticParameters + 1 139 | } else { 140 | Plugin.StaticParameters 141 | } 142 | 143 | if (allParametersAreAnnotated(m, staticParameters)) 144 | Some(PluginConfigMeta(m, staticParameters)) 145 | else 146 | None 147 | } 148 | apply(methods.flatten) 149 | } 150 | 151 | } 152 | 153 | case class Dependency(name: String, config: Option[TypesafeConfig]) 154 | 155 | case class Dependencies(dependencies: Seq[Dependency], t: Class[_], isPluginRef: Boolean) 156 | 157 | object Dependencies { 158 | def apply(dep: String, config: Option[TypesafeConfig], t: Class[_], isPluginRef: Boolean): Dependencies = 159 | apply(Seq(Dependency(dep, config)), t, isPluginRef) 160 | } 161 | 162 | case class ConfiguredPlugin(name: String, method: Method, args: Seq[Either[Object, Dependencies]]) { 163 | 164 | val dependencyEdges = 165 | args.collect { 166 | case Right(deps) => 167 | deps.dependencies.map { d => 168 | (name, d.name) 169 | } 170 | } flatten 171 | 172 | } 173 | 174 | class PluginLoaderActor(rootConfig: TypesafeConfig) extends Actor { 175 | import scala.collection.JavaConverters._ 176 | import PluginLoaderActor._ 177 | import context.dispatcher 178 | import UniverseProxyActor.SetUniverse 179 | 180 | val config = rootConfig.getConfig("konstructs") 181 | val globalConfig = new GlobalConfig(rootConfig.getDouble("globals.simulation-speed").toFloat) 182 | 183 | implicit val selectionTimeout = Timeout(1, java.util.concurrent.TimeUnit.SECONDS) 184 | 185 | val StringType = classOf[String] 186 | val IntegerType = classOf[Int] 187 | val FileType = classOf[File] 188 | val ActorRefType = classOf[ActorRef] 189 | val PluginRefType = classOf[PluginRef] 190 | val SeqType = classOf[Seq[_]] 191 | val ListType = classOf[java.util.List[_]] 192 | val ConfigType = classOf[TypesafeConfig] 193 | val universeProxy = context.actorOf(UniverseProxyActor.props(), "universe-proxy") 194 | 195 | private def actorName(name: String) = name.replace('/', '-') 196 | 197 | private def listType(t: Class[_], seq: Seq[_ <: AnyRef]): Object = t match { 198 | case SeqType => seq 199 | case ListType => seq.toList.asJava 200 | case ActorRefType => seq.head 201 | case PluginRefType => seq.head 202 | } 203 | 204 | private def getOptional(optional: Boolean)(f: => Object): Object = { 205 | if (optional) { 206 | try { 207 | f 208 | } catch { 209 | case _: ConfigException.Missing => null 210 | } 211 | } else { 212 | f 213 | } 214 | } 215 | 216 | private def toConfig(s: String, config: TypesafeConfig): TypesafeConfig = 217 | config.getConfig(s) 218 | 219 | private def toOptionalConfig(s: String, config: TypesafeConfig): Option[TypesafeConfig] = 220 | Try { 221 | config.getConfig(s) 222 | } toOption 223 | 224 | private def asDependency(key: String, config: TypesafeConfig): Dependency = 225 | Dependency(keyAsString(key), toOptionalConfig(key, config)) 226 | 227 | private def keepString(s: String, config: TypesafeConfig): String = config.getString(s) 228 | 229 | private def keyAsString(s: String): String = s.replace("\"", "") 230 | 231 | private def toFile(s: String, config: TypesafeConfig): File = new File(config.getString(s)) 232 | 233 | private def configToSeq[T](get: (String, TypesafeConfig) => T)(config: TypesafeConfig): Seq[T] = 234 | config.root.entrySet.asScala.filter { e => 235 | val v = e.getValue 236 | v.valueType != ConfigValueType.NULL 237 | }.map { e => 238 | val k = e.getKey 239 | get(k, config) 240 | }.toSeq 241 | 242 | def configurePlugin(name: String, config: TypesafeConfig, c: PluginConfigMeta): ConfiguredPlugin = { 243 | val args: Seq[Either[Object, Dependencies]] = c.parameters.map { p => 244 | val opt = 245 | getOptional(p.optional) _ 246 | p.configType match { 247 | case StringType => 248 | if (p.listType.isDefined) { 249 | Left(opt(listType(p.listType.get, configToSeq(keepString)(config.getConfig(p.name))))) 250 | } else { 251 | Left(opt(config.getString(p.name))) 252 | } 253 | case IntegerType => 254 | if (p.listType.isDefined) { 255 | Left(opt(listType(p.listType.get, config.getIntList(p.name).asScala.toSeq))) 256 | } else { 257 | Left(opt(new Integer(config.getInt(p.name)))) 258 | } 259 | case FileType => 260 | if (p.listType.isDefined) { 261 | Left(opt(listType(p.listType.get, configToSeq(toFile)(config.getConfig(p.name))))) 262 | } else { 263 | Left(opt(toFile(p.name, config))) 264 | } 265 | case ActorRefType => 266 | try { 267 | if (p.listType.isDefined) { 268 | Right(Dependencies(configToSeq(asDependency)(config.getConfig(p.name)), p.listType.get, false)) 269 | } else { 270 | Right(Dependencies(config.getString(p.name), toOptionalConfig(p.name, config), ActorRefType, false)) 271 | } 272 | } catch { 273 | case e: ConfigException.Missing => 274 | if (p.optional) { 275 | Left(null) 276 | } else { 277 | throw e 278 | } 279 | } 280 | case PluginRefType => 281 | try { 282 | if (p.listType.isDefined) { 283 | Right(Dependencies(configToSeq(asDependency)(config.getConfig(p.name)), p.listType.get, true)) 284 | } else { 285 | Right(Dependencies(config.getString(p.name), toOptionalConfig(p.name, config), PluginRefType, true)) 286 | } 287 | } catch { 288 | case e: ConfigException.Missing => 289 | if (p.optional) { 290 | Left(null) 291 | } else { 292 | throw e 293 | } 294 | } 295 | case ConfigType => 296 | if (p.listType.isDefined) { 297 | Left(opt(listType(p.listType.get, configToSeq(toConfig)(config.getConfig(p.name))))) 298 | } else { 299 | Left(opt(toConfig(p.name, config))) 300 | } 301 | } 302 | } 303 | val staticArgs = if (c.staticParameters == 2) { 304 | Seq(Left[Object, Dependencies](name), Left[Object, Dependencies](universeProxy)) 305 | } else { 306 | Seq(Left[Object, Dependencies](name), 307 | Left[Object, Dependencies](universeProxy), 308 | Left[Object, Dependencies](config)) 309 | } 310 | ConfiguredPlugin(name, c.method, staticArgs ++ args) 311 | } 312 | 313 | def configurePlugin(name: String, config: TypesafeConfig, meta: PluginMeta): ConfiguredPlugin = { 314 | for (c <- meta.configs.sortBy(_.parameters.size).reverse) { 315 | try { 316 | return configurePlugin(name, config, c) 317 | } catch { 318 | case e: ConfigException.Missing => 319 | } 320 | } 321 | if (!meta.configs.isEmpty) 322 | println(s"Valid configurations: ${meta.configs}") 323 | else 324 | println(s"No valid configurations exists") 325 | throw new Exception(s"No valid plugin constructor found for $name") 326 | } 327 | 328 | def invokePlugins(plugins: List[ConfiguredPlugin]) { 329 | plugins match { 330 | case head :: tail => 331 | val args = Future.sequence(head.args.map { 332 | case Right(d) => 333 | Future 334 | .sequence(d.dependencies.map { dep => 335 | val selection = ActorSelection(self, actorName(dep.name)).resolveOne 336 | if (d.isPluginRef) { 337 | selection.map { ref => 338 | new PluginRef(ref, dep.config.getOrElse(null)) 339 | } 340 | } else { 341 | selection 342 | } 343 | }) 344 | .map { as => 345 | listType(d.t, as) 346 | } 347 | case Left(obj) => Future.successful(obj) 348 | }) 349 | args.onFailure { 350 | case e => println(s"Failed to start plugin ${head.name} due to $e") 351 | } 352 | for (a <- args) { 353 | val props = head.method.invoke(null, a: _*).asInstanceOf[Props] 354 | val actor = context.actorOf(props, actorName(head.name)) 355 | println(s"Started plugin ${head.name}") 356 | if (head.name == "universe") { 357 | println("Universe started, updating proxy") 358 | universeProxy ! SetUniverse(actor) 359 | } 360 | actor.tell(globalConfig, universeProxy) 361 | invokePlugins(tail) 362 | } 363 | case _ => Nil 364 | } 365 | } 366 | 367 | def receive = { 368 | case Start => 369 | val objs = 370 | config.root().entrySet.asScala.filter(_.getValue.valueType == com.typesafe.config.ConfigValueType.OBJECT) 371 | val plugins = (for (e <- objs) yield { 372 | val name = e.getKey 373 | val plugin = config.getConfig(name) 374 | if (plugin.hasPath("class")) { 375 | val clazz = plugin.getString("class") 376 | val meta = PluginMeta(clazz) 377 | 378 | val pluginConf = configurePlugin(name, plugin, meta) 379 | println(s"Validated configuration for $name") 380 | Some(pluginConf) 381 | } else { 382 | println(s"$name has no class set, ignoring") 383 | None 384 | } 385 | }) flatten 386 | 387 | val pluginMap = plugins.map { p => 388 | (p.name, p) 389 | }.toMap 390 | 391 | val pluginEdges = plugins.flatMap(_.dependencyEdges) 392 | 393 | println(s"Plugin dependencies: $pluginEdges") 394 | 395 | println("Resolving dependency order ...") 396 | val sortedPlugins = tsort(pluginEdges).map(pluginMap).toSeq.reverse 397 | val allPlugins = sortedPlugins ++ (plugins.toSet &~ sortedPlugins.toSet).toSeq 398 | println(s"Loading plugins in dependency order: ${allPlugins.map(_.name)}") 399 | invokePlugins(allPlugins.toList) 400 | } 401 | } 402 | 403 | object PluginLoaderActor { 404 | case object Start 405 | import scala.annotation.tailrec 406 | 407 | def tsort[A](edges: Traversable[(A, A)]): Iterable[A] = { 408 | @tailrec 409 | def tsort(toPreds: Map[A, Set[A]], done: Iterable[A]): Iterable[A] = { 410 | val (noPreds, hasPreds) = toPreds.partition { _._2.isEmpty } 411 | if (noPreds.isEmpty) { 412 | if (hasPreds.isEmpty) done else sys.error(hasPreds.toString) 413 | } else { 414 | val found = noPreds.map { _._1 } 415 | tsort(hasPreds.mapValues { _ -- found }, done ++ found) 416 | } 417 | } 418 | 419 | val toPred = edges.foldLeft(Map[A, Set[A]]()) { (acc, e) => 420 | acc + (e._1 -> acc.getOrElse(e._1, Set())) + (e._2 -> (acc.getOrElse(e._2, Set()) + e._1)) 421 | } 422 | tsort(toPred, Seq()) 423 | } 424 | 425 | def props(config: TypesafeConfig) = Props(classOf[PluginLoaderActor], config) 426 | } 427 | 428 | class UniverseProxyActor extends Actor with Stash { 429 | import UniverseProxyActor.SetUniverse 430 | 431 | def receive = { 432 | case SetUniverse(universe) => 433 | unstashAll() 434 | context.become(ready(universe)) 435 | case _ => 436 | stash() 437 | } 438 | 439 | def ready(universe: ActorRef): Receive = { 440 | case o => 441 | universe.forward(o) 442 | } 443 | 444 | } 445 | 446 | object UniverseProxyActor { 447 | case class SetUniverse(universe: ActorRef) 448 | def props() = Props(classOf[UniverseProxyActor]) 449 | } 450 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/blocks.scala: -------------------------------------------------------------------------------- 1 | package konstructs 2 | 3 | import java.util.UUID 4 | import java.awt.image.BufferedImage 5 | import java.awt.Graphics 6 | import java.io.ByteArrayOutputStream 7 | import javax.imageio.ImageIO 8 | 9 | import scala.collection.JavaConverters._ 10 | import scala.util.Try 11 | 12 | import com.typesafe.config.{Config => TypesafeConfig, ConfigValueType, ConfigObject} 13 | import akka.actor.{Actor, Props, ActorRef, Stash} 14 | 15 | import com.google.gson.reflect.TypeToken 16 | 17 | import konstructs.plugin.Plugin._ 18 | import konstructs.api._ 19 | import konstructs.api.messages._ 20 | import konstructs.plugin.{ListConfig, PluginConstructor, Config} 21 | 22 | case class BlockFactoryImpl(blockTypeIdMapping: java.util.Map[BlockTypeId, Integer], 23 | wMapping: java.util.Map[Integer, BlockTypeId], 24 | blockTypes: java.util.Map[BlockTypeId, BlockType]) 25 | extends BlockFactory { 26 | 27 | override def createBlock(uuid: UUID, w: Int, health: Int): Block = { 28 | val t = wMapping.get(w) 29 | new Block(uuid, t, Health.get(health)) 30 | } 31 | 32 | override def createBlock(uuid: UUID, w: Int): Block = { 33 | val t = wMapping.get(w) 34 | new Block(uuid, t) 35 | } 36 | 37 | override def createBlock(w: Int): Block = { 38 | val t = wMapping.get(w) 39 | new Block(null, t) 40 | } 41 | 42 | private def validate[T](w: Integer, t: BlockTypeId): Integer = 43 | if (w != null) w else throw new IndexOutOfBoundsException(s"Block type $t is not registered") 44 | 45 | override def getW(block: Block) = 46 | validate(blockTypeIdMapping.get(block.getType), block.getType) 47 | 48 | override def getW(stack: Stack) = 49 | validate(blockTypeIdMapping.get(stack.getTypeId), stack.getTypeId) 50 | 51 | override def getW(typeId: BlockTypeId) = 52 | validate(blockTypeIdMapping.get(typeId), typeId) 53 | 54 | override def getBlockType(id: BlockTypeId): BlockType = { 55 | val t = blockTypes.get(id) 56 | if (t != null) t else throw new IndexOutOfBoundsException(s"Block type $id is not registered") 57 | } 58 | 59 | override def getBlockTypeId(w: Int) = 60 | wMapping.get(w) 61 | 62 | override def getBlockTypes() = 63 | blockTypes 64 | 65 | override def getWMapping() = 66 | wMapping 67 | 68 | } 69 | 70 | object BlockFactoryImpl { 71 | 72 | private def findFreeW(wMapping: java.util.Map[Integer, BlockTypeId]): Integer = { 73 | for (w <- 0 until 256) { 74 | if (!wMapping.containsKey(w)) { 75 | return w 76 | } 77 | } 78 | throw new IllegalStateException("No free w to allocate for new block type") 79 | } 80 | 81 | private def addBlockType( 82 | blockTypeIdMapping: java.util.Map[BlockTypeId, Integer], 83 | wMapping: java.util.Map[Integer, BlockTypeId], 84 | blockTypeMapping: java.util.Map[BlockTypeId, BlockType]): PartialFunction[(BlockTypeId, BlockType), Unit] = { 85 | case (t, bt) => 86 | val foundW = blockTypeIdMapping.get(t) 87 | val w = if (foundW != null) { 88 | foundW 89 | } else { 90 | findFreeW(wMapping) 91 | } 92 | blockTypeIdMapping.put(t, w) 93 | wMapping.put(w, t) 94 | blockTypeMapping.put(t, bt) 95 | } 96 | 97 | def apply(defined: java.util.Map[Integer, BlockTypeId], 98 | configured: Seq[(BlockTypeId, BlockType)]): BlockFactoryImpl = { 99 | val wMapping = new java.util.HashMap[Integer, BlockTypeId]() 100 | val reverse = new java.util.HashMap[BlockTypeId, Integer]() 101 | val tMapping = new java.util.HashMap[BlockTypeId, BlockType]() 102 | 103 | for ((w, tId) <- defined.asScala) { 104 | wMapping.put(w, tId) 105 | reverse.put(tId, w) 106 | } 107 | configured.map(addBlockType(reverse, wMapping, tMapping)) 108 | apply(reverse, wMapping, tMapping) 109 | } 110 | } 111 | 112 | class BlockMetaActor(val ns: String, 113 | val jsonStorage: ActorRef, 114 | configuredBlocks: Seq[(BlockTypeId, BlockType)], 115 | textures: Array[Byte]) 116 | extends Actor 117 | with Stash 118 | with utils.Scheduled 119 | with JsonStorage { 120 | 121 | import BlockMetaActor._ 122 | 123 | val BlockIdFile = "block-id-mapping" 124 | 125 | val typeOfwMapping = new TypeToken[java.util.Map[Integer, BlockTypeId]]() {}.getType 126 | 127 | def storeDb(factory: BlockFactory) { 128 | storeGson(BlockIdFile, gson.toJsonTree(factory.getWMapping, typeOfwMapping)) 129 | } 130 | 131 | loadGson(BlockIdFile) 132 | def receive() = { 133 | case GsonLoaded(_, json) if json != null => 134 | val defined: java.util.Map[Integer, BlockTypeId] = gson.fromJson(json, typeOfwMapping) 135 | val factory = BlockFactoryImpl(defined, configuredBlocks) 136 | storeDb(factory) 137 | context.become(ready(factory)) 138 | unstashAll() 139 | case GsonLoaded(_, _) => 140 | val map = new java.util.HashMap[Integer, BlockTypeId]() 141 | map.put(0, BlockTypeId.VACUUM) 142 | val factory = BlockFactoryImpl(map, configuredBlocks) 143 | storeDb(factory) 144 | context.become(ready(factory)) 145 | unstashAll() 146 | case _ => 147 | stash() 148 | } 149 | 150 | def ready(factory: BlockFactoryImpl): Receive = { 151 | case GetBlockFactory.MESSAGE => 152 | sender ! factory 153 | case GetTextures => 154 | sender ! Textures(textures) 155 | } 156 | 157 | } 158 | 159 | object BlockMetaActor { 160 | val NumTextures = 16 161 | val TextureSize = 16 162 | 163 | case class BlockClass(obstacle: Option[Boolean], 164 | shape: Option[BlockShape], 165 | state: Option[BlockState], 166 | durability: Option[Float], 167 | damage: Option[Float], 168 | damageMultipliers: Map[BlockOrClassId, Float], 169 | orientable: Option[Boolean], 170 | destroyedAs: Option[BlockTypeId], 171 | lightColour: Option[Colour], 172 | lightLevel: Option[LightLevel]) 173 | case class BlockDefault(obstacle: Boolean, 174 | shape: BlockShape, 175 | state: BlockState, 176 | durability: Float, 177 | damage: Float, 178 | damageMultipliers: Map[BlockOrClassId, Float], 179 | orientable: Boolean, 180 | destroyedAs: BlockTypeId, 181 | lightColour: Colour, 182 | lightLevel: LightLevel) 183 | 184 | object BlockDefault { 185 | def apply(classes: Array[BlockClassId], classMap: Map[BlockClassId, BlockClass]): BlockDefault = { 186 | var obstacle = true 187 | var shape = BlockShape.BLOCK 188 | var state = BlockState.SOLID 189 | var durability = BlockType.DEFAULT_DURABILITY 190 | var damage = BlockType.DEFAULT_DAMAGE 191 | var damageMultipliers: Map[BlockOrClassId, Float] = Map() 192 | var orientable = false 193 | var destroyedAs = BlockTypeId.SELF 194 | var lightColour = Colour.WHITE 195 | var lightLevel = LightLevel.DARK 196 | for (id <- classes.reverse) { 197 | classMap.get(id) map { c => 198 | c.obstacle.map(obstacle = _) 199 | c.shape.map(shape = _) 200 | c.state.map(state = _) 201 | c.durability.map(durability = _) 202 | c.damage.map(damage = _) 203 | damageMultipliers = damageMultipliers ++ c.damageMultipliers 204 | c.orientable.map(orientable = _) 205 | c.destroyedAs.map(destroyedAs = _) 206 | c.lightColour.map(lightColour = _) 207 | c.lightLevel.map(lightLevel = _) 208 | } 209 | } 210 | apply(obstacle, 211 | shape, 212 | state, 213 | durability, 214 | damage, 215 | damageMultipliers, 216 | orientable, 217 | destroyedAs, 218 | lightColour, 219 | lightLevel) 220 | } 221 | } 222 | 223 | def textureFilename(idString: String): String = 224 | s"/textures/$idString.png" 225 | 226 | def loadTexture(idString: String): BufferedImage = { 227 | val textureFile = getClass.getResource(textureFilename(idString)) 228 | if (textureFile == null) throw new IllegalStateException(s"No resource for texture: ${textureFilename(idString)}") 229 | ImageIO.read(textureFile) 230 | } 231 | 232 | def insertTexture(i: Int, texture: BufferedImage, g: Graphics) { 233 | val x = (i % NumTextures) * TextureSize 234 | val y = (NumTextures - (i / NumTextures) - 1) * TextureSize 235 | g.drawImage(texture, x, y, null) 236 | } 237 | 238 | def readClasses(config: TypesafeConfig): Array[BlockClassId] = 239 | if (config.hasPath("classes")) { 240 | val classConfig = config.getConfig("classes") 241 | classConfig.root.entrySet.asScala 242 | .map(e => 243 | if (e.getValue.valueType != ConfigValueType.NULL) { 244 | if (e.getValue.valueType == ConfigValueType.OBJECT) { 245 | val innerConfig = e.getValue.asInstanceOf[ConfigObject].toConfig 246 | if (innerConfig.hasPath("order")) 247 | Some((BlockClassId.fromString(e.getKey.replace("\"", "")), innerConfig.getInt("order"))) 248 | else 249 | Some((BlockClassId.fromString(e.getKey.replace("\"", "")), 0)) 250 | } else { 251 | Some((BlockClassId.fromString(e.getKey.replace("\"", "")), 0)) 252 | } 253 | } else { 254 | None 255 | }) 256 | .flatten 257 | .toSeq 258 | .sortWith(_._2 < _._2) 259 | .map(_._1) 260 | .toArray 261 | } else { 262 | BlockType.NO_CLASSES; 263 | } 264 | 265 | def readDamageMultipliers(config: TypesafeConfig): Map[BlockOrClassId, Float] = 266 | if (config.hasPath("damage-multipliers")) { 267 | val damageConfig = config.getConfig("damage-multipliers") 268 | damageConfig.root.entrySet.asScala 269 | .map(e => 270 | Option(damageConfig.getDouble(e.getKey)).map { multiplier => 271 | (BlockOrClassId.fromString(e.getKey), multiplier.toFloat) 272 | }) 273 | .flatten 274 | .toMap 275 | } else { 276 | Map.empty[BlockOrClassId, Float] 277 | } 278 | 279 | def readIsObstacle(config: TypesafeConfig): Option[Boolean] = 280 | if (config.hasPath("obstacle")) { 281 | Some(config.getBoolean("obstacle")) 282 | } else { 283 | None 284 | } 285 | 286 | def readShape(config: TypesafeConfig): Option[BlockShape] = 287 | if (config.hasPath("shape")) { 288 | Some(BlockShape.fromString(config.getString("shape"))) 289 | } else { 290 | None 291 | } 292 | 293 | def readState(config: TypesafeConfig): Option[BlockState] = 294 | if (config.hasPath("state")) { 295 | Some(BlockState.fromString(config.getString("state"))) 296 | } else { 297 | None 298 | } 299 | 300 | def readDurability(config: TypesafeConfig): Option[Float] = 301 | if (config.hasPath("durability")) { 302 | Some(config.getDouble("durability").toFloat) 303 | } else { 304 | None 305 | } 306 | 307 | def readDamage(config: TypesafeConfig): Option[Float] = 308 | if (config.hasPath("damage")) { 309 | Some(config.getDouble("damage").toFloat) 310 | } else { 311 | None 312 | } 313 | 314 | def readOrientable(config: TypesafeConfig): Option[Boolean] = 315 | if (config.hasPath("orientable")) { 316 | Some(config.getBoolean("orientable")) 317 | } else { 318 | None 319 | } 320 | 321 | def readDestroyedAs(config: TypesafeConfig): Option[BlockTypeId] = 322 | if (config.hasPath("destroyed-as")) { 323 | Some(BlockTypeId.fromString(config.getString("destroyed-as"))) 324 | } else { 325 | None 326 | } 327 | 328 | def readLightColour(config: TypesafeConfig): Option[Colour] = 329 | if (config.hasPath("light-colour")) { 330 | Some(Colour.fromRgbHexString(config.getString("light-colour"))) 331 | } else { 332 | None 333 | } 334 | 335 | def readLightLevel(config: TypesafeConfig): Option[LightLevel] = 336 | if (config.hasPath("light-level")) { 337 | Some(LightLevel.get(config.getInt("light-level"))) 338 | } else { 339 | None 340 | } 341 | 342 | def blockType(idString: String, 343 | config: TypesafeConfig, 344 | texturePosition: Int, 345 | classMap: Map[BlockClassId, BlockClass]): (BlockTypeId, BlockType) = { 346 | val typeId = BlockTypeId.fromString(idString) 347 | val classes = readClasses(config) 348 | val d = BlockDefault(classes, classMap) 349 | val isObstacle = readIsObstacle(config).getOrElse(d.obstacle) 350 | val shape = readShape(config).getOrElse(d.shape) 351 | val state = readState(config).getOrElse(d.state) 352 | val durability = readDurability(config).getOrElse(d.durability) 353 | val damage = readDamage(config).getOrElse(d.damage) 354 | val damageMultipliers = (for ((k, v) <- (readDamageMultipliers(config) ++ d.damageMultipliers)) yield { 355 | k -> Float.box(v) 356 | }) asJava 357 | val orientable = readOrientable(config).getOrElse(d.orientable) 358 | val destroyedAs = readDestroyedAs(config).getOrElse(d.destroyedAs) 359 | val lightLevel = readLightLevel(config).getOrElse(d.lightLevel) 360 | val lightColour = readLightColour(config).getOrElse(d.lightColour) 361 | val blockType = if (config.hasPath("faces")) { 362 | val faces = config.getIntList("faces") 363 | if (faces.size != 6) throw new IllegalStateException("There must be exactly 6 faces") 364 | new BlockType(faces.asScala.map(_ + texturePosition).toArray, 365 | shape, 366 | isObstacle, 367 | false, 368 | state, 369 | classes, 370 | durability, 371 | damage, 372 | damageMultipliers, 373 | orientable, 374 | destroyedAs, 375 | lightColour, 376 | lightLevel) 377 | } else { 378 | /* Default is to assume only one texture for all faces */ 379 | new BlockType( 380 | Array(texturePosition, texturePosition, texturePosition, texturePosition, texturePosition, texturePosition), 381 | shape, 382 | isObstacle, 383 | false, 384 | state, 385 | classes, 386 | durability, 387 | damage, 388 | damageMultipliers, 389 | orientable, 390 | destroyedAs, 391 | lightColour, 392 | lightLevel) 393 | } 394 | typeId -> blockType 395 | } 396 | 397 | def isTransparent(texture: BufferedImage): Boolean = { 398 | for (x <- 0 until texture.getWidth; 399 | y <- 0 until texture.getHeight) { 400 | if (0xFF00FF == (texture.getRGB(x, y) & 0x00FFFFFF)) return true 401 | } 402 | return false 403 | } 404 | 405 | def withTransparent(t: BlockType, transparent: Boolean): BlockType = 406 | new BlockType(t.getFaces, 407 | t.getBlockShape, 408 | t.isObstacle, 409 | transparent, 410 | t.getBlockState, 411 | t.getClasses, 412 | t.getDurability, 413 | t.getDamage, 414 | t.getDamageMultipliers, 415 | t.isOrientable, 416 | t.getDestroyedAs, 417 | t.getLightColour, 418 | t.getLightLevel) 419 | 420 | def parseClass(config: TypesafeConfig): BlockClass = { 421 | val isObstacle = readIsObstacle(config) 422 | val shape = readShape(config) 423 | val state = readState(config) 424 | val durability = readDurability(config) 425 | val damage = readDamage(config) 426 | val damageMultipliers = readDamageMultipliers(config) 427 | val orientable = readOrientable(config) 428 | val destroyedAs = readDestroyedAs(config) 429 | val lightColour = readLightColour(config) 430 | val lightLevel = readLightLevel(config) 431 | BlockClass(isObstacle, 432 | shape, 433 | state, 434 | durability, 435 | damage, 436 | damageMultipliers, 437 | orientable, 438 | destroyedAs, 439 | lightColour, 440 | lightLevel) 441 | } 442 | 443 | def parseClasses(config: TypesafeConfig): Map[BlockClassId, BlockClass] = { 444 | config.root.entrySet.asScala.map { e => 445 | BlockClassId.fromString(e.getKey) -> parseClass(config.getConfig(e.getKey)) 446 | } toMap 447 | } 448 | 449 | def parseBlocks(config: TypesafeConfig, 450 | classMap: Map[BlockClassId, BlockClass]): (Seq[(BlockTypeId, BlockType)], Array[Byte]) = { 451 | val blocks = config.root.entrySet.asScala.map { e => 452 | e.getKey -> config.getConfig(e.getKey) 453 | } 454 | var texturePosition = 0 455 | val textures = new BufferedImage(NumTextures * TextureSize, NumTextures * TextureSize, BufferedImage.TYPE_INT_RGB) 456 | val texturesGraphics = textures.getGraphics() 457 | val blockSeq = (for ((idString, block) <- blocks) yield { 458 | val t = blockType(idString, block, texturePosition, classMap) 459 | val maxIndex = t._2.getFaces().max + 1 460 | val numTextures = maxIndex - t._2.getFaces().min 461 | val img = loadTexture(idString) 462 | var transparent = false 463 | for (i <- 0 until numTextures) { 464 | val texture = img.getSubimage(i * TextureSize, 0, TextureSize, TextureSize) 465 | if (isTransparent(texture)) transparent = true 466 | insertTexture(texturePosition + i, texture, texturesGraphics) 467 | } 468 | texturePosition = maxIndex 469 | t._1 -> withTransparent(t._2, transparent) 470 | }) toSeq 471 | val texturesBinary = new ByteArrayOutputStream() 472 | 473 | ImageIO.write(textures, "png", texturesBinary) 474 | 475 | import java.io.File 476 | ImageIO.write(textures, "png", new File("textures.png")) 477 | (blockSeq, texturesBinary.toByteArray) 478 | } 479 | 480 | @PluginConstructor 481 | def props( 482 | name: String, 483 | universe: ActorRef, 484 | @Config(key = "json-storage") jsonStorage: ActorRef, 485 | @Config(key = "blocks") blockConfig: TypesafeConfig, 486 | @Config(key = "classes", optional = true) classConfig: TypesafeConfig 487 | ): Props = { 488 | print("Loading block data... ") 489 | val classMap = if (classConfig != null) { 490 | parseClasses(classConfig) 491 | } else { 492 | Map.empty[BlockClassId, BlockClass] 493 | } 494 | val (blocks, textures) = parseBlocks(blockConfig, classMap) 495 | println("done!") 496 | Props( 497 | classOf[BlockMetaActor], 498 | name, 499 | jsonStorage, 500 | blocks, 501 | textures 502 | ) 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/shard/ShardActor.scala: -------------------------------------------------------------------------------- 1 | package konstructs.shard 2 | 3 | import java.util.UUID 4 | 5 | import scala.collection.mutable 6 | 7 | import akka.actor.{Actor, Stash, ActorRef, Props} 8 | import akka.util.ByteString 9 | 10 | import com.google.gson.reflect.TypeToken 11 | 12 | import konstructs.api.{ 13 | BinaryLoaded, 14 | Position, 15 | Block, 16 | GsonLoaded, 17 | BlockTypeId, 18 | BlockFilter, 19 | BlockFilterFactory, 20 | BlockFactory, 21 | BlockState, 22 | Direction, 23 | Rotation, 24 | Orientation, 25 | LightLevel, 26 | Colour, 27 | BlockType, 28 | BlockUpdate, 29 | Health, 30 | ReceiveStack, 31 | Stack, 32 | MetricId 33 | } 34 | import konstructs.api.messages.{ 35 | BoxQuery, 36 | BoxQueryResult, 37 | ReplaceBlock, 38 | ReplaceBlockResult, 39 | ViewBlock, 40 | ViewBlockResult, 41 | BlockUpdateEvent, 42 | InteractResult, 43 | InteractTertiaryFilter, 44 | InteractTertiary, 45 | IncreaseMetric 46 | } 47 | 48 | import konstructs.utils.Scheduled 49 | 50 | import konstructs.{Db, BinaryStorage, JsonStorage, DbActor, GeneratorActor} 51 | 52 | class ShardActor(db: ActorRef, 53 | shard: ShardPosition, 54 | val binaryStorage: ActorRef, 55 | val jsonStorage: ActorRef, 56 | blockUpdateEvents: Seq[ActorRef], 57 | chunkGenerator: ActorRef, 58 | tertiaryInteractionFilters: Seq[ActorRef], 59 | universe: ActorRef, 60 | implicit val blockFactory: BlockFactory) 61 | extends Actor 62 | with Stash 63 | with Scheduled 64 | with BinaryStorage 65 | with JsonStorage { 66 | import ShardActor._ 67 | import GeneratorActor._ 68 | import DbActor._ 69 | import Db._ 70 | import Light._ 71 | 72 | val TypeOfPositionMapping = new TypeToken[java.util.Map[String, UUID]]() {}.getType 73 | val ReplaceFilter = BlockFilterFactory 74 | .withBlockState(BlockState.LIQUID) 75 | .or(BlockFilterFactory.withBlockState(BlockState.GAS)) 76 | .or(BlockFilterFactory.VACUUM) 77 | val ns = "chunks" 78 | 79 | private implicit val blockBuffer = new Array[Byte](ChunkData.Size) 80 | private val compressionBuffer = new Array[Byte](ChunkData.Size + ChunkData.Header) 81 | private val chunks = new Array[Option[ChunkData]](ShardSize * ShardSize * ShardSize) 82 | 83 | private var dirty: Set[ChunkPosition] = Set() 84 | private var positionMappingDirty = true 85 | private def chunkId(c: ChunkPosition): String = 86 | s"${c.p}/${c.q}/${c.k}" 87 | 88 | private val shardId = ShardActor.shardId(shard) 89 | 90 | private val positionMappingFile = ShardActor.positionMappingFile(shardId) 91 | 92 | private def chunkFromId(id: String): ChunkPosition = { 93 | val pqk = id.split('/').map(_.toInt) 94 | ChunkPosition(pqk(0), pqk(1), pqk(2)) 95 | } 96 | 97 | private val VacuumData = BlockData( 98 | blockFactory.getW(BlockTypeId.VACUUM), 99 | Health.PRISTINE.getHealth(), 100 | Direction.UP_ENCODING, 101 | Rotation.IDENTITY_ENCODING, 102 | LightLevel.DARK_ENCODING, 103 | Colour.WHITE.getRed(), 104 | Colour.WHITE.getGreen(), 105 | Colour.WHITE.getBlue(), 106 | LightLevel.DARK_ENCODING 107 | ) 108 | private val VacuumBlock = Block.create(BlockTypeId.VACUUM) 109 | private val toolDurabilityBonus = 10.0f 110 | private val SpaceVacuumW = blockFactory.getW(BlockTypeId.fromString("org/konstructs/space/vacuum")) 111 | 112 | schedule(5000, StoreChunks) 113 | 114 | val BlockUpdatedMetric = MetricId.fromString("org/konstructs/updated-blocks") 115 | val BlockQueriedMetric = MetricId.fromString("org/konstructs/queried-blocks") 116 | 117 | val ChunkDecompressedMetric = MetricId.fromString("org/konstructs/decompressed-chunks") 118 | val ChunkCompressedMetric = MetricId.fromString("org/konstructs/compressed-chunks") 119 | val ChunkSentMetric = MetricId.fromString("org/konstructs/sent-chunks") 120 | 121 | val LightUpdatedMetric = MetricId.fromString("org/konstructs/updated-blocks-light") 122 | 123 | def sendEvent(events: java.util.Map[Position, BlockUpdate]) { 124 | universe ! new IncreaseMetric(BlockUpdatedMetric, events.size) 125 | val msg = new BlockUpdateEvent(events) 126 | for (l <- blockUpdateEvents) { 127 | l ! msg 128 | } 129 | } 130 | 131 | def sendEvent(position: Position, from: Block, to: Block) { 132 | val events = new java.util.HashMap[Position, BlockUpdate]() 133 | events.put(position, new BlockUpdate(from, to)) 134 | sendEvent(events) 135 | } 136 | 137 | def loadChunk(chunk: ChunkPosition): Option[ChunkData] = { 138 | val i = index(chunk, shard) 139 | val blocks = chunks(i) 140 | if (blocks != null) { 141 | if (!blocks.isDefined) 142 | stash() 143 | blocks 144 | } else { 145 | loadBinary(chunkId(chunk)) 146 | chunks(i) = None 147 | stash() 148 | None 149 | } 150 | } 151 | 152 | def runQuery(query: BoxQuery, sender: ActorRef) = { 153 | val box = query.getBox 154 | val chunk = ChunkPosition(box.getFrom) 155 | updateChunk(chunk) { () => 156 | val data = new Array[BlockTypeId](box.getNumberOfBlocks + 1) 157 | for (x <- box.getFrom.getX until box.getUntil.getX; 158 | y <- box.getFrom.getY until box.getUntil.getY; 159 | z <- box.getFrom.getZ until box.getUntil.getZ) { 160 | val p = new Position(x, y, z) 161 | data(box.arrayIndex(p)) = blockFactory.getBlockTypeId(BlockData.w(blockBuffer, ChunkData.index(chunk, p))) 162 | 163 | } 164 | sender ! new BoxQueryResult(box, data) 165 | universe ! new IncreaseMetric(BlockQueriedMetric, box.getNumberOfBlocks) 166 | false /* Indicate that we did not update the chunk */ 167 | } 168 | } 169 | 170 | def readBlock(pos: Position)(read: BlockData => Unit) = { 171 | val chunk = ChunkPosition(pos) 172 | loadChunk(chunk).map { c => 173 | val block = c.block(chunk, pos, blockBuffer, compressionBuffer) 174 | read(block) 175 | } 176 | } 177 | 178 | def updateChunk(chunk: ChunkPosition)(update: () => Boolean) { 179 | loadChunk(chunk).map { c => 180 | c.unpackTo(blockBuffer, compressionBuffer) 181 | universe ! new IncreaseMetric(ChunkDecompressedMetric, 1) 182 | if (update()) { 183 | dirty = dirty + chunk 184 | val data = ChunkData(c.revision + 1, blockBuffer, compressionBuffer) 185 | chunks(index(chunk, shard)) = Some(data) 186 | db ! ChunkUpdate(chunk, data) 187 | universe ! new IncreaseMetric(ChunkCompressedMetric, 1) 188 | } 189 | } 190 | } 191 | 192 | def replaceBlocks(chunk: ChunkPosition, 193 | filter: BlockFilter, 194 | blocks: Map[Position, BlockTypeId], 195 | positionMapping: java.util.Map[String, UUID]) { 196 | updateChunk(chunk) { () => 197 | val events = new java.util.HashMap[Position, BlockUpdate]() 198 | val updates = mutable.Set[(Position, BlockData, BlockData)]() 199 | for ((position, newTypeId) <- blocks) { 200 | val i = ChunkData.index(chunk, position) 201 | val typeId = blockFactory.getBlockTypeId(BlockData.w(blockBuffer, i)) 202 | val oldType = blockFactory.getBlockType(typeId) 203 | if (filter.matches(typeId, oldType)) { 204 | val id = positionMapping.remove(str(position)) 205 | val newType = blockFactory.getBlockType(newTypeId) 206 | val oldBlock = BlockData(blockBuffer, i) 207 | val newBlock = BlockData(blockFactory.getW(newTypeId), 208 | Health.PRISTINE.getHealth(), 209 | Direction.UP_ENCODING, 210 | Rotation.IDENTITY_ENCODING, 211 | LightLevel.DARK_ENCODING, 212 | newType.getLightColour.getRed, 213 | newType.getLightColour.getGreen, 214 | newType.getLightColour.getBlue, 215 | newType.getLightLevel.getLevel) 216 | events.put(position, new BlockUpdate(oldBlock.block(id, typeId), newBlock.block(null, newTypeId))) 217 | updates += ((position, oldBlock, newBlock)) 218 | newBlock.write(blockBuffer, i) 219 | if (id != null) 220 | positionMappingDirty = true 221 | } 222 | } 223 | if (!events.isEmpty) { 224 | sendEvent(events) 225 | universe ! new IncreaseMetric(LightUpdatedMetric, updateLight(updates.toSet, chunk, db)) 226 | true // We updated the chunk 227 | } else { 228 | false // We didn't update the chunk 229 | } 230 | } 231 | } 232 | 233 | def damageBlock(position: Position, dealing: Block, positionMapping: java.util.Map[String, UUID])( 234 | ready: (Block, Block) => Unit) { 235 | val chunk = ChunkPosition(position) 236 | updateChunk(chunk) { () => 237 | val i = ChunkData.index(chunk, position) 238 | val old = BlockData(blockBuffer, i) 239 | val receivingTypeId = blockFactory.getBlockTypeId(old.w) 240 | val receivingType = blockFactory.getBlockType(receivingTypeId) 241 | val dealingType = blockFactory.getBlockType(dealing.getType()) 242 | val dealingDamage = dealingType.getDamageWithMultiplier(receivingTypeId, receivingType) 243 | val receivingHealth = Health.get(old.health).damage(dealingDamage, receivingType.getDurability()) 244 | val dealingHealth = 245 | dealing.getHealth().damage(receivingType.getDamage(), dealingType.getDurability() * toolDurabilityBonus) 246 | val dealingBlock = if (dealingHealth.isDestroyed) { 247 | null 248 | } else { 249 | dealing.withHealth(dealingHealth) 250 | } 251 | if (receivingHealth.isDestroyed()) { 252 | val id = positionMapping.remove(str(position)) 253 | val oldBlock = old.block(id, receivingTypeId) 254 | if (id != null) 255 | positionMappingDirty = true 256 | val block = if (receivingType.getDestroyedAs() == BlockTypeId.SELF) { 257 | oldBlock.withOrientation(Orientation.NORMAL).withHealth(Health.PRISTINE) 258 | } else { 259 | oldBlock 260 | .withOrientation(Orientation.NORMAL) 261 | .withHealth(Health.PRISTINE) 262 | .withType(receivingType.getDestroyedAs()) 263 | } 264 | sendEvent(position, oldBlock, VacuumBlock) 265 | VacuumData.write(blockBuffer, i) 266 | universe ! new IncreaseMetric(LightUpdatedMetric, updateLight(Set((position, old, VacuumData)), chunk, db)) 267 | ready(dealingBlock, block) 268 | } else { 269 | old.copy(health = receivingHealth.getHealth()).write(blockBuffer, i) 270 | universe ! new IncreaseMetric(BlockUpdatedMetric, 1) 271 | ready(dealingBlock, null) 272 | } 273 | true 274 | } 275 | } 276 | 277 | def replaceBlock(filter: BlockFilter, 278 | position: Position, 279 | block: Block, 280 | positionMapping: java.util.Map[String, UUID])(ready: Block => Unit) = { 281 | val chunk = ChunkPosition(position) 282 | updateChunk(chunk) { () => 283 | val i = ChunkData.index(chunk, position) 284 | val old = BlockData(blockBuffer, i) 285 | val typeId = blockFactory.getBlockTypeId(old.w) 286 | val blockType = blockFactory.getBlockType(typeId) 287 | if (filter.matches(typeId, blockType)) { 288 | val oldId = if (block.getId() != null) { 289 | positionMappingDirty = true 290 | positionMapping.put(str(position), block.getId()) 291 | } else { 292 | val id = positionMapping.remove(str(position)) 293 | if (id != null) 294 | positionMappingDirty = true 295 | id 296 | } 297 | val oldBlock = old.block(oldId, typeId) 298 | ready(oldBlock) 299 | if (oldBlock != block) { 300 | val newBlockType = blockFactory.getBlockType(block.getType) 301 | val newData = BlockData(blockFactory.getW(block.getType()), 302 | block, 303 | LightLevel.DARK_ENCODING, 304 | newBlockType.getLightColour, 305 | newBlockType.getLightLevel) 306 | newData.write(blockBuffer, i) 307 | sendEvent(position, oldBlock, block) 308 | universe ! new IncreaseMetric(LightUpdatedMetric, updateLight(Set((position, old, newData)), chunk, db)) 309 | true // We updated the chunk 310 | } else { 311 | false 312 | } 313 | } else { 314 | false // We didn't update the chunk 315 | } 316 | } 317 | } 318 | 319 | /* 320 | * Load id mapping for blocks 321 | */ 322 | loadGson(positionMappingFile) 323 | 324 | /* 325 | * Receive position mapping before going ready 326 | */ 327 | def receive() = { 328 | case GsonLoaded(_, json) if json != null => 329 | val positionMapping: java.util.Map[String, UUID] = gson.fromJson(json, TypeOfPositionMapping) 330 | context.become(ready(positionMapping)) 331 | positionMappingDirty = false 332 | case GsonLoaded(_, _) => 333 | val positionMapping = new java.util.HashMap[String, UUID]() 334 | context.become(ready(positionMapping)) 335 | case _ => 336 | stash() 337 | } 338 | 339 | def ready(positionMapping: java.util.Map[String, UUID]): Receive = { 340 | case SendBlocks(chunk) => 341 | val s = sender 342 | loadChunk(chunk).map { c => 343 | s ! BlockList(chunk, c) 344 | } 345 | case i: InteractPrimaryUpdate => 346 | val s = sender 347 | val block = Option(i.block).getOrElse(VacuumBlock) 348 | damageBlock(i.position, block, positionMapping) { (using, damaged) => 349 | if (damaged != null) { 350 | s ! ReceiveStack(Stack.createFromBlock(damaged)) 351 | } 352 | if (block != null) 353 | s ! new InteractResult(i.position, using, damaged) 354 | else 355 | s ! new InteractResult(i.position, null, damaged) 356 | } 357 | case i: InteractSecondaryUpdate => 358 | val s = sender 359 | val blockType = blockFactory.getBlockType(i.block.getType) 360 | 361 | val rb = if (blockType.isOrientable) { 362 | i.block.withOrientation(i.orientation) 363 | } else { 364 | i.block 365 | } 366 | replaceBlock(ReplaceFilter, i.position, rb, positionMapping) { b => 367 | if (b == rb) { 368 | s ! new InteractResult(i.position, i.block, null) 369 | } else { 370 | s ! new InteractResult(i.position, null, null) 371 | } 372 | } 373 | case i: InteractTertiaryUpdate => 374 | val filters = tertiaryInteractionFilters :+ self 375 | val message = i.message 376 | val p = message.getPosition 377 | readBlock(p) { block => 378 | val b = block.block(positionMapping.get(str(p)), blockFactory.getBlockTypeId(block.w)) 379 | val filters = tertiaryInteractionFilters :+ self 380 | filters.head ! new InteractTertiaryFilter(filters.tail.toArray, 381 | message.withBlockAtPosition(b).withWorldPhase(true)) 382 | } 383 | case i: InteractTertiaryFilter if i.getMessage.isWorldPhase => 384 | val filters = tertiaryInteractionFilters :+ self 385 | filters.head ! new InteractTertiaryFilter(filters.tail.toArray, i.getMessage.withWorldPhase(false)) 386 | case s: InteractTertiaryFilter.Skipped => 387 | // If first phase was skipped, update the world and return to user 388 | // This avoids running the second phase 389 | val i = s.getFilter 390 | val message = i.getMessage 391 | val position = message.getPosition 392 | val blockAtPosition = message.getBlockAtPosition 393 | /* Update the block with any changes made by the filter */ 394 | replaceBlock(BlockFilterFactory.EVERYTHING, position, blockAtPosition, positionMapping) { b => 395 | message.getSender ! new InteractResult(position, message.getBlock, blockAtPosition) 396 | } 397 | case i: InteractTertiaryFilter if !i.getMessage.isWorldPhase => 398 | val message = i.getMessage 399 | val position = message.getPosition 400 | val blockAtPosition = message.getBlockAtPosition 401 | /* Update the block with any changes made by the filter */ 402 | replaceBlock(BlockFilterFactory.EVERYTHING, position, blockAtPosition, positionMapping) { b => 403 | message.getSender ! new InteractResult(position, message.getBlock, blockAtPosition) 404 | } 405 | case r: ReplaceBlock => 406 | val s = sender 407 | replaceBlock(r.getFilter, r.getPosition, r.getBlock, positionMapping) { b => 408 | if (b == r.getBlock) { 409 | s ! new ReplaceBlockResult(r.getPosition, b, true) 410 | } else { 411 | s ! new ReplaceBlockResult(r.getPosition, b, false) 412 | } 413 | } 414 | case ReplaceBlocks(chunk, filter, blocks) => 415 | replaceBlocks(chunk, filter, blocks, positionMapping) 416 | case v: ViewBlock => 417 | val s = sender 418 | val p = v.getPosition 419 | readBlock(p) { block => 420 | val b = block.block(positionMapping.get(str(p)), blockFactory.getBlockTypeId(block.w)) 421 | s ! new ViewBlockResult(p, b) 422 | } 423 | case q: BoxQuery => 424 | runQuery(q, sender) 425 | case BinaryLoaded(id, dataOption) => 426 | val chunk = chunkFromId(id) 427 | dataOption match { 428 | case Some(data) => 429 | val version = data(0) 430 | chunks(index(chunk, shard)) = Some(if (version < ChunkData.Version) { 431 | dirty = dirty + chunk 432 | db ! refreshChunkAbove(chunk) 433 | ChunkData.loadOldFormat(version, data, blockBuffer, compressionBuffer, chunk, SpaceVacuumW) 434 | } else { 435 | ChunkData(version, data) 436 | }) 437 | unstashAll() 438 | case None => 439 | chunkGenerator ! Generate(chunk) 440 | } 441 | case Generated(position, data) => 442 | chunks(index(position, shard)) = Some(ChunkData(ChunkData.InitialRevision, data, compressionBuffer)) 443 | dirty = dirty + position 444 | unstashAll() 445 | case StoreChunks => 446 | dirty.map { chunk => 447 | chunks(index(chunk, shard)).map { c => 448 | storeBinary(chunkId(chunk), c.data) 449 | } 450 | } 451 | dirty = Set() 452 | if (positionMappingDirty) { 453 | storeGson(positionMappingFile, gson.toJsonTree(positionMapping, TypeOfPositionMapping)) 454 | positionMappingDirty = false 455 | } 456 | case f: FloodLight => 457 | updateChunk(f.chunk) { () => 458 | val updated = floodLight(f, db) 459 | if (updated > 0) { 460 | universe ! new IncreaseMetric(LightUpdatedMetric, updated) 461 | true //Chunk was updated 462 | } else { 463 | false 464 | } 465 | } 466 | case r: RemoveLight => 467 | updateChunk(r.chunk) { () => 468 | val updated = removeLight(r, db) 469 | if (updated > 0) { 470 | universe ! new IncreaseMetric(LightUpdatedMetric, updated) 471 | true //Chunk was updated 472 | } else { 473 | false 474 | } 475 | } 476 | case r: RemoveAmbientLight => 477 | updateChunk(r.chunk) { () => 478 | val updated = removeAmbientLight(r, db) 479 | if (updated > 0) { 480 | universe ! new IncreaseMetric(LightUpdatedMetric, updated) 481 | true //Chunk was updated 482 | } else { 483 | false 484 | } 485 | } 486 | case f: FloodAmbientLight => 487 | updateChunk(f.chunk) { () => 488 | val updated = floodAmbientLight(f, db) 489 | if (updated > 0) { 490 | universe ! new IncreaseMetric(LightUpdatedMetric, updated) 491 | true //Chunk was updated 492 | } else { 493 | false 494 | } 495 | } 496 | case r: RefreshLight => 497 | updateChunk(r.chunk) { () => 498 | val updated = refreshLight(r, db) 499 | if (updated > 0) { 500 | universe ! new IncreaseMetric(LightUpdatedMetric, updated) 501 | true //Chunk was updated 502 | } else { 503 | false 504 | } 505 | } 506 | case r: RefreshAmbientLight => 507 | updateChunk(r.chunk) { () => 508 | val updated = refreshAmbientLight(r, db) 509 | if (updated > 0) { 510 | universe ! new IncreaseMetric(LightUpdatedMetric, updated) 511 | true //Chunk was updated 512 | } else { 513 | false 514 | } 515 | } 516 | } 517 | 518 | } 519 | 520 | object ShardActor { 521 | case object StoreChunks 522 | 523 | def shardId(shard: ShardPosition) = s"${shard.m}-${shard.n}-${shard.o}" 524 | 525 | def positionMappingFile(shardId: String) = s"${shardId}-position-mapping" 526 | 527 | def str(p: Position) = s"${p.getX}-${p.getY}-${p.getZ}" 528 | 529 | case class ReplaceBlocks(chunk: ChunkPosition, filter: BlockFilter, blocks: Map[Position, BlockTypeId]) 530 | 531 | def index(c: ChunkPosition, shard: ShardPosition): Int = { 532 | val local = shard.local(c) 533 | local.p + local.q * Db.ShardSize + local.k * Db.ShardSize * Db.ShardSize 534 | } 535 | 536 | def props(db: ActorRef, 537 | shard: ShardPosition, 538 | binaryStorage: ActorRef, 539 | jsonStorage: ActorRef, 540 | blockUpdateEvents: Seq[ActorRef], 541 | chunkGenerator: ActorRef, 542 | blockFactory: BlockFactory, 543 | tertiaryInteractionFilters: Seq[ActorRef], 544 | universe: ActorRef) = 545 | Props(classOf[ShardActor], 546 | db, 547 | shard, 548 | binaryStorage, 549 | jsonStorage, 550 | blockUpdateEvents, 551 | chunkGenerator, 552 | tertiaryInteractionFilters, 553 | universe, 554 | blockFactory) 555 | } 556 | -------------------------------------------------------------------------------- /src/main/scala/konstructs/shard/Light.scala: -------------------------------------------------------------------------------- 1 | package konstructs.shard 2 | 3 | import scala.collection.mutable 4 | import akka.actor.ActorRef 5 | import konstructs.Db 6 | import konstructs.api.{Position, LightLevel, Colour, BlockFactory} 7 | 8 | object Light { 9 | 10 | // A specific block which should be flooded by ambient light 11 | case class AmbientLightFlood(position: Position, from: Position, ambient: Int) 12 | 13 | // Message to flood ambient light 14 | case class FloodAmbientLight(chunk: ChunkPosition, update: Set[AmbientLightFlood]) 15 | 16 | // A specific block which should be flooded by light 17 | case class LightFlood(position: Position, from: Position, light: Int, red: Int, green: Int, blue: Int) 18 | 19 | // Message to flood light 20 | case class FloodLight(chunk: ChunkPosition, update: Set[LightFlood]) 21 | 22 | // Message to refresh light in blocks 23 | case class RefreshLight(chunk: ChunkPosition, positions: Set[Position]) 24 | 25 | // Message to refresh ambient light in blocks 26 | case class RefreshAmbientLight(chunk: ChunkPosition, positions: Set[Position]) 27 | 28 | // A specific block which ambient light should be removed 29 | case class AmbientLightRemoval(position: Position, from: Position, ambient: Int) 30 | 31 | // Message to remove ambient light 32 | case class RemoveAmbientLight(chunk: ChunkPosition, removal: Set[AmbientLightRemoval]) 33 | 34 | // A specific position where light needs to be removed 35 | case class LightRemoval(position: Position, from: Position, light: Int) 36 | 37 | // Message to remove light 38 | case class RemoveLight(chunk: ChunkPosition, removal: Set[LightRemoval]) 39 | 40 | // This function validates lightning of blocks that just changed 41 | // It handles light sources as well as normal blocks 42 | def updateLight(positions: Set[(Position, BlockData, BlockData)], chunk: ChunkPosition, db: ActorRef)( 43 | implicit blockFactory: BlockFactory, 44 | blockBuffer: Array[Byte]): Int = { 45 | var bu = 0 46 | 47 | // Blocks that should be refreshed in this chunk 48 | val refresh = mutable.Set[Position]() 49 | val refreshAmbient = mutable.Set[Position]() 50 | // Blocks that should be refreshed in other chunks 51 | val refreshOthers = mutable.HashMap[ChunkPosition, mutable.Set[Position]]() 52 | val refreshAmbientOthers = mutable.HashMap[ChunkPosition, mutable.Set[Position]]() 53 | 54 | // Light sources to remove in this chunk 55 | val remove = mutable.Set[LightRemoval]() 56 | // Light sources to remove in other chunks 57 | val removeOthers = mutable.HashMap[ChunkPosition, mutable.Set[LightRemoval]]() 58 | 59 | // Light sources to remove in this chunk 60 | val removeAmbient = mutable.Set[AmbientLightRemoval]() 61 | // Light sources to remove in other chunks 62 | val removeAmbientOthers = mutable.HashMap[ChunkPosition, mutable.Set[AmbientLightRemoval]]() 63 | 64 | // Iterate through all updated blocks 65 | for ((position, oldBlock, block) <- positions) { 66 | // Old block is a light source or was lit 67 | if (oldBlock.light > 1) { 68 | 69 | // Find all adjacent blocks and add them for light removal 70 | for (adj <- position.getAdjacent) { 71 | val adjChunk = ChunkPosition(adj) 72 | if (adjChunk == chunk) { 73 | // Add for removal in this chunk 74 | remove += LightRemoval(adj, position, oldBlock.light - 1) 75 | } else { 76 | // Add for removal in another chunk 77 | val s = removeOthers.getOrElseUpdate(adjChunk, mutable.Set[LightRemoval]()) 78 | s += LightRemoval(adj, position, oldBlock.light - 1) 79 | } 80 | } 81 | } 82 | 83 | // Old block had ambient light 84 | if (oldBlock.ambient > 1) { 85 | // Find all adjacent blocks and add them for light removal 86 | for (adj <- position.getAdjacent) { 87 | val adjChunk = ChunkPosition(adj) 88 | val ambient = if (position.getY > adj.getY && oldBlock.ambient == LightLevel.FULL_ENCODING) { 89 | oldBlock.ambient 90 | } else { 91 | oldBlock.ambient - 1 92 | } 93 | if (adjChunk == chunk) { 94 | // Add for removal in this chunk 95 | removeAmbient += AmbientLightRemoval(adj, position, ambient) 96 | } else { 97 | // Add for removal in another chunk 98 | val s = removeAmbientOthers.getOrElseUpdate(adjChunk, mutable.Set[AmbientLightRemoval]()) 99 | s += AmbientLightRemoval(adj, position, ambient) 100 | } 101 | } 102 | } 103 | 104 | // The new block has light of itself 105 | if (block.light > 1) { 106 | // Add block to blocks that requires refresh 107 | refresh += position 108 | } 109 | 110 | // Iterate through all adjacent blocks 111 | for (adj <- position.getAdjacent) { 112 | val adjChunk = ChunkPosition(adj) 113 | if (adjChunk == chunk) { 114 | // Add for refresh in this chunk 115 | refresh += adj 116 | refreshAmbient += adj 117 | } else { 118 | // Add for refresh in other chunk 119 | val s = refreshOthers.getOrElseUpdate(adjChunk, mutable.Set[Position]()) 120 | s += adj 121 | val sa = refreshAmbientOthers.getOrElseUpdate(adjChunk, mutable.Set[Position]()) 122 | sa += adj 123 | } 124 | } 125 | } 126 | 127 | bu += removeAmbientLight(RemoveAmbientLight(chunk, removeAmbient.toSet), removeAmbientOthers, refreshAmbient, db) 128 | 129 | // Refresh all lights that requires refresh in this chunk 130 | bu += refreshAmbientLight(RefreshAmbientLight(chunk, refreshAmbient.toSet), db) 131 | 132 | // Send messages to refresh all other chunks 133 | refreshAmbientOthers foreach { 134 | case (chunk, set) => 135 | db ! RefreshAmbientLight(chunk, set.toSet) 136 | } 137 | 138 | // Remove all lights that requires removal in this and other chunks 139 | // (This function sends remove messages to other chunks) 140 | // This also adds blocks that requires refresh due to neighbour removal 141 | bu += removeLight(RemoveLight(chunk, remove.toSet), removeOthers, refresh, db) 142 | 143 | // Refresh all lights that requires refresh in this chunk 144 | bu += refreshLight(RefreshLight(chunk, refresh.toSet), db) 145 | 146 | // Send messages to refresh all other chunks 147 | refreshOthers foreach { 148 | case (chunk, set) => 149 | db ! RefreshLight(chunk, set.toSet) 150 | } 151 | return bu 152 | } 153 | 154 | // Helper method to remove and refresh light 155 | // This is done when receiving a remove light message 156 | def removeLight(removal: RemoveLight, db: ActorRef)(implicit blockFactory: BlockFactory, 157 | blockBuffer: Array[Byte]): Int = { 158 | val refresh = mutable.Set[Position]() 159 | var bu = 0 160 | bu = bu + removeLight(removal, mutable.HashMap[ChunkPosition, mutable.Set[LightRemoval]](), refresh, db) 161 | bu = bu + refreshLight(RefreshLight(removal.chunk, refresh.toSet), db) 162 | return bu 163 | } 164 | 165 | // Removes light and enqueue neighbours that require refresh 166 | def removeLight(removal: RemoveLight, 167 | buffer: mutable.HashMap[ChunkPosition, mutable.Set[LightRemoval]], 168 | refresh: mutable.Set[Position], 169 | db: ActorRef)(implicit blockFactory: BlockFactory, blockBuffer: Array[Byte]): Int = { 170 | var bu = 0 171 | // Queue for BFS search 172 | val queue = mutable.Queue[LightRemoval]() 173 | 174 | // Add all lights that require removal 175 | queue ++= removal.removal 176 | 177 | val chunk = removal.chunk 178 | 179 | // BFS flood removal 180 | while (!queue.isEmpty) { 181 | val r = queue.dequeue 182 | val i = ChunkData.index(chunk, r.position) 183 | val a = BlockData(blockBuffer, i) 184 | val aType = blockFactory.getBlockType(blockFactory.getBlockTypeId(a.w)) 185 | 186 | // If the block is transparent and has light set 187 | if (aType.isTransparent && a.light != 0) { 188 | 189 | // If the light of the block is equal or smaller to the light to be removed, remove it 190 | // Else the block needs to be added to refresh list, since it might flood the area where 191 | // light has been removed with another light 192 | if (a.light <= r.light) { 193 | // Remove light from the block 194 | a.copy(light = 0, red = Colour.WHITE.getRed, green = Colour.WHITE.getGreen, blue = Colour.WHITE.getBlue) 195 | .write(blockBuffer, i) 196 | bu = bu + 1 197 | // If the light to remove is > 1 continue to remove light from all neighbours 198 | if (r.light > 1) { 199 | for (adj <- r.position.getAdjacent) { 200 | if (adj != r.from) { 201 | val adjChunk = ChunkPosition(adj) 202 | if (chunk == adjChunk) { 203 | // Add light to be removed in this chunk 204 | queue.enqueue(LightRemoval(adj, r.position, r.light - 1)) 205 | } else { 206 | // Add light to be removed in another chunk 207 | val set = buffer.getOrElseUpdate(adjChunk, mutable.Set[LightRemoval]()) 208 | set += LightRemoval(adj, r.position, r.light - 1) 209 | } 210 | } 211 | } 212 | } 213 | } else { 214 | // Add block to be refreshed 215 | refresh += r.position 216 | } 217 | } 218 | } 219 | 220 | // Send lights to be removed to other chunks 221 | buffer foreach { 222 | case (chunk, set) => 223 | db ! RemoveLight(chunk, set.toSet) 224 | } 225 | 226 | return bu 227 | } 228 | 229 | // Helper method to remove and refresh light 230 | // This is done when receiving a remove light message 231 | def removeAmbientLight(removal: RemoveAmbientLight, db: ActorRef)(implicit blockFactory: BlockFactory, 232 | blockBuffer: Array[Byte]): Int = { 233 | var bu = 0 234 | val refresh = mutable.Set[Position]() 235 | bu = bu + removeAmbientLight(removal, 236 | mutable.HashMap[ChunkPosition, mutable.Set[AmbientLightRemoval]](), 237 | refresh, 238 | db) 239 | bu = bu + refreshAmbientLight(RefreshAmbientLight(removal.chunk, refresh.toSet), db) 240 | return bu 241 | } 242 | 243 | // Removes light and enqueue neighbours that require refresh 244 | def removeAmbientLight(removal: RemoveAmbientLight, 245 | buffer: mutable.HashMap[ChunkPosition, mutable.Set[AmbientLightRemoval]], 246 | refresh: mutable.Set[Position], 247 | db: ActorRef)(implicit blockFactory: BlockFactory, blockBuffer: Array[Byte]): Int = { 248 | 249 | var bu = 0 250 | 251 | // Queue for BFS search 252 | val queue = mutable.Queue[AmbientLightRemoval]() 253 | 254 | // Add all lights that require removal 255 | queue ++= removal.removal 256 | 257 | val chunk = removal.chunk 258 | 259 | // BFS flood removal 260 | while (!queue.isEmpty) { 261 | val r = queue.dequeue 262 | val i = ChunkData.index(chunk, r.position) 263 | val a = BlockData(blockBuffer, i) 264 | val aTypeId = blockFactory.getBlockTypeId(a.w) 265 | val aType = blockFactory.getBlockType(aTypeId) 266 | 267 | // If the block is transparent and has light set 268 | if (aType.isTransparent && a.ambient != 0 && aTypeId.getNamespace != "org/konstructs/space") { 269 | 270 | // If the light of the block is equal or smaller to the light to be removed, remove it 271 | // Else the block needs to be added to refresh list, since it might flood the area where 272 | // light has been removed with another light 273 | if (a.ambient <= r.ambient) { 274 | 275 | bu = bu + 1 276 | // Remove light from the block 277 | a.copy(ambient = 0).write(blockBuffer, i) 278 | 279 | // If the light to remove is > 1 continue to remove light from all neighbours 280 | if (r.ambient > 1) { 281 | for (adj <- r.position.getAdjacent) { 282 | if (adj != r.from) { 283 | val adjChunk = ChunkPosition(adj) 284 | // TODO: Remove artificial limit at 0 285 | val ambient = if (r.position.getY > adj.getY && r.ambient == LightLevel.FULL_ENCODING) { 286 | r.ambient 287 | } else { 288 | r.ambient - 1 289 | } 290 | if (chunk == adjChunk) { 291 | // Add light to be removed in this chunk 292 | queue.enqueue(AmbientLightRemoval(adj, r.position, ambient)) 293 | } else { 294 | // Add light to be removed in another chunk 295 | val set = buffer.getOrElseUpdate(adjChunk, mutable.Set[AmbientLightRemoval]()) 296 | set += AmbientLightRemoval(adj, r.position, ambient) 297 | } 298 | } 299 | } 300 | } 301 | } else { 302 | // Add block to be refreshed 303 | refresh += r.position 304 | } 305 | } 306 | } 307 | 308 | // Send lights to be removed to other chunks 309 | buffer foreach { 310 | case (chunk, set) => 311 | db ! RemoveAmbientLight(chunk, set.toSet) 312 | } 313 | 314 | return bu 315 | } 316 | 317 | // This function looks at the positions given and 318 | // if the position contains light, tries to propagate 319 | // it to refresh any updated or newly placed block 320 | def refreshLight(refresh: RefreshLight, db: ActorRef)(implicit blockFactory: BlockFactory, 321 | blockBuffer: Array[Byte]): Int = { 322 | // Blocks where to flood light in this chunk 323 | val flood = mutable.Set[LightFlood]() 324 | 325 | // Blocks where to flood light in other chunks 326 | val floodOthers = mutable.HashMap[ChunkPosition, mutable.Set[LightFlood]]() 327 | 328 | val chunk = refresh.chunk 329 | 330 | // Iterate through all blocks that require a refresh 331 | for (position <- refresh.positions) { 332 | val i = ChunkData.index(chunk, position) 333 | val block = BlockData(blockBuffer, i) 334 | 335 | // If block has a light level higher than one 336 | // it can spread light around 337 | if (block.light > 1) { 338 | for (adj <- position.getAdjacent) { 339 | val adjChunk = ChunkPosition(adj) 340 | if (adjChunk == chunk) { 341 | // Add light to flood in this chunk 342 | flood += LightFlood(adj, position, block.light - 1, block.red, block.green, block.blue) 343 | } else { 344 | // Add light to flood in other chunk 345 | val s = floodOthers.getOrElseUpdate(adjChunk, mutable.Set[LightFlood]()) 346 | s += LightFlood(adj, position, block.light - 1, block.red, block.green, block.blue) 347 | } 348 | } 349 | } 350 | } 351 | 352 | // Flood all lights found 353 | return floodLight(FloodLight(chunk, flood.toSet), floodOthers, db) 354 | } 355 | 356 | def refreshAmbientLight(refreshAmbient: RefreshAmbientLight, db: ActorRef)(implicit blockFactory: BlockFactory, 357 | blockBuffer: Array[Byte]): Int = { 358 | 359 | val ambientFlood = mutable.Set[AmbientLightFlood]() 360 | val ambientFloodOthers = mutable.HashMap[ChunkPosition, mutable.Set[AmbientLightFlood]]() 361 | 362 | val chunk = refreshAmbient.chunk 363 | 364 | // Iterate through all blocks that require a refresh 365 | for (position <- refreshAmbient.positions) { 366 | val i = ChunkData.index(chunk, position) 367 | val block = BlockData(blockBuffer, i) 368 | val blockTypeId = blockFactory.getBlockTypeId(block.w) 369 | val blockType = blockFactory.getBlockType(blockTypeId) 370 | // Block has ambient lightning 371 | if (blockType.isTransparent && block.ambient > 1) { 372 | 373 | // Find all adjacent blocks and add them for ambient flooding 374 | for (adj <- position.getAdjacent) { 375 | val adjChunk = ChunkPosition(adj) 376 | val ambient = if (position.getY > adj.getY && block.ambient == LightLevel.FULL_ENCODING) { 377 | block.ambient 378 | } else { 379 | block.ambient - 1 380 | } 381 | if (adjChunk == chunk) { 382 | ambientFlood += AmbientLightFlood(adj, position, ambient) 383 | } else { 384 | val s = ambientFloodOthers.getOrElseUpdate(adjChunk, mutable.Set[AmbientLightFlood]()) 385 | s += AmbientLightFlood(adj, position, ambient) 386 | } 387 | } 388 | } 389 | } 390 | return floodAmbientLight(FloodAmbientLight(chunk, ambientFlood.toSet), ambientFloodOthers, db) 391 | } 392 | 393 | // Helper method to flood lights received via FloodLight message 394 | def floodLight(update: FloodLight, db: ActorRef)(implicit blockFactory: BlockFactory, 395 | blockBuffer: Array[Byte]): Int = { 396 | return floodLight(update, mutable.HashMap[ChunkPosition, mutable.Set[LightFlood]](), db) 397 | } 398 | 399 | // This function propagates light by flooding using a BFS 400 | def floodLight(update: FloodLight, buffer: mutable.HashMap[ChunkPosition, mutable.Set[LightFlood]], db: ActorRef)( 401 | implicit blockFactory: BlockFactory, 402 | blockBuffer: Array[Byte]): Int = { 403 | 404 | var bu = 0 405 | 406 | // The BFS queue 407 | val queue = mutable.Queue[LightFlood]() 408 | queue ++= update.update 409 | 410 | val chunk = update.chunk 411 | 412 | // The BFS 413 | while (!queue.isEmpty) { 414 | val f = queue.dequeue 415 | val i = ChunkData.index(chunk, f.position) 416 | val a = BlockData(blockBuffer, i) 417 | val aType = blockFactory.getBlockType(blockFactory.getBlockTypeId(a.w)) 418 | 419 | // If the block has a lower light level than what is to be flooded 420 | // update it to the new level and continue flooding 421 | if (aType.isTransparent && a.light < f.light) { 422 | bu = bu + 1 423 | 424 | a.copy(light = f.light, red = f.red, green = f.green, blue = f.blue).write(blockBuffer, i) 425 | 426 | // If the flood light level is bigger than 1 continue flooding 427 | if (f.light > 1) { 428 | for (adj <- f.position.getAdjacent) { 429 | if (adj != f.from) { 430 | val adjChunk = ChunkPosition(adj) 431 | if (chunk == adjChunk) { 432 | // Add block to flood in this chunk 433 | queue.enqueue(LightFlood(adj, f.position, f.light - 1, f.red, f.green, f.blue)) 434 | } else { 435 | // Add block to flood in other chunk 436 | val set = buffer.getOrElseUpdate(adjChunk, mutable.Set[LightFlood]()) 437 | set += LightFlood(adj, f.position, f.light - 1, f.red, f.green, f.blue) 438 | } 439 | } 440 | } 441 | } 442 | } 443 | } 444 | 445 | // Send messages to flood all other chunks 446 | buffer foreach { 447 | case (chunk, set) => 448 | db ! FloodLight(chunk, set.toSet) 449 | } 450 | 451 | return bu 452 | } 453 | 454 | // Helper method to flood ambient light received via FloodAmbientLight message 455 | def floodAmbientLight(update: FloodAmbientLight, db: ActorRef)(implicit blockFactory: BlockFactory, 456 | blockBuffer: Array[Byte]): Int = { 457 | return floodAmbientLight(update, mutable.HashMap[ChunkPosition, mutable.Set[AmbientLightFlood]](), db) 458 | } 459 | 460 | def floodAmbientLight(update: FloodAmbientLight, 461 | buffer: mutable.HashMap[ChunkPosition, mutable.Set[AmbientLightFlood]], 462 | db: ActorRef)(implicit blockFactory: BlockFactory, blockBuffer: Array[Byte]): Int = { 463 | 464 | var bu = 0 465 | 466 | // The BFS queue 467 | val queue = mutable.Queue[AmbientLightFlood]() 468 | queue ++= update.update 469 | 470 | val chunk = update.chunk 471 | 472 | // The BFS 473 | while (!queue.isEmpty) { 474 | val f = queue.dequeue 475 | val i = ChunkData.index(chunk, f.position) 476 | val a = BlockData(blockBuffer, i) 477 | val aTypeId = blockFactory.getBlockTypeId(a.w) 478 | val aType = blockFactory.getBlockType(aTypeId) 479 | 480 | // If the block has a lower light level than what is to be flooded 481 | // update it to the new level and continue flooding 482 | if (aType.isTransparent && a.ambient < f.ambient && aTypeId.getNamespace != "org/konstructs/space") { 483 | bu = bu + 1 484 | a.copy(ambient = f.ambient).write(blockBuffer, i) 485 | 486 | // If the flood light level is bigger than 1 continue flooding 487 | if (f.ambient > 1) { 488 | for (adj <- f.position.getAdjacent) { 489 | if (adj != f.from) { 490 | val adjChunk = ChunkPosition(adj) 491 | val ambient = if (f.position.getY > adj.getY && f.ambient == LightLevel.FULL_ENCODING) { 492 | f.ambient 493 | } else { 494 | f.ambient - 1 495 | } 496 | if (chunk == adjChunk) { 497 | // Add block to flood in this chunk 498 | queue.enqueue(AmbientLightFlood(adj, f.position, ambient)) 499 | } else { 500 | // Add block to flood in other chunk 501 | val set = buffer.getOrElseUpdate(adjChunk, mutable.Set[AmbientLightFlood]()) 502 | set += AmbientLightFlood(adj, f.position, ambient) 503 | } 504 | } 505 | } 506 | } 507 | } 508 | } 509 | 510 | // Send messages to flood all other chunks 511 | buffer foreach { 512 | case (chunk, set) => 513 | db ! FloodAmbientLight(chunk, set.toSet) 514 | } 515 | 516 | return bu 517 | } 518 | 519 | def refreshChunkAbove(chunk: ChunkPosition): RefreshAmbientLight = { 520 | val maxChunk = chunk.copy(k = chunk.k + 1) 521 | val set = (for (x <- 0 until Db.ChunkSize; 522 | z <- 0 until Db.ChunkSize) yield { 523 | maxChunk.position(x, 0, z) 524 | }) toSet 525 | 526 | RefreshAmbientLight(maxChunk, set) 527 | } 528 | 529 | } 530 | --------------------------------------------------------------------------------