mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-03-21 17:50:29 -05:00
Preact: Support remote teams (#2390)
Some checks are pending
Node.js CI / build (22.x) (push) Waiting to run
Some checks are pending
Node.js CI / build (22.x) (push) Waiting to run
--------- Co-authored-by: Guangcong Luo <guangcongluo@gmail.com>
This commit is contained in:
parent
96fb9a314a
commit
ba9f3a529e
|
|
@ -105,7 +105,7 @@ export const PSLoginServer = new class {
|
|||
() => null
|
||||
);
|
||||
}
|
||||
query(act: string, data: PostData): Promise<{ [k: string]: any } | null> {
|
||||
query(act: string, data: PostData = {}): Promise<{ [k: string]: any } | null> {
|
||||
return this.rawQuery(act, data).then(
|
||||
res => res ? JSON.parse(res.slice(1)) : null
|
||||
).catch(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import type { MainMenuRoom } from './panel-mainmenu';
|
|||
import { Dex, toID, type ID } from './battle-dex';
|
||||
import { BattleTextParser, type Args } from './battle-text-parser';
|
||||
import type { BattleRoom } from './panel-battle';
|
||||
import { PSTeambuilder } from './panel-teamdropdown';
|
||||
|
||||
declare const BattleTextAFD: any;
|
||||
declare const BattleTextNotAFD: any;
|
||||
|
|
@ -116,6 +117,7 @@ class PSPrefs extends PSStreamModel<string | null> {
|
|||
effectvolume = 50;
|
||||
musicvolume = 50;
|
||||
notifvolume = 50;
|
||||
uploadprivacy = false;
|
||||
|
||||
afd: boolean | 'sprites' = false;
|
||||
|
||||
|
|
@ -267,11 +269,29 @@ class PSPrefs extends PSStreamModel<string | null> {
|
|||
export interface Team {
|
||||
name: string;
|
||||
format: ID;
|
||||
packedTeam: string;
|
||||
folder: string;
|
||||
/** note that this can be wrong if .uploaded?.loaded === false */
|
||||
packedTeam: string;
|
||||
/** The icon cache must be cleared (to `null`) whenever `packedTeam` is modified */
|
||||
iconCache: preact.ComponentChildren;
|
||||
key: string;
|
||||
/** `uploaded` will only exist if you're logged into the correct account. otherwise teamid is still tracked */
|
||||
teamid?: number;
|
||||
uploaded?: {
|
||||
teamid: number,
|
||||
notLoaded: boolean | Promise<void>,
|
||||
/** password, if private. null = public, undefined = unknown, not loaded yet */
|
||||
private?: string | null,
|
||||
};
|
||||
}
|
||||
interface UploadedTeam {
|
||||
name: string;
|
||||
teamid: number;
|
||||
format: ID;
|
||||
/** comma-separated list of species, for generating the icon cache */
|
||||
team: string;
|
||||
/** password, if private */
|
||||
private?: string | null;
|
||||
}
|
||||
if (!window.BattleFormats) window.BattleFormats = {};
|
||||
|
||||
|
|
@ -284,6 +304,7 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
|
|||
list: Team[] = [];
|
||||
byKey: { [key: string]: Team | undefined } = {};
|
||||
deletedTeams: [Team, number][] = [];
|
||||
uploading: Team | null = null;
|
||||
constructor() {
|
||||
super();
|
||||
try {
|
||||
|
|
@ -356,7 +377,10 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
|
|||
}
|
||||
packAll(teams: Team[]) {
|
||||
return teams.map(team => (
|
||||
(team.format ? `${team.format}]` : ``) + (team.folder ? `${team.folder}/` : ``) + team.name + `|` + team.packedTeam
|
||||
(team.teamid ? `${team.teamid}[` : '') +
|
||||
(team.format ? `${team.format}]` : ``) +
|
||||
(team.folder ? `${team.folder}/` : ``) +
|
||||
team.name + `|` + team.packedTeam
|
||||
)).join('\n');
|
||||
}
|
||||
save() {
|
||||
|
|
@ -366,15 +390,21 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
|
|||
this.update('team');
|
||||
}
|
||||
unpackLine(line: string): Team | null {
|
||||
let pipeIndex = line.indexOf('|');
|
||||
const pipeIndex = line.indexOf('|');
|
||||
if (pipeIndex < 0) return null;
|
||||
let bracketIndex = line.indexOf(']');
|
||||
if (bracketIndex > pipeIndex) bracketIndex = -1;
|
||||
let leftBracketIndex = line.indexOf('[');
|
||||
if (leftBracketIndex < 0) leftBracketIndex = 0;
|
||||
const isBox = line.slice(0, bracketIndex).endsWith('-box');
|
||||
let slashIndex = line.lastIndexOf('/', pipeIndex);
|
||||
if (slashIndex < 0) slashIndex = bracketIndex; // line.slice(slashIndex + 1, pipeIndex) will be ''
|
||||
let format = bracketIndex > 0 ? line.slice(0, bracketIndex) : 'gen7';
|
||||
let format = bracketIndex > 0 ? line.slice(
|
||||
(leftBracketIndex ? leftBracketIndex + 1 : 0), isBox ? bracketIndex - 4 : bracketIndex
|
||||
) : 'gen9';
|
||||
if (!format.startsWith('gen')) format = 'gen6' + format;
|
||||
const name = line.slice(slashIndex + 1, pipeIndex);
|
||||
const teamid = leftBracketIndex > 0 ? Number(line.slice(0, leftBracketIndex)) : undefined;
|
||||
return {
|
||||
name,
|
||||
format: format as ID,
|
||||
|
|
@ -382,8 +412,122 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
|
|||
packedTeam: line.slice(pipeIndex + 1),
|
||||
iconCache: null,
|
||||
key: '',
|
||||
teamid,
|
||||
};
|
||||
}
|
||||
loadRemoteTeams() {
|
||||
PSLoginServer.query('getteams').then(data => {
|
||||
if (!data) return;
|
||||
if (data.actionerror) {
|
||||
return PS.alert('Error loading uploaded teams: ' + data.actionerror);
|
||||
}
|
||||
const teams: { [key: string]: UploadedTeam } = {};
|
||||
for (const team of data.teams) {
|
||||
teams[team.teamid] = team;
|
||||
}
|
||||
|
||||
// find exact teamid matches
|
||||
for (const localTeam of this.list) {
|
||||
if (localTeam.teamid) {
|
||||
const team = teams[localTeam.teamid];
|
||||
if (!team) {
|
||||
continue;
|
||||
}
|
||||
const compare = this.compareTeams(team, localTeam);
|
||||
if (compare !== true) {
|
||||
if (!localTeam.name.endsWith(' (local version)')) localTeam.name += ' (local version)';
|
||||
continue;
|
||||
}
|
||||
localTeam.uploaded = {
|
||||
teamid: team.teamid,
|
||||
notLoaded: true,
|
||||
private: team.private,
|
||||
};
|
||||
delete teams[localTeam.teamid];
|
||||
}
|
||||
}
|
||||
|
||||
// do best-guess matches for teams that don't have a local team with matching teamid
|
||||
for (const team of Object.values(teams)) {
|
||||
let matched = false;
|
||||
for (const localTeam of this.list) {
|
||||
if (localTeam.teamid) continue;
|
||||
|
||||
const compare = this.compareTeams(team, localTeam);
|
||||
if (compare === 'rename') {
|
||||
if (!localTeam.name.endsWith(' (local version)')) localTeam.name += ' (local version)';
|
||||
} else if (compare) {
|
||||
// prioritize locally saved teams over remote
|
||||
// as so to not overwrite changes
|
||||
matched = true;
|
||||
localTeam.teamid = team.teamid;
|
||||
localTeam.uploaded = {
|
||||
teamid: team.teamid,
|
||||
notLoaded: true,
|
||||
private: team.private,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
const mons = team.team.split(',').map((m: string) => ({ species: m, moves: [] }));
|
||||
const newTeam: Team = {
|
||||
name: team.name,
|
||||
format: team.format,
|
||||
folder: '',
|
||||
packedTeam: PSTeambuilder.packTeam(mons),
|
||||
iconCache: null,
|
||||
key: this.getKey(team.name),
|
||||
uploaded: {
|
||||
teamid: team.teamid,
|
||||
notLoaded: true,
|
||||
private: team.private,
|
||||
},
|
||||
};
|
||||
this.push(newTeam);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
loadTeam(team: Team | undefined | null, ifNeeded: true): void | Promise<void>;
|
||||
loadTeam(team: Team | undefined | null): Promise<void>;
|
||||
loadTeam(team: Team | undefined | null, ifNeeded?: boolean): void | Promise<void> {
|
||||
if (!team) return ifNeeded ? undefined : Promise.resolve();
|
||||
if (!team.uploaded?.notLoaded) return ifNeeded ? undefined : Promise.resolve();
|
||||
if (team.uploaded.notLoaded !== true) return team.uploaded.notLoaded;
|
||||
|
||||
return (team.uploaded.notLoaded = PSLoginServer.query('getteam', {
|
||||
teamid: team.uploaded.teamid,
|
||||
}).then(data => {
|
||||
if (!team.uploaded) return;
|
||||
if (!data?.team) {
|
||||
PS.alert(`Failed to load team: ${data?.actionerror || "Error unknown. Try again later."}`);
|
||||
return;
|
||||
}
|
||||
team.uploaded.notLoaded = false;
|
||||
team.packedTeam = data.team;
|
||||
PS.teams.save();
|
||||
}));
|
||||
}
|
||||
compareTeams(serverTeam: UploadedTeam, localTeam: Team) {
|
||||
// TODO: decide if we want this
|
||||
// if (serverTeam.teamid === localTeam.teamid && localTeam.teamid) return true;
|
||||
|
||||
// if titles match exactly and mons are the same, assume they're the same team
|
||||
// if they don't match, it might be edited, but we'll go ahead and add it to the user's
|
||||
// teambuilder since they may want that old version around. just go ahead and edit the name
|
||||
let sanitize = (name: string) => (name || "").replace(/\s+\(server version\)/g, '').trim();
|
||||
const nameMatches = sanitize(serverTeam.name) === sanitize(localTeam.name);
|
||||
if (!(nameMatches && serverTeam.format === localTeam.format)) {
|
||||
return false;
|
||||
}
|
||||
// if it's been edited since, invalidate the team id on this one (count it as new)
|
||||
// and load from server
|
||||
const mons = serverTeam.team.split(',').map(toID).sort().join(',');
|
||||
const otherMons = PSTeambuilder.packedTeamSpecies(localTeam.packedTeam).map(toID).sort().join(',');
|
||||
if (mons !== otherMons) return 'rename';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ export class MainMenuRoom extends PSRoom {
|
|||
const named = namedCode === '1';
|
||||
if (named) PS.user.initializing = false;
|
||||
PS.user.setName(fullName, named, avatar);
|
||||
PS.teams.loadRemoteTeams();
|
||||
return;
|
||||
} case 'updatechallenges': {
|
||||
const [, challengesBuf] = args;
|
||||
|
|
@ -388,6 +389,14 @@ export class MainMenuRoom extends PSRoom {
|
|||
}
|
||||
}
|
||||
break;
|
||||
case 'teamupload':
|
||||
if (PS.teams.uploading) {
|
||||
PS.teams.uploading.uploaded = {
|
||||
teamid: response.teamid,
|
||||
notLoaded: false,
|
||||
private: response.private,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -727,22 +736,20 @@ export class TeamForm extends preact.Component<{
|
|||
changeFormat = (ev: Event) => {
|
||||
this.setState({ format: (ev.target as HTMLButtonElement).value });
|
||||
};
|
||||
submit = (ev: Event) => {
|
||||
submit = (ev: Event, validate?: 'validate') => {
|
||||
ev.preventDefault();
|
||||
const format = this.state.format;
|
||||
const teamKey = this.base!.querySelector<HTMLButtonElement>('button[name=team]')!.value;
|
||||
const team = teamKey ? PS.teams.byKey[teamKey] : undefined;
|
||||
this.props.onSubmit?.(ev, format, team);
|
||||
PS.teams.loadTeam(team).then(() => {
|
||||
(validate === 'validate' ? this.props.onValidate : this.props.onSubmit)?.(ev, format, team);
|
||||
});
|
||||
};
|
||||
handleClick = (ev: Event) => {
|
||||
let target = ev.target as HTMLButtonElement | null;
|
||||
while (target && target !== this.base) {
|
||||
if (target.tagName === 'BUTTON' && target.name === 'validate') {
|
||||
ev.preventDefault();
|
||||
const format = this.state.format;
|
||||
const teamKey = this.base!.querySelector<HTMLButtonElement>('button[name=team]')!.value;
|
||||
const team = teamKey ? PS.teams.byKey[teamKey] : undefined;
|
||||
this.props.onSubmit?.(ev, format, team);
|
||||
this.submit(ev, 'validate');
|
||||
return;
|
||||
}
|
||||
target = target.parentNode as HTMLButtonElement | null;
|
||||
|
|
|
|||
|
|
@ -17,17 +17,25 @@ class TeamRoom extends PSRoom {
|
|||
/** Doesn't _literally_ always exist, but does in basically all code
|
||||
* and constantly checking for its existence is legitimately annoying... */
|
||||
team!: Team;
|
||||
uploaded = false;
|
||||
constructor(options: RoomOptions) {
|
||||
super(options);
|
||||
const team = PS.teams.byKey[this.id.slice(5)] || null;
|
||||
this.team = team!;
|
||||
this.title = `[Team] ${this.team?.name || 'Error'}`;
|
||||
if (team) this.setFormat(team.format);
|
||||
this.uploaded = !!team?.uploaded;
|
||||
this.load();
|
||||
}
|
||||
setFormat(format: string) {
|
||||
const team = this.team;
|
||||
team.format = toID(format);
|
||||
}
|
||||
load() {
|
||||
PS.teams.loadTeam(this.team, true)?.then(() => {
|
||||
this.update(null);
|
||||
});
|
||||
}
|
||||
save() {
|
||||
PS.teams.save();
|
||||
const title = `[Team] ${this.team?.name || 'Team'}`;
|
||||
|
|
@ -81,6 +89,35 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
|
|||
room.team.name = textbox.value.trim();
|
||||
room.save();
|
||||
};
|
||||
|
||||
uploadTeam = (ev: Event) => {
|
||||
const room = this.props.room;
|
||||
const team = PS.teams.byKey[room.id.slice(5)];
|
||||
if (!team) return;
|
||||
|
||||
const cmd = team.uploaded ? 'update' : 'save';
|
||||
// teamName, formatid, rawPrivacy, rawTeam
|
||||
const buf = [];
|
||||
if (team.uploaded) {
|
||||
buf.push(team.uploaded.teamid);
|
||||
} else if (team.teamid) {
|
||||
return PS.alert(`This team is for a different account. Please log into the correct account to update it.`);
|
||||
}
|
||||
buf.push(team.name, team.format, PS.prefs.uploadprivacy ? 1 : 0);
|
||||
const exported = team.packedTeam;
|
||||
if (!exported) return PS.alert(`Add a Pokemon to your team before uploading it.`);
|
||||
buf.push(exported);
|
||||
PS.teams.uploading = team;
|
||||
PS.send(`|/teams ${cmd} ${buf.join(', ')}`);
|
||||
room.uploaded = true;
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
changePrivacyPref = (ev: Event) => {
|
||||
this.props.room.uploaded = false;
|
||||
PS.prefs.uploadprivacy = !(ev.currentTarget as HTMLInputElement).checked;
|
||||
PS.prefs.save();
|
||||
};
|
||||
handleChangeFormat = (ev: Event) => {
|
||||
const dropdown = ev.currentTarget as HTMLButtonElement;
|
||||
const room = this.props.room;
|
||||
|
|
@ -91,6 +128,8 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
|
|||
};
|
||||
save = () => {
|
||||
this.props.room.save();
|
||||
this.props.room.uploaded = false;
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
override render() {
|
||||
|
|
@ -137,6 +176,23 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p>
|
||||
<label class="checkbox" style="display: inline-block">
|
||||
<input
|
||||
name="teamprivacy" checked={!PS.prefs.uploadprivacy}
|
||||
type="checkbox" onChange={this.changePrivacyPref}
|
||||
/> Public
|
||||
</label>
|
||||
{room.uploaded ? (
|
||||
<button class="button exportbutton" disabled>
|
||||
<i class="fa fa-check"></i> Saved to your account
|
||||
</button>
|
||||
) : (
|
||||
<button class="button exportbutton" onClick={this.uploadTeam}>
|
||||
<i class="fa fa-upload"></i> Save to my account (use on other devices)
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
</div></PSPanelWrapper>;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -561,7 +561,7 @@ export class PSTeambuilder {
|
|||
return teams;
|
||||
}
|
||||
|
||||
static packedTeamNames(buf: string) {
|
||||
static packedTeamSpecies(buf: string) {
|
||||
if (!buf) return [];
|
||||
|
||||
const team = [];
|
||||
|
|
@ -604,25 +604,21 @@ export function TeamBox(props: { team: Team | null, noLink?: boolean, button?: b
|
|||
const team = props.team;
|
||||
let contents;
|
||||
if (team) {
|
||||
let icons = team.iconCache;
|
||||
if (!icons) {
|
||||
if (!team.packedTeam) {
|
||||
icons = <em>(empty team)</em>;
|
||||
} else {
|
||||
icons = PSTeambuilder.packedTeamNames(team.packedTeam).map(pokemon =>
|
||||
// can't use <PSIcon>, weird interaction with iconCache
|
||||
// don't try this at home; I'm a trained professional
|
||||
PSIcon({ pokemon })
|
||||
);
|
||||
}
|
||||
team.iconCache = icons;
|
||||
}
|
||||
team.iconCache ||= team.packedTeam ? (
|
||||
PSTeambuilder.packedTeamSpecies(team.packedTeam).map(
|
||||
// can't use <PSIcon>, weird interaction with iconCache
|
||||
// don't try this at home; I'm a trained professional
|
||||
pokemon => PSIcon({ pokemon })
|
||||
)
|
||||
) : (
|
||||
<em>(empty team)</em>
|
||||
);
|
||||
let format = team.format as string;
|
||||
if (format.startsWith('gen8')) format = format.slice(4);
|
||||
format = (format ? `[${format}] ` : ``) + (team.folder ? `${team.folder}/` : ``);
|
||||
contents = [
|
||||
<strong>{format && <span>{format}</span>}{team.name}</strong>,
|
||||
<small>{icons}</small>,
|
||||
<small>{team.iconCache}</small>,
|
||||
];
|
||||
} else {
|
||||
contents = [
|
||||
|
|
@ -680,6 +676,7 @@ class TeamDropdownPanel extends PSRoomPanel {
|
|||
}
|
||||
if (!target) return;
|
||||
|
||||
PS.teams.loadTeam(PS.teams.byKey[target.value], true);
|
||||
this.chooseParentValue(target.value);
|
||||
};
|
||||
override render() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user