Commit 40f7217e authored by Chunchi Che's avatar Chunchi Che

Merge branch 'feat/interface/duel_plate' into 'main'

Feat/interface/duel plate

See merge request mycard/Neos!14
parents f5654819 9d5ff8ec
neos-protobuf @ 867be3ce
Subproject commit e7cfaff59f5c1ed640e21c2257d0bd39b6a27648
Subproject commit 867be3cee1dfe449735e81a85aa887724e8067cb
This diff is collapsed.
import { ygoProPacket } from "./packet";
import { YgoProPacket } from "./packet";
import { ygopro } from "../idl/ocgcore";
import {
STOC_CHAT,
......@@ -12,6 +12,7 @@ import {
STOC_HAND_RESULT,
STOC_DECK_COUNT,
STOC_DUEL_START,
STOC_GAME_MSG,
} from "./protoDecl";
import StocChat from "./stoc/stocChat";
import StocJoinGame from "./stoc/stocJoinGame";
......@@ -22,6 +23,7 @@ import StocTypeChange from "./stoc/stocTypeChange";
import StocSelectHand from "./stoc/stocSelectHand";
import StocSelectTp from "./stoc/stocSelectTp";
import StocDeckCount from "./stoc/stocDeckCount";
import StocGameMsg from "./stoc/stocGameMsg/mod";
/*
* 将[`ygoProPacket`]对象转换成[`ygopro.YgoStocMsg`]对象
......@@ -30,7 +32,7 @@ import StocDeckCount from "./stoc/stocDeckCount";
* @returns The ygopro.YgoStocMsg object
*
* */
export function adaptStoc(packet: ygoProPacket): ygopro.YgoStocMsg {
export function adaptStoc(packet: YgoProPacket): ygopro.YgoStocMsg {
let pb = new ygopro.YgoStocMsg({});
switch (packet.proto) {
case STOC_JOIN_GAME: {
......@@ -87,6 +89,11 @@ export function adaptStoc(packet: ygoProPacket): ygopro.YgoStocMsg {
break;
}
case STOC_GAME_MSG: {
pb = new StocGameMsg(packet).upcast();
break;
}
default: {
break;
}
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket } from "../packet";
import { YgoProPacket } from "../packet";
import { CTOS_HAND_RESULT } from "../protoDecl";
/*
......@@ -9,7 +9,7 @@ import { CTOS_HAND_RESULT } from "../protoDecl";
*
* @usage - 告知服务端当前玩家的猜拳选择
* */
export default class CtosHandResultPacket extends ygoProPacket {
export default class CtosHandResultPacket extends YgoProPacket {
constructor(pb: ygopro.YgoCtosMsg) {
const handResult = pb.ctos_hand_result;
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket } from "../packet";
import { YgoProPacket } from "../packet";
import { CTOS_HS_READY } from "../protoDecl";
/*
......@@ -7,7 +7,7 @@ import { CTOS_HS_READY } from "../protoDecl";
*
* @usage - 告诉ygopro服务端当前玩家准备完毕
* */
export default class CtosHsReady extends ygoProPacket {
export default class CtosHsReady extends YgoProPacket {
constructor(_: ygopro.YgoCtosMsg) {
super(1, CTOS_HS_READY, new Uint8Array(0));
}
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket } from "../packet";
import { YgoProPacket } from "../packet";
import { CTOS_HS_START } from "../protoDecl";
/*
......@@ -7,7 +7,7 @@ import { CTOS_HS_START } from "../protoDecl";
*
* @usage - 开始游戏对局
* */
export default class CtosHsStartPacket extends ygoProPacket {
export default class CtosHsStartPacket extends YgoProPacket {
constructor(_: ygopro.YgoCtosMsg) {
super(1, CTOS_HS_START, new Uint8Array(0));
}
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket } from "../packet";
import { YgoProPacket } from "../packet";
import { CTOS_JOIN_GAME } from "../protoDecl";
import { strEncodeUTF16 } from "../util";
......@@ -13,7 +13,7 @@ import { strEncodeUTF16 } from "../util";
*
* @usage - 加入房间
* */
export default class CtosJoinGamePacket extends ygoProPacket {
export default class CtosJoinGamePacket extends YgoProPacket {
constructor(pb: ygopro.YgoCtosMsg) {
const joinGame = pb.ctos_join_game;
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket } from "../packet";
import { YgoProPacket } from "../packet";
import { CTOS_PLAYER_INFO } from "../protoDecl";
import { strEncodeUTF16 } from "../util";
......@@ -10,7 +10,7 @@ import { strEncodeUTF16 } from "../util";
*
* @usage - 告诉ygopro服务端当前玩家的昵称
* */
export default class CtosPlayerInfoPacket extends ygoProPacket {
export default class CtosPlayerInfoPacket extends YgoProPacket {
constructor(pb: ygopro.YgoCtosMsg) {
const player = pb.ctos_player_info.name;
const exData = strEncodeUTF16(player);
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket } from "../packet";
import { YgoProPacket } from "../packet";
import { CTOS_TP_RESULT } from "../protoDecl";
/*
......@@ -10,7 +10,7 @@ import { CTOS_TP_RESULT } from "../protoDecl";
* @usage - 告知服务端当前玩家的先后攻选择
*
* */
export default class CtosTpResultPacket extends ygoProPacket {
export default class CtosTpResultPacket extends YgoProPacket {
constructor(pb: ygopro.YgoCtosMsg) {
const tpResult = pb.ctos_tp_result;
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket } from "../packet";
import { YgoProPacket } from "../packet";
import { CTOS_UPDATE_DECK } from "../protoDecl";
const BYTES_PER_U32 = 4;
......@@ -16,7 +16,7 @@ const BYTES_PER_U32 = 4;
*
* @usage - 更新对局的卡组信息
* */
export default class CtosUpdateDeck extends ygoProPacket {
export default class CtosUpdateDeck extends YgoProPacket {
constructor(pb: ygopro.YgoCtosMsg) {
const updateDeck = pb.ctos_update_deck;
const main = updateDeck.main;
......
......@@ -8,7 +8,7 @@ const littleEndian: boolean = true;
const PACKET_MIN_LEN = 3;
// Ref: https://www.icode9.com/content-1-1341344.html
export class ygoProPacket {
export class YgoProPacket {
packetLen: number; // 数据包长度
proto: number; // ygopro协议标识
exData: Uint8Array; // 数据包内容
......@@ -40,7 +40,7 @@ export class ygoProPacket {
* 返回值可用于业务逻辑处理。
*
* */
static deserialize(array: ArrayBuffer): ygoProPacket {
static deserialize(array: ArrayBuffer): YgoProPacket {
try {
if (array.byteLength < PACKET_MIN_LEN) {
throw new Error(
......@@ -57,7 +57,7 @@ export class ygoProPacket {
const proto = dataView.getInt8(2);
const exData = array.slice(3, packetLen + 2);
return new ygoProPacket(packetLen, proto, new Uint8Array(exData));
return new YgoProPacket(packetLen, proto, new Uint8Array(exData));
}
}
......@@ -68,5 +68,5 @@ export interface StocAdapter {
export interface CtosAdapter {
readonly protobuf: ygopro.YgoCtosMsg;
downcast(): ygoProPacket;
downcast(): YgoProPacket;
}
......@@ -21,3 +21,6 @@ export const STOC_SELECT_TP = 4;
export const STOC_HAND_RESULT = 5;
export const STOC_DECK_COUNT = 9;
export const STOC_DUEL_START = 21;
export const STOC_GAME_MSG = 1;
export const MSG_START = 4;
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet";
import { YgoProPacket, StocAdapter } from "../packet";
/*
* STOC Chat
......@@ -9,10 +9,10 @@ import { ygoProPacket, StocAdapter } from "../packet";
*
* @usage - 更新聊天消息
* */
export default class chatAdapter implements StocAdapter {
packet: ygoProPacket;
export default class ChatAdapter implements StocAdapter {
packet: YgoProPacket;
constructor(packet: ygoProPacket) {
constructor(packet: YgoProPacket) {
this.packet = packet;
}
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet";
import { YgoProPacket, StocAdapter } from "../packet";
const LITTLE_ENDIAN = true;
const INT16_BYTE_OFFSET = 2;
......@@ -12,10 +12,10 @@ const INT16_BYTE_OFFSET = 2;
* @usage - 展示双方卡组信息
* */
export default class deckCountAdapter implements StocAdapter {
packet: ygoProPacket;
export default class DeckCountAdapter implements StocAdapter {
packet: YgoProPacket;
constructor(packet: ygoProPacket) {
constructor(packet: YgoProPacket) {
this.packet = packet;
}
......
/*
* STOC GameMsg协议Adapter逻辑
*
* */
import { ygopro } from "../../../idl/ocgcore";
import { YgoProPacket, StocAdapter } from "../../packet";
import { MSG_START } from "../../protoDecl";
import MsgStartAdapter from "./start";
/*
* STOC GameMsg
*
* @param function: unsigned chat - GameMsg协议的function编号
* @param data: binary bytes - GameMsg协议的数据
*
* @usage - 服务端告诉前端/客户端决斗对局中的UI展示逻辑
* */
export default class GameMsgAdapter implements StocAdapter {
packet: YgoProPacket;
constructor(packet: YgoProPacket) {
this.packet = packet;
}
upcast(): ygopro.YgoStocMsg {
const exData = this.packet.exData;
const dataView = new DataView(exData.buffer);
const func = dataView.getUint8(0);
const gameData = exData.slice(1);
const gameMsg = new ygopro.StocGameMessage({});
switch (func) {
case MSG_START: {
gameMsg.start = MsgStartAdapter(gameData);
break;
}
default: {
console.log("Unhandled GameMessage function=", func);
break;
}
}
return new ygopro.YgoStocMsg({
stoc_game_msg: gameMsg,
});
}
}
import { ygopro } from "../../../idl/ocgcore";
const LITTLE_ENDIAN = true;
const INT16_BYTE_OFFSET = 2;
const INT32_BYTE_OFFSET = 4;
/*
* MSG Start
*
* @param todo
*
* @usage - 服务端在决斗开始时告诉前端/客户端双方的基础信息
* */
export default (data: Uint8Array) => {
const dataView = new DataView(data.buffer);
// TODO: 对DataView包装下实现一个BufferIO类,便于解析二进制数据
const pT = dataView.getUint8(0);
const playerType =
(pT & 0xf) <= 0
? ygopro.StocGameMessage.MsgStart.PlayerType.FirstStrike
: (pT & 0xf0) > 0
? ygopro.StocGameMessage.MsgStart.PlayerType.Observer
: ygopro.StocGameMessage.MsgStart.PlayerType.SecondStrike;
let offset = 1;
if (dataView.byteLength > 17) {
// data长度大于17,会多传一个大师规则字段
const masterRule = dataView.getUint8(offset); // TODO
offset += 1;
}
const life1 = dataView.getInt32(offset, LITTLE_ENDIAN);
offset += INT32_BYTE_OFFSET;
const life2 = dataView.getInt32(offset, LITTLE_ENDIAN);
offset += INT32_BYTE_OFFSET;
const deckSize1 = dataView.getInt16(offset, LITTLE_ENDIAN);
offset += INT16_BYTE_OFFSET;
const extraSize1 = dataView.getInt16(offset, LITTLE_ENDIAN);
offset += INT16_BYTE_OFFSET;
const deckSize2 = dataView.getInt16(offset, LITTLE_ENDIAN);
offset += INT16_BYTE_OFFSET;
const extraSize2 = dataView.getInt16(offset, LITTLE_ENDIAN);
offset += INT16_BYTE_OFFSET;
return new ygopro.StocGameMessage.MsgStart({
playerType,
life1,
life2,
deckSize1,
deckSize2,
extraSize1,
extraSize2,
});
};
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet";
import { YgoProPacket, StocAdapter } from "../packet";
/*
* STOC HsPlayerChange
......@@ -8,10 +8,10 @@ import { ygoProPacket, StocAdapter } from "../packet";
*
* @usage - 更新玩家状态
* */
export default class hsPlayerChangeAdapter implements StocAdapter {
packet: ygoProPacket;
export default class HsPlayerChangeAdapter implements StocAdapter {
packet: YgoProPacket;
constructor(packet: ygoProPacket) {
constructor(packet: YgoProPacket) {
this.packet = packet;
}
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet";
import { YgoProPacket, StocAdapter } from "../packet";
import { UTF16_BUFFER_MAX_LEN } from "../util";
const UINT8_PER_UINT16 = 2;
......@@ -12,10 +12,10 @@ const UINT8_PER_UINT16 = 2;
*
* @usage - 有新玩家进入房间,更新状态
* */
export default class hsPlayerEnterAdapter implements StocAdapter {
packet: ygoProPacket;
export default class HsPlayerEnterAdapter implements StocAdapter {
packet: YgoProPacket;
constructor(packet: ygoProPacket) {
constructor(packet: YgoProPacket) {
this.packet = packet;
}
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet";
import { YgoProPacket, StocAdapter } from "../packet";
/*
* STOC HsWatchChange
......@@ -8,10 +8,10 @@ import { ygoProPacket, StocAdapter } from "../packet";
*
* @usage - 更新观战者数量
* */
export default class hsWatchChangeAdapter implements StocAdapter {
packet: ygoProPacket;
export default class HsWatchChangeAdapter implements StocAdapter {
packet: YgoProPacket;
constructor(packet: ygoProPacket) {
constructor(packet: YgoProPacket) {
this.packet = packet;
}
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet";
import { YgoProPacket, StocAdapter } from "../packet";
/*
* STOC JoinGame
*
* @usage - 告知客户端/前端已成功加入房间
* */
export default class joinGameAdapter implements StocAdapter {
packet: ygoProPacket;
export default class JoinGameAdapter implements StocAdapter {
packet: YgoProPacket;
constructor(packet: ygoProPacket) {
constructor(packet: YgoProPacket) {
this.packet = packet;
}
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet";
import { YgoProPacket, StocAdapter } from "../packet";
/*
* STOC SelectHand
*
* @usage - 通知客户端/前端提醒用户进行猜拳选择
* */
export default class selectHand implements StocAdapter {
packet: ygoProPacket;
export default class SelectHand implements StocAdapter {
packet: YgoProPacket;
constructor(packet: ygoProPacket) {
constructor(packet: YgoProPacket) {
this.packet = packet;
}
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet";
import { YgoProPacket, StocAdapter } from "../packet";
/*
* STOC SelectTp
*
* @usage - 通知客户端/前端提醒用户进行选先后攻
* */
export default class selectTp implements StocAdapter {
packet: ygoProPacket;
export default class SelectTp implements StocAdapter {
packet: YgoProPacket;
constructor(packet: ygoProPacket) {
constructor(packet: YgoProPacket) {
this.packet = packet;
}
......
import { ygopro } from "../../idl/ocgcore";
import { ygoProPacket, StocAdapter } from "../packet";
import { YgoProPacket, StocAdapter } from "../packet";
/*
* STOC TypeChange
......@@ -8,10 +8,10 @@ import { ygoProPacket, StocAdapter } from "../packet";
*
* @usage - 更新玩家状态
* */
export default class typeChangeAdapter implements StocAdapter {
packet: ygoProPacket;
export default class TypeChangeAdapter implements StocAdapter {
packet: YgoProPacket;
constructor(packet: ygoProPacket) {
constructor(packet: YgoProPacket) {
this.packet = packet;
}
......
import { ygopro } from "../../api/ocgcore/idl/ocgcore";
export default function handleGameMsg(pb: ygopro.YgoStocMsg) {
const msg = pb.stoc_game_msg;
switch (msg.gameMsg) {
case "start": {
// TODO
console.log(msg.start);
break;
}
default: {
console.log("Unhandled GameMsg=" + msg.gameMsg);
break;
}
}
}
......@@ -8,11 +8,12 @@ import handleHsPlayerEnter from "./room/hsPlayerEnter";
import handleJoinGame from "./room/joinGame";
import handleChat from "./room/chat";
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 handleSelectHand from "./mora/selectHand";
import handleSelectTp from "./mora/selectTp";
import handleDeckCount from "./mora/deckCount";
import handleGameMsg from "./duel/gameMsg";
/*
* 先将从长连接中读取到的二进制数据通过Adapter转成protobuf结构体,
......@@ -20,7 +21,7 @@ import handleDeckCount from "./mora/deckCount";
*
* */
export default function handleSocketMessage(e: MessageEvent) {
const packet = ygoProPacket.deserialize(e.data);
const packet = YgoProPacket.deserialize(e.data);
const pb = adaptStoc(packet);
switch (pb.msg) {
......@@ -81,6 +82,11 @@ export default function handleSocketMessage(e: MessageEvent) {
break;
}
case "stoc_game_msg": {
handleGameMsg(pb);
break;
}
default: {
console.log(packet);
......
/*
* 决斗界面渲染需要的数据结构
*
* */
export interface Card {
code: number; // Currently only code
}
/*
* 决斗界面的抽象接口
*
* - Neos项目采用UI渲染和数据获取相互解耦的设计,
* UI组件内部不应该存在任何与业务耦合的逻辑,
* 而是应该暴露数据注入的接口,供业务方调用;
* - Neos项目在研发初期,会更多的注重业务逻辑的实现,
* 而不会在UI界面上钻研过深,但在项目最终上线前,
* 会对UI界面提出较高的要求,因此在研发过程中可能会存在
* 多套UI界面。为了减少重复性工作,这里设计一个通用的
* 决斗界面抽象接口,接口实现方需要实现独立的,具体的渲染逻辑,
* 接口调用方不感知具体实现,只负责根据外部输入,
* 进行特定UI模块的渲染和更新。
*
* */
import * as DuelData from "./data";
import React from "react";
import type { RootState } from "../../store";
/*
* 通用的决斗界面抽象接口
*
* */
export interface IDuelPlate {
// 渲染接口,返回一个React组件
render(): React.ReactElement;
// 注册手牌selector
registerHands(selector: TypeSelector<DuelData.Card[]>): void;
}
export interface TypeSelector<T> {
(state: RootState): T;
}
/*
* 决斗页面
*
* */
import SimpleDuelPlateImpl from "./simpleDuel";
export default function Duel() {
return new SimpleDuelPlateImpl().render();
}
/*
* 一个简洁的决斗界面实现
*
* */
import { IDuelPlate, TypeSelector } from "./duel";
import * as DuelData from "./data";
import { useAppSelector } from "../../hook";
import React from "react";
import type { RootState } from "../../store";
export default class SimpleDuelPlateImpl implements IDuelPlate {
handsSelector?: TypeSelector<DuelData.Card[]>;
constructor() {}
render(): React.ReactElement {
// 默认的手牌Selector,返回三个code为-1的Card。
const defaultHandsSelector = (_: RootState) => {
return new Array(5).fill({ code: -1 });
};
const hands = useAppSelector(this.handsSelector || defaultHandsSelector);
return (
<div>
<table border={1}>
<tr>
{hands.map((hand) => (
<td>{hand.code}</td>
))}
</tr>
</table>
</div>
);
}
registerHands(selector: TypeSelector<DuelData.Card[]>): void {
this.handsSelector = selector;
}
}
......@@ -5,14 +5,16 @@ import ThreeJs from "./ThreeJs";
import BabylonJs from "./BabylonJs";
import { Routes, Route } from "react-router-dom";
import Mora from "./Mora";
import Duel from "./Duel/main";
export default function () {
// FIXME: 这里Mora路由应该由每个房间指定一个路径
// FIXME: 这里Mora/Duel路由应该由每个房间指定一个路径
return (
<Routes>
<Route path="/" element={<JoinRoom />} />
<Route path="/:player/:passWd/:ip" element={<WaitRoom />} />
<Route path="/mora" element={<Mora />} />
<Route path="/duel" element={<Duel />} />
<Route path="/three" element={<ThreeJs />} />
<Route path="/babylon" element={<BabylonJs />} />
</Routes>
......
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