Commit 19653d6a authored by Chunchi Che's avatar Chunchi Che

Merge branch 'feat/replay' into 'main'

Feat/replay

See merge request mycard/Neos!244
parents f15b790f d31f77a7
Pipeline #22627 passed with stages
in 14 minutes and 11 seconds
Subproject commit 2b7892f461be231b95ccc3c13512359177f3bb98
Subproject commit 30f4ea7acd79b9cb18a358548520ca939e22dc5f
......@@ -10,6 +10,7 @@
"cardImgUrl":"https://cdn02.moecube.com:444/images/ygopro-images-zh-CN",
"cardsDbUrl":"https://cdn02.moecube.com:444/ygopro-database/zh-CN/cards.cdb",
"stringsUrl":"https://cdn02.moecube.com:444/ygopro-database/zh-CN/strings.conf",
"replayUrl":"replay.neos.moe",
"chainALL":false,
"streamInterval":20,
"startDelay":1000,
......@@ -68,6 +69,9 @@
164,
165,
170,
180
180,
230,
231,
236
]
}
......@@ -10,6 +10,7 @@
"cardImgUrl":"https://cdn02.moecube.com:444/images/ygopro-images-zh-CN",
"cardsDbUrl":"https://cdn02.moecube.com:444/ygopro-database/zh-CN/cards.cdb",
"stringsUrl":"https://cdn02.moecube.com:444/ygopro-database/zh-CN/strings.conf",
"replayUrl":"replay.neos.moe",
"chainALL":false,
"streamInterval":20,
"startDelay":1000,
......@@ -68,6 +69,9 @@
164,
165,
170,
180
180,
230,
231,
236
]
}
This diff is collapsed.
......@@ -69,3 +69,4 @@ export const MSG_FIELD_DISABLED = 56;
export const MSG_HAND_RES = 133;
export const MSG_SHUFFLE_HAND = 33;
export const MSG_SHUFFLE_EXTRA = 39;
export const MSG_SIBYL_NAME = 235;
......@@ -37,6 +37,7 @@ import MsgSelectTributeAdapter from "./selectTribute";
import MsgSelectUnselectCardAdapter from "./selectUnselectCard";
import MsgShuffleHandExtraAdapter from "./shuffleHandExtra";
import MsgShuffleSetCard from "./shuffleSetCard";
import MsgSibylNameAdapter from "./sibylName";
import MsgSortCard from "./sortCard";
import MsgStartAdapter from "./start";
import MsgTossAdapter from "./toss";
......@@ -273,6 +274,11 @@ export default class GameMsgAdapter implements StocAdapter {
break;
}
case GAME_MSG.MSG_SIBYL_NAME: {
gameMsg.sibyl_name = MsgSibylNameAdapter(gameData);
break;
}
default: {
gameMsg.unimplemented = new ygopro.StocGameMessage.MsgUnimplemented({
command: func,
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import MsgSibylName = ygopro.StocGameMessage.MsgSibylName;
const LEN = 100;
/*
* Msg Sibyl Name
* @param - TODO
*
* @usage - Replay模式获取对战双方的昵称
* */
export default (data: Uint8Array) => {
const decoder = new TextDecoder("utf-16");
let offset = 0;
const name_0 = cutString(decoder.decode(data.slice(offset, offset + LEN)));
offset += LEN;
const name_0_tag = cutString(
decoder.decode(data.slice(offset, offset + LEN))
);
offset += LEN;
const name_0_c = cutString(decoder.decode(data.slice(offset, offset + LEN)));
offset += LEN;
const name_1 = cutString(decoder.decode(data.slice(offset, offset + LEN)));
offset += LEN;
const name_1_tag = cutString(
decoder.decode(data.slice(offset, offset + LEN))
);
offset += LEN;
const name_1_c = cutString(decoder.decode(data.slice(offset, offset + LEN)));
return new MsgSibylName({
name_0,
name_0_tag,
name_0_c,
name_1,
name_1_tag,
name_1_c,
});
};
function cutString(str: string): string {
const end = str.indexOf("\0");
return str.substring(0, end);
}
......@@ -26,6 +26,11 @@ export interface socketAction {
player: string;
passWd: string;
};
isReplay?: boolean; // 是否是回放模式
replayInfo?: {
Url: string; // 提供回放服务的地址
data: ArrayBuffer; // 回放数据
};
// 通过长连接发送的数据
payload?: Uint8Array;
}
......@@ -36,12 +41,20 @@ let ws: WebSocketStream | null = null;
export default async function (action: socketAction) {
switch (action.cmd) {
case socketCmd.CONNECT: {
const info = action.initInfo;
const { initInfo: info, isReplay, replayInfo } = action;
if (info) {
ws = new WebSocketStream(info.ip, (conn, _event) =>
handleSocketOpen(conn, info.ip, info.player, info.passWd)
);
await ws.execute(handleSocketMessage);
} else if (isReplay && replayInfo) {
ws = new WebSocketStream(replayInfo.Url, (conn, _event) => {
console.info("replay websocket open.");
conn.binaryType = "arraybuffer";
conn.send(replayInfo.data);
});
await ws.execute(handleSocketMessage);
}
......
import { ygopro } from "@/api";
import { sleep } from "@/infra";
import { matStore } from "@/stores";
import { showWaiting } from "@/ui/Duel/Message";
import onAnnounce from "./announce";
......@@ -40,6 +41,7 @@ import onMsgSet from "./set";
import onMsgShuffleDeck from "./shuffleDeck";
import onMsgShuffleHandExtra from "./shuffleHandExtra";
import onMsgShuffleSetCard from "./shuffleSetCard";
import onMsgSibylName from "./sibylName";
import onMsgSortCard from "./sortCard";
import onMsgSpSummoned from "./spSummoned";
import onMsgSpSummoning from "./spSummoning";
......@@ -69,6 +71,24 @@ const ActiveList = [
"select_yes_no",
];
const ReplayIgnoreMsg = [
"select_idle_cmd",
"select_place",
"select_card",
"select_chain",
"select_effect_yn",
"select_position",
"select_option",
"select_battle_cmd",
"select_unselect_card",
"select_yes_no",
"select_tribute",
"select_counter",
"rock_paper_scissors",
"sort_card",
"announce",
];
let animation: Promise<unknown> = new Promise<void>((rs) => rs());
export default async function handleGameMsg(pb: ygopro.YgoStocMsg) {
......@@ -83,6 +103,8 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
showWaiting(false);
}
if (matStore.isReplay && ReplayIgnoreMsg.includes(msg.gameMsg)) return;
switch (msg.gameMsg) {
case "start": {
await onMsgStart(msg.start);
......@@ -345,6 +367,11 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break;
}
case "sibyl_name": {
onMsgSibylName(msg.sibyl_name);
break;
}
case "unimplemented": {
onUnimplemented(msg.unimplemented);
......
import { ygopro } from "@/api";
import { playerStore } from "@/stores";
type MsgSibylName = ygopro.StocGameMessage.MsgSibylName;
export default (sibylName: MsgSibylName) => {
playerStore.getMePlayer().name = sibylName.name_0;
playerStore.getOpPlayer().name = sibylName.name_1;
};
......@@ -7,6 +7,7 @@ import { fetchCard, ygopro } from "@/api";
import { useConfig } from "@/config";
import { sleep } from "@/infra";
import { cardStore, CardType, matStore } from "@/stores";
import { replayStart } from "@/ui/Replay";
const TOKEN_SIZE = 13; // 每人场上最多就只可能有13个token
export default async (start: ygopro.StocGameMessage.MsgStart) => {
......@@ -72,7 +73,11 @@ export default async (start: ygopro.StocGameMessage.MsgStart) => {
// 设置自己的额外卡组,信息是在waitroom之中拿到的
cardStore
.at(ygopro.CardZone.EXTRA, 1 - opponent)
.forEach((card) => (card.code = myExtraDeckCodes.pop()!));
.forEach((card) => (card.code = myExtraDeckCodes.pop() ?? 0));
if (matStore.isReplay) {
replayStart();
}
// 初始化完后,sleep 1s,让UI初始化完成,
// 否则在和AI对战时,由于后端给传给前端的`MSG`频率太高,会导致一些问题。
......
......@@ -80,6 +80,7 @@ export const matStore: MatState = proxy<MatState>({
enableM2: false, // 允许进入M2阶段
enableEp: false, // 允许回合结束
},
isReplay: false,
unimplemented: 0,
handResults: {
me: 0,
......
......@@ -28,6 +28,8 @@ export interface MatState {
phase: PhaseState;
isReplay: boolean; // 是否是回放模式
result?: {
isWin: boolean;
reason: string;
......
......@@ -9,6 +9,7 @@
import "../styles/core.scss";
import { Input } from "antd";
import Link from "antd/es/typography/Link";
import React, { ChangeEvent, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
......@@ -88,6 +89,9 @@ export default function Login() {
<a href="https://doc.neos.moe">Player Guide</a>
<span className="fa fa-arrow-right"></span>
</p>
<p>
<Link href="replay">Clik here to play ygo replay</Link>
</p>
</div>
<div className="sign-in__actions clearfix">
<ul>
......
......@@ -7,6 +7,7 @@ const Login = React.lazy(() => import("./Login"));
const WaitRoom = React.lazy(() => import("./WaitRoom"));
const Mora = React.lazy(() => import("./Mora"));
const NeosDuel = React.lazy(() => import("./Duel/Main"));
const Replay = React.lazy(() => import("./Replay"));
export default function () {
return (
......@@ -28,6 +29,14 @@ export default function () {
</Suspense>
}
/>
<Route
path="/replay"
element={
<Suspense fallback={<Loading />}>
<Replay />
</Suspense>
}
/>
<Route
path="/duel/:player/:passWd/:ip"
element={
......
import "../../styles/core.scss";
import { UploadOutlined } from "@ant-design/icons";
import { Button, message, Modal, Upload, UploadProps } from "antd";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import rustInit from "rust-src";
import { proxy, useSnapshot } from "valtio";
import { initStrings } from "@/api";
import { useConfig } from "@/config";
import socketMiddleWare, { socketCmd } from "@/middleware/socket";
import sqliteMiddleWare, { sqliteCmd } from "@/middleware/sqlite";
import { matStore } from "@/stores";
const NeosConfig = useConfig();
const localStore = proxy({
hasStart: false,
});
const ReplayModal: React.FC = () => {
const { hasStart } = useSnapshot(localStore);
const [replay, setReplay] = useState<null | ArrayBuffer>(null);
const uploadProps: UploadProps = {
name: "replay",
onChange(info) {
info.file.status = "done";
},
beforeUpload(file, _) {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = (e) => setReplay(e.target?.result as ArrayBuffer);
},
};
const navigate = useNavigate();
useEffect(() => {
if (hasStart) {
// 跳转
navigate(`/duel/neos/replay/${NeosConfig.replayUrl}`);
}
}, [hasStart]);
return (
<Modal
title="选择回放"
open={true}
maskClosable={false}
onOk={async () => {
if (replay === null) {
message.error("请先上传录像文件");
} else {
// 标记为回放模式
matStore.isReplay = true;
// 初始化wasm
const url =
import.meta.env.BASE_URL === "/"
? undefined
: new URL(
"rust_src_bg.wasm",
`${import.meta.env.BASE_URL}assets/`
);
await rustInit(url);
// 初始化额外卡组
// FIXME: 这样写应该不对,有空来修
window.myExtraDeckCodes = [];
// 初始化sqlite
await sqliteMiddleWare({
cmd: sqliteCmd.INIT,
initInfo: { dbUrl: NeosConfig.cardsDbUrl },
});
// 初始化文案
await initStrings();
// 连接回放websocket服务
socketMiddleWare({
cmd: socketCmd.CONNECT,
isReplay: true,
replayInfo: {
Url: NeosConfig.replayUrl,
data: replay,
},
});
}
}}
onCancel={() => {
// 断开websocket连接
socketMiddleWare({ cmd: socketCmd.DISCONNECT });
// 回到初始界面
navigate("/");
}}
>
<Upload {...uploadProps}>
<Button icon={<UploadOutlined />}>点击上传录像文件</Button>
</Upload>
</Modal>
);
};
export const replayStart = () => {
localStore.hasStart = true;
};
export default ReplayModal;
......@@ -59,15 +59,12 @@ const WaitRoom = () => {
useEffect(() => {
if (ip && player && player.length != 0 && passWd && passWd.length != 0) {
const init = async () => {
// 页面第一次渲染时,通过socket中间件向ygopro服务端请求建立长连接
socketMiddleWare({
cmd: socketCmd.CONNECT,
initInfo: {
ip,
player,
passWd,
},
});
// 初始化wasm
const url =
import.meta.env.BASE_URL === "/"
? undefined
: new URL("rust_src_bg.wasm", `${import.meta.env.BASE_URL}assets/`);
await rustInit(url);
// 初始化sqlite
await sqliteMiddleWare({
......@@ -78,12 +75,15 @@ const WaitRoom = () => {
// 初始化文案
await initStrings();
// 初始化wasm
const url =
import.meta.env.BASE_URL === "/"
? undefined
: new URL("rust_src_bg.wasm", `${import.meta.env.BASE_URL}assets/`);
await rustInit(url);
// 页面第一次渲染时,通过socket中间件向ygopro服务端请求建立长连接
socketMiddleWare({
cmd: socketCmd.CONNECT,
initInfo: {
ip,
player,
passWd,
},
});
};
init();
......
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