重构Api接口到Msg

This commit is contained in:
janing
2025-12-18 12:03:03 +08:00
parent 7301adbb43
commit c12e439add
15 changed files with 450 additions and 356 deletions

View File

@@ -80,6 +80,14 @@ Roguelike 游戏服务端开发任务追踪
- [x] 移动API实现 (ApiMove.ts) - [x] 移动API实现 (ApiMove.ts)
- [x] 位置验证和边界检查 - [x] 位置验证和边界检查
- [x] 登录时广播玩家加入 - [x] 登录时广播玩家加入
- [x] **架构重构从HTTP API到WebSocket消息** (2025-12-18)
- [x] 删除旧的API实现 (src/api目录)
- [x] 删除旧的API协议 (PtlLogin.ts, PtlMove.ts, PtlSend.ts)
- [x] 创建消息协议文件 (MsgReq*/MsgRes*)
- [x] 创建消息处理器 (src/msg目录)
- [x] 更新服务器配置为消息监听模式
- [x] 重新生成serviceProto.ts文件
- [x] 修复所有消息名称映射问题
### 进行中 ### 进行中
- 等待下一阶段开发... - 等待下一阶段开发...
@@ -87,6 +95,27 @@ Roguelike 游戏服务端开发任务追踪
### 待办事项 ### 待办事项
- 按照上述规划顺序实施开发 - 按照上述规划顺序实施开发
## 重要架构说明
### 🚨 消息协议设计规范 (2025-12-18更新)
**本项目使用WebSocket服务器后续所有接口设计必须使用消息Message方式而非API方式**
#### 消息命名规范:
- 请求消息:`MsgReq{功能名}.ts` (如MsgReqLogin.ts)
- 响应消息:`MsgRes{功能名}.ts` (如MsgResLogin.ts)
- 广播消息:`Msg{事件名}.ts` (如MsgPlayerMove.ts)
#### 消息处理器位置:
- 文件位置:`src/msg/MsgReq{功能名}.ts`
- 处理函数:使用 `MsgCall<MsgReq{功能名}>` 参数
- 响应方式:`call.conn.sendMsg('Res{功能名}', response)`
- 注册方式:`server.listenMsg('Req{功能名}', handler)`
#### 协议文件生成:
- 使用 `npm run proto` 自动生成 `serviceProto.ts`
- 禁止直接修改 `serviceProto.ts` 文件
- 协议文件会自动去掉 "Msg" 前缀进行注册
## 技术栈 ## 技术栈
- TSRPC 框架 - TSRPC 框架
- TypeScript - TypeScript
@@ -94,5 +123,6 @@ Roguelike 游戏服务端开发任务追踪
## 备注 ## 备注
- 本文档由 AI 助手维护 - 本文档由 AI 助手维护
- 更新日期: 2025-12-14 - 更新日期: 2025-12-18
- 开发过程中会根据实际情况调整任务优先级和细节 - 开发过程中会根据实际情况调整任务优先级和细节
- **重要**项目已从HTTP API架构迁移到WebSocket消息架构请严格按照消息协议规范开发

View File

@@ -1,26 +0,0 @@
import { ApiCall } from "tsrpc";
import { server } from "..";
import { ReqSend, ResSend } from "../shared/protocols/PtlSend";
// This is a demo code file
// Feel free to delete it
export default async function (call: ApiCall<ReqSend, ResSend>) {
// Error
if (call.req.content.length === 0) {
call.error('Content is empty')
return;
}
// Success
let time = new Date();
call.succ({
time: time
});
// Broadcast
server.broadcastMsg('Chat', {
content: call.req.content,
time: time
})
}

View File

@@ -1,7 +1,6 @@
import * as path from "path";
import { WsServer } from "tsrpc"; import { WsServer } from "tsrpc";
import { serviceProto } from './shared/protocols/serviceProto';
import { worldManager } from './managers/WorldManager'; import { worldManager } from './managers/WorldManager';
import { serviceProto } from './shared/protocols/serviceProto';
// Create the Server // Create the Server
export const server = new WsServer(serviceProto, { export const server = new WsServer(serviceProto, {
@@ -12,7 +11,11 @@ export const server = new WsServer(serviceProto, {
// Initialize before server start // Initialize before server start
async function init() { async function init() {
await server.autoImplementApi(path.resolve(__dirname, 'api')); // 不再使用API改为消息监听
// await server.autoImplementApi(path.resolve(__dirname, 'api'));
// 注册消息监听器
await registerMessageHandlers();
// 初始化游戏世界 // 初始化游戏世界
console.log('正在初始化游戏世界...'); console.log('正在初始化游戏世界...');
@@ -20,6 +23,23 @@ async function init() {
console.log('游戏世界初始化完成'); console.log('游戏世界初始化完成');
}; };
// 注册消息处理器
async function registerMessageHandlers() {
// 登录请求消息
const LoginHandler = await import('./msg/MsgReqLogin');
server.listenMsg('ReqLogin', LoginHandler.default);
// 移动请求消息
const MoveHandler = await import('./msg/MsgReqMove');
server.listenMsg('ReqMove', MoveHandler.default);
// 发送消息请求
const SendHandler = await import('./msg/MsgReqSend');
server.listenMsg('ReqSend', SendHandler.default);
console.log('消息处理器注册完成');
}
// Entry function // Entry function
async function main() { async function main() {
await init(); await init();

View File

@@ -1,74 +1,83 @@
import { ApiCall } from "tsrpc"; import { MsgCall } from "tsrpc";
import { ReqLogin, ResLogin, PlayerInfo } from "../shared/protocols/PtlLogin"; import { playerManager } from "../managers/PlayerManager";
import { playerManager } from "../managers/PlayerManager"; import { MsgReqLogin } from "../shared/protocols/MsgReqLogin";
import { MsgResLogin, PlayerInfo } from "../shared/protocols/MsgResLogin";
/**
* API /**
* *
* 1. *
* 2. * 1.
* 3. * 2.
* 4. * 3.
*/ * 4.
export default async function (call: ApiCall<ReqLogin, ResLogin>) { */
const { playerId, playerName } = call.req; export default async function (call: MsgCall<MsgReqLogin>) {
const { playerId, playerName } = call.msg;
// 验证玩家ID
if (!playerId || playerId.trim().length === 0) { // 验证玩家ID
call.error('玩家ID不能为空'); if (!playerId || playerId.trim().length === 0) {
return; call.conn.sendMsg('ResLogin', {
} success: false,
message: '玩家ID不能为空'
try { });
// 检查玩家是否已存在 return;
const isNewPlayer = !playerManager.hasPlayer(playerId); }
// 获取或创建玩家(自动处理注册和创角) try {
const player = playerManager.getOrCreatePlayer(playerId, playerName); // 检查玩家是否已存在
const isNewPlayer = !playerManager.hasPlayer(playerId);
// 设置玩家在线状态
playerManager.setPlayerOnline(playerId, call.conn.id); // 获取或创建玩家(自动处理注册和创角)
const player = playerManager.getOrCreatePlayer(playerId, playerName);
// 保存玩家ID到连接对象供其他API使用
(call.conn as any).playerId = playerId; // 设置玩家在线状态
playerManager.setPlayerOnline(playerId, call.conn.id);
// 转换为协议格式
const playerInfo: PlayerInfo = { // 保存玩家ID到连接对象供其他消息处理器使用
id: player.id, (call.conn as any).playerId = playerId;
name: player.name,
position: { ...player.position }, // 转换为协议格式
spawnPoint: { ...player.spawnPoint }, const playerInfo: PlayerInfo = {
hp: player.hp, id: player.id,
maxHp: player.maxHp, name: player.name,
isAlive: player.isAlive, position: { ...player.position },
createdAt: player.createdAt, spawnPoint: { ...player.spawnPoint },
lastLoginAt: player.lastLoginAt hp: player.hp,
}; maxHp: player.maxHp,
isAlive: player.isAlive,
// 广播玩家加入消息给其他在线玩家 createdAt: player.createdAt,
const { broadcastToAll } = await import('../utils/broadcast'); lastLoginAt: player.lastLoginAt
};
broadcastToAll('PlayerJoin', {
playerId: player.id, // 广播玩家加入消息给其他在线玩家
playerName: player.name, const { broadcastToAll } = await import('../utils/broadcast');
position: { ...player.position },
isNewPlayer, broadcastToAll('PlayerJoin', {
timestamp: Date.now() playerId: player.id,
}, call.conn.id); playerName: player.name,
position: { ...player.position },
// 返回成功结果 isNewPlayer,
call.succ({ timestamp: Date.now()
success: true, }, call.conn.id);
message: isNewPlayer ? '创建角色成功,欢迎来到游戏世界!' : '登录成功,欢迎回来!',
player: playerInfo, // 返回成功结果
isNewPlayer const response: MsgResLogin = {
}); success: true,
message: isNewPlayer ? '创建角色成功,欢迎来到游戏世界!' : '登录成功,欢迎回来!',
console.log(`玩家 ${player.name} (${playerId}) ${isNewPlayer ? '首次登录' : '登录成功'}`); player: playerInfo,
console.log(` 在线玩家数: ${playerManager.getOnlinePlayerCount()}`); isNewPlayer
};
} catch (error) {
console.error('登录失败:', error); call.conn.sendMsg('ResLogin', response);
call.error(`登录失败: ${error instanceof Error ? error.message : '未知错误'}`);
} console.log(`玩家 ${player.name} (${playerId}) ${isNewPlayer ? '首次登录' : '登录成功'}`);
} console.log(` 在线玩家数: ${playerManager.getOnlinePlayerCount()}`);
} catch (error) {
console.error('登录失败:', error);
call.conn.sendMsg('ResLogin', {
success: false,
message: `登录失败: ${error instanceof Error ? error.message : '未知错误'}`
});
}
}

View File

@@ -1,86 +1,104 @@
import { ApiCall } from "tsrpc"; import { MsgCall } from "tsrpc";
import { ReqMove, ResMove } from "../shared/protocols/PtlMove"; import { playerManager } from "../managers/PlayerManager";
import { playerManager } from "../managers/PlayerManager"; import { MsgPlayerMove } from "../shared/protocols/MsgPlayerMove";
import { broadcastToAll } from "../utils/broadcast"; import { MsgReqMove } from "../shared/protocols/MsgReqMove";
import { MsgPlayerMove } from "../shared/protocols/MsgPlayerMove"; import { broadcastToAll } from "../utils/broadcast";
/** /**
* API *
* 广 * 广
*/ */
export default async function (call: ApiCall<ReqMove, ResMove>) { export default async function (call: MsgCall<MsgReqMove>) {
const { x, y } = call.req; const { x, y } = call.msg;
// 从连接中获取玩家ID需要在登录时保存 // 从连接中获取玩家ID需要在登录时保存
const playerId = (call.conn as any).playerId; const playerId = (call.conn as any).playerId;
if (!playerId) { if (!playerId) {
call.error('未登录,请先登录'); call.conn.sendMsg('ResMove', {
return; success: false,
} message: '未登录,请先登录'
});
// 获取玩家信息 return;
const player = playerManager.getPlayer(playerId); }
if (!player) { // 获取玩家信息
call.error('玩家不存在'); const player = playerManager.getPlayer(playerId);
return;
} if (!player) {
call.conn.sendMsg('ResMove', {
// 检查玩家是否存活 success: false,
if (!player.isAlive) { message: '玩家不存在'
call.error('玩家已死亡,无法移动'); });
return; return;
} }
// 验证坐标是否为数字 // 检查玩家是否存活
if (typeof x !== 'number' || typeof y !== 'number') { if (!player.isAlive) {
call.error('坐标格式错误'); call.conn.sendMsg('ResMove', {
return; success: false,
} message: '玩家已死亡,无法移动'
});
// 验证坐标是否为整数 return;
if (!Number.isInteger(x) || !Number.isInteger(y)) { }
call.error('坐标必须为整数');
return; // 验证坐标是否为数字
} if (typeof x !== 'number' || typeof y !== 'number') {
call.conn.sendMsg('ResMove', {
try { success: false,
// 尝试更新玩家位置 message: '坐标格式错误'
const success = playerManager.updatePlayerPosition(playerId, x, y); });
return;
if (!success) { }
call.succ({
success: false, // 验证坐标是否为整数
message: '移动失败,位置无效或超出世界边界' if (!Number.isInteger(x) || !Number.isInteger(y)) {
}); call.conn.sendMsg('ResMove', {
return; success: false,
} message: '坐标必须为整数'
});
// 获取更新后的位置 return;
const newPosition = { x, y }; }
// 广播移动消息给所有其他玩家 try {
const moveMsg: MsgPlayerMove = { // 尝试更新玩家位置
playerId: player.id, const success = playerManager.updatePlayerPosition(playerId, x, y);
playerName: player.name,
position: newPosition, if (!success) {
timestamp: Date.now() call.conn.sendMsg('ResMove', {
}; success: false,
message: '移动失败,位置无效或超出世界边界'
broadcastToAll('MsgPlayerMove', moveMsg, call.conn.id); });
return;
// 返回成功结果 }
call.succ({
success: true, // 获取更新后的位置
message: '移动成功', const newPosition = { x, y };
position: newPosition
}); // 广播移动消息给所有其他玩家
const moveMsg: MsgPlayerMove = {
console.log(`玩家 ${player.name} 移动到 (${x}, ${y})`); playerId: player.id,
playerName: player.name,
} catch (error) { position: newPosition,
console.error('移动失败:', error); timestamp: Date.now()
call.error(`移动失败: ${error instanceof Error ? error.message : '未知错误'}`); };
}
} broadcastToAll('MsgPlayerMove', moveMsg, call.conn.id);
// 返回成功结果
call.conn.sendMsg('ResMove', {
success: true,
message: '移动成功',
position: newPosition
});
console.log(`玩家 ${player.name} 移动到 (${x}, ${y})`);
} catch (error) {
console.error('移动失败:', error);
call.conn.sendMsg('ResMove', {
success: false,
message: `移动失败: ${error instanceof Error ? error.message : '未知错误'}`
});
}
}

View File

@@ -0,0 +1,28 @@
import { MsgCall } from "tsrpc";
import { server } from "..";
import { MsgReqSend } from "../shared/protocols/MsgReqSend";
/**
* 发送消息处理器
*/
export default async function (call: MsgCall<MsgReqSend>) {
// 验证消息内容
if (call.msg.content.length === 0) {
call.conn.sendMsg('ResSend', {
time: new Date()
});
return;
}
// 返回成功响应
let time = new Date();
call.conn.sendMsg('ResSend', {
time: time
});
// 广播消息
server.broadcastMsg('Chat', {
content: call.msg.content,
time: time
});
}

View File

@@ -0,0 +1,11 @@
/**
* 登录请求消息
*/
export interface MsgReqLogin {
/** 玩家ID用于识别玩家 */
playerId: string;
/** 玩家昵称(可选,新玩家时使用) */
playerName?: string;
}

View File

@@ -0,0 +1,10 @@
/**
* 移动请求消息
*/
export interface MsgReqMove {
/** X坐标 */
x: number;
/** Y坐标 */
y: number;
}

View File

@@ -0,0 +1,7 @@
/**
* 发送消息请求
*/
export interface MsgReqSend {
/** 消息内容 */
content: string;
}

View File

@@ -1,61 +1,50 @@
import { Position } from './base'; import { Position } from './base';
/** /**
* *
*/ */
export interface ReqLogin { export interface PlayerInfo {
/** 玩家ID(用于识别玩家) */ /** 玩家ID */
playerId: string; id: string;
/** 玩家昵称(可选,新玩家时使用) */ /** 玩家昵称 */
playerName?: string; name: string;
}
/** 当前位置 */
/** position: Position;
*
*/ /** 出生点 */
export interface PlayerInfo { spawnPoint: Position;
/** 玩家ID */
id: string; /** 当前生命值 */
hp: number;
/** 玩家昵称 */
name: string; /** 最大生命值 */
maxHp: number;
/** 当前位置 */
position: Position; /** 是否存活 */
isAlive: boolean;
/** 出生点 */
spawnPoint: Position; /** 创建时间 */
createdAt: number;
/** 当前生命值 */
hp: number; /** 最后登录时间 */
lastLoginAt: number;
/** 最大生命值 */ }
maxHp: number;
/**
/** 是否存活 */ *
isAlive: boolean; */
export interface MsgResLogin {
/** 创建时间 */ /** 是否成功 */
createdAt: number; success: boolean;
/** 最后登录时间 */ /** 消息 */
lastLoginAt: number; message: string;
}
/** 玩家信息 */
/** player?: PlayerInfo;
*
*/ /** 是否新玩家 */
export interface ResLogin { isNewPlayer?: boolean;
/** 是否成功 */ }
success: boolean;
/** 消息 */
message: string;
/** 玩家信息 */
player?: PlayerInfo;
/** 是否新玩家 */
isNewPlayer?: boolean;
}

View File

@@ -0,0 +1,15 @@
import { Position } from './base';
/**
* 移动响应消息
*/
export interface MsgResMove {
/** 是否成功 */
success: boolean;
/** 消息 */
message: string;
/** 新位置(成功时返回) */
position?: Position;
}

View File

@@ -0,0 +1,7 @@
/**
* 发送消息响应
*/
export interface MsgResSend {
/** 发送时间 */
time: Date;
}

View File

@@ -1,26 +0,0 @@
import { Position } from './base';
/**
* 移动请求
*/
export interface ReqMove {
/** 目标位置 X 坐标 */
x: number;
/** 目标位置 Y 坐标 */
y: number;
}
/**
* 移动响应
*/
export interface ResMove {
/** 是否成功 */
success: boolean;
/** 消息 */
message?: string;
/** 实际移动后的位置 */
position?: Position;
}

View File

@@ -1,10 +0,0 @@
// This is a demo code file
// Feel free to delete it
export interface ReqSend {
content: string
}
export interface ResSend {
time: Date
}

View File

@@ -2,34 +2,32 @@ import { ServiceProto } from 'tsrpc-proto';
import { MsgChat } from './MsgChat'; import { MsgChat } from './MsgChat';
import { MsgPlayerJoin } from './MsgPlayerJoin'; import { MsgPlayerJoin } from './MsgPlayerJoin';
import { MsgPlayerMove } from './MsgPlayerMove'; import { MsgPlayerMove } from './MsgPlayerMove';
import { ReqLogin, ResLogin } from './PtlLogin'; import { MsgReqLogin } from './MsgReqLogin';
import { ReqMove, ResMove } from './PtlMove'; import { MsgReqMove } from './MsgReqMove';
import { ReqSend, ResSend } from './PtlSend'; import { MsgReqSend } from './MsgReqSend';
import { MsgResLogin } from './MsgResLogin';
import { MsgResMove } from './MsgResMove';
import { MsgResSend } from './MsgResSend';
export interface ServiceType { export interface ServiceType {
api: { api: {
"Login": {
req: ReqLogin,
res: ResLogin
},
"Move": {
req: ReqMove,
res: ResMove
},
"Send": {
req: ReqSend,
res: ResSend
}
}, },
msg: { msg: {
"Chat": MsgChat, "Chat": MsgChat,
"PlayerJoin": MsgPlayerJoin, "PlayerJoin": MsgPlayerJoin,
"PlayerMove": MsgPlayerMove "PlayerMove": MsgPlayerMove,
"ReqLogin": MsgReqLogin,
"ReqMove": MsgReqMove,
"ReqSend": MsgReqSend,
"ResLogin": MsgResLogin,
"ResMove": MsgResMove,
"ResSend": MsgResSend
} }
} }
export const serviceProto: ServiceProto<ServiceType> = { export const serviceProto: ServiceProto<ServiceType> = {
"version": 3, "version": 5,
"services": [ "services": [
{ {
"id": 0, "id": 0,
@@ -47,19 +45,34 @@ export const serviceProto: ServiceProto<ServiceType> = {
"type": "msg" "type": "msg"
}, },
{ {
"id": 2, "id": 6,
"name": "Login", "name": "ReqLogin",
"type": "api" "type": "msg"
}, },
{ {
"id": 5, "id": 7,
"name": "Move", "name": "ReqMove",
"type": "api" "type": "msg"
}, },
{ {
"id": 1, "id": 8,
"name": "Send", "name": "ReqSend",
"type": "api" "type": "msg"
},
{
"id": 9,
"name": "ResLogin",
"type": "msg"
},
{
"id": 10,
"name": "ResMove",
"type": "msg"
},
{
"id": 11,
"name": "ResSend",
"type": "msg"
} }
], ],
"types": { "types": {
@@ -176,18 +189,18 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlLogin/ReqLogin": { "MsgReqLogin/MsgReqLogin": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
"id": 1, "id": 0,
"name": "playerId", "name": "playerId",
"type": { "type": {
"type": "String" "type": "String"
} }
}, },
{ {
"id": 2, "id": 1,
"name": "playerName", "name": "playerName",
"type": { "type": {
"type": "String" "type": "String"
@@ -196,7 +209,38 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlLogin/ResLogin": { "MsgReqMove/MsgReqMove": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
},
"MsgReqSend/MsgReqSend": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
}
]
},
"MsgResLogin/MsgResLogin": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
@@ -214,16 +258,16 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
}, },
{ {
"id": 4, "id": 2,
"name": "player", "name": "player",
"type": { "type": {
"type": "Reference", "type": "Reference",
"target": "PtlLogin/PlayerInfo" "target": "MsgResLogin/PlayerInfo"
}, },
"optional": true "optional": true
}, },
{ {
"id": 5, "id": 3,
"name": "isNewPlayer", "name": "isNewPlayer",
"type": { "type": {
"type": "Boolean" "type": "Boolean"
@@ -232,7 +276,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlLogin/PlayerInfo": { "MsgResLogin/PlayerInfo": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
@@ -302,26 +346,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlMove/ReqMove": { "MsgResMove/MsgResMove": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
},
"PtlMove/ResMove": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
@@ -336,8 +361,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
"name": "message", "name": "message",
"type": { "type": {
"type": "String" "type": "String"
}, }
"optional": true
}, },
{ {
"id": 2, "id": 2,
@@ -350,19 +374,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"PtlSend/ReqSend": { "MsgResSend/MsgResSend": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
}
]
},
"PtlSend/ResSend": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {