Commit 542ddefd authored by nanahira's avatar nanahira

migrate to template service and add account bind

parent ad70bfea
......@@ -12,4 +12,6 @@ WX_AESKEY: ''
CDB_REPO: 'https://code.mycard.moe/mycard/ygopro-database.git'
CDB_BRANCH: 'master'
FEEDBACK_NOTIFY_URL: '' # 反馈推送消息。使用 koishi-plugin-apisend 搭建
FEEDBACK_NOTIFY_TOKEN: 'default'
\ No newline at end of file
FEEDBACK_NOTIFY_TOKEN: 'default'
REDIS_URL: 'redis://localhost:6379'
ROOT_URL: 'https://jdaw.moecube.com'
\ No newline at end of file
This diff is collapsed.
......@@ -16,6 +16,13 @@ import { HttpModule } from '@nestjs/axios';
import { Feedback } from './feedback/entities/feedback.entity';
import { OnSubscribeService } from './on-subscribe/on-subscribe.service';
import { BotService } from './bot/bot.service';
import { MycardAuthModule } from 'nestjs-mycard';
import { MycardAccountService } from './mycard-account/mycard-account.service';
import { AragamiModule } from 'nestjs-aragami';
import { User } from './mycard-account/entities/user.entity';
import { MycardAccountController } from './mycard-account/mycard-account.controller';
import { TemplateService } from './template/template.service';
import { ArenaScoreService } from './arena-score/arena-score.service';
@Module({
imports: [
......@@ -41,9 +48,16 @@ import { BotService } from './bot/bot.service';
bigNumberStrings: false,
}),
}),
TypeOrmModule.forFeature([CommandCount, Feedback]),
TypeOrmModule.forFeature([CommandCount, Feedback, User]),
ScheduleModule.forRoot(),
HttpModule,
MycardAuthModule.register(),
AragamiModule.registerAsync({
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
redis: { uri: config.get('REDIS_URL') },
}),
}),
KoishiModule.registerAsync({
inject: [ConfigService],
useWs: true,
......@@ -155,6 +169,10 @@ import { BotService } from './bot/bot.service';
FeedbackService,
OnSubscribeService,
BotService,
MycardAccountService,
TemplateService,
ArenaScoreService,
],
controllers: [MycardAccountController],
})
export class AppModule {}
import { Test, TestingModule } from '@nestjs/testing';
import { ArenaScoreService } from './arena-score.service';
describe('ArenaScoreService', () => {
let service: ArenaScoreService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ArenaScoreService],
}).compile();
service = module.get<ArenaScoreService>(ArenaScoreService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable } from '@nestjs/common';
@Injectable()
export class ArenaScoreService {}
......@@ -13,6 +13,7 @@ import { HttpService } from '@nestjs/axios';
import { lastValueFrom } from 'rxjs';
import { PutSession, UseCommand } from 'koishi-nestjs';
import { BotService } from '../bot/bot.service';
import { TemplateService } from '../template/template.service';
@Injectable()
export class FeedbackService extends CrudService(Feedback) {
......@@ -24,6 +25,7 @@ export class FeedbackService extends CrudService(Feedback) {
private config: ConfigService,
private http: HttpService,
private botService: BotService,
private template: TemplateService,
) {
super(repo);
}
......@@ -75,18 +77,12 @@ export class FeedbackService extends CrudService(Feedback) {
@UseCommand('investigate', '调研')
private async investigateCommand() {
return '欢迎访问MC有奖调研功能。目前暂时无进行中的调研。我们一般每1-2个月都会公布有奖调研,欢迎下次再查询。';
return this.template.render('investigate-not-available');
}
@UseCommand('feedback', '发送反馈。')
private async feedbackCommand(@PutSession() session: Session) {
await session.send(
'您的反馈种类是?(输入数字回复)\n' +
'1. BUG或举报玩家不当行为\n' +
'2. 游戏功能/改善 \n' +
'3. 游戏体验出问题(例如卡、掉线)\n' +
'4. 其他反馈',
);
await session.send(this.template.render('feedback-menu'));
let choice = 0;
while (true) {
const input = await session.prompt();
......@@ -103,31 +99,21 @@ export class FeedbackService extends CrudService(Feedback) {
await session.send('请选择一个正确的选项(数字1~4)');
}
if (choice == 1) {
return (
'举报BUG需要提交对战录像或截图。\n' +
'举报玩家需提供充足的证据,例如骂人截图。\n' +
'因此请到 ygobbs.com 发帖举报。 感谢!'
);
return this.template.render('feedback-menu');
}
const category = ['游戏功能/改善', '游戏体验问题', '其他'][choice - 2];
await session.send('您启动了反馈系统!您的下一段文字会作为反馈发给我们!');
await session.send(this.template.render('feedback-prompt'));
const content = await session.prompt();
if (this.botService.isBuiltinCommand(content)) {
return session.execute(content);
}
const count = await this.createFeedback(session, { category, content });
return `反馈成功!谢谢您的意见!
这是我们在决斗暗网服务号收到的第 ${count} 个反馈!`;
return this.template.render('feedback-success', { count });
}
@UseCommand('feedback.shop', '发送周边反馈。')
private async shopFeedbackCommand(@PutSession() session: Session) {
await session.send(
'感谢进入周边反馈功能!周边对MC来说至关重要,MC需要周边来支持服务器维持。\n' +
'如果您有任何对MCPro游戏周边的反馈,请留言于此,感谢!\n' +
'我们会非常关心您对我们周边的意见和看法!\n' +
'为此,希望您可以尽可能清晰表达您的意见,以便我们更好理解您的需求。',
);
await session.send(this.template.render('feedback-shop'));
const content = await session.prompt();
if (this.botService.isBuiltinCommand(content)) {
return session.execute(content);
......@@ -136,7 +122,6 @@ export class FeedbackService extends CrudService(Feedback) {
category: '周边',
content,
});
return `我们收到了您的周边反馈。
这是我们在决斗暗网服务号收到的第 ${count} 个周边反馈!`;
return this.template.render('feedback-shop-success', { count });
}
}
......@@ -4,17 +4,22 @@ import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { KoishiWsAdapter } from 'koishi-nestjs';
import PackageJson from '../package.json';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.setGlobalPrefix('api');
app.useWebSocketAdapter(new KoishiWsAdapter(app));
app.enableCors();
if (process.env.NODE_ENV !== 'production') {
app.enableCors();
}
app.set('trust proxy', ['172.16.0.0/12', 'loopback']);
const documentConfig = new DocumentBuilder()
.setTitle('app')
.setDescription('The app')
.setVersion('1.0')
.setTitle(PackageJson.name)
.setDescription(PackageJson.description)
.setVersion(PackageJson.version)
.build();
const document = SwaggerModule.createDocument(app, documentConfig);
......
import { IsBase64, IsString, MaxLength, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class BindQueryDto {
@IsBase64()
@IsString()
@ApiProperty({ description: 'MyCard SSO' })
sso: string;
@IsString()
@MaxLength(64)
@MinLength(64)
@ApiProperty({ description: 'session ID' })
session: string;
decodeToken() {
const decoded = Buffer.from(this.sso, 'base64').toString();
const params = new URLSearchParams(decoded);
return params.get('token');
}
}
import { IntColumn, StringIdBase } from 'nicot';
import { Entity, Index } from 'typeorm';
@Entity()
export class User extends StringIdBase({ length: 50 }) {
@Index()
@IntColumn('int', {
unsigned: true,
description: 'MyCard ID',
required: false,
})
mycardId: number;
}
import { Test, TestingModule } from '@nestjs/testing';
import { MycardAccountController } from './mycard-account.controller';
describe('MycardAccountController', () => {
let controller: MycardAccountController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MycardAccountController],
}).compile();
controller = module.get<MycardAccountController>(MycardAccountController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
import {
Controller,
Get,
Header,
Query,
Render,
ValidationPipe,
} from '@nestjs/common';
import { MycardAccountService } from './mycard-account.service';
import { ApiOperation, ApiProduces } from '@nestjs/swagger';
import { BindQueryDto } from './dto/bind-query.dto';
import { TemplateService } from '../template/template.service';
@Controller('mycard-account')
export class MycardAccountController {
constructor(
private mycardAccountService: MycardAccountService,
private template: TemplateService,
) {}
@Get('bind')
@ApiOperation({
summary: '绑定帐号回调。',
})
@ApiProduces('text/html')
@Header('content-type', 'text/html')
@Header('cache-control', 'no-cache')
async bind(
@Query(
new ValidationPipe({
transform: true,
transformOptions: { enableImplicitConversion: true },
}),
)
dto: BindQueryDto,
) {
const view = await this.mycardAccountService.bindCallback(dto);
return this.template.render('bind-result.html', view, true);
}
}
import { Test, TestingModule } from '@nestjs/testing';
import { MycardAccountService } from './mycard-account.service';
describe('MycardAccountService', () => {
let service: MycardAccountService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [MycardAccountService],
}).compile();
service = module.get<MycardAccountService>(MycardAccountService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable } from '@nestjs/common';
import { CrudService } from 'nicot';
import { User } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { PutUserId, UseCommand } from 'koishi-nestjs';
import { Aragami, CacheKey, CacheTTL } from 'aragami';
import { InjectAragami } from 'nestjs-aragami';
import cryptoRandomString from 'crypto-random-string';
import { MycardAuthService, MycardUser } from 'nestjs-mycard';
import { ConfigService } from '@nestjs/config';
import { BindQueryDto } from './dto/bind-query.dto';
@CacheTTL(1800)
class BindSession {
@CacheKey()
sessionId: string;
userId: string;
static create(userId: string) {
const s = new BindSession();
s.sessionId = cryptoRandomString({ length: 64, type: 'url-safe' });
s.userId = userId;
return s;
}
}
function loginUrl(callbackUrl: string) {
let params = new URLSearchParams();
params.set('return_sso_url', callbackUrl);
const payload = Buffer.from(params.toString()).toString('base64');
const url = new URL('https://accounts.moecube.com/signin');
params = url['searchParams'];
params.set('sso', payload);
return url.toString();
}
@Injectable()
export class MycardAccountService extends CrudService(User) {
private rootUrl = this.config.get<string>('ROOT_URL');
constructor(
@InjectRepository(User) repo,
@InjectAragami() private aragami: Aragami,
private mycard: MycardAuthService,
private config: ConfigService,
) {
super(repo);
}
async findOrCreateUser(id: string) {
let user = await this.repo.findOne({ where: { id } });
if (user) {
return user;
}
user = new User();
user.id = id;
return this.repo.save(user);
}
@UseCommand('unbind', '解绑 MyCard 帐号')
async unbind(@PutUserId() id: string) {
const user = await this.findOrCreateUser(id);
if (!user.mycardId) {
return '您还未绑定 MyCard 帐号。';
}
await this.repo.update({ id }, { mycardId: null });
return '解绑成功。';
}
@UseCommand('bind', '绑定 MyCard 帐号')
async bind(@PutUserId() id: string) {
const session = BindSession.create(id);
await this.aragami.set(session);
const url = loginUrl(
`${this.rootUrl}/api/mycard-account/bind?session=${session.sessionId}`,
);
return `请点击下面的链接完成 MyCard 帐号绑定:\n${url}`;
}
async bindCallback(
dto: BindQueryDto,
): Promise<{ error?: string; user?: MycardUser }> {
const session = await this.aragami.get(BindSession, dto.session);
if (!session) {
return { error: '会话不存在。' };
}
try {
const mycardUser = await this.mycard.getUserFromToken(dto.decodeToken());
await this.findOrCreateUser(session.userId);
await this.repo.update(
{ id: session.userId },
{ mycardId: mycardUser.id },
);
return { user: mycardUser };
} catch (e) {
return { error: e.message };
} finally {
await this.aragami.del(BindSession, dto.session);
}
}
}
import { ConsoleLogger, Injectable } from '@nestjs/common';
import { UseEvent } from 'koishi-nestjs';
import { Session } from 'koishi';
import { TemplateService } from '../template/template.service';
@Injectable()
export class OnSubscribeService extends ConsoleLogger {
constructor() {
constructor(private template: TemplateService) {
super('OnSubscribeService');
}
@UseEvent('wechat:subscribe')
async onSubscribe(session: Session) {
this.log(`User ${session.userId} subscribed.`);
await session.send(
'欢迎关注决斗暗网!\n' +
'\n' +
'我们的菜单包含了各种功能,其中有游戏下载方式,玩家社区,举报反馈渠道等。',
);
await session.send(this.template.render('on-subscribe'));
}
@UseEvent('wechat:unsubscribe')
......
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { InjectContext } from 'koishi-nestjs';
import { Context } from 'koishi';
import { TemplateService } from '../template/template.service';
@Injectable()
export class ReplyService implements OnModuleInit {
replies: { key: string; content: string }[] = [
{
key: 'download',
content:
'下列都为游戏王YGOPro在各平台的软件,名字不同,但本质一样。MyCard平台会提供YGOPro和YGOPro2的下载与对战。\n' +
'安卓端YGOMobile下载:https://ygomobile.top\n' +
'苹果端KoishiPro iOS下载:https://koishi.pro/download/\n' +
'电脑端(包含Win和Mac)MyCard萌卡平台下载:https://mycard.moe',
},
{
key: 'servers',
content:
'1. MyCard服。电脑用户请使用MyCard客户端,安卓端用户点击【萌卡平台】,IOS端用户点击【MyCard】\n' +
'2. 233服。主机信息:s1.ygo233.com 端口:233。端口改为23333为先行卡服。\n' +
'3. Koishi服。主机信息:koishi.momobako.com 端口:7210。该服拥有断线重连。\n' +
'4. 轮抽2pick服。轮抽娱乐模式的服务器。 详情 https://ygobbs.com/t/87931\n' +
'5. 分级服。靠特殊卡表限制主流进场的服务器。详情 https://ygobbs.com/t/364147\n' +
'6. 游戏王Rush Duel服。遵循游戏王Rush Duel规则的服务器。详情 https://ygobbs.com/t/200563\n' +
'7. 高速决斗服。人物技能+游戏王5DS高速决斗模式。 详情 https://ygobbs.com/t/166971\n' +
'8. 诡异空间服。平衡的独立卡池,特殊规则服。详情 https://ygobbs.com/t/303983',
},
{
key: 'update',
content:
'安卓端YGOMobile:登录后会提示更新,手动更新请访问https://ygomobile.top\n' +
'苹果端KoishiPro2 iOS:用电脑使用爱思助手自签更新,教程请见 https://koishi.pro/download/\n' +
'电脑端MyCard会自动更新。若自动更新没触发,请在游戏大厅下方点击【校验完整性】更新。',
},
{
key: 'faq',
content:
'如果遇到了游戏安装、出BUG、下载等问题,请访问 https://ygobbs.com/t/108594\n' +
'如果遇到了游戏处理,规则类问题,请查看游戏王OCG规则修订书 https://ocg-rule.readthedocs.io/zh_CN/latest/ \n' +
'\n' +
'其他寻求帮助的方式:\n' +
'1. 在 ygobbs.com 搜索符合您问题描述的帖子,95%以上问题都有其他用户发帖询问过。\n' +
'2. 若没有,您可以在MC玩家社区 ygobbs.com 带上恰当的描述发帖寻求帮助,管理员会在36小时内回帖。\n' +
'3. 若问题很复杂,请加萌卡测试与 bug 反馈QQ群 553241437 询问。',
},
{
key: 'novelai',
content:
'由于AI绘图的普及,MC与各爱好贡献者们合作成立了YGOPro卡图娘化项目。\n' +
'现在为广大玩家们提供娘化补丁下载。\n' +
'详情与下载方式:https://ygobbs.com/t/396440',
},
];
constructor(@InjectContext() private ctx: Context) {}
onModuleInit() {
export class ReplyService implements OnApplicationBootstrap {
constructor(
@InjectContext() private ctx: Context,
private template: TemplateService,
) {}
onApplicationBootstrap() {
const base = this.ctx.command('reply', '默认回复。');
for (const reply of this.replies) {
base.subcommand(`.${reply.key}`).action(() => reply.content);
for (const templateName of this.template
.getTemplateList()
.filter((t) => t.startsWith('reply-'))) {
const commandName = templateName.slice(6);
base.subcommand(`.${commandName}`).action(() => {
return this.template.render(templateName, {});
});
}
}
}
import { Test, TestingModule } from '@nestjs/testing';
import { TemplateService } from './template.service';
describe('TemplateService', () => {
let service: TemplateService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TemplateService],
}).compile();
service = module.get<TemplateService>(TemplateService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { ConsoleLogger, Injectable, OnModuleInit } from '@nestjs/common';
import fs from 'fs';
import path from 'path';
import Mustache from 'mustache';
@Injectable()
export class TemplateService extends ConsoleLogger implements OnModuleInit {
private templates = new Map<string, string>();
constructor() {
super('TemplateService');
}
async onModuleInit() {
const fileList = await fs.promises.readdir('views');
for (const file of fileList) {
if (!file.endsWith('.mustache')) {
continue;
}
const template = file.slice(0, file.length - 9);
this.templates.set(
template,
await fs.promises.readFile(path.join('views', file), 'utf-8'),
);
this.log(`Loaded template ${template}`);
}
}
render(template: string, view: any = {}, htmlSafe = false) {
const text = this.templates.get(template);
if (!text) {
this.warn(`Template ${template} not found.`);
return template;
}
let result: string;
if (htmlSafe) {
result = Mustache.render(text, view);
} else {
result = Mustache.render(text, view, undefined, { escape: (v) => v });
}
return result.trim();
}
getTemplateList() {
return [...this.templates.keys()];
}
}
import yaml from 'yaml';
import * as fs from 'fs';
import * as process from 'process';
const defaultConfig = {
host: '::',
......@@ -17,6 +18,11 @@ const defaultConfig = {
CDB_BRANCH: 'master',
FEEDBACK_NOTIFY_URL: '',
FEEDBACK_NOTIFY_TOKEN: 'default',
REDIS_URL: 'redis://localhost:6379',
ROOT_URL:
process.env.NODE_ENV === 'production'
? 'https://jdaw.moecube.com'
: 'http://localhost:3000',
};
export type Config = typeof defaultConfig;
......
......@@ -12,7 +12,8 @@
"baseUrl": "./",
"incremental": true,
"esModuleInterop": true,
"skipLibCheck": true
"skipLibCheck": true,
"resolveJsonModule": true
},
"compileOnSave": true,
"allowJs": true
......
{{#error}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>错误</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
min-height: 100vh;
flex-direction: column;
transform: translate(0, -10rem);
}
svg {
width: 400px;
height: 400px;
}
h2 {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 36px;
margin-top: 40px;
color: #333;
opacity: 1;
}
/*h2 {*/
/* animation: 0s title ease-in-out;*/
/* animation-delay: 0s;*/
/* animation-fill-mode: forwards;*/
/*}*/
.btn {
width: 60%;
padding: 1rem 3rem;
/*border: 1px solid green;*/
border-radius: 1rem;
color: white;
background-color: #8ae155;
}
.circle {
stroke-dasharray: 1194;
stroke-dashoffset: 1194;
}
.img1 {
translate: 0 -4em;
transform: scale(2);
width: 100px;
height: 100px;
background-color: red;
border-radius: 50px;
}
.img2 {
text-align: center;
transform: scale(1.5);
height: 100px;
line-height: 100px;
display: block;
color: #FFF;
/*text-align: center;*/
font-size: 60px
}
@keyframes circle {
from {
stroke-dashoffset: 1194;
}
to {
stroke-dashoffset: 2388;
}
}
/*控制外圈动画*/
/* svg .circle {*/
/* animation: circle 0.8s ease-in-out;*/
/* animation-fill-mode: forwards;*/
/* }*/
.tick {
stroke-dasharray: 350;
stroke-dashoffset: 350;
}
/*svg .tick {*/
/* animation: tick 0.8s ease-in-out;*/
/* animation-fill-mode: forwards;*/
/* animation-delay: .95s;*/
/*}*/
@keyframes title {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
</head>
<body>
<div class="img1">
<span class="img2">!</span>
</div>
<br>
<h2>绑定错误: {{.}}</h2>
<h2 class="btn" id="btn-confirm">确定</h2>
<script type="text/javascript">
const closeButton = document.getElementById('btn-confirm');
closeButton.addEventListener('click', () => {
if(WeixinJSBridge) {
WeixinJSBridge.call('closeWindow');
} else {
window.close();
}
})
</script>
</body>
</html>
{{/error}}
{{^error}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>提示</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
min-height: 100vh;
flex-direction: column;
transform: translate(0, -10rem);
}
svg {
width: 400px;
height: 400px;
}
h2 {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 36px;
margin-top: 40px;
color: #333;
opacity: 0;
}
h2 {
animation: .6s title ease-in-out;
animation-delay: 1.2s;
animation-fill-mode: forwards;
}
.btn {
width: 60%;
padding: 1rem 3rem;
/*border: 1px solid green;*/
border-radius: 1rem;
color: white;
background-color:#8ae155;
}
.circle {
stroke-dasharray: 1194;
stroke-dashoffset: 1194;
}
@keyframes circle {
from {
stroke-dashoffset: 1194;
}
to {
stroke-dashoffset: 2388;
}
}
/*控制外圈动画*/
svg .circle {
animation: circle 0.8s ease-in-out;
animation-fill-mode: forwards;
}
.tick {
stroke-dasharray: 350;
stroke-dashoffset: 350;
}
@keyframes tick {
from {
stroke-dashoffset: 350;
}
to {
stroke-dashoffset: 0;
}
}
svg .tick {
animation: tick 0.8s ease-in-out;
animation-fill-mode: forwards;
animation-delay: .95s;
}
@keyframes title {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
</head>
<body>
<svg>
<circle class="circle" fill="#8ae155" stroke="#8ae155" stroke-width="20" cx="200" cy="200" r="190"
stroke-linecap="round" transform="rotate(-90,200,200)"/>
<polyline class="tick" fill="none" stroke="white" stroke-width="24" points="88,214 173,284 304,138"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<h2>MyCard 帐号 {{user.username}} 绑定成功</h2>
<h2 class="btn" id="btn-confirm">确定</h2>
<script type="text/javascript">
const closeButton = document.getElementById('btn-confirm');
closeButton.addEventListener('click', () => {
if(WeixinJSBridge) {
WeixinJSBridge.call('closeWindow');
} else {
window.close();
}
})
</script>
</body>
</html>
{{/error}}
\ No newline at end of file
举报BUG需要提交对战录像或截图。
举报玩家需提供充足的证据,例如骂人截图。
因此请到 ygobbs.com 发帖举报。感谢!
\ No newline at end of file
您的反馈种类是?(输入数字回复)
1. BUG或举报玩家不当行为
2. 游戏功能/改善
3. 游戏体验出问题(例如卡、掉线)
4. 其他反馈
\ No newline at end of file
您启动了反馈系统!您的下一段文字会作为反馈发给我们!
\ No newline at end of file
我们收到了您的周边反馈。
这是我们在决斗暗网服务号收到的第 {{count}} 个周边反馈!
\ No newline at end of file
感谢进入周边反馈功能!周边对 MC 来说至关重要,MC 需要周边来支持服务器维持。
如果您有任何对 MCPro 游戏周边的反馈,请留言于此,感谢!
我们会非常关心您对我们周边的意见和看法!
为此,希望您可以尽可能清晰表达您的意见,以便我们更好理解您的需求。
\ No newline at end of file
反馈成功!谢谢您的意见!
这是我们在决斗暗网服务号收到的第 {{count}} 个反馈!
\ No newline at end of file
欢迎访问 MC 有奖调研功能。目前暂时无进行中的调研。我们一般每 1-2 个月都会公布有奖调研,欢迎下次再查询。
\ No newline at end of file
欢迎关注决斗暗网!
我们的菜单包含了各种功能,其中有游戏下载方式,玩家社区,举报反馈渠道等。
\ No newline at end of file
下列都为游戏王 YGOPro 在各平台的软件,名字不同,但本质一样。MyCard平台会提供YGOPro和YGOPro2的下载与对战。
安卓端 YGOMobile 下载:https://ygomobile.top
苹果端 KoishiPro2 iOS 下载:https://koishi.pro/download/
电脑端(包含 Win 和 Mac)MyCard 萌卡平台下载:https://mycard.moe
\ No newline at end of file
如果遇到了游戏安装、出BUG、下载等问题,请访问 https://ygobbs.com/t/108594\n' +
如果遇到了游戏处理,规则类问题,请查看游戏王OCG规则修订书 https://ocg-rule.readthedocs.io/zh_CN/latest/
其他寻求帮助的方式:\n' +
1. 在 ygobbs.com 搜索符合您问题描述的帖子,95%以上问题都有其他用户发帖询问过。\n' +
2. 若没有,您可以在MC玩家社区 ygobbs.com 带上恰当的描述发帖寻求帮助,管理员会在36小时内回帖。\n' +
3. 若问题很复杂,请加萌卡测试与 bug 反馈QQ群 553241437 询问。',
\ No newline at end of file
由于 AI 绘图的普及,MC 与各爱好贡献者们合作成立了 YGOPro 卡图娘化项目。
现在为广大玩家们提供娘化补丁下载。
详情与下载方式:https://ygobbs.com/t/396440
\ No newline at end of file
1. MyCard服。电脑用户请使用MyCard客户端,安卓端用户点击【萌卡平台】,IOS端用户点击【MyCard】
2. 233服。主机信息:s1.ygo233.com 端口:233。端口改为23333为先行卡服。
3. Koishi服。主机信息:koishi.momobako.com 端口:7210。该服拥有断线重连。
4. 轮抽2pick服。轮抽娱乐模式的服务器。 详情 https://ygobbs.com/t/87931
5. 分级服。靠特殊卡表限制主流进场的服务器。详情 https://ygobbs.com/t/364147
6. 游戏王Rush Duel服。遵循游戏王Rush Duel规则的服务器。详情 https://ygobbs.com/t/200563
7. 高速决斗服。人物技能+游戏王5DS高速决斗模式。 详情 https://ygobbs.com/t/166971
8. 诡异空间服。平衡的独立卡池,特殊规则服。详情 https://ygobbs.com/t/303983
\ No newline at end of file
安卓端 YGOMobile:登录后会提示更新,手动更新请访问https://ygomobile.top
苹果端 KoishiPro2 iOS:用电脑使用爱思助手自签更新,教程请见 https://koishi.pro/download/
电脑端 MyCard 会自动更新。若自动更新没触发,请在游戏大厅下方点击【校验完整性】更新。
\ No newline at end of file
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