Commit fffcb74a authored by Chunchi Che's avatar Chunchi Che

Merge branch 'fix/match' into 'main'

fix match

See merge request !424
parents 736537f0 7f078ab1
Pipeline #42890 passed with stages
in 2 minutes and 22 seconds
...@@ -8,12 +8,12 @@ ...@@ -8,12 +8,12 @@
}, },
{ {
"name": "mycard-athletic", "name": "mycard-athletic",
"ip": "tiramisu.moecube.com", "ip": "tiramisu.moenext.com",
"port": "8912" "port": "8912"
}, },
{ {
"name": "mycard-custom", "name": "mycard-custom",
"ip": "tiramisu.moecube.com", "ip": "tiramisu.moenext.com",
"port": "7912" "port": "7912"
}, },
{ {
......
...@@ -8,12 +8,12 @@ ...@@ -8,12 +8,12 @@
}, },
{ {
"name": "mycard-athletic", "name": "mycard-athletic",
"ip": "tiramisu.moecube.com", "ip": "tiramisu.moenext.com",
"port": "8912" "port": "8912"
}, },
{ {
"name": "mycard-custom", "name": "mycard-custom",
"ip": "tiramisu.moecube.com", "ip": "tiramisu.moenext.com",
"port": "7912" "port": "7912"
}, },
{ {
......
...@@ -3,4 +3,5 @@ export * from "./account"; ...@@ -3,4 +3,5 @@ export * from "./account";
export * from "./match"; export * from "./match";
export * from "./options"; export * from "./options";
export * from "./room"; export * from "./room";
export * from "./u16Secret";
export * from "./user"; export * from "./user";
...@@ -6,13 +6,21 @@ export interface MatchInfo { ...@@ -6,13 +6,21 @@ export interface MatchInfo {
password: string; password: string;
} }
/**
* 请求匹配
*
* @param username 用户名
* @param secret 认证密钥(优先使用 u16Secret,如果没有则使用 external_id)
* @param arena 匹配类型(athletic: 竞技, entertain: 娱乐)
* @returns 匹配信息(服务器地址、端口、密码)
*/
export async function match( export async function match(
username: string, username: string,
extraId: number, secret: number,
arena: "athletic" | "entertain" = "entertain", arena: "athletic" | "entertain" = "entertain",
): Promise<MatchInfo | undefined> { ): Promise<MatchInfo | undefined> {
const headers = { const headers = {
Authorization: "Basic " + customBase64Encode(username + ":" + extraId), Authorization: "Basic " + customBase64Encode(username + ":" + secret),
}; };
let response: Response | undefined = undefined; let response: Response | undefined = undefined;
const params = new URLSearchParams({ const params = new URLSearchParams({
......
...@@ -16,19 +16,20 @@ export interface Room { ...@@ -16,19 +16,20 @@ export interface Room {
options: Options; options: Options;
} }
// 通过房间ID和external_id加密得出房间密码 // 通过房间ID和secret加密得出房间密码
// //
// 用于加入MC服房间 // 用于加入MC服房间
// @param secret - 优先使用 u16Secret,如果没有则使用 external_id
export function getJoinRoomPasswd( export function getJoinRoomPasswd(
roomID: string, roomID: string,
external_id: number, secret: number,
_private: boolean = false, _private: boolean = false,
): string { ): string {
const optionsBuffer = new Uint8Array(6); const optionsBuffer = new Uint8Array(6);
optionsBuffer[1] = optionsBuffer[1] =
(_private ? RoomAction.JoinPrivate : RoomAction.JoinPublic) << 4; (_private ? RoomAction.JoinPrivate : RoomAction.JoinPublic) << 4;
encryptBuffer(optionsBuffer, external_id); encryptBuffer(optionsBuffer, secret);
const base64String = btoa(String.fromCharCode(...optionsBuffer)); const base64String = btoa(String.fromCharCode(...optionsBuffer));
...@@ -36,10 +37,11 @@ export function getJoinRoomPasswd( ...@@ -36,10 +37,11 @@ export function getJoinRoomPasswd(
} }
// 获取创建房间的密码 // 获取创建房间的密码
// @param secret - 优先使用 u16Secret,如果没有则使用 external_id
export function getCreateRoomPasswd( export function getCreateRoomPasswd(
options: Options, options: Options,
roomID: string, roomID: string,
external_id: number, secret: number,
_private: boolean = false, _private: boolean = false,
) { ) {
// ref: https://docs.google.com/document/d/1rvrCGIONua2KeRaYNjKBLqyG9uybs9ZI-AmzZKNftOI/edit // ref: https://docs.google.com/document/d/1rvrCGIONua2KeRaYNjKBLqyG9uybs9ZI-AmzZKNftOI/edit
...@@ -57,14 +59,15 @@ export function getCreateRoomPasswd( ...@@ -57,14 +59,15 @@ export function getCreateRoomPasswd(
writeUInt16LE(optionsBuffer, 3, options.start_lp); writeUInt16LE(optionsBuffer, 3, options.start_lp);
optionsBuffer[5] = (options.start_hand << 4) | options.draw_count; optionsBuffer[5] = (options.start_hand << 4) | options.draw_count;
encryptBuffer(optionsBuffer, external_id); encryptBuffer(optionsBuffer, secret);
const base64String = btoa(String.fromCharCode(...optionsBuffer)); const base64String = btoa(String.fromCharCode(...optionsBuffer));
return base64String + roomID; return base64String + roomID;
} }
// 填充校验码和加密 // 填充校验码和加密
function encryptBuffer(buffer: Uint8Array, external_id: number) { // @param secret - 优先使用 u16Secret,如果没有则使用 external_id
function encryptBuffer(buffer: Uint8Array, secret: number) {
let checksum = 0; let checksum = 0;
for (let i = 1; i < buffer.length; i++) { for (let i = 1; i < buffer.length; i++) {
...@@ -73,11 +76,11 @@ function encryptBuffer(buffer: Uint8Array, external_id: number) { ...@@ -73,11 +76,11 @@ function encryptBuffer(buffer: Uint8Array, external_id: number) {
buffer[0] = checksum & 0xff; buffer[0] = checksum & 0xff;
const secret = (external_id % 65535) + 1; const encryptSecret = (secret % 65535) + 1;
for (let i = 0; i < buffer.length; i += 2) { for (let i = 0; i < buffer.length; i += 2) {
const value = readUInt16LE(buffer, i); const value = readUInt16LE(buffer, i);
const xorResult = value ^ secret; const xorResult = value ^ encryptSecret;
writeUInt16LE(buffer, i, xorResult); writeUInt16LE(buffer, i, xorResult);
} }
} }
......
/**
* 获取用户的 u16Secret
*
* u16Secret 是用于匹配和房间认证的时间轮换密钥
* 每次使用前都需要重新获取,因为它会按时间轮换
*/
const API_URL = "https://sapi.moecube.com:444/accounts/authUser";
interface U16SecretResponse {
u16Secret: number;
}
export async function getUserU16Secret(token: string): Promise<number> {
if (!token) {
throw new Error("获取用户密钥失败:token 不存在,请重新登录");
}
try {
const response = await fetch(API_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data: U16SecretResponse = await response.json();
if (data.u16Secret === null || data.u16Secret === undefined) {
throw new Error("服务器返回的数据中没有 u16Secret");
}
return data.u16Secret;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "未知错误";
console.error("获取 u16Secret 失败:", errorMsg);
throw new Error(`获取用户密钥失败:${errorMsg},请尝试重新登录`);
}
}
...@@ -15,6 +15,7 @@ import { ...@@ -15,6 +15,7 @@ import {
getCreateRoomPasswd, getCreateRoomPasswd,
getJoinRoomPasswd, getJoinRoomPasswd,
getPrivateRoomID, getPrivateRoomID,
getUserU16Secret,
match, match,
} from "@/api"; } from "@/api";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
...@@ -61,11 +62,17 @@ export const Component: React.FC = () => { ...@@ -61,11 +62,17 @@ export const Component: React.FC = () => {
const onMatch = async (arena: "athletic" | "entertain") => { const onMatch = async (arena: "athletic" | "entertain") => {
if (!user) { if (!user) {
message.error("请先登录萌卡账号"); message.error("请先登录萌卡账号");
} else { return;
}
try {
arena === "athletic" arena === "athletic"
? setAthleticMatchLoading(true) ? setAthleticMatchLoading(true)
: setEntertainMatchLoading(true); : setEntertainMatchLoading(true);
const matchInfo = await match(user.username, user.external_id, arena);
// 每次匹配前都要重新获取 u16Secret,因为它会按时间轮换
const u16Secret = await getUserU16Secret(user.token);
const matchInfo = await match(user.username, u16Secret, arena);
if (matchInfo) { if (matchInfo) {
await connectSrvpro({ await connectSrvpro({
...@@ -75,7 +82,16 @@ export const Component: React.FC = () => { ...@@ -75,7 +82,16 @@ export const Component: React.FC = () => {
}); });
} else { } else {
message.error("匹配失败T_T"); message.error("匹配失败T_T");
arena === "athletic"
? setAthleticMatchLoading(false)
: setEntertainMatchLoading(false);
} }
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "未知错误";
message.error(errorMsg);
arena === "athletic"
? setAthleticMatchLoading(false)
: setEntertainMatchLoading(false);
} }
}; };
...@@ -107,15 +123,21 @@ export const Component: React.FC = () => { ...@@ -107,15 +123,21 @@ export const Component: React.FC = () => {
// 创建MC自定义房间 // 创建MC自定义房间
const onCreateMCRoom = async () => { const onCreateMCRoom = async () => {
if (user) { if (!user) {
return;
}
try {
const mcServer = serverList.find( const mcServer = serverList.find(
(server) => server.name === "mycard-custom", (server) => server.name === "mycard-custom",
); );
if (mcServer) { if (mcServer) {
// 每次操作前都要重新获取 u16Secret
const u16Secret = await getUserU16Secret(user.token);
const passWd = getCreateRoomPasswd( const passWd = getCreateRoomPasswd(
mcCustomRoomStore.options, mcCustomRoomStore.options,
String(getPrivateRoomID(user.external_id)), String(getPrivateRoomID(user.external_id)),
user.external_id, u16Secret,
true, true,
); );
await connectSrvpro({ await connectSrvpro({
...@@ -124,30 +146,43 @@ export const Component: React.FC = () => { ...@@ -124,30 +146,43 @@ export const Component: React.FC = () => {
passWd, passWd,
}); });
} }
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "未知错误";
message.error(errorMsg);
} }
}; };
// 加入MC自定义房间 // 加入MC自定义房间
const onJoinMCRoom = async () => { const onJoinMCRoom = async () => {
if (user) { if (!user) {
if (mcCustomRoomStore.friendPrivateID !== undefined) { return;
const mcServer = serverList.find( }
(server) => server.name === "mycard-custom",
if (mcCustomRoomStore.friendPrivateID === undefined) {
message.error("请输入朋友的私密房间密码!");
return;
}
try {
const mcServer = serverList.find(
(server) => server.name === "mycard-custom",
);
if (mcServer) {
// 每次操作前都要重新获取 u16Secret
const u16Secret = await getUserU16Secret(user.token);
const passWd = getJoinRoomPasswd(
String(mcCustomRoomStore.friendPrivateID),
u16Secret,
true,
); );
if (mcServer) { await connectSrvpro({
const passWd = getJoinRoomPasswd( ip: mcServer.ip + ":" + mcServer.port,
String(mcCustomRoomStore.friendPrivateID), player: user.username,
user.external_id, passWd,
true, });
);
await connectSrvpro({
ip: mcServer.ip + ":" + mcServer.port,
player: user.username,
passWd,
});
}
} else {
message.error("请输入朋友的私密房间密码!");
} }
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "未知错误";
message.error(errorMsg);
} }
}; };
...@@ -161,7 +196,12 @@ export const Component: React.FC = () => { ...@@ -161,7 +196,12 @@ export const Component: React.FC = () => {
width: "40vw", width: "40vw",
okText: i18n("EnterSpectatorMode"), okText: i18n("EnterSpectatorMode"),
onOk: async () => { onOk: async () => {
if (watchStore.watchID) { if (!watchStore.watchID) {
message.error(`${i18n("PleaseSelectTheRoomToSpectate")}`);
return;
}
try {
setWatchLoading(true); setWatchLoading(true);
// 找到MC竞技匹配的Server // 找到MC竞技匹配的Server
...@@ -169,10 +209,9 @@ export const Component: React.FC = () => { ...@@ -169,10 +209,9 @@ export const Component: React.FC = () => {
(server) => server.name === "mycard-athletic", (server) => server.name === "mycard-athletic",
); );
if (mcServer) { if (mcServer) {
const passWd = getJoinRoomPasswd( // 每次操作前都要重新获取 u16Secret
watchStore.watchID, const u16Secret = await getUserU16Secret(user.token);
user.external_id, const passWd = getJoinRoomPasswd(watchStore.watchID, u16Secret);
);
await connectSrvpro({ await connectSrvpro({
ip: mcServer.ip + ":" + mcServer.port, ip: mcServer.ip + ":" + mcServer.port,
player: user.username, player: user.username,
...@@ -182,9 +221,13 @@ export const Component: React.FC = () => { ...@@ -182,9 +221,13 @@ export const Component: React.FC = () => {
message.error( message.error(
"Something unexpected happened, please contact <ccc@neos.moe> to fix", "Something unexpected happened, please contact <ccc@neos.moe> to fix",
); );
setWatchLoading(false);
} }
} else { } catch (error) {
message.error(`${i18n("PleaseSelectTheRoomToSpectate")}`); const errorMsg =
error instanceof Error ? error.message : "未知错误";
message.error(errorMsg);
setWatchLoading(false);
} }
}, },
centered: true, centered: true,
......
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