diff --git a/gulpfile.js b/gulpfile.js index d40f156..73be9e0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -23,6 +23,14 @@ exports.clean = function clean() { return gulp.src('target/*', {read: false}).pipe(gulpClean()); }; +exports.generate = function generate() { + return new Haxe().haxelib(['run', 'protohx', 'generate', 'protohx.json']).then(({stdout}) => { + if (stdout.indexOf('FAIL') > -1) { + throw stdout; + } + }); +}; + const config = new Project.Config({ meta: { title: 'Puzzle\'z', @@ -52,6 +60,7 @@ const app = new Project( config.branch({ name: 'app', sources: [ + 'src-gen/haxe', 'src/common/haxe', 'src/app/haxe', ], @@ -79,6 +88,7 @@ const server = new Project( config.branch({ name: 'server', sources: [ + 'src-gen/haxe', 'src/common/haxe', 'src/server/haxe', ], @@ -90,6 +100,7 @@ module.exports.publish = publish(packageInfo.name, packageInfo.version, Config.P const defaultSeries = [ exports.clean, + exports.generate, module.exports['app:flash:build'], module.exports['app:flash:html'], module.exports['app:html5:build'], diff --git a/package.json b/package.json index 3725b88..8af3038 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "lime": "7.7.0", "openfl": "8.9.6", "hxcpp": "4.0.52", - "svg": "1.1.3" + "svg": "1.1.3", + "haxe-crypto": "0.0.7" }, "haxe": "4.0.5", "dependencies": {} diff --git a/protohx.json b/protohx.json new file mode 100755 index 0000000..4990425 --- /dev/null +++ b/protohx.json @@ -0,0 +1,12 @@ +{ + "protoPath": "src/common/proto", + "protoFiles": [ + "src/common/proto/core.proto", + "src/common/proto/game.proto", + "src/common/proto/room.proto", + "src/common/proto/pack.proto" + ], + "cleanOut": true, + "haxeOut": "src-gen/haxe", + "javaOut": null +} diff --git a/src/app/haxe/ru/m/puzzlez/PuzzlezApp.hx b/src/app/haxe/ru/m/puzzlez/PuzzlezApp.hx index fc99f08..d9894b6 100644 --- a/src/app/haxe/ru/m/puzzlez/PuzzlezApp.hx +++ b/src/app/haxe/ru/m/puzzlez/PuzzlezApp.hx @@ -1,5 +1,9 @@ package ru.m.puzzlez; +import hw.connect.ConnectionFactory; +import ru.m.puzzlez.proto.pack.Request; +import ru.m.puzzlez.proto.pack.Response; +import hw.connect.IConnection; import hw.log.TraceLogger; import hw.app.App; import hw.app.Const; @@ -12,6 +16,7 @@ import ru.m.update.Updater; class PuzzlezApp { @:provide static var updater:Updater; + @:provide static var connection:IConnection; public static function main() { // ToDo: fix @:provide macro @@ -19,6 +24,8 @@ class PuzzlezApp { ImageStorage; SettingsStorage; L.push(new TraceLogger()); + connection = ConnectionFactory.buildClientConnection("127.0.0.1", 6000, Request); + connection.connect().then(_ -> L.i("connect", "success")).catchError(error -> L.e("connect", "", error)); updater = new Updater(Const.instance.VERSION, "https://shmyga.ru/repo/puzzlez/packages.json"); var app = new App(); app.theme = new PuzzlezTheme(); diff --git a/src/app/haxe/ru/m/puzzlez/render/Render.hx b/src/app/haxe/ru/m/puzzlez/render/Render.hx index ec3274f..a4f2278 100644 --- a/src/app/haxe/ru/m/puzzlez/render/Render.hx +++ b/src/app/haxe/ru/m/puzzlez/render/Render.hx @@ -186,7 +186,7 @@ class Render extends SpriteView implements IRender { } activePart = pointPart; tableView.setChildIndex(activePart, tableView.numChildren - 1); - activePoint = tableView.globalToLocal(point); + activePoint = RenderUtil.convertPoint(tableView.globalToLocal(point)); tableView.stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove); tableView.stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp); signal.emit(ACTION(PART_TAKE(activePart.id))); @@ -194,14 +194,14 @@ class Render extends SpriteView implements IRender { } private function onMouseMove(event:MouseEvent):Void { - var newPoint:Point = tableView.globalToLocal(new FlashPoint(event.stageX, event.stageY)); + var newPoint:Point = RenderUtil.convertPoint(tableView.globalToLocal(new FlashPoint(event.stageX, event.stageY))); var partPosition = activePart.position.add(newPoint).subtract(activePoint); signal.emit(ACTION(PART_MOVE(activePart.id, partPosition.clone()))); activePoint = newPoint; } private function onMouseUp(event:MouseEvent):Void { - var newPoint:Point = tableView.globalToLocal(new FlashPoint(event.stageX, event.stageY)); + var newPoint:Point = RenderUtil.convertPoint(tableView.globalToLocal(new FlashPoint(event.stageX, event.stageY))); var partPosition = activePart.position.add(newPoint).subtract(activePoint); signal.emit(ACTION(PART_PUT(activePart.id, partPosition.clone()))); tableView.stage.removeEventListener(MouseEvent.MOUSE_MOVE, onMouseMove); diff --git a/src/app/haxe/ru/m/puzzlez/render/RenderUtil.hx b/src/app/haxe/ru/m/puzzlez/render/RenderUtil.hx index 5cfab7f..b54ddc7 100644 --- a/src/app/haxe/ru/m/puzzlez/render/RenderUtil.hx +++ b/src/app/haxe/ru/m/puzzlez/render/RenderUtil.hx @@ -116,4 +116,8 @@ class RenderUtil { var height = source.height * s; return new Rectangle((target.width - width) / 2, (target.height - height) / 2, width, height); } + + public static function convertPoint(point:flash.geom.Point):Point { + return new Point(point.x, point.y); + } } diff --git a/src/common/proto/core.proto b/src/common/proto/core.proto new file mode 100644 index 0000000..72da300 --- /dev/null +++ b/src/common/proto/core.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package ru.m.puzzlez.proto.core; + +message UserProto { + string uuid = 1; + string name = 2; +} + +message GameProto { + int32 id = 1; +} diff --git a/src/common/proto/game.proto b/src/common/proto/game.proto new file mode 100644 index 0000000..b1910a0 --- /dev/null +++ b/src/common/proto/game.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package ru.m.puzzlez.proto.game; + +message GameEventProto { + int32 time = 1; + string event = 2; +} diff --git a/src/common/proto/pack.proto b/src/common/proto/pack.proto new file mode 100644 index 0000000..a738456 --- /dev/null +++ b/src/common/proto/pack.proto @@ -0,0 +1,55 @@ +syntax = "proto3"; + +import "core.proto"; +import "game.proto"; +import "room.proto"; + +package ru.m.puzzlez.proto.pack; + +message ErrorResponse { + int32 code = 1; + string message = 2; +} + +message LoginRequest { + string uuid = 1; + string name = 2; +} + +message LoginResponse { + ru.m.puzzlez.proto.core.UserProto user = 1; +} + +message LogoutRequest {} + +message LogoutResponse {} + +message GameEventRequest { + ru.m.puzzlez.proto.game.GameEventProto event = 1; +} + +message GameEventResponse { + ru.m.puzzlez.proto.game.GameEventProto event = 1; +} + +message Request { + oneof content { + LoginRequest login = 1; + LogoutRequest logout = 2; + ru.m.puzzlez.proto.room.RoomRequest room = 3; + ru.m.puzzlez.proto.room.RoomListRequest roomList = 4; + GameEventRequest gameEvent = 6; + } +} + +message Response { + oneof content { + LoginResponse login = 1; + LogoutResponse logout = 2; + ru.m.puzzlez.proto.room.RoomResponse room = 3; + ru.m.puzzlez.proto.room.RoomListResponse roomList = 4; + GameEventResponse gameEvent = 6; + + ErrorResponse error = 999; + } +} diff --git a/src/common/proto/room.proto b/src/common/proto/room.proto new file mode 100644 index 0000000..8bf45e3 --- /dev/null +++ b/src/common/proto/room.proto @@ -0,0 +1,64 @@ +syntax = "proto3"; + +import "core.proto"; + +package ru.m.puzzlez.proto.room; + +message SlotProto { + string team = 3; + int32 index = 4; +} + +message RoomSlotProto { + SlotProto slot = 1; + ru.m.puzzlez.proto.core.UserProto user = 2; +} + +message RoomProto { + ru.m.puzzlez.proto.core.GameProto game = 1; + ru.m.puzzlez.proto.core.UserProto creator = 2; + repeated ru.m.puzzlez.proto.core.UserProto users = 3; + repeated RoomSlotProto slots = 4; +} + +message CreateRequest { + string type = 2; + int32 level = 3; +} + +message JoinRequest { + int32 gameId = 1; + bool restore = 2; +} + +message LeaveRequest { +} + +message SlotRequest { + SlotProto slot = 3; +} + +message StartRequest { +} + +message RoomRequest { + oneof content { + CreateRequest create = 1; + JoinRequest join = 2; + LeaveRequest leave = 3; + SlotRequest slot = 4; + StartRequest start = 5; + } +} + +message RoomResponse { + RoomProto room = 1; +} + +message RoomListRequest { + bool subscribe = 1; +} + +message RoomListResponse { + repeated RoomProto rooms = 1; +} diff --git a/src/server/haxe/ru/m/puzzlez/GameSession.hx b/src/server/haxe/ru/m/puzzlez/GameSession.hx new file mode 100644 index 0000000..362ca8b --- /dev/null +++ b/src/server/haxe/ru/m/puzzlez/GameSession.hx @@ -0,0 +1,179 @@ +package ru.m.puzzlez; + +import com.hurlant.crypto.extra.UUID; +import com.hurlant.crypto.prng.Random; +import haxe.Serializer; +import haxe.Unserializer; +import hw.connect.session.ProtoSession; +import hw.log.BaseLogger.LoggerUtil; +import ru.m.puzzlez.core.GameEvent; +import ru.m.puzzlez.game.IGameManager; +import ru.m.puzzlez.game.ServerGame; +import ru.m.puzzlez.proto.core.UserProto; +import ru.m.puzzlez.proto.game.GameEventProto; +import ru.m.puzzlez.proto.pack.ErrorResponse; +import ru.m.puzzlez.proto.pack.GameEventResponse; +import ru.m.puzzlez.proto.pack.LoginResponse; +import ru.m.puzzlez.proto.pack.LogoutResponse; +import ru.m.puzzlez.proto.pack.Request; +import ru.m.puzzlez.proto.pack.Response; +import ru.m.puzzlez.proto.room.RoomListResponse; +import ru.m.puzzlez.proto.room.RoomResponse; +import sys.net.Socket; + +class GameSession extends ProtoSession implements GameManagerListener { + private static inline var TAG = "Session"; + + @:provide static var gameManager:IGameManager; + + public var user(default, null):UserProto; + public var gameId(default, null):Int; + + private var subscribed:Bool; + private var tag(get, never):String; + + private function get_tag():String { + return '[${id}|${user == null ? '-' : user.name}|${gameId == -1 ? '-' : Std.string(gameId)}]'; + } + + public function new(socket:Socket) { + super(socket, Request); + gameId = -1; + } + + private function sendError(code:Int, message:String):Void { + send(new Response().setError(new ErrorResponse().setCode(code).setMessage(message))); + } + + private function listGame():RoomListResponse { + var games = gameManager.games; + return new RoomListResponse().setRooms([for (game in games) game.room]); + } + + override public function send(packet:Response):Void { + #if proto_debug L.d(TAG, '$tag send: ${packet}'); #end + try { + super.send(packet); + } catch (error:Dynamic) { + L.e(TAG, '$tag send ', error); + } + } + + private function logout(leave:Bool = true):Void { + gameId = -1; + gameManager.disconnect(this); + if (user != null && leave) { + gameManager.leave(user); + user = null; + } + } + + private function join(gameId:Int, restore:Bool):Void { + this.gameId = gameId; + gameManager.join(gameId, user); + var game = gameManager.gamesById[gameId]; + if (restore) { + // ToDo: restore + } + } + + override private function onRequest(request:Request):Void { + #if proto_debug L.d(TAG, '$tag onRequest: ${request}'); #end + try { + if (!request.hasLogin() && user == null) { + throw "Not Authorized"; + } + // login + if (request.hasLogin()) { + user = new UserProto() + .setUuid(request.login.uuid != null ? request.login.uuid : UUID.generateRandom(new Random()).toString()) + .setName(request.login.name); + gameManager.connect(this); + send(new Response().setLogin(new LoginResponse().setUser(user))); + if (gameManager.gamesByUser.exists(user.uuid)) { + join(gameManager.gamesByUser[user.uuid].id, false); + } + // logout + } else if (request.hasLogout()) { + logout(); + send(new Response().setLogout(new LogoutResponse())); + // room + } else if (request.hasRoom()) { + if (request.room.hasCreate()) { + var game = gameManager.create(user); + gameId = game.id; + send(new Response().setRoom(new RoomResponse().setRoom(game.room))); + } else if (request.room.hasJoin()) { + join(request.room.join.gameId, request.room.join.restore); + } else if (request.room.hasLeave()) { + gameManager.leave(user); + } else if (request.room.hasSlot()) { + gameManager.slot(user, request.room.slot.slot); + } else if (request.room.hasStart()) { + gameManager.start(gameId); + } + // room list + } else if (request.hasRoomList()) { + subscribed = request.roomList.subscribe; + if (subscribed) { + send(new Response().setRoomList(listGame())); + } + } else if (request.hasGameEvent()) { + if (gameManager.gamesById.exists(gameId)) { + var event:GameEvent = Unserializer.run(request.gameEvent.event.event); + // ToDo: emit event + ///gameManager.gamesById[gameId].gameEventSignal.emit(event); + } + } + } catch (error:Dynamic) { + L.e(TAG, '$tag onRequest ', error); + sendError(500, LoggerUtil.printError(error)); + } + } + + override public function disconnect():Void { + L.d(TAG, '$tag disconnect'); + logout(false); + super.disconnect(); + } + + public function onCreate(game:ServerGame):Void { + if (subscribed) { + send(new Response().setRoomList(listGame())); + } + } + + public function onChange(game:ServerGame, change:GameChange):Void { + if (gameId == game.id) { + switch change { + case LEAVE(user): + if (user.uuid == this.user.uuid) { + gameId = -1; + send(new Response().setRoom(new RoomResponse())); + return; + } + case _: + } + send(new Response().setRoom(new RoomResponse().setRoom(game.room))); + } + if (subscribed) { + send(new Response().setRoomList(listGame())); + } + } + + public function onDelete(game:ServerGame):Void { + if (gameId == game.id) { + gameId = -1; + send(new Response().setRoom(new RoomResponse())); + } + if (subscribed) { + send(new Response().setRoomList(listGame())); + } + } + + public function onEvent(game:ServerGame, event:GameEvent):Void { + if (gameId == game.id) { + send(new Response().setGameEvent(new GameEventResponse().setEvent(new GameEventProto().setTime(0).setEvent(Serializer.run(event))))); + } + } +} diff --git a/src/server/haxe/ru/m/puzzlez/PuzzlezServer.hx b/src/server/haxe/ru/m/puzzlez/PuzzlezServer.hx index 6809fec..a94172a 100644 --- a/src/server/haxe/ru/m/puzzlez/PuzzlezServer.hx +++ b/src/server/haxe/ru/m/puzzlez/PuzzlezServer.hx @@ -5,34 +5,34 @@ import cpp.net.ThreadServer; import sys.net.Socket; import haxe.io.Bytes; -typedef Session = Dynamic; typedef Message = Bytes; + typedef ClientMessage = { var msg:M; var bytes:Int; } -class PuzzlezServer extends ThreadServer { +class PuzzlezServer extends ThreadServer { private static inline var TAG = 'Server'; - override public function clientConnected(socket:Socket):Session { - var session = null; // new Session(socket); + override public function clientConnected(socket:Socket):GameSession { + var session = new GameSession(socket); L.d(TAG, 'Client connected'); return session; } - override public function clientDisconnected(session:Session) { + override public function clientDisconnected(session:GameSession) { L.d(TAG, 'Client disconnected'); //session.disconnect(); } - override public function readClientMessage(session:Session, buf:Bytes, pos:Int, len:Int): ClientMessage { + override public function readClientMessage(session:GameSession, buf:Bytes, pos:Int, len:Int): ClientMessage { //L.d(TAG, 'Client message: ${buf}'); return {msg: buf.sub(pos, len), bytes: len}; } - override public function clientMessage(session:Session, message:Message) { + override public function clientMessage(session:GameSession, message:Message) { //session.pushData(bytes); } @@ -41,7 +41,7 @@ class PuzzlezServer extends ThreadServer { L.d(TAG, 'Running'); L.i(TAG, 'Build: ${CompilationOption.get("build")}'); var host:String = Sys.args().length > 0 ? Sys.args()[0] : "0.0.0.0"; - var port:Int = Sys.args().length > 1 ? Std.parseInt(Sys.args()[1]) : 5000; + var port:Int = Sys.args().length > 1 ? Std.parseInt(Sys.args()[1]) : 6000; var wserver = new PuzzlezServer(); L.i(TAG, 'Start on ${host}:${port}'); wserver.run(host, port); diff --git a/src/server/haxe/ru/m/puzzlez/game/GameListener.hx b/src/server/haxe/ru/m/puzzlez/game/GameListener.hx new file mode 100644 index 0000000..7cb53df --- /dev/null +++ b/src/server/haxe/ru/m/puzzlez/game/GameListener.hx @@ -0,0 +1,4 @@ +package ru.m.puzzlez.game; + +interface GameListener { +} diff --git a/src/server/haxe/ru/m/puzzlez/game/GameManager.hx b/src/server/haxe/ru/m/puzzlez/game/GameManager.hx new file mode 100644 index 0000000..5d57d9a --- /dev/null +++ b/src/server/haxe/ru/m/puzzlez/game/GameManager.hx @@ -0,0 +1,124 @@ +package ru.m.puzzlez.game; + +import ru.m.puzzlez.core.GameEvent; +import ru.m.puzzlez.game.IGameManager; +import ru.m.puzzlez.proto.room.SlotProto; +import ru.m.puzzlez.proto.room.RoomSlotProto; +import ru.m.puzzlez.proto.core.GameProto; +import ru.m.puzzlez.proto.room.RoomProto; +import ru.m.puzzlez.proto.core.UserProto; + +class _GameListener implements GameListener { + private var game:ServerGame; + private var dispatcher:IGameManager; + + public function new(game:ServerGame, dispatcher:IGameManager) { + this.game = game; + this.dispatcher = dispatcher; + } + + public function onGameEvent(event:GameEvent):Void { + dispatcher.dispatchEvent(game, event); + switch event { + case COMPLETE: + dispatcher.delete(game.id); + dispose(); + case _: + } + } + + public function dispose():Void { + game.disconnect(this); + game = null; + dispatcher = null; + } +} + +@:dispatcher(GameManagerListener) class GameManager implements IGameManager { + public var games(default, null):Array; + public var gamesById(default, null):Map; + public var gamesByCreator(default, null):Map; + public var gamesByUser(default, null):Map; + + private var counter:Int; + + public function new() { + counter = 0; + games = []; + gamesById = new Map(); + gamesByCreator = new Map(); + gamesByUser = new Map(); + } + + public function create(user:UserProto):ServerGame { + if (gamesByCreator.exists(user.uuid)) { + delete(gamesByCreator[user.uuid].id); + } + var room = new RoomProto() + .setGame( + new GameProto() + .setId(++counter) + ) + .setCreator(user); + var game = new ServerGame(room); + var slots:Array = []; + game.room.setSlots(slots); + games.push(game); + gamesById[game.id] = game; + gamesByCreator[game.room.creator.uuid] = game; + createSignal.emit(game); + join(game.id, user); + return game; + } + + public function join(gameId:Int, user:UserProto):Void { + if (gamesById.exists(gameId)) { + var game = gamesById[gameId]; + game.join(user); + gamesByUser[user.uuid] = game; + changeSignal.emit(game, JOIN(user)); + } + } + + public function delete(gameId:Int):Void { + if (gamesById.exists(gameId)) { + var game = gamesById[gameId]; + games.remove(game); + gamesById.remove(game.id); + gamesByCreator.remove(game.room.creator.uuid); + deleteSignal.emit(game); + } + } + + public function leave(user:UserProto):Void { + /*if (gamesByCreator.exists(user.uuid)) { + delete(gamesByCreator[user.uuid].proto.id); + } else*/ if (gamesByUser.exists(user.uuid)) { + var game = gamesByUser[user.uuid]; + gamesByUser.remove(user.uuid); + game.leave(user); + changeSignal.emit(game, LEAVE(user)); + } + } + + public function slot(user:UserProto, slot:SlotProto):Void { + if (gamesByUser.exists(user.uuid)) { + var game = gamesByUser[user.uuid]; + game.slot(user, slot); + changeSignal.emit(game, SLOT(user, slot)); + } + } + + public function start(gameId:Int):Void { + if (gamesById.exists(gameId)) { + var game:ServerGame = gamesById[gameId]; + changeSignal.emit(game, START); + game.connect(new _GameListener(game, this)); + game.start(); + } + } + + public function dispatchEvent(game:ServerGame, event:GameEvent):Void { + eventSignal.emit(game, event); + } +} diff --git a/src/server/haxe/ru/m/puzzlez/game/IGameManager.hx b/src/server/haxe/ru/m/puzzlez/game/IGameManager.hx new file mode 100644 index 0000000..a8bb529 --- /dev/null +++ b/src/server/haxe/ru/m/puzzlez/game/IGameManager.hx @@ -0,0 +1,44 @@ +package ru.m.puzzlez.game; + +import hw.signal.Signal; +import ru.m.puzzlez.core.GameEvent; +import ru.m.puzzlez.proto.core.UserProto; +import ru.m.puzzlez.proto.room.SlotProto; + +enum GameChange { + JOIN(user:UserProto); + LEAVE(user:UserProto); + SLOT(user:UserProto, slot:SlotProto); + START(); +} + +interface GameManagerListener { + public function onCreate(game:ServerGame):Void; + public function onChange(game:ServerGame, change:GameChange):Void; + public function onDelete(game:ServerGame):Void; + public function onEvent(game:ServerGame, event:GameEvent):Void; +} + +@:provide(GameManager) interface IGameManager { + public var games(default, null):Array; + public var gamesById(default, null):Map; + public var gamesByCreator(default, null):Map; + public var gamesByUser(default, null):Map; + + private var createSignal(default, null):Signal; + private var changeSignal(default, null):Signal2; + private var deleteSignal(default, null):Signal; + private var eventSignal(default, null):Signal2; + + public function dispatchEvent(game:ServerGame, event:GameEvent):Void; + + public function connect(listener:GameManagerListener):Void; + public function disconnect(listener:GameManagerListener):Void; + + public function create(user:UserProto):ServerGame; + public function delete(gameId:Int):Void; + public function join(gameId:Int, user:UserProto):Void; + public function slot(user:UserProto, slot:SlotProto):Void; + public function leave(user:UserProto):Void; + public function start(gameId:Int):Void; +} diff --git a/src/server/haxe/ru/m/puzzlez/game/ServerGame.hx b/src/server/haxe/ru/m/puzzlez/game/ServerGame.hx new file mode 100644 index 0000000..985c75c --- /dev/null +++ b/src/server/haxe/ru/m/puzzlez/game/ServerGame.hx @@ -0,0 +1,72 @@ +package ru.m.puzzlez.game; + +import haxe.Timer; +import ru.m.puzzlez.core.GameEvent; +import ru.m.puzzlez.proto.core.UserProto; +import ru.m.puzzlez.proto.room.RoomProto; +import ru.m.puzzlez.proto.room.SlotProto; + +@:dispatcher(GameListener) class ServerGame { + + public var room(default, null):RoomProto; + public var id(get, null):Int; + + private var timer:Timer; + + public function new(room:RoomProto) { + this.room = room; + } + + private inline function get_id():Int { + return room.game.id; + } + + public function contains(user:UserProto):Bool { + for (slot in room.slots) { + if (slot.hasUser() && slot.user.uuid == user.uuid) { + return true; + } + } + return false; + } + + public function join(user:UserProto):Void { + if (!contains(user)) { + room.users.push(user); + } + } + + public function slot(user:UserProto, slot:SlotProto):Void { + join(user); + for (s in room.slots) { + if (s.hasUser() && s.user.uuid == user.uuid) { + s.clearUser(); + break; + } + } + for (s in room.slots) { + if (s.slot.team == slot.team && s.slot.index == slot.index) { + s.setUser(user); + break; + } + } + } + + public function leave(user:UserProto):Void { + for (slot in room.slots) { + if (slot.user != null && slot.user.uuid == user.uuid) { + slot.clearUser(); + break; + } + } + room.setUsers(room.users.filter(function(u:UserProto) return u.uuid != user.uuid)); + } + + public function start():Void { + + } + + public function restore():Array { + return []; + } +}