import StateMachine from 'javascript-state-machine';
import { deserializeObject } from './CodecUtils';
import { tap } from './Utils';
import Player from './Player';
import ReceiverGroup from './ReceiverGroup';
import { ERROR_EVENT, DISCONNECT_EVENT } from './Connection';
import GameConnection, {
PLAYER_JOINED_EVENT,
PLAYER_LEFT_EVENT,
MASTER_CHANGED_EVENT,
ROOM_OPEN_CHANGED_EVENT,
ROOM_VISIBLE_CHANGED_EVENT,
ROOM_PROPERTIES_CHANGED_EVENT,
ROOM_SYSTEM_PROPERTIES_CHANGED_EVENT,
PLAYER_PROPERTIES_CHANGED_EVENT,
PLAYER_OFFLINE_EVENT,
PLAYER_ONLINE_EVENT,
SEND_CUSTOM_EVENT,
ROOM_KICKED_EVENT,
} from './GameConnection';
import Event from './Event';
import PlayError from './PlayError';
import PlayErrorCode from './PlayErrorCode';
/**
* 房间类
*/
export default class Room {
constructor(client) {
this._client = client;
this._fsm = new StateMachine({
init: 'init',
final: 'closed',
transitions: [
{ name: 'join', from: 'init', to: 'joining' },
{ name: 'joined', from: 'joining', to: 'game' },
{ name: 'joinFailed', from: 'joining', to: 'init' },
{ name: 'leave', from: 'game', to: 'leaving' },
{ name: 'leaveFailed', from: 'leaving', to: 'game' },
{ name: 'disconnect', from: 'game', to: 'disconnected' },
{
name: 'close',
from: ['init', 'joining', 'game', 'leaving', 'disconnected'],
to: 'closed',
},
],
methods: {
onEnterGame: () => {
// 为 reconnectAndRejoin() 保存房间 id
this._lastRoomId = this.name;
// 注册事件
this._gameConn.on(ERROR_EVENT, async ({ code, detail }) => {
this._gameConn.close();
this._client.emit(Event.ERROR, {
code,
detail,
});
});
this._gameConn.on(PLAYER_JOINED_EVENT, newPlayerData => {
const newPlayer = new Player(this);
newPlayer._init(newPlayerData);
this._addPlayer(newPlayer);
this._client.emit(Event.PLAYER_ROOM_JOINED, {
newPlayer,
});
});
this._gameConn.on(PLAYER_LEFT_EVENT, actorId => {
const leftPlayer = this.getPlayer(actorId);
this._removePlayer(actorId);
this._client.emit(Event.PLAYER_ROOM_LEFT, {
leftPlayer,
});
});
this._gameConn.on(MASTER_CHANGED_EVENT, newMasterActorId => {
let newMaster = null;
this._masterActorId = newMasterActorId;
if (newMasterActorId > 0) {
newMaster = this.getPlayer(newMasterActorId);
}
this._client.emit(Event.MASTER_SWITCHED, {
newMaster,
});
});
this._gameConn.on(ROOM_OPEN_CHANGED_EVENT, open => {
this._open = open;
this._client.emit(Event.ROOM_OPEN_CHANGED, {
open,
});
});
this._gameConn.on(ROOM_VISIBLE_CHANGED_EVENT, visible => {
this._visible = visible;
this._client.emit(Event.ROOM_VISIBLE_CHANGED, {
visible,
});
});
this._gameConn.on(ROOM_PROPERTIES_CHANGED_EVENT, changedProps => {
this._mergeProperties(changedProps);
this._client.emit(Event.ROOM_CUSTOM_PROPERTIES_CHANGED, {
changedProps,
});
});
this._gameConn.on(
ROOM_SYSTEM_PROPERTIES_CHANGED_EVENT,
changedProps => {
this._mergeSystemProps(changedProps);
this._client.emit(Event.ROOM_SYSTEM_PROPERTIES_CHANGED, {
changedProps,
});
}
);
this._gameConn.on(
PLAYER_PROPERTIES_CHANGED_EVENT,
(actorId, changedProps) => {
const player = this.getPlayer(actorId);
player._mergeProperties(changedProps);
this._client.emit(Event.PLAYER_CUSTOM_PROPERTIES_CHANGED, {
player,
changedProps,
});
}
);
this._gameConn.on(PLAYER_OFFLINE_EVENT, actorId => {
const player = this.getPlayer(actorId);
player._active = false;
this._client.emit(Event.PLAYER_ACTIVITY_CHANGED, {
player,
});
});
this._gameConn.on(PLAYER_ONLINE_EVENT, (actorId, props) => {
const player = this.getPlayer(actorId);
player._mergeProperties(props);
player._active = true;
this._client.emit(Event.PLAYER_ACTIVITY_CHANGED, {
player,
});
});
this._gameConn.on(
SEND_CUSTOM_EVENT,
(eventId, eventData, senderId) => {
this._client.emit(Event.CUSTOM_EVENT, {
eventId,
eventData,
senderId,
});
}
);
this._gameConn.on(DISCONNECT_EVENT, () => {
this._fsm.disconnect();
this._client.emit(Event.DISCONNECTED);
});
this._gameConn.on(ROOM_KICKED_EVENT, async info => {
await this.close();
if (info) {
this._client.emit(Event.ROOM_KICKED, info);
} else {
this._client.emit(Event.ROOM_KICKED);
}
});
},
onExitGame: () => {
this._gameConn.removeAllListeners();
},
},
});
}
async create(roomName, roomOptions, expectedUserIds) {
if (roomName !== null && !(typeof roomName === 'string')) {
throw new TypeError(`${roomName} is not a string`);
}
if (roomOptions !== null && !(roomOptions instanceof Object)) {
throw new TypeError(`${roomOptions} is not a Object`);
}
if (expectedUserIds !== null && !Array.isArray(expectedUserIds)) {
throw new TypeError(`${expectedUserIds} is not an Array with string`);
}
this._fsm.join();
try {
const { _lobbyService } = this._client;
const { cid, addr } = await _lobbyService.createRoom(roomName);
const { sessionToken } = await _lobbyService.authorize();
// 合并
this._gameConn = new GameConnection();
const { _appId, _gameVersion, _userId } = this._client;
await this._gameConn.connect(
_appId,
addr,
_gameVersion,
_userId,
sessionToken
);
const room = await this._gameConn.createRoom(
cid,
roomOptions,
expectedUserIds
);
this._init(room);
this._fsm.joined();
} catch (err) {
await this.close();
throw err;
}
}
async join(roomName, expectedUserIds) {
if (!(typeof roomName === 'string')) {
throw new TypeError(`${roomName} is not a string`);
}
if (expectedUserIds !== null && !Array.isArray(expectedUserIds)) {
throw new TypeError(`${expectedUserIds} is not an array with string`);
}
this._fsm.join();
try {
const { _lobbyService } = this._client;
const { cid, addr } = await _lobbyService.joinRoom({ roomName });
const { sessionToken } = await _lobbyService.authorize();
this._gameConn = new GameConnection();
const { _appId, _gameVersion, _userId } = this._client;
await this._gameConn.connect(
_appId,
addr,
_gameVersion,
_userId,
sessionToken
);
const room = await this._gameConn.joinRoom(cid, null, expectedUserIds);
this._init(room);
this._fsm.joined();
} catch (err) {
await this.close();
throw err;
}
}
async joinRandom(matchProperties, expectedUserIds) {
if (matchProperties != null && !(typeof matchProperties === 'object')) {
throw new TypeError(`${matchProperties} is not an object`);
}
if (expectedUserIds !== null && !Array.isArray(expectedUserIds)) {
throw new TypeError(`${expectedUserIds} is not an array with string`);
}
this._fsm.join();
try {
const { _lobbyService } = this._client;
const { cid, addr } = await _lobbyService.joinRandomRoom(
matchProperties,
expectedUserIds
);
const { sessionToken } = await _lobbyService.authorize();
this._gameConn = new GameConnection();
const { _appId, _gameVersion, _userId } = this._client;
await this._gameConn.connect(
_appId,
addr,
_gameVersion,
_userId,
sessionToken
);
const room = await this._gameConn.joinRoom(cid, null, expectedUserIds);
this._init(room);
this._fsm.joined();
} catch (err) {
await this.close();
throw err;
}
}
async rejoin(roomName) {
if (!(typeof roomName === 'string')) {
throw new TypeError(`${roomName} is not a string`);
}
this._fsm.join();
try {
const { _lobbyService } = this._client;
const { cid, addr } = await _lobbyService.joinRoom({
roomName,
rejoin: true,
});
const { sessionToken } = await _lobbyService.authorize();
this._gameConn = new GameConnection();
const { _appId, _gameVersion, _userId } = this._client;
await this._gameConn.connect(
_appId,
addr,
_gameVersion,
_userId,
sessionToken
);
const room = await this._gameConn.joinRoom(cid);
this._init(room);
this._fsm.joined();
} catch (err) {
await this.close();
throw err;
}
}
async joinOrCreate(roomName, roomOptions, expectedUserIds) {
if (!(typeof roomName === 'string')) {
throw new TypeError(`${roomName} is not a string`);
}
if (roomOptions !== null && !(roomOptions instanceof Object)) {
throw new TypeError(`${roomOptions} is not a Object`);
}
if (expectedUserIds !== null && !Array.isArray(expectedUserIds)) {
throw new TypeError(`${expectedUserIds} is not an array with string`);
}
this._fsm.join();
try {
const { _lobbyService } = this._client;
const { cid, addr, roomCreated } = await _lobbyService.joinRoom({
roomName,
createOnNotFound: true,
});
const { sessionToken } = await _lobbyService.authorize();
this._gameConn = new GameConnection();
const { _appId, _gameVersion, _userId } = this._client;
await this._gameConn.connect(
_appId,
addr,
_gameVersion,
_userId,
sessionToken
);
// 根据返回确定是创建还是加入房间
let room = null;
if (roomCreated) {
room = await this._gameConn.createRoom(
cid,
roomOptions,
expectedUserIds
);
} else {
room = await this._gameConn.joinRoom(cid, null, expectedUserIds);
}
this._init(room);
this._fsm.joined();
} catch (err) {
await this.close();
throw err;
}
}
/**
* 离开房间
*/
async leave() {
this._client._room = null;
this._fsm.leave();
try {
await this._gameConn.leaveRoom();
} catch (e) {
this._fsm.leaveFailed();
throw e;
}
await this.close();
}
/**
* 关闭
*/
async close() {
if (this._fsm.cannot('close')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
if (this._gameConn) {
await this._gameConn.close();
}
this._client._room = null;
this._fsm.close();
}
/**
* 设置房间开启 / 关闭
* @param {Boolean} open 是否开启
*/
setOpen(open) {
if (!(typeof open === 'boolean')) {
throw new TypeError(`${open} is not a boolean value`);
}
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn.setRoomOpen(open).then(
tap(o => {
this._mergeSystemProps({ open: o });
})
);
}
/**
* 设置房间可见 / 不可见
* @param {Boolean} visible 是否可见
*/
setVisible(visible) {
if (!(typeof visible === 'boolean')) {
throw new TypeError(`${visible} is not a boolean value`);
}
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn.setRoomVisible(visible).then(
tap(v => {
this._mergeSystemProps({ visible: v });
})
);
}
/**
* 设置房间允许的最大玩家数量
* @param {*} count 数量
*/
setMaxPlayerCount(count) {
if (!(typeof count === 'number') || count < 1) {
throw new TypeError(`${count} is not a positive number`);
}
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn.setRoomMaxPlayerCount(count).then(
tap(c => {
this._mergeSystemProps({ maxPlayerCount: c });
})
);
}
/**
* 设置房间占位玩家 Id 列表
* @param {*} expectedUserIds 玩家 Id 列表
*/
setExpectedUserIds(expectedUserIds) {
if (!Array.isArray(expectedUserIds)) {
throw new TypeError(`${expectedUserIds} is not an array`);
}
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn.setRoomExpectedUserIds(expectedUserIds).then(
tap(ids => {
this._mergeSystemProps({ expectedUserIds: ids });
})
);
}
/**
* 清空房间占位玩家 Id 列表
*/
clearExpectedUserIds() {
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn.clearRoomExpectedUserIds().then(
tap(ids => {
this._mergeSystemProps({ expectedUserIds: ids });
})
);
}
/**
* 增加房间占位玩家 Id 列表
* @param {*} expectedUserIds 增加的玩家 Id 列表
*/
addExpectedUserIds(expectedUserIds) {
if (!Array.isArray(expectedUserIds)) {
throw new TypeError(`${expectedUserIds} is not an array`);
}
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn.addRoomExpectedUserIds(expectedUserIds).then(
tap(ids => {
this._mergeSystemProps({ expectedUserIds: ids });
})
);
}
/**
* 移除房间占位玩家 Id 列表
* @param {*} expectedUserIds 移除的玩家 Id 列表
*/
removeExpectedUserIds(expectedUserIds) {
if (!Array.isArray(expectedUserIds)) {
throw new TypeError(`${expectedUserIds} is not an array`);
}
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn.removeRoomExpectedUserIds(expectedUserIds).then(
tap(ids => {
this._mergeSystemProps({ expectedUserIds: ids });
})
);
}
/**
* 设置房主
* @param {Number} newMasterId 新房主 ID
*/
setMaster(newMasterId) {
if (!(typeof newMasterId === 'number')) {
throw new TypeError(`${newMasterId} is not a number`);
}
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn.setMaster(newMasterId).then(
tap(masterId => {
this._masterActorId = masterId;
})
);
}
/**
* 发送自定义消息
* @param {Number|String} eventId 事件 ID
* @param {Object} eventData 事件参数
* @param {Object} options 发送事件选项
* @param {ReceiverGroup} options.receiverGroup 接收组
* @param {Array.<Number>} options.targetActorIds 接收者 Id。如果设置,将会覆盖 receiverGroup
*/
sendEvent(
eventId,
eventData = {},
options = { receiverGroup: ReceiverGroup.All }
) {
if (!(typeof eventId === 'number')) {
throw new TypeError(`${eventId} is not a number`);
}
if (eventId < -128 || eventId > 127) {
throw new TypeError('eventId must be [-128, 127]');
}
if (!(typeof eventData === 'object')) {
throw new TypeError(`${eventData} is not an object`);
}
if (!(options instanceof Object)) {
throw new TypeError(`${options} is not a Object`);
}
if (
options.receiverGroup === undefined &&
options.targetActorIds === undefined
) {
throw new TypeError(`receiverGroup and targetActorIds are null`);
}
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn.sendEvent(eventId, eventData, options);
}
_init(roomData) {
this._name = roomData.getCid();
this._open = roomData.getOpen().getValue();
this._visible = roomData.getVisible().getValue();
this._maxPlayerCount = roomData.getMaxMembers();
this._masterActorId = roomData.getMasterActorId();
this._expectedUserIds = roomData.getExpectMembersList();
this._players = {};
roomData.getMembersList().forEach(member => {
const player = new Player(this);
player._init(member);
this._players[player.actorId] = player;
if (player._userId === this._client._userId) {
this._player = player;
}
});
// 属性
if (roomData.getAttr()) {
this._properties = deserializeObject(roomData.getAttr());
} else {
this._properties = {};
}
}
/**
* 房间名称
* @type {String}
* @readonly
*/
get name() {
return this._name;
}
/**
* 房间是否开启
* @type {Boolean}
* @readonly
*/
get open() {
return this._open;
}
/**
* 房间是否可见
* @type {Boolean}
* @readonly
*/
get visible() {
return this._visible;
}
/**
* 房间允许的最大玩家数量
* @type {Number}
* @readonly
*/
get maxPlayerCount() {
return this._maxPlayerCount;
}
/**
* 获取房主
* @type {Player}
* @readonly
*/
get master() {
return this.getPlayer(this.masterId);
}
/**
* 房间主机玩家 ID
* @type {Number}
* @readonly
*/
get masterId() {
return this._masterActorId;
}
/**
* 邀请的好友 ID 列表
* @type {Array.<String>}
* @readonly
*/
get expectedUserIds() {
return this._expectedUserIds;
}
/**
* 获取自定义属性
* @type {Object}
* @readonly
*/
get customProperties() {
return this._properties;
}
/**
* 根据 actorId 获取 Player 对象
* @param {Number} actorId 玩家在房间中的 Id
* @return {Player}
*/
getPlayer(actorId) {
if (typeof actorId !== 'number') {
throw new TypeError(`${actorId} is not a number`);
}
if (actorId === 0) return null;
const player = this._players[actorId];
if (player === null) {
throw new Error(`player with id:${actorId} not found`);
}
return player;
}
/**
* 获取房间内的玩家列表
* @type {Array.<Player>}
* @readonly
*/
get playerList() {
return Object.values(this._players);
}
/**
* 设置房间的自定义属性
* @param {Object} properties 自定义属性
* @param {Object} [opts] 设置选项
* @param {Object} [opts.expectedValues] 期望属性,用于 CAS 检测
*/
setCustomProperties(properties, { expectedValues = null } = {}) {
if (typeof properties !== 'object') {
throw new TypeError(`${properties} is not an object`);
}
if (expectedValues && typeof expectedValues !== 'object') {
throw new TypeError(`${expectedValues} is not an object`);
}
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn
.setRoomCustomProperties(properties, expectedValues)
.then(
tap(res => {
const { attr } = res;
if (attr) {
// 如果属性没变化,服务端则不会下发 attr 属性
this._mergeProperties(attr);
}
})
);
}
setPlayerProperties(actorId, properties, expectedValues) {
if (typeof actorId !== 'number') {
throw new TypeError(`${actorId} is not a number`);
}
if (typeof properties !== 'object') {
throw new TypeError(`${properties} is not an object`);
}
if (expectedValues && typeof expectedValues !== 'object') {
throw new TypeError(`${expectedValues} is not an object`);
}
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn.setPlayerCustomProperties(
actorId,
properties,
expectedValues
);
}
/**
* 踢人
* @param {Number} actorId 踢用户的 actorId
* @param {Object} [opts] 附带参数
* @param {Number} [opts.code] 编码
* @param {String} [opts.msg] 附带信息
*/
kickPlayer(actorId, { code = null, msg = null } = {}) {
if (typeof actorId !== 'number') {
throw new TypeError(`${actorId} is not a number`);
}
if (code && typeof code !== 'number') {
throw new TypeError(`${code} is not a number`);
}
if (msg && typeof msg !== 'string') {
throw new TypeError(`${msg} is not a string`);
}
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
return this._gameConn.kickPlayer(actorId, code, msg).then(
tap(aId => {
this._removePlayer(aId);
})
);
}
pauseMessageQueue() {
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
this._gameConn._pauseMessageQueue();
}
resumeMessageQueue() {
if (!this._fsm.is('game')) {
throw new PlayError(
PlayErrorCode.STATE_ERROR,
`Error state: ${this._fsm.state}`
);
}
this._gameConn._resumeMessageQueue();
}
_addPlayer(newPlayer) {
if (!(newPlayer instanceof Player)) {
throw new TypeError(`${newPlayer} is not a Player`);
}
this._players[newPlayer.actorId] = newPlayer;
}
_removePlayer(actorId) {
delete this._players[actorId];
}
_mergeProperties(changedProperties) {
this._properties = Object.assign(this._properties, changedProperties);
}
_mergeSystemProps(changedProps) {
const { open, visible, maxPlayerCount, expectedUserIds } = changedProps;
if (open !== undefined) {
this._open = open;
}
if (visible !== undefined) {
this._visible = visible;
}
if (maxPlayerCount !== undefined) {
this._maxPlayerCount = maxPlayerCount;
}
if (expectedUserIds !== undefined) {
this._expectedUserIds = expectedUserIds;
}
}
_simulateDisconnection() {
return this._gameConn._simulateDisconnection();
}
}