mm_os
Version:
MM_OS服务端架构,用于快速构建应用程序,支持网站建设、小程序后台、AI应用、物联网(IOT/AIOT)、游戏服务端等多种场景。
728 lines (653 loc) • 18.9 kB
JavaScript
const {
cacheAdmin
} = require('mm_cache');
const {
sqlAdmin
} = require('mm_sql');
const {
Drive
} = require('mm_machine');
const { idGen } = require('../../../ulits/id_gen.js');
const {
GameChannel
} = require('../../channel/index.js');
/**
* 游戏分区
*/
class Zone extends Drive {
static config = {
// 区域名称
'name': 'default',
// 区域标题
'title': '枫叶区',
// 区域描述
'description': '枫叶区是一个区域',
// 区域主脚本 - 可自定义区域初始化,一般情况不需要定义
'main': '',
// 应用域
'scope': '',
// 区域状态 - 1 正常 0 禁用
'state': 1,
// 区域排序 - 数字越小越靠前
'sort': 100,
// 相关数据表
'table': {
// 玩家表名
'player': 'game_player'
},
// 频道数
'channels': 20,
// 自定义数据库 - 1 开启 0 关闭
'diy_sql': 0,
// 自定义缓存 - 1 开启 0 关闭
'diy_cache': 0,
// sql 配置 - 可以没有,没有则使用默认数据库
'sql': {
// 数据库方式
'way': 'mysql',
// 数据库主机
'host': '127.0.0.1',
// 数据库端口
'port': 3306,
// 数据库用户名
'username': 'root',
// 数据库密码
'password': 'Asd159357',
// 数据库名称
'database': 'mos'
},
// 缓存配置 - 可以没有,没有则使用默认缓存
'cache': {
// 缓存方式方式
'way': 'redis',
// 缓存主机
'host': '127.0.0.1',
// 缓存端口
'port': 6379,
// 缓存密码
'password': 'asd159357',
// 缓存数据库
'database': 0
}
};
/**
* 构造函数
* @param {object} config 配置参数
* @param {string} config.name 区域名称
* @param {string} config.title 区域标题
* @param {string} config.description 区域描述
* @param {string} config.main 区域主脚本
* @param {string} config.scope 应用域
* @param {number} config.state 区域状态
* @param {number} config.sort 区域排序
* @param {object} config.table 相关数据表
* @param {number} config.channels 频道数
* @param {number} config.diy_sql 自定义数据库
* @param {number} config.diy_cache 自定义缓存
* @param {object} config.sql sql 配置
* @param {object} config.cache 缓存配置
* @param {object} parent 父分区对象
*/
constructor(config, parent) {
super({ ...Zone.config, ...config }, parent);
// 游戏频道集合
this.channel = {};
this.room = {};
this.worlds = {};
// 玩家对象集合 - 分区统一管理所有玩家
this.players = new Map();
}
}
/**
* 预设
*/
Zone.prototype._preset = function () {
// 分区专用数据库
this.sql = null;
// 分区专用缓存
this.cache = null;
};
/**
* 获取模板目录
* @returns {string} 模板目录
*/
Zone.prototype.getTplDir = function () {
return __dirname;
};
/**
* 创建频道
* @param {number} channel_id 频道ID
* @param {object} config 频道配置
* @returns {object} 创建的频道对象
*/
Zone.prototype.createChannel = function (channel_id, config) {
let channel = new GameChannel({ scope: this.config.scope, ...config || {} }, this);
channel.channel_id = channel_id || this.channel.size + 1;
this.channel.set(channel.channel_id, channel);
return channel;
};
/**
* 初始化管理器
*/
Zone.prototype._initManager = async function () {
// 初始化游戏频道
for (let i = 1; i <= this.config.channels; i++) {
this.createChannel(i);
}
};
/**
* 加载资源
* @private
*/
Zone.prototype._loadSources = async function () {
// 并行加载游戏频道
var promises = [];
for (let key of this.channels.keys()) {
promises.push(this.channels.get(key).do('load'));
}
await Promise.all(promises);
};
/**
* 初始化资源
* @private
*/
Zone.prototype._initSources = async function () {
// 并行初始化游戏频道
var promises = [];
for (let o of this.channels.values()) {
promises.push(o.do('init', this, this.getEventer(), this.getLogger()));
}
await Promise.all(promises);
};
/**
* 初始化基础组件
* @private
*/
Zone.prototype._initBase = async function () {
// 初始化数据库
if (this.config.diy_sql) {
this.sql = sqlAdmin(this.config.name, this.config.sql);
await this.sql.open();
}
else {
this.sql = this.getApp().sql;
}
// 初始化缓存
if (this.config.diy_cache) {
this.cache = cacheAdmin(this.config.name, this.config.cache);
await this.cache.connect();
}
else {
this.cache = this.getApp().cache;
}
};
/**
* 初始化核心
* @param {object} app 游戏应用
* @param {object} eventer 事件总线
* @param {object} logger 日志管理器
* @returns {object} 返回分区对象
*/
Zone.prototype._initCore = async function (app, eventer, logger) {
// 初始化依赖项
if (logger) {
this.setLogger(logger);
}
if (eventer) {
this.getEventer = function () {
return eventer;
};
}
if (app) {
this.getApp = function () {
return app;
};
}
// // 初始化基础组件
// await this._initBase();
// // 初始化管理器
// await this._initManager();
// // 加载资源
// await this._loadSources();
return this;
};
/**
* 启动资源
* @private
*/
Zone.prototype._startSources = async function () {
// 并行启动游戏频道
var promises = [];
for (let key of this.channels.keys()) {
promises.push(this.channels.get(key).do('start'));
}
await Promise.all(promises);
};
/**
* 启动核心
* @private
*/
Zone.prototype._startCore = async function () {
// // 初始化资源
// await this._initSources();
// 加载所有登录玩家
await this._loadPlayers();
// 启动资源
await this._startSources();
};
/**
* 获取玩家对象
* @param {string} player_id 玩家ID
* @returns {object} 玩家对象
*/
Zone.prototype.getPlayer = function (player_id) {
// 参数校验
if (!player_id || typeof player_id !== 'string') {
throw new TypeError('玩家ID必须是有效字符串');
}
var player = this.players.get(player_id);
if (!player) {
throw new Error(`玩家 ${player_id} 不存在`);
}
return player;
};
/**
* 从分区移除玩家
* @param {string} player_id 玩家ID
* @returns {boolean} 是否移除成功
*/
Zone.prototype.delPlayer = function (player_id) {
// 参数校验
if (!player_id || typeof player_id !== 'string') {
throw new TypeError('玩家ID必须是有效字符串');
}
var player = this.players.get(player_id);
if (!player) {
throw new Error('玩家不在分区中');
}
// 如果玩家在某个频道中,先退出频道
if (player.channel_id && this.channels.get(player.channel_id)) {
this.channels.get(player.channel_id).leave(player_id);
}
// 从分区移除玩家
this.players.delete(player_id);
// 触发玩家离开分区事件
this.emitEvent('player_leave_zone', {
player_id: player_id,
zone_name: this.config.name,
timestamp: Date.now()
});
return true;
};
/**
* 玩家进入频道
* @param {string} player_id 玩家ID
* @param {number} channel_id 频道ID
* @returns {boolean} 是否进入成功
*/
Zone.prototype.enterChannel = async function (player_id, channel_id) {
// 参数校验
if (!player_id || typeof player_id !== 'string') {
throw new TypeError('玩家ID必须是有效字符串');
}
if (!channel_id || typeof channel_id !== 'number') {
throw new TypeError('频道ID必须是有效数字');
}
// 获取玩家对象
var player = this.getPlayer(player_id);
// 检查频道是否存在
if (!this.channels.has(channel_id)) {
throw new Error(`频道 ${channel_id} 不存在`);
}
// 如果玩家已在其他频道,先退出
if (player.channel_id && this.channels.get(player.channel_id)) {
await this.channels.get(player.channel_id).leave(player_id);
}
// 玩家进入频道
await this.channels.get(channel_id).enter(player_id);
// 更新玩家频道ID
player.channel_id = channel_id;
return true;
};
/**
* 切换频道
* @param {string} player_id 玩家ID
* @param {number} channel_id 频道ID
* @returns {boolean} 是否切换成功
*/
Zone.prototype.switchChannel = async function (player_id, channel_id) {
// 参数校验
if (!player_id || typeof player_id !== 'string') {
throw new TypeError('玩家ID必须是有效字符串');
}
if (!channel_id || typeof channel_id !== 'number') {
throw new TypeError('频道ID必须是有效数字');
}
// 获取玩家对象
var player = this.getPlayer(player_id);
// 检查目标频道是否存在
if (!this.channels[channel_id]) {
throw new Error(`频道 ${channel_id} 不存在`);
}
// 如果玩家已在目标频道,直接返回
if (player.channel_id === channel_id) {
return true;
}
// 退出当前频道
if (player.channel_id && this.channels.get(player.channel_id)) {
await this.channels.get(player.channel_id).leave(player_id);
}
// 进入目标频道
await this.channels.get(channel_id).enter(player_id);
// 更新玩家频道ID
player.channel_id = channel_id;
return true;
};
/**
* 加载所有登录玩家
*/
Zone.prototype._loadPlayers = async function () {
var players = await this.emitWait('load_players', {
zone_name: this.config.name,
zone: this
});
if (players && players.length > 0) {
for (var i = 0; i < players.length; i++) {
var info = players[i];
var player = this._model.player.create(info);
this.players.set(player.player_id, player);
}
}
};
/**
* 保存所有玩家数据
* @returns {Promise<object>} 保存结果
*/
Zone.prototype._savePlayers = async function () {
var all_players = Array.from(this.players.values());
if (all_players.length === 0) {
return { count: 0, success: true };
}
await this.emitWait('save_players', {
zone_name: this.config.name,
zone: this,
players: all_players
});
this.log('info', `保存所有玩家数据 ${all_players.length} 个`);
return { count: all_players.length, success: true };
};
/**
* 保存脏数据玩家
* @returns {Promise<object>} 保存结果
*/
Zone.prototype._saveDirtyPlayers = async function () {
var dirty_players = [];
for (var player of this.players.values()) {
if (player.isDirty && player.isDirty()) {
dirty_players.push(player);
}
}
if (dirty_players.length === 0) {
return { count: 0, success: true };
}
await this.emitWait('save_players', {
zone_name: this.config.name,
zone: this,
players: dirty_players
});
for (var i = 0; i < dirty_players.length; i++) {
dirty_players[i].isDirty(false);
}
this.log('debug', `保存脏数据玩家 ${dirty_players.length} 个`);
return { count: dirty_players.length, success: true };
};
/**
* 全分区数据发送
* @param {object} data 发送数据
* @param {string} msg_type 消息类型
* @param {string} msg_group 消息范围
* @param {string} sender_id 发送者ID
* @returns {object} 发送结果
*/
Zone.prototype.sendData = async function (data, msg_type = 'system', msg_group = 'world', sender_id = 'system') {
// 参数校验
if (!data || typeof data !== 'object') {
throw new TypeError('发送数据必须是有效对象');
}
// 创建发送对象
var msg = {
data: data,
msg_type: msg_type,
msg_group: msg_group,
sender_id: sender_id,
timestamp: Date.now(),
zone_name: this.config.name,
msg_id: this._generateMsgId()
};
// 触发全分区数据发送事件,由业务层处理具体的发送逻辑
this.emitEvent('zone_data_send', {
...msg,
players: this.players.size
});
this.log('debug', `全分区数据发送事件已触发: 玩家数 ${this.players.size}`);
// 返回默认结果
return {
players: this.players.size,
success_count: 0,
fail_count: 0,
failed_players: []
};
};
/**
* 生成发送ID
* @returns {string} 发送ID
*/
Zone.prototype._generateMsgId = function () {
return idGen.genId(this.config.name);
};
/**
* 频道数据发送
* @param {number} channel_id 频道ID
* @param {object} data 发送数据
* @param {string} msg_type 消息类型
* @param {string} msg_group 消息范围
* @param {string} sender_id 发送者ID
* @returns {object} 发送结果
*/
Zone.prototype.sendToChannel = async function (channel_id, data, msg_type = 'text', msg_group = 'channel', sender_id = 'system') {
// 参数校验
if (!channel_id || typeof channel_id !== 'number') {
throw new TypeError('频道ID必须是有效数字');
}
if (!data || typeof data !== 'object') {
throw new TypeError('发送数据必须是有效对象');
}
try {
// 检查频道是否存在
var channel = this.channels.get(channel_id);
if (!channel) {
throw new Error(`频道 ${channel_id} 不存在`);
}
// 创建发送对象
var send_obj = {
data: data,
msg_type: msg_type,
msg_group: msg_group,
sender_id: sender_id,
timestamp: Date.now(),
channel_id: channel_id,
zone_name: this.config.name,
msg_id: this._generateMsgId()
};
// 触发频道数据发送事件,由业务层处理具体的发送逻辑
this.emitEvent('channel_send', {
...send_obj
});
return {
players: channel.getPlayerNum(),
success: 0,
fail: 0,
failed_players: []
};
} catch (error) {
this.log('error', `频道数据发送失败: `, error);
throw error;
}
};
/**
* 向指定玩家发送数据
* @param {string} player_id 玩家ID
* @param {object} data 发送数据
* @param {string} msg_type 消息类型
* @param {string} msg_group 消息范围
* @param {string} sender_id 发送者ID
* @returns {boolean} 是否发送成功
*/
Zone.prototype.sendToPlayer = async function (player_id, data, msg_type = 'text', msg_group = 'private', sender_id = 'system') {
// 参数校验
if (!player_id || typeof player_id !== 'string') {
throw new TypeError('玩家ID必须是有效字符串');
}
if (!data || typeof data !== 'object') {
throw new TypeError('发送数据必须是有效对象');
}
try {
// 获取玩家对象
var player = this.getPlayer(player_id);
if (!player) {
throw new Error(`玩家 ${player_id} 不存在`);
}
// 触发私聊数据发送事件,由业务层处理具体的发送逻辑
this.emitEvent('private_send', {
data: data,
msg_type: msg_type,
msg_group: msg_group,
sender_id: sender_id,
timestamp: Date.now(),
player_id: player_id,
zone_name: this.config.name,
msg_id: this._generateMsgId()
});
return true;
} catch (error) {
this.log('error', `向玩家 ${player_id} 发送数据失败: `, error);
throw error;
}
};
/**
* 条件数据发送
* @param {Function} func 筛选函数
* @param {object} data 发送数据
* @param {string} msg_type 消息类型
* @param {string} msg_group 消息范围
* @param {string} sender_id 发送者ID
* @returns {object} 发送结果
*/
Zone.prototype.sendWithCondition = async function (func, data, msg_type = 'system', msg_group = 'world', sender_id = 'system') {
// 参数校验
if (typeof func !== 'function') {
throw new TypeError('筛选函数必须是有效函数');
}
if (!data || typeof data !== 'object') {
throw new TypeError('发送数据必须是有效对象');
}
try {
// 触发条件数据发送事件,由业务层处理具体的发送逻辑
this.emitEvent('conditional_send', {
data: data,
msg_type: msg_type,
msg_group: msg_group,
sender_id: sender_id,
timestamp: Date.now(),
zone_name: this.config.name,
msg_id: this._generateMsgId(),
condition: func.toString(),
players: this.players.size
});
// 返回默认结果
return {
players: this.players.size,
filtered_players: 0,
success_count: 0,
fail_count: 0,
failed_players: []
};
} catch (error) {
this.log('error', `条件数据发送失败: `, error);
throw error;
}
};
/**
* 获取分区统计信息
* @returns {object} 分区统计信息
*/
Zone.prototype.getStats = function () {
var channel_stats = {};
var players = 0;
// 统计各频道玩家数量
for (var channel_id in this.channels) {
if (this.channels[channel_id] && this.channels[channel_id].getStats) {
var stats = this.channels[channel_id].getStats();
channel_stats[channel_id] = stats;
players += stats.player || 0;
}
}
return {
zone_name: this.config.name,
players: this.players.size,
online: players,
channels: this.channels.size,
channel_stats: channel_stats,
current_time: Date.now()
};
};
/**
* 添加玩家到分区
* @param {object} player 玩家对象
* @returns {object} 玩家对象
*/
Zone.prototype.addPlayer = function (player) {
this.players.set(player.player_id, player);
// 触发玩家加入分区事件
this.emitEvent('player_enter_zone', {
player_id: player.player_id,
zone_name: this.config.name,
timestamp: Date.now()
});
return player;
};
/**
* 获取频道对象
* @param {string} channel_id 频道ID
* @returns {object} 频道对象
*/
Zone.prototype.getChannel = function (channel_id) {
return this.channels.get(channel_id);
};
/**
* 获取玩家所在频道
* @param {string} player_id 玩家ID
* @returns {object} 频道对象
*/
Zone.prototype.getChannelByPlayerId = function (player_id) {
var player = this.players.get(player_id);
if (player) {
return this.channels.get(player.channel_id);
}
return null;
};
/**
* 销毁分区
* @returns {Promise<void>}
*/
Zone.prototype._destroy = async function () {
await this._savePlayers();
for (var channel of this.channels.values()) {
if (channel.do) {
await channel.do('destroy');
}
}
this.players.clear();
this.channels.clear();
};
exports.Zone = Zone;