Merge remote-tracking branch 'origin/timesealClock'
* origin/timesealClock: (21 commits) Fix outOfTime messages from spectators fix double reporting of player lag update scalachess Update Player.scala Update socket.js Minor syntax update apply lag grace on move update scalachess Always use performance.now Don't send 'd' key if empty fix timeseal bugs Fix compile error Update scalachess Don't use grace for self-outoftime reports Update socket.js Update scalachess Switch to base 36 for time Encode move time with radix Update scalachess Use client move time for lag computations ...
This commit is contained in:
commit
6adf49db68
|
@ -1 +1 @@
|
|||
Subproject commit e913f3179c8c81f89c5c7e7a0bf977a929f53c48
|
||||
Subproject commit f0329923c7018ddfb7a9d9ad1fc2f7f043f6ec8c
|
|
@ -80,11 +80,24 @@ object BinaryFormat {
|
|||
}
|
||||
|
||||
case class clock(start: Timestamp) {
|
||||
|
||||
def legacyElapsed(clock: Clock, color: Color) =
|
||||
clock.limit - clock.players(color).remaining
|
||||
|
||||
def computeRemaining(config: Clock.Config, legacyElapsed: Centis) =
|
||||
config.limit - legacyElapsed
|
||||
|
||||
// TODO: new binary clock format
|
||||
// - clock history
|
||||
// - berserk bits
|
||||
// - "real" elapsed
|
||||
// - lag stats
|
||||
|
||||
def write(clock: Clock): ByteArray = {
|
||||
Array(writeClockLimit(clock.limitSeconds), clock.incrementSeconds.toByte) ++
|
||||
writeSignedInt24(clock.whiteTime.centis) ++
|
||||
writeSignedInt24(clock.blackTime.centis) ++
|
||||
clock.timerOption.fold(Array.empty[Byte])(writeTimer)
|
||||
writeSignedInt24(legacyElapsed(clock, White).centis) ++
|
||||
writeSignedInt24(legacyElapsed(clock, Black).centis) ++
|
||||
clock.timer.fold(Array.empty[Byte])(writeTimer)
|
||||
}
|
||||
|
||||
def read(ba: ByteArray, whiteBerserk: Boolean, blackBerserk: Boolean): Color => Clock = color => {
|
||||
|
@ -101,27 +114,23 @@ object BinaryFormat {
|
|||
ia match {
|
||||
case Array(b1, b2, b3, b4, b5, b6, b7, b8, _*) => {
|
||||
val config = Clock.Config(readClockLimit(b1), b2)
|
||||
val whiteTime = Centis(readSignedInt24(b3, b4, b5))
|
||||
val blackTime = Centis(readSignedInt24(b6, b7, b8))
|
||||
timer.fold[Clock](
|
||||
PausedClock(
|
||||
config = config,
|
||||
color = color,
|
||||
whiteTime = whiteTime,
|
||||
blackTime = blackTime,
|
||||
whiteBerserk = whiteBerserk,
|
||||
blackBerserk = blackBerserk
|
||||
)
|
||||
)(t =>
|
||||
RunningClock(
|
||||
val legacyWhite = Centis(readSignedInt24(b3, b4, b5))
|
||||
val legacyBlack = Centis(readSignedInt24(b6, b7, b8))
|
||||
Clock(
|
||||
config = config,
|
||||
color = color,
|
||||
players = Color.Map(
|
||||
ClockPlayer(
|
||||
config = config,
|
||||
color = color,
|
||||
whiteTime = whiteTime,
|
||||
blackTime = blackTime,
|
||||
whiteBerserk = whiteBerserk,
|
||||
blackBerserk = blackBerserk,
|
||||
timer = t
|
||||
))
|
||||
berserk = whiteBerserk
|
||||
).setRemaining(computeRemaining(config, legacyWhite)),
|
||||
ClockPlayer(
|
||||
config = config,
|
||||
berserk = blackBerserk
|
||||
).setRemaining(computeRemaining(config, legacyBlack))
|
||||
),
|
||||
timer = timer
|
||||
)
|
||||
}
|
||||
case _ => sys error s"BinaryFormat.clock.read invalid bytes: ${ba.showBytes}"
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import chess.Color.{ White, Black }
|
|||
import chess.format.{ Uci, FEN }
|
||||
import chess.opening.{ FullOpening, FullOpeningDB }
|
||||
import chess.variant.{ Variant, Crazyhouse }
|
||||
import chess.{ History => ChessHistory, CheckCount, Castles, Board, MoveOrDrop, Pos, Game => ChessGame, Clock, Status, Color, Mode, PositionHash, UnmovedRooks, Centis }
|
||||
import chess.{ MoveMetrics, History => ChessHistory, CheckCount, Castles, Board, MoveOrDrop, Pos, Game => ChessGame, Clock, Status, Color, Mode, PositionHash, UnmovedRooks, Centis }
|
||||
import org.joda.time.DateTime
|
||||
|
||||
import lila.common.Sequence
|
||||
|
@ -189,7 +189,7 @@ case class Game(
|
|||
game: ChessGame,
|
||||
moveOrDrop: MoveOrDrop,
|
||||
blur: Boolean = false,
|
||||
lag: Option[Centis] = None
|
||||
moveMetrics: MoveMetrics = MoveMetrics()
|
||||
): Progress = {
|
||||
val (history, situation) = (game.board.history, game.situation)
|
||||
|
||||
|
@ -446,12 +446,12 @@ case class Game(
|
|||
|
||||
def drawn = finished && winner.isEmpty
|
||||
|
||||
def outoftime(playerLag: Color => Centis): Boolean =
|
||||
outoftimeClock(playerLag) || outoftimeCorrespondence
|
||||
def outoftime(withGrace: Boolean): Boolean =
|
||||
if (isCorrespondence) outoftimeCorrespondence else outoftimeClock(withGrace)
|
||||
|
||||
private def outoftimeClock(playerLag: Color => Centis): Boolean = clock ?? { c =>
|
||||
private def outoftimeClock(withGrace: Boolean): Boolean = clock ?? { c =>
|
||||
started && playable && (bothPlayersHaveMoved || isSimul) && {
|
||||
(!c.isRunning && !c.isInit) || c.outoftimeWithGrace(turnColor, playerLag(turnColor))
|
||||
(!c.isRunning && !c.isInit) || c.outOfTime(turnColor, withGrace)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ class BinaryClockTest extends Specification {
|
|||
isomorphism(c3) must_== c3
|
||||
|
||||
val c4 = clock.start
|
||||
isomorphism(c4).timerOption.get.value must beCloseTo(c4.timer.value, 10)
|
||||
isomorphism(c4).timer.get.value must beCloseTo(c4.timer.get.value, 10)
|
||||
|
||||
Clock(120, 60) |> { c =>
|
||||
isomorphism(c) must_== c
|
||||
|
|
|
@ -9,7 +9,7 @@ import scala.concurrent.duration.Duration
|
|||
import scala.concurrent.Promise
|
||||
|
||||
import chess.format.Uci
|
||||
import chess.{ Centis, Pos }
|
||||
import chess.Pos
|
||||
import Forecast.Step
|
||||
import lila.game.{ Pov, Game }
|
||||
import lila.hub.actorApi.map.Tell
|
||||
|
@ -55,7 +55,6 @@ final class ForecastApi(coll: Coll, roundMap: akka.actor.ActorSelection) {
|
|||
playerId = pov.playerId,
|
||||
uci = uci,
|
||||
blur = true,
|
||||
lag = Centis(0),
|
||||
promise = promise.some
|
||||
))
|
||||
saveSteps(pov, steps) >> promise.future
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package lila.round
|
||||
|
||||
import chess.format.{ Forsyth, FEN, Uci }
|
||||
import chess.{ Centis, Status, Color, MoveOrDrop }
|
||||
import chess.{ MoveMetrics, Centis, Status, Color, MoveOrDrop }
|
||||
|
||||
import actorApi.round.{ HumanPlay, DrawNo, TakebackNo, ForecastPlay }
|
||||
import akka.actor.ActorRef
|
||||
|
@ -19,7 +19,7 @@ private[round] final class Player(
|
|||
def human(play: HumanPlay, round: ActorRef)(pov: Pov)(implicit proxy: GameProxy): Fu[Events] = play match {
|
||||
case p @ HumanPlay(playerId, uci, blur, lag, promiseOption) => pov match {
|
||||
case Pov(game, color) if game playableBy color =>
|
||||
p.trace.segmentSync("applyUci", "logic")(applyUci(game, uci, blur, lag + humanLag)).prefixFailuresWith(s"$pov ")
|
||||
p.trace.segmentSync("applyUci", "logic")(applyUci(game, uci, blur, lag)).prefixFailuresWith(s"$pov ")
|
||||
.fold(errs => fufail(ClientError(errs.shows)), fuccess).flatMap {
|
||||
case (progress, moveOrDrop) =>
|
||||
p.trace.segment("save", "db")(proxy save progress) >>- {
|
||||
|
@ -49,8 +49,8 @@ private[round] final class Player(
|
|||
def fishnet(game: Game, uci: Uci, currentFen: FEN, round: ActorRef)(implicit proxy: GameProxy): Fu[Events] =
|
||||
if (game.playable && game.player.isAi) {
|
||||
if (currentFen == FEN(Forsyth >> game.toChess))
|
||||
if (game.outoftime(_ => fishnetLag)) finisher.outOfTime(game)
|
||||
else applyUci(game, uci, blur = false, lag = fishnetLag)
|
||||
if (game.outoftime(withGrace = true)) finisher.outOfTime(game)
|
||||
else applyUci(game, uci, blur = false, metrics = fishnetLag)
|
||||
.fold(errs => fufail(ClientError(errs.shows)), fuccess).flatMap {
|
||||
case (progress, moveOrDrop) =>
|
||||
proxy.save(progress) >>-
|
||||
|
@ -70,21 +70,18 @@ private[round] final class Player(
|
|||
else fuccess(round ! actorApi.round.ResignAi)
|
||||
}
|
||||
|
||||
private val clientLag = Centis(3)
|
||||
private val serverLag = Centis(1)
|
||||
private val humanLag = clientLag + serverLag
|
||||
private val fishnetLag = Centis(1) + serverLag
|
||||
private val fishnetLag = MoveMetrics()
|
||||
|
||||
private def applyUci(game: Game, uci: Uci, blur: Boolean, lag: Centis) = (uci match {
|
||||
case Uci.Move(orig, dest, prom) => game.toChess.apply(orig, dest, prom, lag) map {
|
||||
private def applyUci(game: Game, uci: Uci, blur: Boolean, metrics: MoveMetrics) = (uci match {
|
||||
case Uci.Move(orig, dest, prom) => game.toChess.apply(orig, dest, prom, metrics) map {
|
||||
case (ncg, move) => ncg -> (Left(move): MoveOrDrop)
|
||||
}
|
||||
case Uci.Drop(role, pos) => game.toChess.drop(role, pos, lag) map {
|
||||
case Uci.Drop(role, pos) => game.toChess.drop(role, pos, metrics) map {
|
||||
case (ncg, drop) => ncg -> (Right(drop): MoveOrDrop)
|
||||
}
|
||||
}).map {
|
||||
case (newChessGame, moveOrDrop) =>
|
||||
game.update(newChessGame, moveOrDrop, blur, lag.some) -> moveOrDrop
|
||||
game.update(newChessGame, moveOrDrop, blur, metrics) -> moveOrDrop
|
||||
}
|
||||
|
||||
private def notifyMove(moveOrDrop: MoveOrDrop, game: Game) {
|
||||
|
|
|
@ -2,7 +2,7 @@ package lila.round
|
|||
|
||||
import chess.format.Forsyth
|
||||
import chess.variant._
|
||||
import chess.{ Clock, Game => ChessGame, Board, Color => ChessColor, Castles }
|
||||
import chess.{ Game => ChessGame, Board, Color => ChessColor, Castles, Clock }
|
||||
import ChessColor.{ White, Black }
|
||||
|
||||
import lila.game.{ GameRepo, Game, Event, Progress, Pov, Source, AnonCookie, PerfPicker }
|
||||
|
|
|
@ -41,16 +41,6 @@ private[round] final class Round(
|
|||
|
||||
implicit val proxy = new GameProxy(gameId)
|
||||
|
||||
object lags { // player lag in centis
|
||||
var white = Centis(0)
|
||||
var black = Centis(0)
|
||||
def get(c: Color) = c.fold(white, black)
|
||||
def set(c: Color, v: Centis) {
|
||||
if (c.white) white = v
|
||||
else black = v
|
||||
}
|
||||
}
|
||||
|
||||
var takebackSituation = Round.TakebackSituation()
|
||||
|
||||
def process = {
|
||||
|
@ -62,9 +52,8 @@ private[round] final class Round(
|
|||
case p: HumanPlay =>
|
||||
p.trace.finishFirstSegment()
|
||||
handleHumanPlay(p) { pov =>
|
||||
if (pov.game outoftime lags.get) finisher.outOfTime(pov.game)
|
||||
if (pov.game.outoftime(withGrace = true)) finisher.outOfTime(pov.game)
|
||||
else {
|
||||
lags.set(pov.color, p.lag)
|
||||
reportNetworkLag(pov)
|
||||
player.human(p, self)(pov)
|
||||
}
|
||||
|
@ -120,8 +109,12 @@ private[round] final class Round(
|
|||
}
|
||||
}
|
||||
|
||||
case Outoftime => handle { game =>
|
||||
game.outoftime(lags.get) ?? finisher.outOfTime(game)
|
||||
case Outoftime(Some(playerId)) => handle(playerId) { pov =>
|
||||
pov.game.outoftime(withGrace = !pov.isMyTurn) ?? finisher.outOfTime(pov.game)
|
||||
}
|
||||
|
||||
case Outoftime(None) => handle { game =>
|
||||
game.outoftime(withGrace = true) ?? finisher.outOfTime(game)
|
||||
}
|
||||
|
||||
// exceptionally we don't block nor publish events
|
||||
|
@ -195,7 +188,7 @@ private[round] final class Round(
|
|||
case ForecastPlay(lastMove) => handle { game =>
|
||||
forecastApi.nextMove(game, lastMove) map { mOpt =>
|
||||
mOpt foreach { move =>
|
||||
self ! HumanPlay(game.player.id, move, false, Centis(0))
|
||||
self ! HumanPlay(game.player.id, move, false)
|
||||
}
|
||||
Nil
|
||||
}
|
||||
|
@ -228,8 +221,12 @@ private[round] final class Round(
|
|||
}
|
||||
|
||||
private def reportNetworkLag(pov: Pov) =
|
||||
if (pov.game.turns == 20 || pov.game.turns == 21) List(lags.white, lags.black).foreach { lag =>
|
||||
if (lag.centis > 0) lila.mon.round.move.networkLag(lag.centis * 10l)
|
||||
if (pov.game.turns == 20) {
|
||||
for {
|
||||
clock <- pov.game.clock
|
||||
color <- Color.all
|
||||
lag <- clock.lag(color)
|
||||
} lila.mon.round.move.networkLag(lag.millis)
|
||||
}
|
||||
|
||||
private def handle[A](op: Game => Fu[Events]): Funit =
|
||||
|
|
|
@ -2,11 +2,12 @@ package lila.round
|
|||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.Try
|
||||
|
||||
import akka.actor._
|
||||
import akka.pattern.ask
|
||||
import chess.format.Uci
|
||||
import chess.Centis
|
||||
import chess.{ Centis, MoveMetrics }
|
||||
import play.api.libs.json.{ JsObject, Json }
|
||||
|
||||
import actorApi._, round._
|
||||
|
@ -48,7 +49,7 @@ private[round] final class SocketHandler(
|
|||
member.playerIdOption.fold[Handler.Controller](({
|
||||
case ("p", o) => ping(o)
|
||||
case ("talk", o) => o str "d" foreach { messenger.watcher(gameId, member, _) }
|
||||
case ("outoftime", _) => send(Outoftime)
|
||||
case ("outoftime", _) => send(Outoftime(None))
|
||||
}: Handler.Controller) orElse evalCacheHandler(member, me) orElse lila.chat.Socket.in(
|
||||
chatId = s"$gameId/w",
|
||||
member = member,
|
||||
|
@ -87,7 +88,7 @@ private[round] final class SocketHandler(
|
|||
case ("draw-force", _) => send(DrawForce(playerId))
|
||||
case ("abort", _) => send(Abort(playerId))
|
||||
case ("moretime", _) => send(Moretime(playerId))
|
||||
case ("outoftime", _) => send(Outoftime)
|
||||
case ("outoftime", _) => send(Outoftime(playerId.some))
|
||||
case ("bye2", _) => socket ! Bye(ref.color)
|
||||
case ("talk", o) => o str "d" foreach { messenger.owner(gameId, member, _) }
|
||||
case ("hold", o) => for {
|
||||
|
@ -183,8 +184,10 @@ private[round] final class SocketHandler(
|
|||
blur = d int "b" contains 1
|
||||
} yield (drop, blur, parseLag(d))
|
||||
|
||||
private def parseLag(d: JsObject) =
|
||||
Centis.ofMillis(d.int("l") orElse d.int("lag") getOrElse 0)
|
||||
private def parseLag(d: JsObject) = MoveMetrics(
|
||||
d.int("l") orElse d.int("lag") map Centis.ofMillis,
|
||||
d.str("s") flatMap { v => Try(Centis(Integer.parseInt(v, 36))).toOption }
|
||||
)
|
||||
|
||||
private val ackEvent = Json.obj("t" -> "ack")
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ private[round] final class Titivate(
|
|||
if (game.finished || game.isPgnImport || game.playedThenAborted)
|
||||
GameRepo unsetCheckAt game
|
||||
|
||||
else if (game.outoftime(_ => chess.Clock.maxGrace)) fuccess {
|
||||
else if (game.outoftime(withGrace = true)) fuccess {
|
||||
roundMap ! Tell(game.id, Outoftime)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ package actorApi
|
|||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.concurrent.Promise
|
||||
|
||||
import chess.{ Centis, Color }
|
||||
import chess.{ MoveMetrics, Color }
|
||||
import chess.format.Uci
|
||||
|
||||
import lila.common.IpAddress
|
||||
|
@ -102,7 +102,7 @@ package round {
|
|||
playerId: String,
|
||||
uci: Uci,
|
||||
blur: Boolean,
|
||||
lag: Centis,
|
||||
moveMetrics: MoveMetrics = MoveMetrics(),
|
||||
promise: Option[Promise[Unit]] = None
|
||||
) {
|
||||
|
||||
|
@ -129,7 +129,7 @@ package round {
|
|||
case class TakebackYes(playerId: String)
|
||||
case class TakebackNo(playerId: String)
|
||||
case class Moretime(playerId: String)
|
||||
case object Outoftime
|
||||
case class Outoftime(playerId: Option[String])
|
||||
case object Abandon
|
||||
case class ForecastPlay(lastMove: chess.Move)
|
||||
case class Cheat(color: Color)
|
||||
|
|
|
@ -44,7 +44,8 @@ module.exports = function(opts, redraw) {
|
|||
justDropped: null,
|
||||
justCaptured: null,
|
||||
preDrop: null,
|
||||
lastDrawOfferAtPly: null
|
||||
lastDrawOfferAtPly: null,
|
||||
lastMoveMillis: null
|
||||
};
|
||||
this.vm.goneBerserk[this.data.player.color] = opts.data.player.berserk;
|
||||
this.vm.goneBerserk[this.data.opponent.color] = opts.data.opponent.berserk;
|
||||
|
@ -163,6 +164,22 @@ module.exports = function(opts, redraw) {
|
|||
|
||||
this.setTitle = () => title.set(this);
|
||||
|
||||
this.actualSendMove = function(type, action, meta) {
|
||||
meta = meta === undefined ? {} : meta
|
||||
var socketOpts = {
|
||||
ackable: true,
|
||||
// withLag: !!this.clock
|
||||
}
|
||||
var startTime = this.vm.lastMoveMillis;
|
||||
if (startTime !== null) socketOpts.millis = performance.now() - startTime;
|
||||
this.socket.send(type, action, socketOpts);
|
||||
|
||||
this.vm.justDropped = meta.justDropped;
|
||||
this.vm.justCaptured = meta.justCaptured;
|
||||
this.vm.preDrop = null;
|
||||
redraw();
|
||||
}
|
||||
|
||||
this.sendMove = function(orig, dest, prom, meta) {
|
||||
var move = {
|
||||
u: orig + dest
|
||||
|
@ -172,13 +189,9 @@ module.exports = function(opts, redraw) {
|
|||
this.resign(false);
|
||||
if (this.data.pref.submitMove && !meta.premove) {
|
||||
this.vm.moveToSubmit = move;
|
||||
} else this.socket.send('move', move, {
|
||||
ackable: true,
|
||||
withLag: !!this.clock
|
||||
});
|
||||
this.vm.justDropped = null;
|
||||
this.vm.justCaptured = meta.captured;
|
||||
redraw();
|
||||
redraw();
|
||||
} else this.actualSendMove('move', move, {justCaptured: meta.captured});
|
||||
|
||||
}.bind(this);
|
||||
|
||||
this.sendNewPiece = function(role, key, isPredrop) {
|
||||
|
@ -190,14 +203,10 @@ module.exports = function(opts, redraw) {
|
|||
this.resign(false);
|
||||
if (this.data.pref.submitMove && !isPredrop) {
|
||||
this.vm.dropToSubmit = drop;
|
||||
} else this.socket.send('drop', drop, {
|
||||
ackable: true,
|
||||
withLag: !!this.clock
|
||||
});
|
||||
this.vm.preDrop = null;
|
||||
this.vm.justDropped = role;
|
||||
this.vm.justCaptured = null;
|
||||
redraw();
|
||||
redraw();
|
||||
} else {
|
||||
this.actualSendMove('drop', drop, {justDropped: role});
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
var showYourMoveNotification = function() {
|
||||
|
@ -224,6 +233,8 @@ module.exports = function(opts, redraw) {
|
|||
this.apiMove = function(o) {
|
||||
var d = this.data,
|
||||
playing = game.isPlayerPlaying(d);
|
||||
|
||||
if (playing) this.vm.lastMoveMillis = performance.now();
|
||||
d.game.turns = o.ply;
|
||||
d.game.player = o.ply % 2 === 0 ? 'white' : 'black';
|
||||
var playedColor = o.ply % 2 === 0 ? 'black' : 'white';
|
||||
|
@ -481,16 +492,11 @@ module.exports = function(opts, redraw) {
|
|||
|
||||
this.submitMove = function(v) {
|
||||
if (v && (this.vm.moveToSubmit || this.vm.dropToSubmit)) {
|
||||
if (this.vm.moveToSubmit)
|
||||
this.socket.send('move', this.vm.moveToSubmit, {
|
||||
ackable: true,
|
||||
withLag: !!this.clock
|
||||
});
|
||||
else if (this.vm.dropToSubmit)
|
||||
this.socket.send('drop', this.vm.dropToSubmit, {
|
||||
ackable: true,
|
||||
withLag: !!this.clock
|
||||
});
|
||||
if (this.vm.moveToSubmit) {
|
||||
this.actualSendMove('move', this.vm.moveToSubmit);
|
||||
} else {
|
||||
this.actualSendMove('drop', this.vm.dropToSubmit);
|
||||
}
|
||||
lichess.sound.confirmation();
|
||||
} else this.jump(this.vm.ply);
|
||||
this.cancelMove();
|
||||
|
|
|
@ -65,14 +65,15 @@ lichess.StrongSocket = function(url, version, settings) {
|
|||
};
|
||||
|
||||
var send = function(t, d, o, noRetry) {
|
||||
d = d == null ? {} : d,
|
||||
o = o || {};
|
||||
if (o.withLag) d.l = Math.round(averageLag);
|
||||
o = o || {};
|
||||
var msg = { t: t };
|
||||
if (d !== undefined) {
|
||||
if (o.withLag) d.l = Math.round(averageLag);
|
||||
if ('millis' in o) d.s = Math.floor(o.millis * 0.1).toString(36);
|
||||
msg.d = d;
|
||||
}
|
||||
if (o.ackable) ackable.register(t, d);
|
||||
var message = JSON.stringify({
|
||||
t: t,
|
||||
d: d
|
||||
});
|
||||
var message = JSON.stringify(msg);
|
||||
debug("send " + message);
|
||||
try {
|
||||
ws.send(message);
|
||||
|
|
Loading…
Reference in a new issue