diff --git a/config/formats.ts b/config/formats.ts
index 49220cde96b3a..de0f91e8a0122 100644
--- a/config/formats.ts
+++ b/config/formats.ts
@@ -165,6 +165,23 @@ export const Formats: FormatList = [
'Lugia', 'Lunala', 'Magearna', 'Marshadow', 'Mewtwo', 'Naganadel', 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Palkia', 'Pheromosa',
'Rayquaza', 'Reshiram', 'Solgaleo', 'Xerneas', 'Yveltal', 'Zacian', 'Zamazenta', 'Zekrom', 'Zygarde-Base',
'Moody', 'Power Construct', 'Shadow Tag', 'Terrain Extender', 'Baton Pass',
+ 'Damp Rock', 'Smooth Rock', 'Moody', 'Shadow Tag', 'Baton Pass',
+ ],
+ },
+ {
+ name: "[Gen 8] Anything Goes",
+ threads: [
+ `• Anything Goes`,
+ ],
+
+ mod: 'gen8',
+ searchShow: false,
+ ruleset: ['Obtainable', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Endless Battle Clause'],
+ },
+ {
+ name: "[Gen 8] ZU",
+ threads: [
+ `• ZU Metagame Discussion`,
],
},
{
diff --git a/data/abilities.ts b/data/abilities.ts
index b3f0f0f9eb032..6397e21bf74ef 100644
--- a/data/abilities.ts
+++ b/data/abilities.ts
@@ -2816,7 +2816,7 @@ export const Abilities: {[abilityid: string]: AbilityData} = {
}
},
name: "Quick Draw",
- rating: 2.5,
+ rating: 2,
num: 259,
},
quickfeet: {
diff --git a/lib/utils.ts b/lib/utils.ts
index 6ce0cc8d60c6f..7bf6cf35c2c43 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -14,7 +14,8 @@
* - A lot of Chat functions are kind of iffy, but I'm going to say for now
* that if it's English-specific, it should be left out of here.
*/
-
+import {Net} from './net';
+import * as crypto from 'crypto';
type Comparable = number | string | boolean | Comparable[] | {reverse: Comparable};
export const Utils = new class Utils {
@@ -302,4 +303,68 @@ export const Utils = new class Utils {
// Step 7
return d[n][m];
}
+ async namecolor(name: string): Promise {
+ const config = Net('https://play.pokemonshowdown.com/config/config.js');
+ const customcolors = await config.get().then(res => {
+ const idx = res.indexOf(' = {');
+ res = res.slice(idx + 4);
+ const colors: {[k: string]: string} = {};
+ for (const line of res.split('\n')) {
+ const [name, base] = line.split(':')
+ .map(item => item.split('//')[0])
+ .map(toID).filter(Boolean);
+ if (!base || !name) continue;
+ colors[name] = base;
+ }
+ return colors;
+ });
+ const HSLToRGB = (H: number, S: number, L: number) => {
+ let C = (100 - Math.abs(2 * L - 100)) * S / 100 / 100;
+ let X = C * (1 - Math.abs((H / 60) % 2 - 1));
+ let m = L / 100 - C / 2;
+
+ let R1;
+ let G1;
+ let B1;
+ switch (Math.floor(H / 60)) {
+ case 1: R1 = X; G1 = C; B1 = 0; break;
+ case 2: R1 = 0; G1 = C; B1 = X; break;
+ case 3: R1 = 0; G1 = X; B1 = C; break;
+ case 4: R1 = X; G1 = 0; B1 = C; break;
+ case 5: R1 = C; G1 = 0; B1 = X; break;
+ case 0: default: R1 = C; G1 = X; B1 = 0; break;
+ }
+ let R = R1 + m;
+ let G = G1 + m;
+ let B = B1 + m;
+ return {R, G, B};
+ }
+ if (customcolors[name]) return this.namecolor(customcolors[name]);
+ let hash = crypto.createHash('md5').update(name).digest('hex');
+ let H = parseInt(hash.substr(4, 4), 16) % 360; // 0 to 360
+ let S = parseInt(hash.substr(0, 4), 16) % 50 + 40; // 40 to 89
+ let L = Math.floor(parseInt(hash.substr(8, 4), 16) % 20 + 30); // 30 to 49
+
+ let {R, G, B} = HSLToRGB(H, S, L);
+ let lum = R * R * R * 0.2126 + G * G * G * 0.7152 + B * B * B * 0.0722; // 0.013 (dark blue) to 0.737 (yellow)
+
+ let HLmod = (lum - 0.2) * -150; // -80 (yellow) to 28 (dark blue)
+ if (HLmod > 18) HLmod = (HLmod - 18) * 2.5;
+ else if (HLmod < 0) HLmod = (HLmod - 0) / 3;
+ else HLmod = 0;
+ // let mod = ';border-right: ' + Math.abs(HLmod) + 'px solid ' + (HLmod > 0 ? 'red' : '#0088FF');
+ let Hdist = Math.min(Math.abs(180 - H), Math.abs(240 - H));
+ if (Hdist < 15) {
+ HLmod += (15 - Hdist) / 3;
+ }
+
+ L += HLmod;
+
+ let {R: r, G: g, B: b} = HSLToRGB(H, S, L);
+ const toHex = (x: number) => {
+ const hex = Math.round(x * 255).toString(16);
+ return hex.length === 1 ? '0' + hex : hex;
+ };
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+ }
};
diff --git a/package.json b/package.json
index fd50a53c21dd5..883d929b60bf8 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,8 @@
},
"optionalDependencies": {
"cloud-env": "^0.2.3",
+ "githubhook": "^1.9.3",
+ "node-oom-heapdump": "^1.2.0",
"node-static": "^0.7.11",
"nodemailer": "^6.4.6",
"permessage-deflate": "^0.1.7",
diff --git a/server/chat-commands/info.ts b/server/chat-commands/info.ts
index a35b4115b8f67..b9c0471fa897a 100644
--- a/server/chat-commands/info.ts
+++ b/server/chat-commands/info.ts
@@ -1623,7 +1623,6 @@ export const commands: ChatCommands = {
},
bugreport: 'bugs',
- bugreports: 'bugs',
bugs(target, room, user) {
if (!this.runBroadcast()) return;
if (room?.battle) {
diff --git a/server/chat-plugins/github.ts b/server/chat-plugins/github.ts
new file mode 100644
index 0000000000000..b7e4a63f2c939
--- /dev/null
+++ b/server/chat-plugins/github.ts
@@ -0,0 +1,192 @@
+/**
+ * Plugin to notify given rooms / the Development room on Pokemon Showdown
+ * of github updates.
+ * Some html / design from xfix's github bot, plugin by Mia.
+ * @author mia-pi-git
+ */
+// @ts-ignore old enough module it has no types
+import * as githubhook from 'githubhook';
+import {Utils} from '../../lib/utils';
+import {FS} from '../../lib/fs';
+
+const cooldown = 15 * 60 * 1000;
+const rooms: RoomID[] = Config.github?.rooms ? Config.github.rooms : ['staff', 'upperstaff', 'development'];
+
+export const github = githubhook({
+ port: (Config.github?.port || Config.port + 1),
+ secret: (Config.github?.secret || ''),
+ callback: '/server/chat-plugins/github.ts',
+});
+
+interface GithubData {
+ names: {[k: string]: string};
+ bans: string[];
+ repos: {[k: string]: string};
+}
+
+export let gitData: GithubData = {
+ bans: ['dependabot-preview[bot]'],
+ names: {},
+ repos: {
+ 'pokemon-showdown': 'server',
+ 'pokemon-showdown-client': 'client',
+ 'Pokemon-Showdown-Dex': 'dex',
+ },
+};
+
+try {
+ gitData = JSON.parse(FS('config/chat-plugins/github.json').readIfExistsSync());
+} catch (e) {};
+
+github.on('push',
+ (repo: string, ref: string, result: AnyObject) => GithubParser.push(repo, ref, result)
+);
+
+github.on(
+ 'pull_request',
+ (repo: string, ref: string, result: AnyObject) => GithubParser.pull(repo, ref, result)
+);
+
+export const GithubParser = new class {
+ pushes: AnyObject;
+ constructor() {
+ this.pushes = {};
+ }
+ shouldShow(id: string, repo: string) {
+ if (gitData.bans.includes(id)) return false;
+ if (!this.pushes[repo]) this.pushes[repo] = {};
+ if (Date.now() - this.pushes[repo][id] > cooldown) return true;
+ if (Date.now() - this.pushes[repo][id] < cooldown) this.pushes[repo][id] = Date.now();
+ return false;
+ }
+ repository(repo: string) {
+ if (gitData.repos[repo]) return gitData.repos[repo];
+ return repo;
+ }
+ save() {
+ FS('config/chat-plugins/github.json').writeUpdate(() => JSON.stringify(gitData));
+ }
+ report(html: string) {
+ Rooms.global.notifyRooms(rooms, `|c|&|/uhtml github,${html}
`);
+ }
+ push(repo: string, ref: string, result: AnyObject) {
+ const url = result.compare;
+ const buffer = result.commits.map((commit: AnyObject) => {
+ const message = commit.message;
+ const shortMessage = /.+/.exec(message)![0];
+ const id = commit.id.slice(0, 6);
+ const formattedRepo = `[${this.repository(repo)}]`;
+ const userName = Utils.html`(${this.getName(result.sender.login)})`;
+ return `${formattedRepo} ${id} ${shortMessage} ${userName}`;
+ }).join('
');
+ if (!this.pushes[repo]) this.pushes[repo] = {};
+ if (!this.shouldShow(result.sender.login, repo)) return;
+ this.pushes[repo][result.pusher.name] = Date.now();
+ this.report(buffer);
+ }
+ pull(repo: string, ref: string, result: AnyObject) {
+ const committer = toID(result.sender.login);
+ if (gitData.bans.includes(committer)) return;
+ const num = result.pull_request.number;
+ const url = result.pull_request.html_url;
+ const title = result.pull_request.title;
+ const name = this.getName(result.sender.login);
+ if (!this.shouldShow(result.sender.login, repo)) return;
+ let action = result.action;
+ if (action === 'synchronize') {
+ action = 'updated';
+ }
+ if (action === 'review_requested') {
+ action = 'requested a review for';
+ }
+ const blacklisted = ['converted_to_draft', 'ready_for_review', 'converted_to_draft'];
+ if (blacklisted.includes(action)) {
+ return;
+ }
+ this.report(
+ `[${repo}] ${name} ` +
+ `${action} PR#${num}: ${title}`
+ );
+ }
+ getName(name: string) {
+ if (gitData.names[name]) return gitData.names[name];
+ return name;
+ }
+};
+
+export const commands: ChatCommands = {
+ git: 'github',
+ gh: 'github',
+ github: {
+ ban(target, room, user) {
+ room = this.requireRoom('development');
+ this.checkCan('mute', null, room);
+ if (gitData.bans.includes(target)) return this.errorReply(`${target} is already gitbanned.`);
+ gitData.bans.push(target);
+ GithubParser.save();
+ this.privateModAction(`(${user.name} prevented ${target} from being reported by the plugin.)`);
+ return this.modlog('GITBAN', null, target);
+ },
+ unban(target, room, user) {
+ room = this.requireRoom('development');
+ this.checkCan('mute', null, room);
+ const index = gitData.bans.indexOf(target);
+ if (index < 0) return this.errorReply(`${target} is not gitbanned.`);
+ gitData.bans.splice(index, 1);
+ GithubParser.save();
+ this.privateModAction(`(${user.name} allowed ${target} to be reported by the plugin.)`);
+ return this.modlog('UNGITBAN', null, target);
+ },
+ bans(target, room, user) {
+ room = this.requireRoom('development');
+ this.checkCan('mute', null, room);
+ let buf = `IDs banned from being reported by the Github Plugin:
`;
+ for (const id of gitData.bans) {
+ buf += `- ${id}
`;
+ }
+ return this.sendReplyBox(buf);
+ },
+ setname(target, room, user) {
+ room = this.requireRoom('development');
+ this.checkCan('mute', null, room);
+ const [oldID, newName] = target.split(',');
+ if (!target || !oldID || !newName) return this.errorReply(`Specify a GitHub username and new name.`);
+ if (gitData.names[oldID] === newName) return this.errorReply("That Git ID is already set to that name.");
+ gitData.names[oldID] = newName;
+ GithubParser.save();
+ this.privateModAction(`(${user.name} set ${oldID}'s name to be ${newName} in the Github Plugin.)`);
+ this.modlog(`GIT NAME`, null, `"${oldID}" to "${newName}"`);
+ },
+ resetname(target, room, user) {
+ room = this.requireRoom('development');
+ this.checkCan('mute', null, room);
+ if (!target) return this.errorReply(`Specify a name.`);
+ if (!gitData.names[target]) return this.errorReply("That Git ID does not have a name set.");
+ delete gitData.names[target];
+ GithubParser.save();
+ this.privateModAction(`(${user.name} reset ${target}'s name in the Github Plugin.)`);
+ return this.modlog(`GIT RESETNAME`, null, `${target}`);
+ },
+ reponame(target, room, user) {
+ room = this.requireRoom('development');
+ this.checkCan('mute', null, room);
+ const [oldRepo, newName] = Utils.splitFirst(target, ',');
+ if (!oldRepo || !newName) return this.errorReply("Specify a repo name and a new name to display.");
+ gitData.repos[oldRepo] = newName;
+ GithubParser.save();
+ this.privateModAction(`(${user.name} set the repo ${oldRepo}'s name in the Github Plugin to ${newName}.)`);
+ return this.modlog(`GIT REPONAME`, null, `${target}`);
+ },
+ resetreponame(target, room, user) {
+ room = this.requireRoom('development');
+ this.checkCan('mute', null, room);
+ if (!gitData.repos[target]) return this.errorReply(`${target} does not have a name in the Github Plugin.`);
+ delete gitData.repos[target];
+ GithubParser.save();
+ this.privateModAction(`(${user.name} reset the repo ${target}'s name in the Github Plugin.)`);
+ return this.modlog(`GIT RESETREPONAME`, null, `${target}`);
+ },
+ },
+};
+
+github.listen();
diff --git a/server/rooms.ts b/server/rooms.ts
index 9040533778fb0..81ddfebfaa590 100644
--- a/server/rooms.ts
+++ b/server/rooms.ts
@@ -803,10 +803,8 @@ export abstract class BasicRoom {
if (user.named) {
this.reportJoin('j', user.getIdentityWithStatus(this.roomid), user);
}
-
const staffIntro = this.getStaffIntroMessage(user);
if (staffIntro) this.sendUser(user, staffIntro);
-
this.users[user.id] = user;
this.userCount++;