[server] add session

This commit is contained in:
2020-03-25 21:05:15 +03:00
parent 8e3b9e2830
commit 8da310a6e5
16 changed files with 609 additions and 12 deletions

View File

@@ -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'],

View File

@@ -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": {}

12
protohx.json Executable file
View File

@@ -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
}

View File

@@ -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<Response, Request>;
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();

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
syntax = "proto3";
package ru.m.puzzlez.proto.game;
message GameEventProto {
int32 time = 1;
string event = 2;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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<Response, Request> 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)))));
}
}
}

View File

@@ -5,34 +5,34 @@ import cpp.net.ThreadServer;
import sys.net.Socket;
import haxe.io.Bytes;
typedef Session = Dynamic;
typedef Message = Bytes;
typedef ClientMessage<M> = {
var msg:M;
var bytes:Int;
}
class PuzzlezServer extends ThreadServer<Session, Message> {
class PuzzlezServer extends ThreadServer<GameSession, Message> {
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<Message> {
override public function readClientMessage(session:GameSession, buf:Bytes, pos:Int, len:Int): ClientMessage<Message> {
//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<Session, Message> {
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);

View File

@@ -0,0 +1,4 @@
package ru.m.puzzlez.game;
interface GameListener {
}

View File

@@ -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<ServerGame>;
public var gamesById(default, null):Map<Int, ServerGame>;
public var gamesByCreator(default, null):Map<String, ServerGame>;
public var gamesByUser(default, null):Map<String, ServerGame>;
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<RoomSlotProto> = [];
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);
}
}

View File

@@ -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<ServerGame>;
public var gamesById(default, null):Map<Int, ServerGame>;
public var gamesByCreator(default, null):Map<String, ServerGame>;
public var gamesByUser(default, null):Map<String, ServerGame>;
private var createSignal(default, null):Signal<ServerGame>;
private var changeSignal(default, null):Signal2<ServerGame, GameChange>;
private var deleteSignal(default, null):Signal<ServerGame>;
private var eventSignal(default, null):Signal2<ServerGame, GameEvent>;
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;
}

View File

@@ -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<GameEvent> {
return [];
}
}