You need to sign in or sign up before continuing.
Commit f2c72f63 authored by Chunchi Che's avatar Chunchi Che

Merge branch 'comment' into 'main'

Comment

See merge request !11
parents 8002bf11 2598467f
...@@ -6,6 +6,13 @@ export interface IDeck { ...@@ -6,6 +6,13 @@ export interface IDeck {
side?: number[]; side?: number[];
} }
/*
* 返回卡组资源。
*
* @param deck- 卡组名称
* @returns 卡组数据
*
* */
export async function fetchDeck(deck: string): Promise<IDeck> { export async function fetchDeck(deck: string): Promise<IDeck> {
const res = await axios.get<IDeck>("http://localhost:3030/deck/" + deck); const res = await axios.get<IDeck>("http://localhost:3030/deck/" + deck);
......
import { StocAdapter, ygoProPacket } from "./packet"; import { ygoProPacket } from "./packet";
import { ygopro } from "../idl/ocgcore"; import { ygopro } from "../idl/ocgcore";
import { import {
STOC_CHAT, STOC_CHAT,
...@@ -15,6 +15,13 @@ import StocHsPlayerChange from "./stoc/stocHsPlayerChange"; ...@@ -15,6 +15,13 @@ import StocHsPlayerChange from "./stoc/stocHsPlayerChange";
import StocHsWatchChange from "./stoc/stocHsWatchChange"; import StocHsWatchChange from "./stoc/stocHsWatchChange";
import StocTypeChange from "./stoc/stocTypeChange"; import StocTypeChange from "./stoc/stocTypeChange";
/*
* 将[`ygoProPacket`]对象转换成[`ygopro.YgoStocMsg`]对象
*
* @param packet - The ygoProPacket object
* @returns The ygopro.YgoStocMsg object
*
* */
export function adaptStoc(packet: ygoProPacket): ygopro.YgoStocMsg { export function adaptStoc(packet: ygoProPacket): ygopro.YgoStocMsg {
let pb = new ygopro.YgoStocMsg({}); let pb = new ygopro.YgoStocMsg({});
switch (packet.proto) { switch (packet.proto) {
......
...@@ -2,6 +2,11 @@ import { ygopro } from "../../idl/ocgcore"; ...@@ -2,6 +2,11 @@ import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket } from "../packet"; import { ygoProPacket } from "../packet";
import { CTOS_HS_READY } from "../protoDecl"; import { CTOS_HS_READY } from "../protoDecl";
/*
* CTOS HsReady
*
* @usage - 告诉ygopro服务端当前玩家准备完毕
* */
export default class CtosHsReady extends ygoProPacket { export default class CtosHsReady extends ygoProPacket {
constructor(_: ygopro.YgoCtosMsg) { constructor(_: ygopro.YgoCtosMsg) {
super(1, CTOS_HS_READY, new Uint8Array(0)); super(1, CTOS_HS_READY, new Uint8Array(0));
......
...@@ -2,6 +2,11 @@ import { ygopro } from "../../idl/ocgcore"; ...@@ -2,6 +2,11 @@ import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket } from "../packet"; import { ygoProPacket } from "../packet";
import { CTOS_HS_START } from "../protoDecl"; import { CTOS_HS_START } from "../protoDecl";
/*
* CTOS HsStart
*
* @usage - 开始游戏对局
* */
export default class CtosHsStartPacket extends ygoProPacket { export default class CtosHsStartPacket extends ygoProPacket {
constructor(_: ygopro.YgoCtosMsg) { constructor(_: ygopro.YgoCtosMsg) {
super(1, CTOS_HS_START, new Uint8Array(0)); super(1, CTOS_HS_START, new Uint8Array(0));
......
...@@ -3,6 +3,16 @@ import { ygoProPacket } from "../packet"; ...@@ -3,6 +3,16 @@ import { ygoProPacket } from "../packet";
import { CTOS_JOIN_GAME } from "../protoDecl"; import { CTOS_JOIN_GAME } from "../protoDecl";
import { strEncodeUTF16 } from "../util"; import { strEncodeUTF16 } from "../util";
/*
* CTOS JoinGame
*
* @param version: unsigned short - 版本号
* @param align: unsigned short - 对齐填充
* @param gameid: unsigned int - 永远是0
* @param passWd: [unsigned short; 20] - 房间密码
*
* @usage - 加入房间
* */
export default class CtosJoinGamePacket extends ygoProPacket { export default class CtosJoinGamePacket extends ygoProPacket {
constructor(pb: ygopro.YgoCtosMsg) { constructor(pb: ygopro.YgoCtosMsg) {
const joinGame = pb.ctos_join_game; const joinGame = pb.ctos_join_game;
......
...@@ -3,6 +3,13 @@ import { ygoProPacket } from "../packet"; ...@@ -3,6 +3,13 @@ import { ygoProPacket } from "../packet";
import { CTOS_PLAYER_INFO } from "../protoDecl"; import { CTOS_PLAYER_INFO } from "../protoDecl";
import { strEncodeUTF16 } from "../util"; import { strEncodeUTF16 } from "../util";
/*
* CTOS PlayerInfo
*
* @param player: [unsigned short; 20] - 玩家昵称
*
* @usage - 告诉ygopro服务端当前玩家的昵称
* */
export default class CtosPlayerInfoPacket extends ygoProPacket { export default class CtosPlayerInfoPacket extends ygoProPacket {
constructor(pb: ygopro.YgoCtosMsg) { constructor(pb: ygopro.YgoCtosMsg) {
const player = pb.ctos_player_info.name; const player = pb.ctos_player_info.name;
......
...@@ -4,6 +4,18 @@ import { CTOS_UPDATE_DECK } from "../protoDecl"; ...@@ -4,6 +4,18 @@ import { CTOS_UPDATE_DECK } from "../protoDecl";
const BYTES_PER_U32 = 4; const BYTES_PER_U32 = 4;
/*
* CTOS UpdateDeck
*
* @param main: unsigned int - 主卡组数目
* @param extra: unsigned int - 额外卡组数目
* @param side: unsigned int - 副卡组数目
* @param mainCards: [unsigned int; main] - 主卡组数据
* @param extraCards: [unsigned int; extra] - 额外卡组数据
* @param side: [unsigned int; side] - 副卡组数据
*
* @usage - 更新对局的卡组信息
* */
export default class CtosUpdateDeck extends ygoProPacket { export default class CtosUpdateDeck extends ygoProPacket {
constructor(pb: ygopro.YgoCtosMsg) { constructor(pb: ygopro.YgoCtosMsg) {
const updateDeck = pb.ctos_update_deck; const updateDeck = pb.ctos_update_deck;
......
/*
* Adapter模块的抽象层。
*
* */
import { ygopro } from "../idl/ocgcore"; import { ygopro } from "../idl/ocgcore";
const littleEndian: boolean = true; const littleEndian: boolean = true;
const PACKET_MIN_LEN = 3; const PACKET_MIN_LEN = 3;
// Ref: https://www.icode9.com/content-1-1341344.html
export class ygoProPacket { export class ygoProPacket {
packetLen: number; packetLen: number; // 数据包长度
proto: number; proto: number; // ygopro协议标识
exData: Uint8Array; exData: Uint8Array; // 数据包内容
constructor(packetLen: number, proto: number, exData: Uint8Array) { constructor(packetLen: number, proto: number, exData: Uint8Array) {
this.packetLen = packetLen; this.packetLen = packetLen;
...@@ -14,6 +19,11 @@ export class ygoProPacket { ...@@ -14,6 +19,11 @@ export class ygoProPacket {
this.exData = exData; this.exData = exData;
} }
/*
* 将[`ygoProPacket`]对象序列化,
* 返回的二进制数数组可通过长连接发送到ygopro服务端。
*
* */
serialize(): Uint8Array { serialize(): Uint8Array {
const array = new Uint8Array(this.packetLen + 2); const array = new Uint8Array(this.packetLen + 2);
const dataView = new DataView(array.buffer); const dataView = new DataView(array.buffer);
...@@ -25,6 +35,11 @@ export class ygoProPacket { ...@@ -25,6 +35,11 @@ export class ygoProPacket {
return array; return array;
} }
/*
* 将二进制数据反序列化成[`ygoProPacket`]对象,
* 返回值可用于业务逻辑处理。
*
* */
static deserialize(array: ArrayBuffer): ygoProPacket { static deserialize(array: ArrayBuffer): ygoProPacket {
try { try {
if (array.byteLength < PACKET_MIN_LEN) { if (array.byteLength < PACKET_MIN_LEN) {
......
/*
* Ygopro的协议标识声明。
*
* */
export const CTOS_PLAYER_INFO = 16; export const CTOS_PLAYER_INFO = 16;
export const CTOS_JOIN_GAME = 18; export const CTOS_JOIN_GAME = 18;
export const CTOS_UPDATE_DECK = 2; export const CTOS_UPDATE_DECK = 2;
......
import { ygopro } from "../../idl/ocgcore"; import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet"; import { ygoProPacket, StocAdapter } from "../packet";
/*
* STOC Chat
*
* @param player: unsigned short - 玩家编号
* @param message: [unsigned short] - 聊天消息文本
*
* @usage - 更新聊天消息
* */
export default class chatAdapter implements StocAdapter { export default class chatAdapter implements StocAdapter {
packet: ygoProPacket; packet: ygoProPacket;
......
import { ygopro } from "../../idl/ocgcore"; import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet"; import { ygoProPacket, StocAdapter } from "../packet";
/*
* STOC HsPlayerChange
*
* @param todo
*
* @usage - 更新玩家状态
* */
export default class hsPlayerChangeAdapter implements StocAdapter { export default class hsPlayerChangeAdapter implements StocAdapter {
packet: ygoProPacket; packet: ygoProPacket;
......
...@@ -4,6 +4,14 @@ import { UTF16_BUFFER_MAX_LEN } from "../util"; ...@@ -4,6 +4,14 @@ import { UTF16_BUFFER_MAX_LEN } from "../util";
const UINT8_PER_UINT16 = 2; const UINT8_PER_UINT16 = 2;
/*
* STOC HsPlayerEnter
*
* @param name: [unsigned short; 20] - 玩家昵称
* @param pos: unsigned chat - 玩家进入房间的位置
*
* @usage - 有新玩家进入房间,更新状态
* */
export default class hsPlayerEnterAdapter implements StocAdapter { export default class hsPlayerEnterAdapter implements StocAdapter {
packet: ygoProPacket; packet: ygoProPacket;
......
import { ygopro } from "../../idl/ocgcore"; import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet"; import { ygoProPacket, StocAdapter } from "../packet";
/*
* STOC HsWatchChange
*
* @param count: unsigned short - 观观者数量
*
* @usage - 更新观战者数量
* */
export default class hsWatchChangeAdapter implements StocAdapter { export default class hsWatchChangeAdapter implements StocAdapter {
packet: ygoProPacket; packet: ygoProPacket;
......
import { ygopro } from "../../idl/ocgcore"; import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet"; import { ygoProPacket, StocAdapter } from "../packet";
/*
* STOC JoinGame
*
* @usage - 告知客户端/前端已成功加入房间
* */
export default class joinGameAdapter implements StocAdapter { export default class joinGameAdapter implements StocAdapter {
packet: ygoProPacket; packet: ygoProPacket;
......
import { ygopro } from "../../idl/ocgcore"; import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet"; import { ygoProPacket, StocAdapter } from "../packet";
/*
* STOC TypeChange
*
* @param todo
*
* @usage - 更新玩家状态
* */
export default class typeChangeAdapter implements StocAdapter { export default class typeChangeAdapter implements StocAdapter {
packet: ygoProPacket; packet: ygoProPacket;
......
/*
* 一些基础函数。
*
* */
export const UTF16_BUFFER_MAX_LEN = 20; export const UTF16_BUFFER_MAX_LEN = 20;
const FILLING_TOKEN: number = 0xcccc; const FILLING_TOKEN: number = 0xcccc;
/*
* 将`string`类型字符串转成`utf-16`编码的二进制数组。
*
* @param str - The `string` type string
* @returns The `utf-16` `Uint8Array`
*
* */
export function strEncodeUTF16(str: string) { export function strEncodeUTF16(str: string) {
let buf = new ArrayBuffer(UTF16_BUFFER_MAX_LEN * 2); let buf = new ArrayBuffer(UTF16_BUFFER_MAX_LEN * 2);
let bufView = new Uint16Array(buf); let bufView = new Uint16Array(buf);
......
/*
* 一些发ygopro协议数据包的辅助函数,用于简化业务代码。
*
* */
import { ygopro } from "./idl/ocgcore"; import { ygopro } from "./idl/ocgcore";
import socketMiddleWare, { socketCmd } from "../../middleware/socket"; import socketMiddleWare, { socketCmd } from "../../middleware/socket";
import { IDeck } from "../Card"; import { IDeck } from "../Card";
...@@ -16,7 +20,7 @@ export function sendUpdateDeck(deck: IDeck) { ...@@ -16,7 +20,7 @@ export function sendUpdateDeck(deck: IDeck) {
}), }),
}); });
// 如果要实现UI层和Adapter层解耦,这里应该不感知具体Adapter类型 // FIXME: 如果要实现UI层和Adapter层解耦,这里应该不感知具体Adapter类型
const payload = new UpdateDeckAdapter(updateDeck).serialize(); const payload = new UpdateDeckAdapter(updateDeck).serialize();
socketMiddleWare({ cmd: socketCmd.SEND, payload }); socketMiddleWare({ cmd: socketCmd.SEND, payload });
...@@ -46,7 +50,7 @@ export function sendPlayerInfo(ws: WebSocket, player: string) { ...@@ -46,7 +50,7 @@ export function sendPlayerInfo(ws: WebSocket, player: string) {
name: player, name: player,
}), }),
}); });
const packet = new PlayerInfoAdapter(playerInfo); // todo: 需要收敛在一个层次里 const packet = new PlayerInfoAdapter(playerInfo);
ws.send(packet.serialize()); ws.send(packet.serialize());
} }
......
/*
* Neos是基于[React](https://reactjs.org/)和[Babylon.js](https://www.babylonjs.com/)框架
* 研发的Web版[Ygopro](https://github.com/Fluorohydride/ygopro),游戏王对战平台。
*
* - 路由管理:[React Router](https://reactrouter.com/en/main)
* - 状态管理:[Redux](https://redux.js.org/)
* - 3D渲染:[Babylon.js](https://www.babylonjs.com/)
*
* 项目整体架构分为以下模块:
* - UI模块:使用React组件和Babylon.js提供的渲染能力进行UI展示;
* - Service模块:一些具体业务逻辑的实现,通常是一些事件处理函数;
* - MiddleWare(中间件)模块:收敛Websocket长连接的处理逻辑;
* - Adapter模块:进行ygopro数据协议从二进制buffer到TypeScript结构体之间的转换;
* - Api模块:提供长连接以外请求网络数据的接口,比如获取卡组数据;
* - Reducer模块:进行全局的状态更新;
* - Store模块:存储全局状态。
*
* 在设计上各个模块之间都是解耦的,模块之间的依赖应该通过调用接口,而非调用实例。
* 在进行代码开发的时候需要注意这点。
*
* */
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./ui/App"; import App from "./ui/App";
......
/*
* Socket中间件
*
* 所有长连接/Websocket相关的逻辑都应该收敛在这里。
*
* */
import handleSocketOpen from "../service/onSocketOpen"; import handleSocketOpen from "../service/onSocketOpen";
import handleSocketMessage from "../service/onSocketMessage"; import handleSocketMessage from "../service/onSocketMessage";
export enum socketCmd { export enum socketCmd {
// 建立长连接
CONNECT, CONNECT,
// 断开长连接
DISCONNECT, DISCONNECT,
// 通过长连接发送数据
SEND, SEND,
} }
export interface socketAction { export interface socketAction {
cmd: socketCmd; cmd: socketCmd;
// 创建长连接需要业务方传入的数据
initInfo?: { initInfo?: {
ip: string; ip: string;
player: string; player: string;
passWd: string; passWd: string;
}; };
// 通过长连接发送的数据
payload?: Uint8Array; payload?: Uint8Array;
} }
let ws: WebSocket | null = null; let ws: WebSocket | null = null;
// FIXME: 应该有个返回值,告诉业务方本次请求的结果。比如建立长连接失败。
export default function (action: socketAction) { export default function (action: socketAction) {
switch (action.cmd) { switch (action.cmd) {
case socketCmd.CONNECT: { case socketCmd.CONNECT: {
......
/*
* Chat状态更新逻辑
*
* */
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "../store"; import { RootState } from "../store";
......
/*
* 加入房间状态更新逻辑
*
* */
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../store"; import { RootState } from "../store";
......
/*
* 进入房间的玩家状态更新逻辑
*
* */
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "../store"; import { RootState } from "../store";
......
/*
* 长连接消息事件订阅处理逻辑
*
* */
import handleHsPlayerChange from "./room/hsPlayerChange"; import handleHsPlayerChange from "./room/hsPlayerChange";
import handleTypeChange from "./room/typeChange"; import handleTypeChange from "./room/typeChange";
import handleHsPlayerEnter from "./room/hsPlayerEnter"; import handleHsPlayerEnter from "./room/hsPlayerEnter";
...@@ -7,6 +11,11 @@ import handleHsWatchChange from "./room/hsWatchChange"; ...@@ -7,6 +11,11 @@ import handleHsWatchChange from "./room/hsWatchChange";
import { ygoProPacket } from "../api/ocgcore/ocgAdapter/packet"; import { ygoProPacket } from "../api/ocgcore/ocgAdapter/packet";
import { adaptStoc } from "../api/ocgcore/ocgAdapter/adapter"; import { adaptStoc } from "../api/ocgcore/ocgAdapter/adapter";
/*
* 先将从长连接中读取到的二进制数据通过Adapter转成protobuf结构体,
* 然后再分发到各个处理函数中去处理。
*
* */
export default function handleSocketMessage(e: MessageEvent) { export default function handleSocketMessage(e: MessageEvent) {
const packet = ygoProPacket.deserialize(e.data); const packet = ygoProPacket.deserialize(e.data);
const pb = adaptStoc(packet); const pb = adaptStoc(packet);
......
/*
* 长连接建立事件订阅处理逻辑
*
* */
import { sendJoinGame, sendPlayerInfo } from "../api/ocgcore/ocgHelper"; import { sendJoinGame, sendPlayerInfo } from "../api/ocgcore/ocgHelper";
/*
* 长连接建立后,需要马上发送PlayerInfo和JoinGame两个数据包,
* 否则ygopro服务端超过2s后会自动断连。
*
* */
export default function handleSocketOpen( export default function handleSocketOpen(
ws: WebSocket | null, ws: WebSocket | null,
ip: string, ip: string,
......
/*
* 全局状态存储模块
* */
import { configureStore } from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import joinedReducer from "./reducers/joinSlice"; import joinedReducer from "./reducers/joinSlice";
import chatReducer from "./reducers/chatSlice"; import chatReducer from "./reducers/chatSlice";
......
// 测试用的Babylon.js demo页面
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import * as BABYLON from "@babylonjs/core"; import * as BABYLON from "@babylonjs/core";
......
/*
* 加入房间页面
*
* player: 玩家昵称;
* addr: IP地址;
* passWd: 房间密码。
*
* */
import React, { useState, ChangeEvent } from "react"; import React, { useState, ChangeEvent } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import "../css/JoinRoom.css"; import "../css/JoinRoom.css";
......
/*
* 等待房间页面
*
* */
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { fetchDeck } from "../api/Card"; import { fetchDeck } from "../api/Card";
...@@ -32,6 +36,7 @@ export default function WaitRoom() { ...@@ -32,6 +36,7 @@ export default function WaitRoom() {
useEffect(() => { useEffect(() => {
if (ip && player && player.length != 0 && passWd && passWd.length != 0) { if (ip && player && player.length != 0 && passWd && passWd.length != 0) {
// 页面第一次渲染时,通过socket中间件向ygopro服务端请求建立长连接
socketMiddleWare({ socketMiddleWare({
cmd: socketCmd.CONNECT, cmd: socketCmd.CONNECT,
initInfo: { initInfo: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment