From 0c21d82258ed00b51870970bbfc4b42b63a75e01 Mon Sep 17 00:00:00 2001 From: Cola_Colin Date: Sun, 26 Jan 2014 23:50:29 +0100 Subject: [PATCH] work on matchmaking --- src/main/resources/props/default.props | 2 + src/main/scala/bootstrap/liftweb/Boot.scala | 18 +- .../info/nanodesu/lib/RefreshRunner.scala | 10 +- .../info/nanodesu/rest/LadderService.scala | 221 ++++++++++++++++++ 4 files changed, 242 insertions(+), 9 deletions(-) create mode 100644 src/main/scala/info/nanodesu/rest/LadderService.scala diff --git a/src/main/resources/props/default.props b/src/main/resources/props/default.props index 93f7192..c0cf40e 100644 --- a/src/main/resources/props/default.props +++ b/src/main/resources/props/default.props @@ -14,6 +14,8 @@ liveGameThreshold=10000 listGamesPageSize = 35 +allowStarOrigin = true + jslibs=jquery jqueryui knockout kodeferred globalize d3 d3layout rickshaw csslibs=jqueryuicss rickshawcss diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala index 87ed1bf..ea0216a 100644 --- a/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -31,6 +31,7 @@ import info.nanodesu.lib.db.CookieBox import info.nanodesu.model.db.collectors.stats.RuntimeInfoCollector import info.nanodesu.model.db.collectors.stats.ExtraNumbersCollector import info.nanodesu.snippet.lib.IFrameSnip +import info.nanodesu.rest.LadderService // see: http://cookbook.liftweb.net/#InstallAndRunning /** @@ -42,13 +43,15 @@ import info.nanodesu.snippet.lib.IFrameSnip class Boot extends Loggable { def boot { - LiftRules.supplimentalHeaders = s => s.addHeaders( - List(HTTPParam("X-Lift-Version", LiftRules.liftVersion), - HTTPParam("Access-Control-Allow-Origin", "*"), - HTTPParam("Access-Control-Allow-Credentials", "true"), - HTTPParam("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS"), - HTTPParam("Access-Control-Allow-Headers", "WWW-Authenticate,Keep-Alive,User-Agent,X-Requested-With,Cache-Control,Content-Type"))) - + // in production this is done by the apache in front of the jetty + if (Props.getBool("allowStarOrigin", false)) { + LiftRules.supplimentalHeaders = s => s.addHeaders( + List(HTTPParam("X-Lift-Version", LiftRules.liftVersion), + HTTPParam("Access-Control-Allow-Origin", "*"), + HTTPParam("Access-Control-Allow-Credentials", "true"), + HTTPParam("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS"), + HTTPParam("Access-Control-Allow-Headers", "WWW-Authenticate,Keep-Alive,User-Agent,X-Requested-With,Cache-Control,Content-Type"))) + } // no need for jmx access to c3p0 System.getProperties().setProperty("com.mchange.v2.c3p0.management.ManagementCoordinator", "com.mchange.v2.c3p0.management.NullManagementCoordinator") @@ -75,6 +78,7 @@ class Boot extends Loggable { PlayerHighscoreCollector.init() MostPlaytimesCollector.init() ExtraNumbersCollector.init() + LadderService.initService() // Build SiteMap val entries = List( diff --git a/src/main/scala/info/nanodesu/lib/RefreshRunner.scala b/src/main/scala/info/nanodesu/lib/RefreshRunner.scala index 57450c2..de9663c 100644 --- a/src/main/scala/info/nanodesu/lib/RefreshRunner.scala +++ b/src/main/scala/info/nanodesu/lib/RefreshRunner.scala @@ -37,13 +37,19 @@ trait RefreshRunner extends Loggable { LiftRules.unloadHooks.append(() => { killReloading() }) } + def shouldLog = true + private def doWork() = { while (containerIsAlive) { if (lastUpdate + RUN_INTERVAL < System.currentTimeMillis()) { try { - logger info "starting task "+processName + if (shouldLog) { + logger info "starting task "+processName + } runQuery() - logger info "completed task "+processName + if (shouldLog) { + logger info "completed task "+processName + } } catch { case th: Throwable => { diff --git a/src/main/scala/info/nanodesu/rest/LadderService.scala b/src/main/scala/info/nanodesu/rest/LadderService.scala new file mode 100644 index 0000000..e68b4f4 --- /dev/null +++ b/src/main/scala/info/nanodesu/rest/LadderService.scala @@ -0,0 +1,221 @@ +package info.nanodesu.rest + +import scala.collection.mutable + +import info.nanodesu.lib.RefreshRunner +import net.liftweb._ +import net.liftweb.common.Box +import net.liftweb.common.Loggable +import net.liftweb.http._ +import net.liftweb.http.rest._ +import net.liftweb.json.Extraction +import net.liftweb.json.JsonAST.JValue +import net.liftweb.util.Helpers._ +import net.liftweb.util.Helpers + +// Quicky, old, horrible code +object LadderService extends RestHelper with Loggable with RefreshRunner{ + + def initService(): Unit = { + LiftRules.statelessDispatch append LadderService + init(); + } + + override val firstLoadDelay = 5*1000 + override val RUN_INTERVAL = 3*1000 + val processName = "ladder service" + + override def shouldLog = false + + def runQuery() = { + mutex synchronized { + val foo = gameCreationTimes.toMap + for (x <- foo.filterKeys(System.currentTimeMillis() - _ > 60 * 1000)) { + gameCreationTimes retain ((k, v) => (v != x._2)) + registeredGames remove x._2 + val lobbyId = registeredLobbyIdByHost remove x._2 + for (gid <- lobbyId) { + clientsReadyByLobby remove gid + } + } + + val rPlay = registeredPlayers.toMap + for (x <- rPlay.filter(System.currentTimeMillis() - _._2 > 10 * 1000)) { + registeredPlayers remove x._1 + } + + if (!registeredPlayers.isEmpty) { + logger info "registered Players are => " + registeredPlayers + } + if (!registeredGames.isEmpty) { + logger info "registered Games are => " + registeredGames + } + if (!registeredLobbyIdByHost.isEmpty) { + logger info "registered Lobby id for hosts => " + registeredLobbyIdByHost + } + if (!clientsReadyByLobby.isEmpty) { + logger info "clients ready for lobby => " + clientsReadyByLobby + } + if (!gameCreationTimes.isEmpty) { + logger info "game creation times => " + gameCreationTimes + } + } + } + + val mutex = new Object() + + val clientsReadyByLobby = mutable.Set[String]() + val registeredLobbyIdByHost = mutable.Map[String, String]() + val registeredGames = mutable.Map[String, String]() + val gameCreationTimes = mutable.Map[Long, String]() + val registeredPlayers = mutable.Map[String, Long]() + + case class GameInfo(serverCreated: Boolean, hasTimeOut: Boolean, lobbyId: String) + serve { + case "pollGameId" :: Nil Get _ => + val uberName = S.param("ubername") + + for (uN <- uberName) yield { + logger info "pollGameId for " + uN + + var timeout = false; + + val other = mutex synchronized { + val r = for (x <- registeredGames if x._2 == uN) yield { + x._1 + } + + if (r.nonEmpty) { + logger info "get " + r.head + " from " + registeredLobbyIdByHost + + (registeredLobbyIdByHost get r.head).map((r.head, _)) getOrElse ("", "") + } else { + timeout = true; + ("", "") + } + } + + val inf = GameInfo(!other._1.isEmpty(), timeout, other._2) + + logger info inf + " polled for " + uberName + + Extraction decompose inf + } + } + + case class NameMessage(uber_name: String, game_id: String) + object NameMessage { + def apply(in: JValue): Box[NameMessage] = Helpers.tryo(in.extract[NameMessage]) + def unapply(in: JValue): Option[NameMessage] = apply(in) + } + + serve { + case "unregister" :: Nil JsonPost NameMessage(data) -> _ => { + mutex synchronized { + registeredPlayers remove data.uber_name + registeredGames retain ((k, v) => (k != data.uber_name && v != data.uber_name)) + + logger info "unregistered " + data.uber_name + } + + OkResponse() + } + } + + serve { + case "register" :: Nil JsonPost NameMessage(data) -> _ => { + mutex synchronized { + registeredPlayers put (data.uber_name, System.currentTimeMillis()) + + logger info "registered " + data.uber_name + + if (registeredPlayers.size >= 2) { + val iter = registeredPlayers.iterator + val pA = iter.next._1 + val pB = iter.next._1 + + registeredPlayers remove pA + registeredPlayers remove pB + + logger info "registered game " + pA + " vs " + pB + + registeredGames put (pA, pB) + gameCreationTimes put (System.currentTimeMillis(), pA) + } + } + + OkResponse() + } + } + + case class HasGameResponse(hasGame: Boolean, isHost: Boolean) + + serve { + case "hasGame" :: Nil JsonPost NameMessage(data) -> _ => { + val r = mutex synchronized { + val isHost = registeredGames.keySet.contains(data.uber_name) + val isPlayer = registeredGames.values.toList.contains(data.uber_name) + (isHost, isPlayer) + } + + val inf = HasGameResponse(r._1 || r._2, r._1) + + logger info "has game response for " + data.uber_name + " is => " + inf + + Extraction decompose inf + } + } + + serve { + case "gameHosted" :: Nil JsonPost NameMessage(data) -> _ => { + mutex synchronized { + registeredLobbyIdByHost put (data.uber_name, data.game_id) + + logger info "gameHosted for " + data + } + + OkResponse() + } + } + + case class ShouldStartResponse(shouldStart: Boolean, hasTimeOut: Boolean) + serve { + case "shouldStartServer" :: Nil JsonPost NameMessage(data) -> _ => { + val resp = ShouldStartResponse (mutex synchronized (clientsReadyByLobby contains data.game_id), !registeredGames.contains(data.uber_name)) + + logger info "answer to should start server for " + data.uber_name + " is => " + resp + + if (resp.shouldStart) { + mutex synchronized { + clientsReadyByLobby remove data.game_id + registeredLobbyIdByHost remove data.uber_name + registeredGames remove data.uber_name + gameCreationTimes retain ((k,v) => (v != data.uber_name)) + + logger info "===done for " + data.game_id+"===" + } + } + + Extraction decompose resp + } + } + + serve { + case "readyToStart" :: Nil JsonPost NameMessage(data) -> _ => { + + if (data.game_id.isEmpty()) { + logger info "got empty game_id from " + data.uber_name + BadResponse() + } else { + mutex synchronized { + + logger info "rdy to start note from " + data + + clientsReadyByLobby add data.game_id + } + + OkResponse() + } + } + } +} \ No newline at end of file