-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9f457ce
commit 0ae5a85
Showing
2 changed files
with
106 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package service | ||
|
||
import play.api.Logger | ||
import play.api.libs.json.* | ||
|
||
import java.net.{InetSocketAddress, SocketAddress} | ||
import java.nio.ByteBuffer | ||
import java.nio.channels.DatagramChannel | ||
import java.nio.charset.StandardCharsets | ||
import java.time.{Duration, Instant} | ||
import java.util.concurrent.ConcurrentHashMap | ||
import scala.jdk.CollectionConverters.{CollectionHasAsScala, EnumerationHasAsScala} | ||
import scala.math.Ordering.Implicits.infixOrderingOps | ||
import scala.util.Random | ||
|
||
sealed trait ResponseMessage | ||
final case class HostResponseMessage(host: String, port: Int) extends ResponseMessage | ||
final case class JoinResponseMessage(host: String, port: Int, playerCount: Int) extends ResponseMessage | ||
|
||
object ResponseMessage { | ||
implicit val hostResponseFormat: Format[HostResponseMessage] = Json.format | ||
implicit val joinResponseFormat: Format[JoinResponseMessage] = Json.format | ||
} | ||
|
||
sealed trait Message | ||
final case class HostMessage() extends Message | ||
final case class HostingMessage(host: String, port: Int, playerCount: Int) extends Message | ||
final case class JoinMessage() extends Message | ||
|
||
object Message { | ||
implicit val hostFormat: Format[HostMessage] = Json.format | ||
implicit val hostingFormat: Format[HostingMessage] = Json.format | ||
implicit val joinFormat: Format[JoinMessage] = Json.format | ||
implicit object MessageFormat extends Format[Message] { | ||
override def reads(json: JsValue): JsResult[Message] = json match | ||
case JsObject(obj) => | ||
obj.get("type") match | ||
case Some(JsString("HOST")) => hostFormat.reads(json) | ||
case Some(JsString("HOSTING")) => hostingFormat.reads(json) | ||
case Some(JsString("JOIN")) => joinFormat.reads(json) | ||
case t => JsError(s"Unknown type: $t") | ||
case _ => JsError("Messages must be string!") | ||
|
||
override def writes(o: Message): JsValue = o match | ||
case m: HostMessage => hostFormat.writes(m) | ||
case m: HostingMessage => hostingFormat.writes(m) | ||
case m: JoinMessage => joinFormat.writes(m) | ||
} | ||
} | ||
|
||
final case class HostingHost(host: String, port: Int, playerCount: Int, lastHosted: Instant) | ||
|
||
class Ld56MasterServer { | ||
private val logger = Logger(classOf[Ld56MasterServer]) | ||
private val channel = DatagramChannel.open() | ||
channel.bind(InetSocketAddress(39875)) | ||
private val readBuffer = ByteBuffer.allocateDirect(8196) | ||
private val writeBuffer = ByteBuffer.allocateDirect(8196) | ||
private val hostMap = ConcurrentHashMap[String, HostingHost]() | ||
|
||
private val ioThread = Thread.ofVirtual().start(() => { | ||
while (!Thread.interrupted()) { | ||
readBuffer.clear() | ||
val sourceAddress = channel.receive(readBuffer).asInstanceOf[InetSocketAddress] | ||
readBuffer.flip() | ||
if (readBuffer.remaining() > 0) { | ||
val stringContent = StandardCharsets.UTF_8.decode(readBuffer).toString | ||
try | ||
val message = Json.parse(stringContent).as[Message] | ||
processMessage(message, sourceAddress) | ||
catch | ||
case e: Exception => | ||
logger.error(s"Invalid message: $stringContent", e) | ||
} | ||
} | ||
}) | ||
|
||
CacheHelper | ||
|
||
private def sendMessage[T <: ResponseMessage](message: T, to: SocketAddress)(implicit writes: Writes[T]): Unit = { | ||
logger.info(s"Sending message to $to: $message") | ||
val responseBytes = Json.toBytes(Json.toJson(message)) | ||
channel.send(ByteBuffer.wrap(responseBytes), to) | ||
} | ||
|
||
private def processMessage(message: Message, sourceAddr: InetSocketAddress): Unit = { | ||
logger.info(s"Received message from $sourceAddr: $message") | ||
val now = Instant.now() | ||
message match | ||
case HostMessage() => | ||
sendMessage(HostResponseMessage(sourceAddr.getAddress.getHostAddress, sourceAddr.getPort), sourceAddr) | ||
case HostingMessage(host, port, playerCount) => | ||
val hostPort = s"$host:$port" | ||
hostMap.put(hostPort, HostingHost(host, port, playerCount, now)) | ||
case JoinMessage() => | ||
hostMap.entrySet().removeIf(entry => { | ||
entry.getValue.lastHosted < now.minus(Duration.ofSeconds(20)) | ||
}) | ||
val hosts = hostMap.values().asScala.toVector | ||
if hosts.nonEmpty then | ||
val randomHost = hosts(Random.nextInt(hosts.length)) | ||
|
||
sendMessage(JoinResponseMessage(randomHost.host, randomHost.port, 0), sourceAddr) | ||
} | ||
} |