Commit 4012759d authored by nanahira's avatar nanahira

Merge branch 'multiserver'

parents 9d1505fc 4ce5ee7a
import { ErrorHandler, isDevMode, NgModule, Provider } from '@angular/core'; import { ErrorHandler, NgModule, Provider } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { import {
MatAutocompleteModule, MatAutocompleteModule,
...@@ -41,6 +41,7 @@ import * as Raven from 'raven-js'; ...@@ -41,6 +41,7 @@ import * as Raven from 'raven-js';
import { DecksComponent } from './decks/decks.component'; import { DecksComponent } from './decks/decks.component';
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
import { LogoutDialogComponent } from './logout-dialog/logout-dialog.component'; import { LogoutDialogComponent } from './logout-dialog/logout-dialog.component';
import { ServerSelectComponent } from './server-select/server-select.component';
export class RavenErrorHandler implements ErrorHandler { export class RavenErrorHandler implements ErrorHandler {
handleError(err: any): void { handleError(err: any): void {
...@@ -69,7 +70,8 @@ if (environment.production) { ...@@ -69,7 +70,8 @@ if (environment.production) {
ResultDialogComponent, ResultDialogComponent,
DecksComponent, DecksComponent,
ConfirmDialogComponent, ConfirmDialogComponent,
LogoutDialogComponent LogoutDialogComponent,
ServerSelectComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
......
...@@ -46,16 +46,16 @@ ...@@ -46,16 +46,16 @@
<br>娱乐匹配 <br>娱乐匹配
</button> </button>
</mat-grid-tile> </mat-grid-tile>
<mat-grid-tile><a mat-raised-button color="primary" routerLink="/ygopro/rooms"> <mat-grid-tile><a mat-raised-button color="primary" (click)='reloadServers(["custom"])' routerLink="/ygopro/rooms">
<mat-icon>games</mat-icon> <mat-icon>games</mat-icon>
<br>房间列表</a></mat-grid-tile> <br>房间列表</a></mat-grid-tile>
<mat-grid-tile><a mat-raised-button color="primary" routerLink="/ygopro/rooms/new"> <mat-grid-tile><a mat-raised-button color="primary" (click)='reloadServers(["custom"])' routerLink="/ygopro/rooms/new">
<mat-icon>&#xe146;</mat-icon><!--add_box--> <mat-icon>&#xe146;</mat-icon><!--add_box-->
<br>创建房间</a></mat-grid-tile> <br>创建房间</a></mat-grid-tile>
<mat-grid-tile><a mat-raised-button routerLink="/ygopro/windbot"> <mat-grid-tile><a mat-raised-button (click)='reloadServers(["windbot"])' routerLink="/ygopro/windbot">
<mat-icon>&#xe195;</mat-icon><!-- airplanemode_active --> <mat-icon>&#xe195;</mat-icon><!-- airplanemode_active -->
<br>单人模式</a></mat-grid-tile> <br>单人模式</a></mat-grid-tile>
<mat-grid-tile><a mat-raised-button routerLink="/ygopro/watch"> <mat-grid-tile><a mat-raised-button (click)='reloadServers(["replay"])' routerLink="/ygopro/watch">
<mat-icon>&#xe417;</mat-icon><!--remove_red_eye--> <mat-icon>&#xe417;</mat-icon><!--remove_red_eye-->
<br>观战</a></mat-grid-tile> <br>观战</a></mat-grid-tile>
<!--<mat-grid-tile>--> <!--<mat-grid-tile>-->
......
...@@ -4,13 +4,14 @@ import { environment } from '../../environments/environment'; ...@@ -4,13 +4,14 @@ import { environment } from '../../environments/environment';
import { LoginService } from '../login.service'; import { LoginService } from '../login.service';
import { routerTransition } from '../router.animations'; import { routerTransition } from '../router.animations';
import { StorageService } from '../storage.service'; import { StorageService } from '../storage.service';
import { YGOProService } from '../ygopro.service'; import { YGOProService, Server } from '../ygopro.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/internal/operators'; import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/internal/operators';
import { MatDialog } from '@angular/material'; import { MatDialog } from '@angular/material';
import { LogoutDialogComponent } from '../logout-dialog/logout-dialog.component'; import { LogoutDialogComponent } from '../logout-dialog/logout-dialog.component';
@Component({ @Component({
selector: 'app-lobby', selector: 'app-lobby',
templateUrl: 'lobby.component.html', templateUrl: 'lobby.component.html',
...@@ -57,6 +58,10 @@ export class LobbyComponent { ...@@ -57,6 +58,10 @@ export class LobbyComponent {
} }
} }
reloadServers(fields: (keyof Server)[]) {
this.ygopro.reloadSelectableServers(s => fields.some((field) => !!s[field]));
}
search(key: string) { search(key: string) {
const url = new URL('http://www.ourocg.cn/S.aspx'); const url = new URL('http://www.ourocg.cn/S.aspx');
url.searchParams.set('key', key); url.searchParams.set('key', key);
......
...@@ -15,7 +15,7 @@ export class RoomListComponent implements OnInit { ...@@ -15,7 +15,7 @@ export class RoomListComponent implements OnInit {
@HostBinding('@routerTransition') animation; @HostBinding('@routerTransition') animation;
displayedColumns = ['title', 'users', 'mode', 'extra']; displayedColumns = ['title', 'users', 'mode', 'extra'];
dataSource = new RoomListDataSource(this.ygopro.servers.filter(server => server.custom!)); dataSource = new RoomListDataSource(this.ygopro);
constructor(public login: LoginService, public ygopro: YGOProService, private changeDetector: ChangeDetectorRef) {} constructor(public login: LoginService, public ygopro: YGOProService, private changeDetector: ChangeDetectorRef) {}
......
mat-form-field {
width: 100px;
}
mat-select::ng-deep .mat-select-arrow-wrapper {
transform: initial;
}
mat-form-field::ng-deep .mat-form-field-underline {
display: none;
}
mat-form-field::ng-deep .mat-form-field-wrapper, mat-form-field::ng-deep .mat-form-field-flex {
padding: initial;
}
mat-form-field::ng-deep .mat-form-field-infix {
padding: initial;
border: initial;
}
环境:
<mat-form-field appearance='standard'>
<mat-select [formControl]='ygopro.serverForm' [(ngModel)]='ygopro.currentServer'>
<mat-option *ngFor='let server of ygopro.selectableServers' [value]='server'>
{{server.name}}
</mat-option>
</mat-select>
</mat-form-field>
import { Component, OnInit } from '@angular/core';
import { YGOProService } from '../ygopro.service';
@Component({
selector: 'app-server-select',
templateUrl: './server-select.component.html',
styleUrls: ['./server-select.component.css']
})
export class ServerSelectComponent implements OnInit {
constructor(public ygopro: YGOProService) {
}
ngOnInit() {
}
}
...@@ -2,3 +2,7 @@ button { ...@@ -2,3 +2,7 @@ button {
margin-left: -8px; margin-left: -8px;
margin-right: 24px; margin-right: 24px;
} }
span {
flex: 1
}
...@@ -3,4 +3,5 @@ ...@@ -3,4 +3,5 @@
<mat-icon>arrow_back</mat-icon> <mat-icon>arrow_back</mat-icon>
</button> </button>
<span><ng-content></ng-content></span> <span><ng-content></ng-content></span>
<app-server-select></app-server-select>
</mat-toolbar> </mat-toolbar>
...@@ -14,7 +14,7 @@ export class WatchComponent implements OnInit { ...@@ -14,7 +14,7 @@ export class WatchComponent implements OnInit {
@HostBinding('@routerTransition') animation; @HostBinding('@routerTransition') animation;
displayedColumns = ['mode', 'title', 'users', 'extra']; displayedColumns = ['mode', 'title', 'users', 'extra'];
dataSource = new RoomListDataSource(this.ygopro.servers, 'started'); dataSource = new RoomListDataSource(this.ygopro, 'started');
constructor(public login: LoginService, public ygopro: YGOProService, private changeDetector: ChangeDetectorRef) {} constructor(public login: LoginService, public ygopro: YGOProService, private changeDetector: ChangeDetectorRef) {}
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<mat-icon mat-list-icon>&#xe195;</mat-icon><!-- airplanemode_active --> <mat-icon mat-list-icon>&#xe195;</mat-icon><!-- airplanemode_active -->
<h4 mat-line>随机</h4> <h4 mat-line>随机</h4>
</mat-list-item> </mat-list-item>
<mat-list-item *ngFor="let windbot of ygopro.windbot | async" (click)="ygopro.join_windbot(windbot)"> <mat-list-item *ngFor="let windbot of (ygopro.currentServer ? ygopro.currentServer.windbot : [])" (click)="ygopro.join_windbot(windbot)">
<img mat-list-icon [src]="login.avatar(windbot)"> <img mat-list-icon [src]="login.avatar(windbot)">
<h4 mat-line>{{windbot}}</h4> <h4 mat-line>{{windbot}}</h4>
</mat-list-item> </mat-list-item>
......
...@@ -9,8 +9,9 @@ import { MatchDialogComponent } from './match-dialog/match-dialog.component'; ...@@ -9,8 +9,9 @@ import { MatchDialogComponent } from './match-dialog/match-dialog.component';
import { ResultDialogComponent } from './result-dialog/result-dialog.component'; import { ResultDialogComponent } from './result-dialog/result-dialog.component';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { catchError, filter, map, mergeMap, publishLast, refCount, scan, startWith } from 'rxjs/internal/operators'; import { catchError, filter, map, mergeMap, publishLast, refCount, scan, startWith, switchMap, tap } from 'rxjs/internal/operators';
import { webSocket } from 'rxjs/webSocket'; import { webSocket } from 'rxjs/webSocket';
import { FormControl } from '@angular/forms';
export interface User { export interface User {
admin: boolean; admin: boolean;
...@@ -119,8 +120,8 @@ export interface Points { ...@@ -119,8 +120,8 @@ export interface Points {
export class YGOProService { export class YGOProService {
news: Promise<News[]>; news: Promise<News[]>;
topics: Promise<any[]>; topics: Promise<any[]>;
windbot: Promise<string[]>;
points = new BehaviorSubject<Points | undefined>(undefined); points = new BehaviorSubject<Points | undefined>(undefined);
serverForm = new FormControl();
readonly default_options: Options = { readonly default_options: Options = {
mode: 1, mode: 1,
...@@ -136,27 +137,25 @@ export class YGOProService { ...@@ -136,27 +137,25 @@ export class YGOProService {
time_limit: 180, time_limit: 180,
auto_death: false auto_death: false
}; };
readonly servers: Server[] = [ serversPromise: Promise<Server[]>;
{ servers: Server[] = [];
id: 'tiramisu', selectableServers: Server[] = [];
url: 'wss://tiramisu.mycard.moe:7923', currentServer: Server;
address: 'tiramisu.mycard.moe',
port: 7911,
custom: true,
replay: true
},
{
id: 'tiramisu-athletic',
url: 'wss://tiramisu.mycard.moe:8923',
address: 'tiramisu.mycard.moe',
port: 8911,
custom: false,
replay: true
}
];
constructor(private login: LoginService, private http: HttpClient, private dialog: MatDialog, private storage: StorageService) { constructor(private login: LoginService, private http: HttpClient, private dialog: MatDialog, private storage: StorageService) {
const app = this.http.get<App[]>('https://sapi.moecube.com:444/apps.json').pipe(map(apps => apps.find(_app => _app.id === 'ygopro')!), publishLast(), refCount()); const app = this.http.get<App[]>('https://sapi.moecube.com:444/apps.json').pipe(
map(apps => apps.find(_app => _app.id === 'ygopro')!),
publishLast(),
refCount()
);
this.serversPromise = app.pipe(map(_app => _app.data.servers)).toPromise();
this.serversPromise.then(servers => {
this.servers = servers;
this.reloadSelectableServers();
});
this.news = app this.news = app
.pipe( .pipe(
map(_app => map(_app =>
...@@ -171,7 +170,7 @@ export class YGOProService { ...@@ -171,7 +170,7 @@ export class YGOProService {
) )
) )
.toPromise(); .toPromise();
this.windbot = app.pipe(map(_app => (<YGOProData>_app.data).windbot['zh-CN'])).toPromise(); // this.windbot = app.pipe(map(_app => (<YGOProData>_app.data).windbot['zh-CN'])).toPromise();
this.topics = this.http this.topics = this.http
.get<TopResponse>('https://ygobbs.com/top/quarterly.json') .get<TopResponse>('https://ygobbs.com/top/quarterly.json')
...@@ -246,6 +245,19 @@ export class YGOProService { ...@@ -246,6 +245,19 @@ export class YGOProService {
}); });
} }
reloadSelectableServers(condition: (server: Server) => boolean = () => true) {
this.selectableServers = this.servers.filter(s => {
if (s.hidden) {
return false;
}
return condition(s);
});
if (!this.currentServer || !this.selectableServers.includes(this.currentServer)) {
this.currentServer = this.selectableServers[0];
this.serverForm.setValue(this.currentServer);
}
}
async request_match(arena: string) { async request_match(arena: string) {
const data = await this.dialog const data = await this.dialog
.open(MatchDialogComponent, { data: arena, disableClose: true }) .open(MatchDialogComponent, { data: arena, disableClose: true })
...@@ -259,15 +271,14 @@ export class YGOProService { ...@@ -259,15 +271,14 @@ export class YGOProService {
create_room(room: Room, host_password: string) { create_room(room: Room, host_password: string) {
const options_buffer = Buffer.alloc(6); const options_buffer = Buffer.alloc(6);
// 建主密码 https://docs.google.com/document/d/1rvrCGIONua2KeRaYNjKBLqyG9uybs9ZI-AmzZKNftOI/edit // 建主密码 https://docs.google.com/document/d/1rvrCGIONua2KeRaYNjKBLqyG9uybs9ZI-AmzZKNftOI/edit
options_buffer.writeUInt8(((room.private ? 2 : 1) << 4) | options_buffer.writeUInt8(((room.private ? 2 : 1) << 4) | (room.options.duel_rule << 1) | (room.options.auto_death ? 0x1 : 0), 1);
(room.options.duel_rule << 1) |
(room.options.auto_death ? 0x1 : 0), 1);
options_buffer.writeUInt8( options_buffer.writeUInt8(
room.options.rule << 5 | (room.options.rule << 5) |
room.options.mode << 3 | (room.options.mode << 3) |
(room.options.no_check_deck ? 1 << 1 : 0) | (room.options.no_check_deck ? 1 << 1 : 0) |
(room.options.no_shuffle_deck ? 1 : 0) (room.options.no_shuffle_deck ? 1 : 0),
, 2); 2
);
options_buffer.writeUInt16LE(room.options.start_lp, 3); options_buffer.writeUInt16LE(room.options.start_lp, 3);
options_buffer.writeUInt8((room.options.start_hand << 4) | room.options.draw_count, 5); options_buffer.writeUInt8((room.options.start_hand << 4) | room.options.draw_count, 5);
let checksum = 0; let checksum = 0;
...@@ -276,7 +287,7 @@ export class YGOProService { ...@@ -276,7 +287,7 @@ export class YGOProService {
} }
options_buffer.writeUInt8(checksum & 0xff, 0); options_buffer.writeUInt8(checksum & 0xff, 0);
const secret = this.login.user.external_id % 65535 + 1; const secret = (this.login.user.external_id % 65535) + 1;
for (let i = 0; i < options_buffer.length; i += 2) { for (let i = 0; i < options_buffer.length; i += 2) {
options_buffer.writeUInt16LE(options_buffer.readUInt16LE(i) ^ secret, i); options_buffer.writeUInt16LE(options_buffer.readUInt16LE(i) ^ secret, i);
} }
...@@ -291,7 +302,7 @@ export class YGOProService { ...@@ -291,7 +302,7 @@ export class YGOProService {
// body: `房间密码是 ${this.host_password}, 您的对手可在自定义游戏界面输入密码与您对战。` // body: `房间密码是 ${this.host_password}, 您的对手可在自定义游戏界面输入密码与您对战。`
// }); // });
// } // }
this.join(password, this.servers[0]); this.join(password, this.currentServer);
} }
join_room(room: Room) { join_room(room: Room) {
...@@ -303,7 +314,7 @@ export class YGOProService { ...@@ -303,7 +314,7 @@ export class YGOProService {
} }
options_buffer.writeUInt8(checksum & 0xff, 0); options_buffer.writeUInt8(checksum & 0xff, 0);
const secret = this.login.user.external_id % 65535 + 1; const secret = (this.login.user.external_id % 65535) + 1;
for (let i = 0; i < options_buffer.length; i += 2) { for (let i = 0; i < options_buffer.length; i += 2) {
options_buffer.writeUInt16LE(options_buffer.readUInt16LE(i) ^ secret, i); options_buffer.writeUInt16LE(options_buffer.readUInt16LE(i) ^ secret, i);
} }
...@@ -322,21 +333,21 @@ export class YGOProService { ...@@ -322,21 +333,21 @@ export class YGOProService {
} }
options_buffer.writeUInt8(checksum & 0xff, 0); options_buffer.writeUInt8(checksum & 0xff, 0);
const secret = this.login.user.external_id % 65535 + 1; const secret = (this.login.user.external_id % 65535) + 1;
for (let i = 0; i < options_buffer.length; i += 2) { for (let i = 0; i < options_buffer.length; i += 2) {
options_buffer.writeUInt16LE(options_buffer.readUInt16LE(i) ^ secret, i); options_buffer.writeUInt16LE(options_buffer.readUInt16LE(i) ^ secret, i);
} }
const name = options_buffer.toString('base64') + password.replace(/\s/, String.fromCharCode(0xfeff)); const name = options_buffer.toString('base64') + password.replace(/\s/, String.fromCharCode(0xfeff));
this.join(name, this.servers[0]); this.join(name, this.currentServer);
} }
async join_windbot(name?: string) { async join_windbot(name?: string) {
if (!name) { if (!name) {
name = sample(await this.windbot); name = sample(this.currentServer.windbot!);
} }
return this.join('AI#' + name, this.servers[0]); return this.join('AI#' + name, this.currentServer);
} }
join(password: string, server: Server) { join(password: string, server: Server) {
...@@ -414,6 +425,10 @@ export class YGOProService { ...@@ -414,6 +425,10 @@ export class YGOProService {
alert(JSON.stringify({ method: 'share', params: [text] })); alert(JSON.stringify({ method: 'share', params: [text] }));
} }
} }
isRoomAvailableToDisplay(r: Room) {
return (r.arena && this.currentServer && this.currentServer.id === 'tiramisu') || r.server === this.currentServer;
}
} }
type Message = type Message =
...@@ -427,15 +442,19 @@ export class RoomListDataSource extends DataSource<Room> { ...@@ -427,15 +442,19 @@ export class RoomListDataSource extends DataSource<Room> {
empty = new EventEmitter(); empty = new EventEmitter();
error = new EventEmitter(); error = new EventEmitter();
constructor(private servers: Server[], private type = 'waiting') { constructor(private ygopro: YGOProService, private type = 'waiting') {
super(); super();
} }
/** Connect function called by the table to retrieve one stream containing the data to render. */ /** Connect function called by the table to retrieve one stream containing the data to render. */
connect(): Observable<Room[]> { connect(): Observable<Room[]> {
this.loading.emit(true); this.loading.emit(true);
return combineLatest(
this.servers.map(server => { return this.ygopro.serverForm.valueChanges.pipe(
this.ygopro.serverForm.value ? startWith(this.ygopro.serverForm.value) : tap(),
switchMap(env =>
combineLatest(
[env].filter(s => s.url && (s.custom || s.replay)).map(server => {
const url = new URL(server.url!); const url = new URL(server.url!);
url.searchParams.set('filter', this.type); url.searchParams.set('filter', this.type);
// 协议处理 // 协议处理
...@@ -455,13 +474,15 @@ export class RoomListDataSource extends DataSource<Room> { ...@@ -455,13 +474,15 @@ export class RoomListDataSource extends DataSource<Room> {
}, []) }, [])
); );
}) })
).pipe( )
),
// 把多个服务器的数据拼接起来 // 把多个服务器的数据拼接起来
map((sources: Room[][]) => (<Room[]>[]).concat(...sources)), map((sources: Room[][]) => (<Room[]>[]).concat(...sources)),
// 筛选一下房间,只扔进去当前房间或者竞技匹配的
// 房间排序 // 房间排序
map(rooms => map(rooms =>
sortBy(rooms, room => { // TODO: reload server on change
sortBy(rooms.filter(r => this.ygopro.isRoomAvailableToDisplay(r)), room => {
if (room.arena === 'athletic') { if (room.arena === 'athletic') {
return 0; return 0;
} else if (room.arena === 'entertain') { } else if (room.arena === 'entertain') {
...@@ -474,10 +495,9 @@ export class RoomListDataSource extends DataSource<Room> { ...@@ -474,10 +495,9 @@ export class RoomListDataSource extends DataSource<Room> {
}) })
), ),
// loading、empty、error // loading、empty、error
filter(rooms => { tap(rooms => {
this.loading.emit(false); this.loading.emit(false);
this.empty.emit(rooms.length === 0); this.empty.emit(rooms.length === 0);
return true;
}), }),
catchError(error => { catchError(error => {
this.loading.emit(false); this.loading.emit(false);
......
<svg class="lds-spinner" width="200px" height="200px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style="background: none;"><g transform="rotate(0 50 50)"> <svg class='lds-spinner' width='200px' height='200px' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'
preserveAspectRatio='xMidYMid' style='background: none;'><g transform="rotate(0 50 50)">
<rect x="47" y="24" rx="9.4" ry="4.8" width="6" height="12" fill="#4b99ea"> <rect x="47" y="24" rx="9.4" ry="4.8" width="6" height="12" fill="#4b99ea">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate> <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
</rect> </rect>
......
...@@ -44,6 +44,10 @@ ...@@ -44,6 +44,10 @@
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
// import 'core-js/es7/reflect'; // import 'core-js/es7/reflect';
import 'core-js/fn/symbol/async-iterator'; import 'core-js/fn/symbol/async-iterator';
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/** /**
* Required to support Web Animations `@angular/platform-browser/animations`. * Required to support Web Animations `@angular/platform-browser/animations`.
...@@ -66,11 +70,6 @@ import 'core-js/fn/symbol/async-iterator'; ...@@ -66,11 +70,6 @@ import 'core-js/fn/symbol/async-iterator';
*/ */
// (window as any).__Zone_enable_cross_context_check = true; // (window as any).__Zone_enable_cross_context_check = true;
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/*************************************************************************************************** /***************************************************************************************************
* APPLICATION IMPORTS * APPLICATION IMPORTS
*/ */
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