UNPKG

mm_os

Version:

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

616 lines (560 loc) 16.1 kB
const { cacheAdmin } = require('mm_cache'); const { sqlAdmin } = require('mm_sql'); const { Drive, Manager } = require('mm_machine'); const { Channel } = require('../channel/index.js'); const { Room } = require('../room/index.js'); const { World } = require('../world/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.manager = {}; // 游戏频道集合 this.channel = {}; // 游戏房间集合 this.room = {}; // 游戏世界集合 this.world = {}; // 玩家对象集合 - 分区统一管理所有玩家 this.players = new Map(); } } /** * 预设 */ Zone.prototype._preset = function () { // 分区专用数据库 this.sql = null; // 分区专用缓存 this.cache = null; }; /** * 获取模板目录 * @returns {string} 模板目录 */ Zone.prototype.getTplDir = function () { return __dirname; }; /** * 初始化核心 * @param {object} server 服务器实例 * @param {object} eventer 事件总线 * @param {object} logger 日志管理器 * @private */ Zone.prototype._initCore = async function (server, eventer, logger) { // 初始化依赖项 if (logger) { this.setLogger(logger); } if (eventer) { this.getEventer = function () { return eventer; }; } if (server) { this.getServer = function () { return server; }; } // 初始化基础组件 await this._initBase(); // 初始化管理器 await this._initManager(); // 加载资源 await this._loadSources(); }; /** * 初始化基础组件 * @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.getServer().sql; } // 初始化缓存 if (this.config.diy_cache) { this.cache = cacheAdmin(this.config.name, this.config.cache); await this.cache.connect(); } else { this.cache = this.getServer().cache; } }; /** * 创建管理器实例 * @param {string} name 管理器名称 * @param {string} title 管理器标题 * @param {Function} cls 管理器类 * @returns {object} 管理器实例 */ Zone.prototype._createManager = function (name, title, cls) { let dir = this.getDir(); var manager = new Manager({ name: name, title: title, filename: name + '.json', tpl_dir: `../${name}/`.fullname(__dirname), base_dir: '', dir: `./${name}`.fullname(dir) }, this, this[name], cls); this.manager[name] = manager; return manager; }; /** * 初始化管理器集合 * @returns {Promise<void>} 初始化完成 */ Zone.prototype._initManager = async function () { // 管理器配置列表 var mgr_configs = [ { name: 'channel', title: '频道', Module: Channel }, { name: 'room', title: '房间', Module: Room }, { name: 'world', title: '世界', Module: World } ]; // 管理器实例集合 var managers = []; // 使用for循环初始化所有管理器 for (var i = 0; i < mgr_configs.length; i++) { var config = mgr_configs[i]; var manager = this._createManager(config.name, config.title, config.Module); managers.push(manager); } // 并行执行所有管理器的初始化操作 var promises = []; for (var j = 0; j < managers.length; j++) { promises.push(managers[j].do('init')); } await Promise.all(promises); }; /** * 加载资源 * @returns {Promise<void>} 加载完成 */ Zone.prototype._loadSources = async function () { // 加载无需依赖关系,所以可以同时加载,内部并行执行 let loadPromises = [ this.manager.channel.runAll('load'), this.manager.room.runAll('load'), this.manager.world.runAll('load') ]; await Promise.all(loadPromises); }; /** * 初始化资源 * @returns {Promise<void>} 初始化完成 */ Zone.prototype._initSources = async function () { // 初始化顺序 world -> channel -> room,需要依赖,所以根据依赖关系自下而上初始化,内部并行执行 await this.manager.world.runAll('init', this, this.getEventer(), this.getLogger()); await this.manager.channel.runAll('init', this, this.getEventer(), this.getLogger()); await this.manager.room.runAll('init', this, this.getEventer(), this.getLogger()); }; /** * 启动资源 */ App.prototype._startSources = async function () { // 启动无顺序的管理器(保持并行执行) let promises = [ this.manager.channel.runAll('start'), this.manager.room.runAll('start'), this.manager.world.runAll('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.channel.get(player.channel_id)) { this.channel.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; }; /** * 加载所有登录玩家 */ 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: [] }; }; /** * 向指定玩家发送数据 * @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.channel) { if (this.channel[channel_id] && this.channel[channel_id].getStats) { var stats = this.channel[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.channel.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.channel.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.channel.get(player.channel_id); } return null; }; /** * 销毁分区 * @returns {Promise<void>} */ Zone.prototype._destroy = async function () { await this._savePlayers(); this.players.clear(); }; exports.Zone = Zone;