Skip to content

Commit

Permalink
Moved most chunk loading logic to the server
Browse files Browse the repository at this point in the history
  • Loading branch information
Martomate committed Jul 27, 2024
1 parent 33b7434 commit 9b8db19
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 92 deletions.
126 changes: 74 additions & 52 deletions client/src/main/scala/hexacraft/client/GameClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import hexacraft.util.{Channel, Result, TickableTimer}
import hexacraft.world.*
import hexacraft.world.block.{Block, BlockSpec, BlockState}
import hexacraft.world.chunk.{Chunk, ChunkColumnData, ChunkColumnHeightMap, ChunkColumnTerrain}
import hexacraft.world.coord.{BlockCoords, BlockRelWorld, CoordUtils, CylCoords, NeighborOffsets}
import hexacraft.world.coord.{BlockCoords, BlockRelWorld, ChunkRelWorld, CoordUtils, CylCoords, NeighborOffsets}

import com.martomate.nbt.Nbt
import org.joml.{Matrix4f, Vector2f, Vector3d, Vector3f}
Expand Down Expand Up @@ -480,16 +480,18 @@ class GameClient(
.sum
}

private val prio = ChunkLoadingPrioritizer(world.renderDistance)

def tick(ctx: TickContext): Unit = {
try {
if socket.isDisconnected then {
logout()
return
}

val playerNbt = socket.sendPacketAndWait(NetworkPacket.GetPlayerState)
val Seq(playerNbt, worldEventsNbtPacket, worldLoadingEventsNbt) =
socket.sendMultiplePacketsAndWait(
Seq(NetworkPacket.GetPlayerState, NetworkPacket.GetEvents, NetworkPacket.GetWorldLoadingEvents(5))
)

val syncedPlayer = Player.fromNBT(player.id, playerNbt.asInstanceOf[Nbt.MapTag])
// println(syncedPlayer.position)
if player.position.sub(syncedPlayer.position, new Vector3d).length() > 10.0 then {}
Expand All @@ -504,9 +506,8 @@ class GameClient(
// player.rotation.add(rotationDiff.mul(0.1))
player.flying = syncedPlayer.flying

val worldEventsNbtPacket = socket.sendPacketAndWait(NetworkPacket.GetEvents).asMap.get

val blockUpdatesNbtList = worldEventsNbtPacket.getList("block_updates").getOrElse(Seq()).map(_.asMap.get)
val worldEventsNbt = worldEventsNbtPacket.asMap.get
val blockUpdatesNbtList = worldEventsNbt.getList("block_updates").getOrElse(Seq()).map(_.asMap.get)
val blockUpdates =
for u <- blockUpdatesNbtList
yield {
Expand All @@ -521,7 +522,7 @@ class GameClient(
world.setBlock(coords, blockState)
}

val entityEventsNbt = worldEventsNbtPacket.getMap("entity_events").get
val entityEventsNbt = worldEventsNbt.getMap("entity_events").get
val entityEventIds = entityEventsNbt.getList("ids").get.map(_.asInstanceOf[Nbt.StringTag].v)
val entityEventData = entityEventsNbt.getList("events").get.map(_.asMap.get)
val entityEvents = for (id, eventNbt) <- entityEventIds.zip(entityEventData) yield {
Expand All @@ -539,54 +540,55 @@ class GameClient(

updateSoundListener()

prio.tick(PosAndDir.fromCameraView(camera.view))

var chunksToLoad = 5
while chunksToLoad > 0 do {
prio.nextAddableChunk match {
case Some(chunkCoords) =>
var success = true
val columnCoords = chunkCoords.getColumnRelWorld
if world.getColumn(columnCoords).isEmpty then {
val columnNbt = socket.sendPacketAndWait(NetworkPacket.LoadColumnData(columnCoords))
if columnNbt != Nbt.emptyMap then {
val column = ChunkColumnTerrain.create(
ChunkColumnHeightMap.fromData2D(world.worldGenerator.getHeightmapInterpolator(columnCoords)),
Some(ChunkColumnData.fromNbt(columnNbt.asInstanceOf[Nbt.MapTag]))
)
world.setColumn(columnCoords, column)
} else {
success = false
}
}
if success && world.getChunk(chunkCoords).isEmpty then {
val chunkNbt = socket.sendPacketAndWait(NetworkPacket.LoadChunkData(chunkCoords))
if chunkNbt != Nbt.emptyMap then {
val chunk = Chunk.fromNbt(chunkNbt.asInstanceOf[Nbt.MapTag])
world.setChunk(chunkCoords, chunk)
} else {
success = false
}
}
if success then {
prio += chunkCoords
} else {
chunksToLoad = 0 // no need to try to load the same chunk again
}
case None =>
val worldLoadingEvents = worldLoadingEventsNbt.asMap.get
val loadedChunks = mutable.ArrayBuffer.empty[(ChunkRelWorld, Nbt)]
for e <- worldLoadingEvents.getList("chunks_loaded").getOrElse(Seq()) do {
val m = e.asMap.get
val coords = m.getLong("coords", -1L)
val data = m.getMap("data")
if coords != -1L && data.isDefined then {
loadedChunks += ChunkRelWorld(coords) -> data.get
}
}
val unloadedChunks = mutable.ArrayBuffer.empty[ChunkRelWorld]
for e <- worldLoadingEvents.getList("chunks_unloaded").getOrElse(Seq()) do {
e match {
case Nbt.LongTag(coords) =>
unloadedChunks += ChunkRelWorld(coords)
case _ =>
}
chunksToLoad -= 1
}

var chunksToUnload = 6
while chunksToUnload > 0 do {
prio.nextRemovableChunk match {
case Some(chunkCoords) =>
world.removeChunk(chunkCoords)
prio -= chunkCoords
case None =>
for (chunkCoords, chunkNbt) <- loadedChunks do {
var success = true
val columnCoords = chunkCoords.getColumnRelWorld
if world.getColumn(columnCoords).isEmpty then {
val columnNbt = socket.sendPacketAndWait(NetworkPacket.LoadColumnData(columnCoords))
if columnNbt != Nbt.emptyMap then {
val column = ChunkColumnTerrain.create(
ChunkColumnHeightMap.fromData2D(world.worldGenerator.getHeightmapInterpolator(columnCoords)),
Some(ChunkColumnData.fromNbt(columnNbt.asInstanceOf[Nbt.MapTag]))
)
world.setColumn(columnCoords, column)
} else {
success = false
}
}
if success && world.getChunk(chunkCoords).isEmpty then {
if chunkNbt != Nbt.emptyMap then {
val chunk = Chunk.fromNbt(chunkNbt.asInstanceOf[Nbt.MapTag])
world.setChunk(chunkCoords, chunk)
} else {
success = false
}
}
if !success then {
println("Client got chunk from server, but was not ready to handle it")
}
chunksToUnload -= 1
}

for chunkCoords <- unloadedChunks do {
world.removeChunk(chunkCoords)
}

val playerCoords = CoordUtils.approximateIntCoords(CylCoords(player.position).toBlockCoords)
Expand Down Expand Up @@ -893,6 +895,26 @@ class GameClientSocket(serverIp: String, serverPort: Int) {
tag
}

def sendMultiplePacketsAndWait(packets: Seq[NetworkPacket]): Seq[Nbt] = {
for p <- packets do {
if !socket.send(p.serialize()) then {
val err = socket.errno()
throw new ZMQException("Could not send message", err)
}
}

for _ <- packets.indices yield {
val response = socket.recv(0)
if response == null then {
val err = socket.errno()
throw new ZMQException("Could not receive message", err)
}

val (_, tag) = Nbt.fromBinary(response)
tag
}
}

def close(): Unit = {
context.synchronized {
context.close()
Expand Down
20 changes: 20 additions & 0 deletions common/src/main/scala/hexacraft/util/SeqUtils.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package hexacraft.util

import java.util.Random
import scala.collection.mutable

object SeqUtils {
def whileSome[T](maxCount: Int, maker: => Option[T])(taker: T => Any): Unit = {
Expand All @@ -25,4 +26,23 @@ object SeqUtils {
arr(idx) = temp
}
}

/** @return a new sequence taking elements from the input lists in a round-robin order */
def roundRobin[T](lists: Seq[Seq[T]]): Seq[T] = {
val result: mutable.ArrayBuffer[T] = mutable.ArrayBuffer.empty

val its = lists.map(_.iterator)
var left = true
while left do {
left = false
for it <- its do {
if it.hasNext then {
left = true
result += it.next
}
}
}

result.toSeq
}
}
10 changes: 10 additions & 0 deletions game/src/main/scala/hexacraft/game/NetworkPacket.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ enum NetworkPacket {

case GetPlayerState
case GetEvents
case GetWorldLoadingEvents(maxChunksToLoad: Int)

case PlayerRightClicked
case PlayerLeftClicked
Expand Down Expand Up @@ -74,6 +75,9 @@ object NetworkPacket {
NetworkPacket.GetPlayerState
case "get_events" =>
NetworkPacket.GetEvents
case "get_world_loading_events" =>
val maxChunksToLoad = root.getShort("max_chunks", 1)
NetworkPacket.GetWorldLoadingEvents(maxChunksToLoad)
case "right_mouse_clicked" =>
NetworkPacket.PlayerRightClicked
case "left_mouse_clicked" =>
Expand Down Expand Up @@ -115,6 +119,7 @@ object NetworkPacket {
case NetworkPacket.LoadWorldData => "load_world_data"
case NetworkPacket.GetPlayerState => "get_player_state"
case NetworkPacket.GetEvents => "get_events"
case NetworkPacket.GetWorldLoadingEvents(_) => "get_world_loading_events"
case NetworkPacket.PlayerRightClicked => "right_mouse_clicked"
case NetworkPacket.PlayerLeftClicked => "left_mouse_clicked"
case NetworkPacket.PlayerToggledFlying => "toggle_flying"
Expand Down Expand Up @@ -173,6 +178,11 @@ object NetworkPacket {
"args" -> Nbt.ListTag(args.map(arg => Nbt.StringTag(arg)))
)
)

case NetworkPacket.GetWorldLoadingEvents(maxChunksToLoad) =>
Nbt.makeMap(
"max_chunks" -> Nbt.ShortTag(maxChunksToLoad.toShort)
)
}

tag.toBinary(name)
Expand Down
28 changes: 28 additions & 0 deletions game/src/main/scala/hexacraft/world/loader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,34 @@ class ChunkLoadingPrioritizer(maxDist: Double)(using CylinderSize) {
}
}

def nextAddableChunks(n: Int): Seq[ChunkRelWorld] = {
if n == 1 then {
return nextAddableChunk.toSeq
} else if n < 1 then {
return Seq()
}

while addableChunks.nonEmpty && !edge.canLoad(addableChunks.head) do {
addableChunks.dequeue()
}

val result: mutable.ArrayBuffer[ChunkRelWorld] = mutable.ArrayBuffer.empty

if addableChunks.nonEmpty then {
result ++= addableChunks.take(n)
} else {
val startCoords = CoordUtils.approximateChunkCoords(origin.pos)
if !edge.isLoaded(startCoords) then {
result += startCoords
result ++= addableChunks.take(n - 1)
} else {
result ++= addableChunks.take(n)
}
}

result.filter(coords => distSq(origin, coords) <= maxDistSqInBlocks).toSeq
}

def nextRemovableChunk: Option[ChunkRelWorld] = {
while removableChunks.nonEmpty && !edge.onEdge(removableChunks.head) do {
removableChunks.dequeue()
Expand Down
69 changes: 67 additions & 2 deletions server/src/main/scala/hexacraft/server/GameServer.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package hexacraft.server

import hexacraft.game.{GameKeyboard, NetworkPacket, PlayerInputHandler, PlayerPhysicsHandler}
import hexacraft.util.Result
import hexacraft.util.{Result, SeqUtils}
import hexacraft.world.*
import hexacraft.world.block.{Block, BlockState}
import hexacraft.world.chunk.ChunkColumnData
Expand Down Expand Up @@ -94,6 +94,8 @@ class GameServer(isOnline: Boolean, port: Int, worldProvider: WorldProvider, wor
val PlayerData(player, entity, camera) = p
val playerCoords = CoordUtils.approximateIntCoords(CylCoords(player.position).toBlockCoords)

chunksLoadedPerPlayer.get(player.id).foreach(_.tick(PosAndDir.fromCameraView(camera.view)))

if world.getChunk(playerCoords.getChunkRelWorld).isDefined then {
val maxSpeed = playerInputHandler.determineMaxSpeed(p.pressedKeys)
val isInFluid = playerEffectiveViscosity(player) > Block.Air.viscosity.toSI * 2
Expand Down Expand Up @@ -126,7 +128,14 @@ class GameServer(isOnline: Boolean, port: Int, worldProvider: WorldProvider, wor
entity.motion.flying = player.flying
}

val tickResult = world.tick(players.values.map(_.camera).toSeq)
val tickResult = world.tick(
players.values.map(_.camera).toSeq,
SeqUtils.roundRobin(chunksLoadedPerPlayer.values.map(_.nextAddableChunks(15)).toSeq),
chunksLoadCount.filter((coords, count) => count == 0).keys.map(ChunkRelWorld(_)).toSeq
)
for coords <- tickResult.chunksRemoved do {
chunksLoadCount.remove(coords.value)
}

for coords <- tickResult.blocksUpdated do {
val blockState = world.getBlock(coords)
Expand Down Expand Up @@ -304,6 +313,9 @@ class GameServer(isOnline: Boolean, port: Int, worldProvider: WorldProvider, wor
println(s"Stopped server")
}

private val chunksLoadedPerPlayer: mutable.HashMap[UUID, ChunkLoadingPrioritizer] = mutable.HashMap.empty
private val chunksLoadCount = mutable.LongMap.empty[Int]

private def handlePacket(clientId: Long, packet: NetworkPacket, socket: ZMQ.Socket): Option[Nbt.MapTag] = {
import NetworkPacket.*

Expand Down Expand Up @@ -389,6 +401,14 @@ class GameServer(isOnline: Boolean, port: Int, worldProvider: WorldProvider, wor
worldProvider.savePlayerData(player.toNBT, player.id)
world.removeEntity(playerData.entity)
players.remove(clientId)
chunksLoadedPerPlayer.remove(player.id) match {
case Some(prio) =>
while prio.nextRemovableChunk.isDefined do {
val coords = prio.popChunkToRemove().get
chunksLoadCount(coords.value) -= 1
}
case None =>
}
None
case GetWorldInfo =>
val info = worldProvider.getWorldInfo
Expand Down Expand Up @@ -457,6 +477,51 @@ class GameServer(isOnline: Boolean, port: Int, worldProvider: WorldProvider, wor
.withField("entity_events", Nbt.makeMap("ids" -> Nbt.ListTag(ids), "events" -> Nbt.ListTag(events)))

Some(response)
case GetWorldLoadingEvents(maxChunksToLoad) =>
val prio = chunksLoadedPerPlayer.getOrElseUpdate(player.id, ChunkLoadingPrioritizer(world.renderDistance))

val loadedChunks = mutable.ArrayBuffer.empty[(ChunkRelWorld, Nbt)]
val unloadedChunks = mutable.ArrayBuffer.empty[ChunkRelWorld]

var chunksToLoad = maxChunksToLoad
while chunksToLoad > 0 do {
chunksToLoad -= 1

prio.nextAddableChunk.flatMap(coords => world.getChunk(coords).map(coords -> _)) match {
case Some(coords -> chunk) =>
loadedChunks += ((coords, chunk.toNbt))
prio += coords
chunksLoadCount(coords.value) = chunksLoadCount.getOrElse(coords.value, 0) + 1
case None =>
chunksToLoad = 0
}
}

var moreChunksToUnload = true
while moreChunksToUnload do {
prio.popChunkToRemove() match {
case Some(coords) =>
unloadedChunks += coords
chunksLoadCount(coords.value) -= 1
case None =>
moreChunksToUnload = false
}
}

Some(
Nbt.makeMap(
"chunks_loaded" -> Nbt.ListTag(
loadedChunks
.map((coords, data) => Nbt.makeMap("coords" -> Nbt.LongTag(coords.value), "data" -> data))
.toSeq
),
"chunks_unloaded" -> Nbt.ListTag(
unloadedChunks
.map(coords => Nbt.LongTag(coords.value))
.toSeq
)
)
)
case PlayerRightClicked =>
performRightMouseClick(player, playerCamera)
None
Expand Down
Loading

0 comments on commit 9b8db19

Please sign in to comment.