UNPKG

mm_os

Version:

MM_OS服务端架构,用于快速构建应用程序,支持网站建设、小程序后台、AI应用、物联网(IOT/AIOT)、游戏服务端等多种场景。

728 lines (653 loc) 18.9 kB
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;